Google has a great document that talks in extreme clear terms what site owners should do to enable better indexing of their content by majore search engines. This document can be found at http://googlewebmastercentral.blogspot.com/2008/11/googles-seo-starter-guide.html. I strongly recommend every site owner to read this guide.
Here are some important highlights taken from the above document.
Create unique, accurate page titles
• Accurately describe the page's content - Choose a title that effectively communicates the topic of the page's content.
Avoid:
• choosing a title that has no relation to the content on the page
• using default or vague titles like "Untitled" or "New Page 1"
• Create unique title tags for each page - Each of your pages should ideally have a unique title tag, which helps Google know how the page is distinct from the others on your site.
Avoid:
• using a single title tag across all of your site's pages or a large group of pages
• Use brief, but descriptive titles - Titles can be both short and informative. If the title is too long, Google will show only a portion of it in the search result.
Avoid:
• using extremely lengthy titles that are unhelpful to users
• stuffing unneeded keywords in your title tags
Make use of the "description" meta tag
• Accurately summarize the page's content - Write a description that would both inform and interest users if they saw your description meta tag as a snippet in a search result.
Avoid:
• writing a description meta tag that has no relation to the content on the page
• using generic descriptions like "This is a webpage" or "Page about baseball cards"
• filling the description with only keywords
• copy and pasting the entire content of the document into the description meta tag
• Use unique descriptions for each page - Having a different description meta tag for each page helps both users and Google, especially in searches where users may bring up multiple pages on your domain e.g. searches using the site: operator). If your site has thousands or even millions of pages, hand-crafting description meta tags probably isn't feasible. In this case, you could automatically generate description meta tags based on each page's content.
Avoid:
• using a single description meta tag across all of your site's pages or a large group of pages
Improve the structure of your URLs
• Use words in URLs - URLs with words that are relevant to your site's content and structure are friendlier for visitors navigating your site. Visitors remember them better and might be more willing to link to them.
Avoid:
• using lengthy URLs with unnecessary parameters and session IDs
• choosing generic page names like "page1.html"
• using excessive keywords like "baseball-cards-baseball-cards-baseballcards.htm"
• Create a simple directory structure - Use a directory structure that organizes your content well and is easy for visitors to know where they're at on your site. Try using your directory structure to indicate the type of content found at that URL.
Avoid:
• having deep nesting of subdirectories like ".../dir1/dir2/dir3/dir4/dir5/dir6/page.html"
• using directory names that have no relation to the content in them
• Provide one version of a URL to reach a document - To prevent users from linking to one version of a URL and others linking to a different version (this could split the reputation of that content between the URLs), focus on using and referring to one URL in the structure and internal linking of your pages. If you do find that people are accessing the same content through multiple URLs, setting up a 301 redirect from non-preferred URLs to the dominant URL is a good solution for this.
Avoid:
• having pages from subdomains and the root directory (e.g. "domain.com/page.htm" and "sub.domain.com/page.htm") access the same content
• mixing www. and non-www. versions of URLs in your internal linking structure
• using odd capitalization of URLs (many users expect lower-case URLs and remember them better)
Make your site easier to navigate
• Create a naturally flowing hierarchy - Make it as easy as possible for users to go from general content to the more specific content they want on your site. Add navigation pages when it makes sense and effectively work these into your internal link structure.
Avoid:
• creating complex webs of navigation links, e.g. linking every page on your site to every other page
• going overboard with slicing and dicing your content (it takes twenty clicks to get to deep content)
• Use mostly text for navigation - Controlling most of the navigation from page to page on your site through text links makes it easier for search engines to crawl and understand your site. Many users also prefer this over other approaches, especially on some devices that might not handle Flash or JavaScript.
Avoid:
• having a navigation based entirely on drop-down menus, images, or animations (many, but not all, search engines can discover such links on a site, but if a user can reach all pages on a site via normal text links, this will improve the accessibility of your site; more on how Google deals with non-text files)
• Use "breadcrumb" navigation - A breadcrumb is a row of internal links at the top or bottom of the page that allows visitors to quickly navigate back to a previous section or the root page. Many breadcrumbs have the most general page (usually the root page) as the first, left-most link and list the more specific sections out to the right.
• Put an HTML sitemap page on your site, and use an XML Sitemap file - A simple sitemap page with links to all of the pages or the most important pages (if you have hundreds or thousands) on your site can be useful. Creating an XML Sitemap file for your site helps ensure that search engines discover the pages on your site.
Avoid:
• letting your HTML sitemap page become out of date with broken links
• creating an HTML sitemap that simply lists pages without organizing them, for example by subject
• Consider what happens when a user removes part of your URL - Some users might navigate your site in odd ways, and you should anticipate this. For example, instead of using the breadcrumb links on the page, a user might drop off a part of the URL in the hopes of finding more general content. He or she might be visiting http://www.brandonsbaseballcards.com/news/2008/upcoming-baseball-card-shows.htm, but then enter http://www.brandonsbaseballcards.com/news/2008/ into the browser's address bar, believing that this will show all news from 2008. Is your site prepared to show content in
this situation or will it give the user a 404 ("page not found" error)? What about moving up a directory level to http://www.brandonsbaseballcards.com/news/?
• Have a useful 404 page - Users will occasionally come to a page that doesn't exist on your site, either by following a broken link or typing in the wrong URL. Having a custom 404 page that kindly guides users back to a working page on your site can greatly improve a user's experience. Your 404 page should probably have a link back to your root page and could also provide links to popular or related content on your site. Google provides a 404 widget that you can embed in your 404 page to automatically populate it with many useful features. You can also use Google Webmaster Tools to find the sources of URLs causing "not found" errors.
Avoid:
• allowing your 404 pages to be indexed in search engines (make sure that your webserver is configured to give a 404 HTTP status code when non-existent pages are requested)
• providing only a vague message like "Not found", "404", or no 404 page at all
• using a design for your 404 pages that isn't consistent with the rest of your site
Use heading tags appropriately
• Imagine you're writing an outline - Similar to writing an outline for a large paper, put some thought into what the main points and sub-points of the content on the page will be and decide where to use heading tags appropriately.
Avoid:
• placing text in heading tags that wouldn't be helpful in defining the structure of the page
• using heading tags where other tags like <em> and <strong> may be more appropriate
• erratically moving from one heading tag size to another
• Use headings sparingly across the page - Use heading tags where it makes sense. Too many heading tags on a page can make it hard for users to scan the content and determine where one topic ends and another begins.
Avoid:
• excessively using heading tags throughout the page
• putting all of the page's text into a heading tag
• using heading tags only for styling text and not presenting structure
Optimize your use of images
• Use brief, but descriptive filenames and alt text - Like many of the other parts of the page targeted for optimization, filenames and alt text (for ASCII languages) are best when they're short, but descriptive.
Avoid:
• using generic filenames like "image1.jpg", "pic.gif", "1.jpg" when possible (some sites with thousands of images might consider automating the naming of images)
• writing extremely lengthy filenames
• stuffing keywords into alt text or copying and pasting entire sentences
• Supply alt text when using images as links - If you do decide to use an image as a link, filling out its alt text helps Google understand more about the page you're linking to. Imagine that you're writing anchor text for a text link.
Avoid:
• writing excessively long alt text that would be considered spammy
• using only image links for your site's navigation
• Store images in a directory of their own - Instead of having image files spread out in numerous directories and subdirectories across your domain, consider consolidating your images into a single directory (e.g. brandonsbaseballcards.com/images/). This simplifies the path to your images.
• Use commonly supported filetypes - Most browsers support JPEG, GIF, PNG, and BMP image formats. It's also a good idea to have the extension of your filename match with the filetype.
Make effective use of robots.txt
• Use more secure methods for sensitive content - You shouldn't feel comfortable using robots.txt to block sensitive or confidential material. One reason is that search engines could still reference the URLs you block (showing just the URL, no title or snippet) if there happen to be links to those URLs somewhere on the Internet (like referrer logs). Also, non-compliant or rogue search engines that don't acknowledge the Robots Exclusion Standard could disobey the instructions of your robots.txt. Finally, a curious user could examine the directories or subdirectories in your robots.txt file and guess the URL of the content that you don't want seen. Encrypting the content or password-protecting it with .htaccess are more secure alternatives.
Avoid:
• allowing search result-like pages to be crawled (users dislike leaving one search result page and landing on another search result page that doesn't add significant value for them)
• allowing a large number of auto-generated pages with the same or only slightly different content to be crawled: "Should these 100,000 near-duplicate pages really be in a search engine's index?"
• allowing URLs created as a result of proxy services to be crawled
There was a great article in Visual Studio magazine (June 2008) by Ian Stirk in which he talks in detail about how to improve application performance by creating a utility that tells you which processes are being blocked.
You can read the article at http://visualstudiomagazine.com/features/article.aspx?editorialsid=2490. The two sql sprocs that you will need to create are:
The database utility dba_BlockTracer extracts data about the running processes by inspecting the system view sys.sysprocesses. This view then queries the underlying system table sysprocesses.
|
CREATE PROC [dbo].[dba_BlockTracer]
AS
/*--------------------------------------------------
Purpose: Shows details of the root blocking process, together with details of any blocked processed
----------------------------------------------------
Parameters: None.
Revision History:
19/07/2007 Ian_Stirk@yahoo.com Initial version
Example Usage:
1. exec YourServerName.master.dbo.dba_BlockTracer
--------------------------------------------------*/
BEGIN
-- Do not lock anything, and do not get held up by any locks.
SET TRANSACTION ISOLATION LEVEL READ
UNCOMMITTED
-- If there are blocked processes...
IF EXISTS(SELECT 1 FROM sys.sysprocesses WHERE
blocked != 0)
BEGIN
-- Identify the root-blocking spid(s)
SELECT distinct t1.spid AS [Root blocking spids]
, t1.[loginame] AS [Owner]
, master.dbo.dba_GetSQLForSpid(t1.spid) AS
'SQL Text'
, t1.[cpu]
, t1.[physical_io]
, DatabaseName = DB_NAME(t1.[dbid])
, t1.[program_name]
, t1.[hostname]
, t1.[status]
, t1.[cmd]
, t1.[blocked]
, t1.[ecid]
FROM sys.sysprocesses t1, sys.sysprocesses t2
WHERE t1.spid = t2.blocked
AND t1.ecid = t2.ecid
AND t1.blocked = 0
ORDER BY t1.spid, t1.ecid
-- Identify the spids being blocked.
SELECT t2.spid AS 'Blocked spid'
, t2.blocked AS 'Blocked By'
, t2.[loginame] AS [Owner]
, master.dbo.dba_GetSQLForSpid(t2.spid) AS
'SQL Text'
, t2.[cpu]
, t2.[physical_io]
, DatabaseName = DB_NAME(t2.[dbid])
, t2.[program_name]
, t2.[hostname]
, t2.[status]
, t2.[cmd]
, t2.ecid
FROM sys.sysprocesses t1, sys.sysprocesses t2
WHERE t1.spid = t2.blocked
AND t1.ecid = t2.ecid
ORDER BY t2.blocked, t2.spid, t2.ecid
END
ELSE -- No blocked processes.
PRINT 'No processes blocked.'
END |
Make a call to the database function dba_GetSQLForSpid to get the underlying SQL, which can show you why performance is slow. The function accepts one parameter: the SQL Server process ID (@spid) of a running process.
|
CREATE Function [dbo].[dba_GetSQLForSpid]
(
@spid SMALLINT
)
RETURNS NVARCHAR(4000)
/*-------------------------------------------------
Purpose: Returns the SQL text for a given spid.
---------------------------------------------------
Parameters: @spid - SQL Server process ID.
Returns: @SqlText - SQL text for a given spid.
Revision History:
01/12/2006 Ian_Stirk@yahoo.com Initial version
Example Usage:
SELECT dbo.dba_GetSQLForSpid(51)
SELECT dbo.dba_GetSQLForSpid(spid) AS [SQL text]
, * FROM sys.sysprocesses WITH (NOLOCK)
--------------------------------------------------*/
BEGIN
DECLARE @SqlHandle BINARY(20)
DECLARE @SqlText NVARCHAR(4000)
-- Get sql_handle for the given spid.
SELECT @SqlHandle = sql_handle
FROM sys.sysprocesses WITH (nolock) WHERE
spid = @spid
-- Get the SQL text for the given sql_handle.
SELECT @SqlText = [text] FROM
sys.dm_exec_sql_text(@SqlHandle)
RETURN @SqlText
END |
One of the common problems that we face in designing a Windows service is the ease of debugging it. I have followed a pattern to solve this problem where I can run a service from either command line or as a service.
The steps below outline the changes you would need to make to enable this in your service. Also included are code snippets that show the point.
1. Modify your Service.
a. Add a RunConsole() method that performs a OnStart(), waits for user to quit and a OnStop() call.
b. Add a RunService() method to Run the process as a normal service.
/// <summary>
/// Runs the service as a console app.
/// </summary>
internal static void RunConsole()
{
using (MyService consoleService = new MyService())
{
consoleService.OnStart(null);
// Wait for the user to quit the program.
Console.WriteLine("Press \'q\' to quit");
while (Console.Read() != 'q') ;
consoleService.OnStop();
}
}
/// <summary>
/// Runs normally
/// </summary>
internal static void RunService()
{
System.ServiceProcess.ServiceBase.Run(new MyService());
}
2. Modify your startup program.
a. Update your Main(string[] args) to accept arguments, and if the first argument is a /c, call RunConsole() (above), else call RunService().
/// <summary>
/// The main entry point for the service...
/// It will run the executable as a service
/// unless /c is specified in which case it will run as a console app.
/// </summary>
static int Main(string[] args)
{
int retCode = 0;
if (args.Length > 1)
{
PrintUsage();
retCode = 1;
}
else if (args.Length == 1)
{
if ((args[0] != null) && (args[0].Length >= 2))
{
switch (args[0][0])
{
case '-':
case '/':
switch (args[0][1])
{
case 'c':
case 'C':
MyService.RunConsole();
break;
default:
PrintUsage();
retCode = 1;
break;
}
break;
default:
PrintUsage();
retCode = 1;
break;
}
}
else
{
PrintUsage();
retCode = 1;
}
}
else
{
MyService.RunService();
}
return retCode;
}
That's it.
One common requirement with any decent sized multi-version product is to automatically update the version numbers of the binaries on a regular basis. This is generally achieved by updating the AssemblyInfo.cs (or other language equivalent ) files.
There are a couple of ways to do this:
1. Assign one developer to remember to increment the numbers on a daily basis. Yes, don’t get mad at me. I know this sucks. I just listed it here as an option.
2. Use the nightly build (or equivalent tool) to do this automatically.
3.
If you use Team Foundation Server to manage your daily (or nightly builds), you can use one custom task developed by the nice people at Microsoft. The task is aptly named AssemblyInfoTask. This task used to be hosted at www.gotdotnet.com but since it closed people often to go www.codeplex.com to search for it. Well, it now resides at code.msdn.com. The direct link to the project is http://code.msdn.microsoft.com/AssemblyInfoTaskvers.
Once you download it and follow the instructions and set it up, you will run into the next problem. You will see an error similar to this:
Error emitting 'System.Reflection.AssemblyVersionAttribute' attribute -- 'The version specified '1.0.08102.1' is invalid
This is an interesting problem. See, we have had version numbering since ages. During those early days, when disk space was a premium, a good developer never wasted space. So, they ended up using Int16 for the build number (the 3rd set of numbers). This caused a huge problem with the arrival of 2007.
The solution for this problem is described here: http://blogs.msdn.com/msbuild/archive/2007/01/03/fixing-invalid-version-number-problems-with-the-assemblyinfotask.aspx
Ok. Now you have got this build to happen on your dev box. How do you set this up on your build server?
Simple. Note: The project documentation says to add it to the *.csproj file (or *.vbproj file). DON’T DO THAT if you are going to use the build server for nightly builds. Let’s open your TFSBuild.proj file (again, if you used the defaults, it will be under your root dir of the project/TeamBuildTypes/Release (or Debug). Let’s check out this file and open it.
At the very top of the file (after comments and start), you will see a line like:
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
ADD the following line after this:
<Import Project="$(MSBuildExtensionsPath)\Microsoft\AssemblyInfoTask\Microsoft.VersionNumber.Targets"/>
But this is not all. If you run the build now, you will see errors of File not accessible. The reason, as you would have guessed, is that AssemblyInfo.* files would be under source control as read only and could not be saved. The solution now is to check out these files before the compile gets executed and check them back in after compile is done.
Here is the sample config on how to do this. Note that in my example below, both C# and VB AssemblyInfo files will get covered.
Step 1: In the <PropertyGroup> section, add the following:
<PropertyGroup>
<!-- TF.exe -->
<TF>"$(TeamBuildRefPath)\..\tf.exe"</TF>
<!-- AssemblyInfo file spec -->
<AssemblyInfoSpec>AssemblyInfo.*</AssemblyInfoSpec>
</PropertyGroup>
The first line defines an alias for tf.exe to checkout/checkin the files.
The second line defines a file pattern.
Step 2: We need to add <targets>. You will need to add these lines towards the end of the project (after the <ItemGroup> sections.
<Target Name="AfterGet" Condition="'$(IsDesktopBuild)'!='true'">
<!-- Set the AssemblyInfoFiles items dynamically -->
<CreateItem Include="$(SolutionRoot)\**\$(AssemblyInfoSpec)">
<Output ItemName="AssemblyInfoFiles" TaskParameter="Include" />
</CreateItem>
<Exec WorkingDirectory="$(SolutionRoot)"
Command="$(TF) checkout /recursive $(AssemblyInfoSpec)"/>
</Target>
Hopefully, it’s apparent what’s happening here. The target tells the build system to execute this task after getting all the source code. The CreateItem creates an array of files that meet the file spec we defined earlier. The xec checks these out.
Now that we have added the checkout, let’s check the files in after AssemblyInfoTask has done the edits. Let’s add the following lines:
<Target Name="AfterCompile" Condition="'$(IsDesktopBuild)'!='true'">
<Exec WorkingDirectory="$(SolutionRoot)"
Command="$(TF) checkin /comment:"Auto-Build: Version Update" /noprompt /override:"Auto-Build: Version Update" /recursive "/>
</Target>
Good so far. What happens if you build broke. Ah-ha, you see where I am going.
<!-- In case of Build failure, the AfterCompile target is not executed. Undo the changes -->
<Target Name="BeforeOnBuildBreak" Condition="'$(IsDesktopBuild)'!='true'">
<Exec WorkingDirectory="$(SolutionRoot)"
Command="$(TF) undo /noprompt /recursive $(AssemblyInfoSpec)"/>
</Target>
Now we have covered this properly. Let’s check this file in and fire a build. If you have enabled check-in mails, you should get a mail that will tell about AssemblyInfo files getting updated.
Happy building.
Team Foundation servers use MSBuild to build our projects. MSBuild does not support the Visual Studio Setup/Deployment projects natively. In many of today's applications it's a must to have msi based installs created via Visual Studio.
Till someone actually takes the time to build a custom task library to handle vdproj files in MSBuild, we will have to use a hack to make our vdproj files get built on MSBuild. The hack is simple. After the compilation of the main solution is done, we invoke the Visual studio command line to build the vdproj project and copy the msi and setup.exe to the appropriate output folder.
The following simple steps will get you going in the correct direction.
- Find your TFSBuild.proj file in TFS source control for your project. Generally, if you had selected all defaults when creating a build definition, it would reside under TFSProject/TeamBuildTypes/{Build/Release} folder.
- Make sure your vdproj project is part of the main solution. Or else, you could create a standalone solution with the same name as the vdproj project. Either way,
- Edit the TFSBuild.proj file and add the following XML snippet (generally after the <ItemGroup> node).
- Replace the folder and files in the snippet with your project specific paths and names. Also, in the example below, I have shown the build type to be {Release/Any CPU}. You would replace this with your build specific settings.
- Check this file back in and fire a build. You are good to go!!!
<!-- Hack to build setup projects using MSBuild -->
<Target Name="AfterCompile">
<Exec Command=""$(ProgramFiles)\Microsoft Visual Studio 9.0\Common7\IDE\devenv" "$(SolutionRoot)\MyAppSetup\MyAppSetup.vdproj" /Build "Release|Any CPU""/>
<Copy SourceFiles="$(SolutionRoot)\MyAppSetup\Release\MyApp.msi; $(SolutionRoot)\MyAppSetup\Release\setup.exe" DestinationFolder="$(OutDir)\Setup\" />
</Target>
One of the most tiresome (but important) things when developing web services is handling un-handled exceptions. A good design principle forces you to catch and cast relevant exceptions raised by your web methods into more meaningful SOAP exceptions. But exceptions will occur.
It is quite tedious to wrap each web method in a try/catch loop. This dictates a need for a common framework to handle unhandled web services exceptions. In this blog post, I will guide you through a step by step process for building one.
Summary:
- Extend SoapExtension class and override the ProcessMessage() method.
- In ProcessMessage(), add special handler code for message stage of SoapMessageStage.AfterSerialize.
- Modify the web service's web.config file and add a <soapExtensionTypes> node to <webServices> section.
Details:
Ok, now let's dig deeper into the code.
Step 1:
The first step in the whole process is to extend the SoapExtension class override the ProcessMessage() method.
using System.IO;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml;
using System;
public class TelspaceSoapExtension : SoapExtension
{
private Stream originalStream;
private Stream updatedStream;
public override object GetInitializer(Type serviceType)
{
return null;
}
public override object GetInitializer
(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return null;
}
public override void Initialize(object initializer)
{
}
public override Stream ChainStream(Stream stream)
{
originalStream = stream;
updatedStream = new MemoryStream();
return updatedStream;
}
private void Transfer(Stream inStream, Stream outStream)
{
StreamReader sr = new StreamReader(inStream);
StreamWriter sw = new StreamWriter(outStream);
sw.Write(sr.ReadToEnd());
sw.Flush();
}
public override void ProcessMessage(SoapMessage soapMessage)
{
switch (soapMessage.Stage)
{
case SoapMessageStage.BeforeDeserialize:
Transfer(originalStream, updatedStream);
updatedStream.Position = 0;
break;
case SoapMessageStage.AfterSerialize:
if ((soapMessage.Exception != null))
{
ExceptionProcessor processor = new ExceptionProcessor();
string details;
// handle our exception, and get the SOAP <detail> string
details = processor.HandleWebServiceException(soapMessage);
// read the entire SOAP message stream into a string
updatedStream.Position = 0;
TextReader tr = new StreamReader(updatedStream);
// insert our exception details into the string
string s = tr.ReadToEnd();
s = s.Replace("<detail />", details);
// overwrite the stream with our modified string
updatedStream = new MemoryStream();
TextWriter tw = new StreamWriter(updatedStream);
tw.Write(s);
tw.Flush();
}
updatedStream.Position = 0;
Transfer(updatedStream, originalStream);
break;
}
}
}
Step 2:
The next step is to handle the exception and get out meaningful details from the exception. For this purpose, let's dig deeper into the ExceptionProcessor class. This class has one public method: HandleWebServiceException(System.Web.Services.Protocols.SoapMessage sm). This method is called from our ProcessMessage()case SoapMessageStage.AfterSerialize.
Here is the code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Web.Services.Protocols;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Reflection;
using SASMTPLib;
using System.IO;
class ExceptionProcessor
{
private string currentExceptionDetails;
private string currentExceptionTypeName;
private const string RootExceptionName = "System.Web.HttpUnhandledException";
private const string RootWsExceptionName = "System.Web.Services.Protocols.SoapException";
public string HandleWebServiceException(System.Web.Services.Protocols.SoapMessage sm)
{
HandleException(sm.Exception);
XmlDocument doc = new XmlDocument();
XmlNode detailNode = doc.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace);
XmlNode typeNode = doc.CreateNode(XmlNodeType.Element, "ExceptionType", SoapException.DetailElementName.Namespace);
typeNode.InnerText = currentExceptionTypeName;
detailNode.AppendChild(typeNode);
XmlNode messageNode = doc.CreateNode(XmlNodeType.Element, "ExceptionMessage", SoapException.DetailElementName.Namespace);
messageNode.InnerText = sm.Exception.Message;
detailNode.AppendChild(messageNode);
XmlNode infoNode = doc.CreateNode(XmlNodeType.Element, "ExceptionInfo", SoapException.DetailElementName.Namespace);
infoNode.InnerText = currentExceptionDetails;
detailNode.AppendChild(infoNode);
return detailNode.OuterXml.ToString();
}
private void HandleException(Exception ex)
{
try
{
currentExceptionDetails = NormalizeException(ex);
currentExceptionTypeName = ex.GetType().FullName;
// ignore root exceptions
if (((currentExceptionTypeName == RootExceptionName) || (currentExceptionTypeName == RootWsExceptionName)))
{
if (ex.InnerException != null)
{
currentExceptionTypeName = ex.InnerException.GetType().FullName;
}
}
}
catch (Exception e)
{
currentExceptionDetails = string.Format("Error \'{0}\' while generating exception string", e.Message );
}
ExceptionToEmail();
}
private string NormalizeException(Exception ex)
{
StringBuilder sb = new StringBuilder();
if (ex.InnerException != null)
{
if (((ex.GetType().ToString() == RootExceptionName) || (ex.GetType().ToString() == RootWsExceptionName)))
{
return NormalizeException(ex.InnerException);
}
else
{
sb.Append(NormalizeException(ex.InnerException));
sb.Append(Environment.NewLine);
sb.Append("(Outer Exception)");
sb.Append(Environment.NewLine);
}
}
// get exception-specific information
sb.Append("Exception Type:\t\t");
try
{
sb.Append(ex.GetType().FullName);
}
catch (Exception e)
{
sb.Append(e.Message);
}
sb.Append(Environment.NewLine);
sb.Append("Exception Message:\t\t");
try
{
sb.Append(ex.Message);
}
catch (Exception e)
{
sb.Append(e.Message);
}
sb.Append(Environment.NewLine);
sb.Append("Exception Target Method:\t");
try
{
sb.Append(ex.TargetSite.Name);
}
catch (Exception e)
{
sb.Append(e.Message);
}
sb.Append(Environment.NewLine);
sb.Append("Stack Trace:\t\t");
sb.Append(Environment.NewLine);
try
{
sb.Append(ex.StackTrace);
}
catch (Exception e)
{
sb.Append(e.Message);
}
sb.Append(Environment.NewLine);
return sb.ToString();
}
private void ExceptionToEmail()
{
// Send e-mail code
}
}
Step 3:
The final step is to modify web.config of the web services to include the following section (under <system.web> node)
<webServices>
<soapExtensionTypes>
<add type="TelspaceSoapExtension, ExceptionHandler" priority="1" group="High"/>
</soapExtensionTypes>
</webServices>
One of the common problems I have seen is to bulk upload data to a SQL Server database. If you have the flexibility to directly run your code in SQL, you have a ton of options. But let's say that you have to massage the data before you throw it in to the database, then you have to really know your SQL (well to do it in SQL).
Let's say you have to read data from an RSS feed, parse it and then load it into SQL. Let's assume further that this feed updates every 2 hours. It would be a trivial task to write a C# app that reads and parses the feed. One crude way to upload this data would be to do a single row insert for each data element. This would be terribly inefficient. The other option would be to use .Net framework's SqlBulkCopy class.
The basic template would be something like
private void WriteToDatabase()
{
// get your connection string
string connString = "";
// connect to SQL
using (SqlConnection connection =
new SqlConnection(connString))
{
// make sure to enable triggers
// more on triggers in next post
SqlBulkCopy bulkCopy =
new SqlBulkCopy
(
connection,
SqlBulkCopyOptions.TableLock |
SqlBulkCopyOptions.FireTriggers |
SqlBulkCopyOptions.UseInternalTransaction,
null
);
// set the destination table name
bulkCopy.DestinationTableName = this.tableName;
connection.Open();
// write the data in the "dataTable"
bulkCopy.WriteToServer(dataTable);
connection.Close();
}
// reset
this.dataTable.Clear();
this.recordCount = 0;
}
The above code snippet shows you the API usage. But before you actually do that, you need to follow a couple of steps to setup your data table.
First, let's look at a simple record structure (as reflected in C# class):
using System;
using System.Data;
using System.Configuration;
/// <summary>
/// Summary description for MyRecord
/// </summary>
public class MyRecord
{
public int TestInt;
public string TestString;
public MyRecord()
{
}
public MyRecord(int myInt, string myString)
{
this.TestInt = myInt;
this.TestString = myString;
}
}
Now, let's start dissecting the class that we will use to upload the data:
using System;
using System.Data;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Configuration;
using System.IO;
namespace SqlExamples.FileLoader
{
/// <summary>
/// Summary description for BulkUploadToSql
/// </summary>
public class BulkUploadToSql
{
private List<MyRecord> internalStore;
protected string tableName;
protected DataTable dataTable = new DataTable();
protected int recordCount;
protected int commitBatchSize;
Note that we have an internal List data structure as well as the DataTable. This is redundant and you can avoid using the internalStore if your application does not need to massage the data before it's sent to SQL.
I then define 2 private constructors. The reason is that we want to use the factory pattern to return our object to the caller.
private BulkUploadToSql(
string tableName,
int commitBatchSize)
{
internalStore = new List<MyRecord>();
this.tableName = tableName;
this.dataTable = new DataTable(tableName);
this.recordCount = 0;
this.commitBatchSize = commitBatchSize;
// add columns to this data table
InitializeStructures();
}
private BulkUploadToSql() :
this("MyTableName", 1000) {}
Note that we set the commit batch size. This is a very important factor that needs to be fine tuned for your database. What this defines is the number of records that we would send in one shot to the database.
The next step is to Initialize the data table with columns that reflect the actual table structure.
private void InitializeStructures()
{
this.dataTable.Columns.Add("TI", typeof(Int32));
this.dataTable.Columns.Add("TS", typeof(string));
}
I then provided a factory method to load data into my internal structure from a data source. In the example code below, I use a Stream, but this can be any data source from where you wish to populate your data.
public static BulkUploadToSql Load(Stream dataSource)
{
// create a new object to return
BulkUploadToSql o = new BulkUploadToSql();
// replace the code below
// with your custom logic
for (int cnt = 0; cnt < 10000; cnt++)
{
MyRecord rec =
new MyRecord
(
cnt,
string.Format("string{0}", cnt)
);
o.internalStore.Add(rec);
}
return o;
}
This would make sure that our class is properly initialized and loaded with data. Once the caller has a valid object, they can now "Flush" the data as shown below:
public void Flush()
{
// transfer data to the datatable
foreach (MyRecord rec in this.internalStore)
{
this.PopulateDataTable(rec);
if (this.recordCount >= this.commitBatchSize)
this.WriteToDatabase();
}
// write remaining records to the DB
if (this.recordCount > 0)
this.WriteToDatabase();
}
private void PopulateDataTable(MyRecord record)
{
DataRow row;
// populate the values
// using your custom logic
row = this.dataTable.NewRow();
row[0] = record.TestInt;
row[1] = record.TestString;
// add it to the base for final addition to the DB
this.dataTable.Rows.Add(row);
this.recordCount++;
}
In the example above, the call to Flush() actually massages the data (and at the same time loads it into the actual data table). As I mentioned before, you can actually skip this step if your application does not require massaging.
As a example of an app that uses this class:
using System;
using System.Collections.Generic;
using System.Text;
using SqlExamples.FileLoader;
using System.IO;
namespace DemoApp
{
class Program
{
static void Main(string[] args)
{
using (Stream s =
new StreamReader(@"C:\TestData.txt"))
{
BulkUploadToSql myData =
BulkUploadToSql.Load(s);
myData.Flush();
}
}
}
}
As always, this is JUST demo code to explain a concept. This is NOT production quality code and please make sure to follow the coding guidelines in your team.
Happy coding....