Wednesday, October 24, 2007

Troubles with Health Monitoring, System.Net.Mail.SmtpClient and SSL

The web is full of desperate pleas for help by prematurely bald developers who have discovered the fatal flaw in the shiny new ASP.NET 2.0 System.Net.Mail.SmtpClient class. This is touted as overcoming all the problems of the old variant, that could not even be configured for credentials without resorting to some heavy-duty tricks.

However, as has been discovered by countless people, while the new SmtpClient() class is neat, easy to use, and configurable via Web.Config - they forgot one thing to make configurable. The EnableSsl property is not settable via Web.Config. So big deal you say - just write a line of code and set it manually... Problem is - you frequently did not write the code that instantiates the SmtpClient. The most well known problem is with the suite of new login controls, which have the capability of sending mail in some circumstances. Works fine - unless you need to enable SSL for the SMTP connection. Fortunately, there's a well-known workaround since these controls expose an event called SendingMail, where you can do magic things including affecting how the mail is sent - most simply by taking over responsibility of sending it.

Then I hit the wall, really hard, trying to use the System.Web.Management.TemplatedMailWebEventProvider class. This is a provider that can subscribe to health monitoring events, and send them via e-mail. Using SmtpClient() of course, with the instantiation hidden deep in its innards of sealed and internal classes in System.Web.dll. No events to the rescue this time either.

After hours of fruitless searching, I finally come to the conclusion that I needed a work-around, ugly as it may be. So, here's where the decorator pattern meets reflection. Sigh. It aint pretty, but it does work, and I do get to use the otherwise rather nice and advanced TemplatedMailWebEventProvider (the same technique can be used for the SimpleMailWebEventProvider, or any provider derived from MailWebEventProvider).

In the end, it's just a few lines of code (comments and veritical white space removed for brevity):

using System; using System.Collections.Specialized; using System.Reflection; using System.Web.Management; public class TemplatedMailWithSslWebEventProvider : WebEventProvider {     private TemplatedMailWebEventProvider _templatedProvider;         public TemplatedMailWithSslWebEventProvider()     {         ConstructorInfo constructor = typeof(TemplatedMailWebEventProvider)             .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic,                             null, new Type[0], null);         _templatedProvider = (TemplatedMailWebEventProvider)constructor             .Invoke(null);     }     public override void Initialize(string name, NameValueCollection config)     {         if (config == null)         {             throw new ArgumentNullException("config");         }         _templatedProvider.Initialize(name, config);         FieldInfo field = typeof(MailWebEventProvider)             .GetField("_smtpClient",                       BindingFlags.Instance | BindingFlags.NonPublic);         field.SetValue(_templatedProvider, new SmtpClientWithSsl());     }     public static MailEventNotificationInfo CurrentNotification     {         get         {             return TemplatedMailWebEventProvider.CurrentNotification;         }     }     public override void Flush()     {         _templatedProvider.Flush();     }     public override void ProcessEvent(WebBaseEvent raisedEvent)     {         _templatedProvider.ProcessEvent(raisedEvent);     }     public override void Shutdown()     {         _templatedProvider.Shutdown();     } }
All that's left for you to do is to define the SmtpClientWithSsl() class, deriving from System.Net.Mail.SmtpClient() whose developer probably by the same oversight that forgot about SSL, also forgot to make it sealed. Fortunately. Here two wrong almost makes one right!

One of the morales of this story is to really think about the use of sealed and internal. My first try was to implement a custom templated e-mail provider, but it turns out that was quite a job, and I could not override or use anything from System.Web.dll because it was all sealed and used lots of internal helpers. If you really need to hide the implementation that bad, it might be better to introduce a public base class, where the essential interfaces are exposed as protected methods and properties. When you limit a class to sealed, and it depends on lots of additional logic, do consider making that logic available at least to alternative implementations and give it a base to inherit from.

No comments:

Post a Comment