Aug 312008
 

Why clutter your inbox with error messages? Why make special code provisions for users to receive error messages via email? Why not log your error messages and have users subscribe to receive them in their favorite RSS aggregator?


If you are logging your exceptions already, you may find it easier to provide a syndication service. The process is ridiculously simple, and starts by creating a new project using the “Syndication Service Library” template. This template creates everything for you. All you need to do now is fill the SyndicationFeed with SyndicationItem objects.


Add a new class file called Feeds.cs:

 



    1 using System;


    2 using System.Linq;


    3 using System.ServiceModel;


    4 using System.ServiceModel.Syndication;


    5 using System.ServiceModel.Web;


    6 


    7 namespace SyndicationService


    8 {


    9     [ServiceContract]


   10     [ServiceKnownType(typeof(Atom10FeedFormatter))]


   11     [ServiceKnownType(typeof(Rss20FeedFormatter))]


   12     public interface IFeeds


   13     {


   14         [OperationContract]


   15         [WebGet(UriTemplate = “{type}?env={env}&app={app}”, BodyStyle = WebMessageBodyStyle.Bare)]


   16         SyndicationFeedFormatter CreateFeed(string type, string env, string app);


   17     }


   18 


   19     public class Feeds : IFeeds


   20     {


   21         public SyndicationFeedFormatter CreateFeed(string type, string env, string app)


   22         {


   23             SyndicationFeed feed = CreateSyndicationFeed(type, env, app);


   24 


   25             // Return ATOM or RSS based on query string


   26             // rss -> http://localhost:8000/Feeds/Errors?env=Production&app=MyAppName


   27             // atom -> http://localhost:8000/Feeds/Errors?env=Production&app=MyAppName&format=atom


   28             string query = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters[“format”];


   29             SyndicationFeedFormatter formatter = null;


   30             if (query == “atom”)


   31             {


   32                 formatter = new Atom10FeedFormatter(feed);


   33             }


   34             else


   35             {


   36                 formatter = new Rss20FeedFormatter(feed);


   37             }


   38 


   39             return formatter;


   40         }


   41 


   42         private static SyndicationFeed CreateSyndicationFeed(string type, string env, string app)


   43         {


   44             SyndicationFeed feed;


   45             switch (type.ToLower())


   46             {


   47                 case “errors”:


   48                     feed = CreateErrorsFeed(type, env, app);


   49                     break;


   50                 default:


   51                     feed = new SyndicationFeed(


   52                         String.Format(“Feed is unavailable – Type: {0} / Environment: {1} / Application: {2}”,


   53                         type, env, app), null, null);


   54                     break;


   55             }


   56             return feed;


   57         }


   58 


   59         private static SyndicationFeed CreateErrorsFeed(string type, string env, string app)


   60         {


   61             ApplicationLogDataContext db = new ApplicationLogDataContext();


   62 


   63             SyndicationFeed feed = new SyndicationFeed


   64             {


   65                 Title = new TextSyndicationContent(String.Format(“{0} {1} {2}”, env, app, type)),


   66                 Description = new TextSyndicationContent(


   67                     String.Format(“Application error syndication for the {0} applicaiton ({1}).”, app, env)),


   68                 Items = from e in db.Exceptions


   69                         where e.Environment == env && e.Application == app


   70                         select new SyndicationItem


   71                         {


   72                             Title = new TextSyndicationContent(e.Message),


   73                             Content = new TextSyndicationContent(e.StackTrace)


   74                         }


   75             };


   76             return feed;


   77         }


   78     }


   79 }



Modify the App.config file:

 



    1 <?xml version=”1.0encoding=”utf-8” ?>


    2 <configuration>


    3     <configSections>


    4     </configSections>


    5     <connectionStrings>


    6         <add name=”SyndicationService.Properties.Settings.ApplicationLogConnectionString


    7             connectionString=”Data Source=Scorpion;Initial Catalog=ApplicationLog;Integrated Security=True


    8             providerName=”System.Data.SqlClient” />


    9     </connectionStrings>


   10     <system.serviceModel>


   11         <services>


   12             <service name=”SyndicationService.Feeds“>


   13                 <host>


   14                     <baseAddresses>


   15                         <add baseAddress=”http://localhost:8000/” />


   16                     </baseAddresses>


   17                 </host>


   18                 <endpoint contract=”SyndicationService.IFeeds


   19                           address=”Feeds


   20                           binding=”webHttpBinding


   21                           behaviorConfiguration=”WebHttpBinding_Common“/>


   22             </service>


   23         </services>


   24         <behaviors>


   25             <endpointBehaviors>


   26                 <behavior name=”WebHttpBinding_Common“>


   27                     <webHttp/>


   28                 </behavior>


   29             </endpointBehaviors>


   30         </behaviors>


   31     </system.serviceModel>


   32 </configuration>



You will need to adjust your project’s Debug options to have command arguments that look similar to the following to F5-debug your service.



“/client:iexplore.exe” “/clientArgs:http://localhost:8000/Feeds/Errors?env=Production&app=GeoTracker”


Press F5 to test it out.


Here is the IE7 RSS viewer:


IE7_RSS_Viewer


Here is your RSS aggregator viewing the same feed:


RSS_Aggregator


You will, of course, want to add some additional information to the content of your SyndidationItem, a bogus phrase works for this example.


Also, it is unusual that you would care to keep your exception details around for a long period of time. Since this is a syndicated feed of application errors, you should make special arrangements to archive or delete your exception log on a regular basis. This will not only keep your insert and select times low, but will also alleviate the burden placed on a new subscriber when all of the exceptions from the database appear at once. An alternative would also be to modify the LINQ in the code above to only bring back exceptions from the last 7-60 days depending on your counts. I already archive my exceptions to a master exception repository for all environments by way of an ETL job. This way I can report on my errors without disturbing the live environments too.

Aug 272008
 

I decided to come out of my cave and look around 3.5 a bit. I haven’t read much about extension methods, but find them quite useful. They are nothing more than a syntactically superior static helper method. Let’s look at a quick example so I can get back to coming up with more excuses to use them everywhere.


I like to batch my database calls as much as possible to avoid repeated opening/closing of connections, etc. To do this, I pass a bunch of ID values into a stored procedure as a comma-separated string. In the stored proc, I break the string apart with everyone’s favorite table-valued function fn_MakeTable() to make a table of IDs. Then I can JOIN, UPDATE, or INSERT as needed.


So let’s say I have a collection of Orders which I can easily convert to an array of OrderID integers with LINQ. My new best friend to create a comma-separated string of OrderIDs is the following.



    1 using System;


    2 using System.Configuration;


    3 


    4 namespace Common


    5 {


    6     public static class ArrayHelper


    7     {


    8         public static string ToCsv<T>(this T[] array)


    9         {


   10             Converter<T, string> converter = (t) =>


   11                 {


   12                     return t.ToString();


   13                 };


   14             return ToCsv(array, converter);


   15         }


   16 


   17         public static string ToCsv<T>(this T[] array, Converter<T, string> converter)


   18         {


   19             CommaDelimitedStringCollection csv = new CommaDelimitedStringCollection();


   20             foreach (T t in array)


   21             {


   22                 csv.Add(converter(t));


   23             }


   24             return csv.ToString();


   25         }


   26     }


   27 }


 


You’ll see that I have two ToCsv() methods. The first takes a generic array using the this keyword and uses .ToString() as a default converter to string. The second method requires you to additionally pass in a converter to convert the object of type T to a string. Take those converted strings, add them to a CommaDelimitedStringCollection and .ToString() that collection to a full CSV string of integer values.


There are two ways to call these extension methods. The first is the more familiar way. Since they are really nothing more than static helper methods, call them just like any other:



   14             int[] array = { 123, 456 };


   15             string csv = Common.ArrayHelper.ToCsv(array);


 


The second is the more elegant and more intuitive way. Call it as if it was built into the Framework:



   14             int[] array = { 123, 456 };


   15             string csv = array.ToCsv();


 


You may be wondering, what if I write a method that matches the signature of a built-in method like .ToString(). Well, the built-in methods take precedence over extension methods, so array.ToString() will still appear as System.Int32[]. To get your new meaning of .ToString(), you just have to call it in the static helper method way detailed above.


For a generic array of T, you will likely want to provide your own Converter if T’s .ToString() method does not display the information you want to show in the CSV string. Below is a lame example of a converter. It takes the int value, converts it to the char value.



   21             Converter<int, string> converter = (i) =>


   22             {


   23                 return ((char)i).ToString();


   24             };


   25             string csv = array.ToCsv(converter);


 


I think something so simple, and definitely re-usable, would benefit any developer.

Aug 242008
 

Our friends at Microsoft may have slipped one in on us. After installing the 3.5 Framework Service Pack 1, it appears that you no longer need the [DataContract] or [DataMember] attributes on your DataContracts and DataMembers. I’m not sure what the motivation was for this “enhancement”, but it caused some trouble for me the other day.

For this example I will be using the base project VS2008 gives you when you create a new WCF Service Library. I am simply adding a NestedType to the CompositeType given in the base project.

Before installing SP1, having code as it appears below would cause an error during Metadata Exchange that reads something like “Metadata contains a reference that cannot be resolved”. Notice that CompositeType‘s NestedObject is marked as [DataMember] and also notice that the NestedType class is not marked as [DataContract] and has no [DataMember] attributes. Adding [DataContract] on NestedType and [DataMember] on IsVisible will clear this error and everything will work as expected. 

   24     [DataContract]

   25     public class CompositeType

   26     {

   27         bool boolValue = true;

   28         string stringValue = “Hello “;

   29         NestedType nestedObject = new NestedType();

   30 

   31         [DataMember]

   32         public bool BoolValue

   33         {

   34             get { return boolValue; }

   35             set { boolValue = value; }

   36         }

   37 

   38         [DataMember]

   39         public string StringValue

   40         {

   41             get { return stringValue; }

   42             set { stringValue = value; }

   43         }

   44 

   45         [DataMember]

   46         public NestedType NestedObject

   47         {

   48             get { return nestedObject; }

   49             set { nestedObject = value; }

   50         }

   51     }

   52 

   53     public class NestedType

   54     {

   55         bool isVisible = false;

   56 

   57         public bool IsVisible

   58         {

   59             get { return isVisible; }

   60             set { isVisible = value; }

   61         }

   62     }

 

The same code in use after SP1 will not cause this error. WCF will interpret from CompositeType‘s [DataContract] attribute and NestedObject‘s [DataMember] attribute that you meant to put [DataContract] on NestedType. So what’s the big deal, right? WCF is doing me a solid by guessing at what I meant to do. To me, this violates the repeated opt-in theme present in WCF. For every other important decision, the developer must write code to opt-in to a feature. For example, TransactionFlow defaults to false so we don’t use the client’s incoming transaction with explicitly writing code that says to do so.

This is clearly not on the same level as TransactionFlow. But why does it assume something about my objects? Why does it assume that every member of my object should be a DataMember?

I noticed this new “feature” when troubleshooting some code that had different namespace names specified in the DataContract attribute. Since the NestedType did not have a [DataContract] attribute, the namespace was using the original namespace name. The equivalent of CompositeType came through correctly, but the NestedObject had no value.