Amazon.com Widgets ASP.Net

WilliaBlog.Net

I dream in code

About the author

Robert Williams is an internet application developer for the Salem Web Network.
E-mail me Send mail
Code Project Associate Logo
Ads by Lake Quincy Media

Recent comments

Authors

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.


Override Configuration Manager

Recently I have been working on ways to solve configuration issues in large, multi environment solutions. In the beginning, I simply wanted to store shared app settings and connection strings with a class library so I didn't have to keep copying common configuration settings from project to project within the same solution. Taking that a step further, I thought it would be great to auto detect the runtime environment and use the right app settings and connection strings from that shared configuration file. This all works great, but it has two major drawbacks: firstly, third party tools such as Elmah, and built in tools such as the Membership, Profile and Role Providers look no further that the built in ConfigurationManager object for appSettings and connection strings which forces us to subclass (Dynamically setting the Elmah connection string at runtime) or override their initialization (Setting Membership-Profile-Role provider's connection string at runtime) in order for them to work with our new settings. Not all third party tools will be as easy to fix. Secondly, all the developers working on the project must be trained to use the new techniques and always remember to use Core.Configuration.AppSettings["key"] instead of ConfigurationManager because ConfigurationManager.AppSettings["key"] may be null or hold the wrong value.

With that in mind, the next logical step was to find a way to override the built in ConfigurationManager ensuring that the Core.Configuration settings are fully integrated. In short: any call to ConfigurationManager.AppSettings or ConfigurationManager.ConnectionStrings should return the correct setting, whether that setting comes from the local web/app.config or the Core.Config. In order to do this it is assumed that if a setting appears both in the local app/web.config and the Core.Config files, then the value in the Core.Config file will be the value returned.

Download the latest version of the Williablog.Core project:

Williablog.Core.zip (110.11 kb)

Add a reference to it from your project (either to the project or the dll in the bin folder) and the first line in void Main() of your console Application or (if a web application) Application_Start()  in Global.asax should be:

Williablog.Core.Configuration.ConfigSystem.Install();

This will reinitialize the Configuration forcing it to rebuild the static cache of values but this time we are in control, and as a result we are able to effectively override the ConfigurationManager. Here is the code:

namespace Williablog.Core.Configuration

{

    using System;

    using System.Collections.Specialized;

    using System.Configuration;

    using System.Configuration.Internal;

    using System.Reflection;

 

    using Extensions;

 

    public sealed class ConfigSystem : IInternalConfigSystem

    {

        private static IInternalConfigSystem clientConfigSystem;

 

        private object appsettings;

 

        private object connectionStrings;

 

        /// <summary>

        /// Re-initializes the ConfigurationManager, allowing us to merge in the settings from Core.Config

        /// </summary>

        public static void Install()

        {

            FieldInfo[] fiStateValues = null;

            Type tInitState = typeof(System.Configuration.ConfigurationManager).GetNestedType("InitState", BindingFlags.NonPublic);

 

            if (null != tInitState)

            {

                fiStateValues = tInitState.GetFields();

            }

 

            FieldInfo fiInit = typeof(System.Configuration.ConfigurationManager).GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static);

            FieldInfo fiSystem = typeof(System.Configuration.ConfigurationManager).GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static);

 

            if (fiInit != null && fiSystem != null && null != fiStateValues)

            {

                fiInit.SetValue(null, fiStateValues[1].GetValue(null));

                fiSystem.SetValue(null, null);

            }

 

            ConfigSystem confSys = new ConfigSystem();

            Type configFactoryType = Type.GetType("System.Configuration.Internal.InternalConfigSettingsFactory, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", true);

            IInternalConfigSettingsFactory configSettingsFactory = (IInternalConfigSettingsFactory)Activator.CreateInstance(configFactoryType, true);

            configSettingsFactory.SetConfigurationSystem(confSys, false);

 

            Type clientConfigSystemType = Type.GetType("System.Configuration.ClientConfigurationSystem, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", true);

            clientConfigSystem = (IInternalConfigSystem)Activator.CreateInstance(clientConfigSystemType, true);

        }

 

        #region IInternalConfigSystem Members

 

        public object GetSection(string configKey)

        {

            // get the section from the default location (web.config or app.config)

            object section = clientConfigSystem.GetSection(configKey);

 

            switch (configKey)

            {

                case "appSettings":

                    if (this.appsettings != null)

                    {

                        return this.appsettings;

                    }

 

                    if (section is NameValueCollection)

                    {

                        // create a new collection because the underlying collection is read-only

                        var cfg = new NameValueCollection((NameValueCollection)section);

 

                        // merge the settings from core with the local appsettings

                        this.appsettings = cfg.Merge(Core.Configuration.ConfigurationManager.AppSettings);

                        section = this.appsettings;

                    }

 

                    break;

                case "connectionStrings":

                    if (this.connectionStrings != null)

                    {

                        return this.connectionStrings;

                    }

 

                    // create a new collection because the underlying collection is read-only

                    var cssc = new ConnectionStringSettingsCollection();

 

                    // copy the existing connection strings into the new collection

                    foreach (ConnectionStringSettings connectionStringSetting in ((ConnectionStringsSection)section).ConnectionStrings)

                    {

                        cssc.Add(connectionStringSetting);

                    }

 

                    // merge the settings from core with the local connectionStrings

                    cssc = cssc.Merge(ConfigurationManager.ConnectionStrings);

 

                    // Cannot simply return our ConnectionStringSettingsCollection as the calling routine expects a ConnectionStringsSection result

                    ConnectionStringsSection connectionStringsSection = new ConnectionStringsSection();

 

                    // Add our merged connection strings to the new ConnectionStringsSection

                    foreach (ConnectionStringSettings connectionStringSetting in cssc)

                    {

                        connectionStringsSection.ConnectionStrings.Add(connectionStringSetting);

                    }

 

                    this.connectionStrings = connectionStringsSection;

                    section = this.connectionStrings;

                    break;

            }

 

            return section;

        }

 

        public void RefreshConfig(string sectionName)

        {

            if (sectionName == "appSettings")

            {

                this.appsettings = null;

            }

 

            if (sectionName == "connectionStrings")

            {

                this.connectionStrings = null;

            }

 

            clientConfigSystem.RefreshConfig(sectionName);

        }

 

        public bool SupportsUserConfig

        {

            get { return clientConfigSystem.SupportsUserConfig; }

        }

 

        #endregion

    }

}

The code to actually merge our collections is implemented as Extension methods:

namespace Williablog.Core.Extensions

{

    using System;

    using System.Collections.Generic;

    using System.Collections.Specialized;

    using System.Configuration;

    using System.Linq;

    using System.Linq.Expressions;

    using System.Text;

 

    public static class IEnumerableExtensions

    {

        /// <summary>

        /// Merges two NameValueCollections.

        /// </summary>

        /// <param name="first"></param>

        /// <param name="second"></param>

        /// <remarks>Used by <see cref="Williablog.Core.Configuration.ConfigSystem">ConfigSystem</c> to merge AppSettings</remarks>

        public static NameValueCollection Merge(this NameValueCollection first, NameValueCollection second)

        {

            if (second == null)

            {

                return first;

            }

 

            foreach (string item in second)

            {

                if (first.AllKeys.Contains(item))

                {

                    // if first already contains this item, update it to the value of second

                    first[item] = second[item];

                }

                else

                {

                    // otherwise add it

                    first.Add(item, second[item]);

                }

            }

 

            return first;

        }

 

        /// <summary>

        /// Merges two ConnectionStringSettingsCollections.

        /// </summary>

        /// <param name="first"></param>

        /// <param name="second"></param>

        /// <remarks>Used by <see cref="Williablog.Core.Configuration.ConfigSystem">ConfigSystem</c> to merge ConnectionStrings</remarks>

        public static ConnectionStringSettingsCollection Merge(this ConnectionStringSettingsCollection first, ConnectionStringSettingsCollection second)

        {

            if (second == null)

            {

                return first;

            }

 

            foreach (ConnectionStringSettings item in second)

            {

                ConnectionStringSettings itemInSecond = item;

                ConnectionStringSettings existingItem = first.Cast<ConnectionStringSettings>().FirstOrDefault(x => x.Name == itemInSecond.Name);

 

                if (existingItem != null)

                {

                    first.Remove(item);

                }

 

                first.Add(item);

            }

 

            return first;

        }

    }

}

If we create a console application to test with, complete with it's own app.config file that looks like this:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <appSettings>

    <add key="WebServiceUrl" value="http://webservices.yourserver.com/YourService.asmx"/>

    <add key="SmtpServer" value="smtp.yourmailserver.com"/>

    <add key="LocalOnly" value="This is from the local app.config"/>

  </appSettings>

  <connectionStrings>

    <add name="AppData" connectionString="data source=Audi01;initial catalog=MyDB;User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>

    <add name="ElmahDB" connectionString="Database=ELMAH;Server=Audi02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>

  </connectionStrings>

</configuration>

And run it with the following code:

        static void Main(string[] args)

        {

            ConfigSystem.Install();

 

            Console.WriteLine(System.Configuration.ConfigurationManager.AppSettings["SmtpServer"]);

            Console.WriteLine(System.Configuration.ConfigurationManager.AppSettings["LocalOnly"]);

            Console.WriteLine(System.Configuration.ConfigurationManager.ConnectionStrings["AppData"]);

        }

 

The output is:

smtp.yourlocalmailserver.com

This is from the local app.config
data source=Ford01;initial catalog=MyDB;User ID=User;Password=Password;

With the exception of the middle one (LocalOnly) all of these settings come from Williablog.Core.Config, not the local app.config proving that the config files were successfully merged.

The ConfigSystem class could be modified to retrive the additional appsettings from the registry, from a database or from any other source you care to use.

I'd like to thank the contributers/authors of the following articles which I found very helpful:

http://stackoverflow.com/questions/158783/is-there-a-way-to-override-configurationmanager-appsettings

http://andypook.blogspot.com/2007/07/overriding-configurationmanager.html


Posted by Williarob on Monday, March 29, 2010 7:13 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Dynamically setting the Elmah connection string at runtime

If you have read my other articles about setting the SQL Membership provider's connection string at runtime, or automatically detecting the server name and using the appropriate connection strings then it will come as no surprise to see that I also had to find a way to set the Elmah connection string property dynamically too. If you are reading this, I'll assume that you already know what Elmah is and how to configure it. The problem then is simply that the connection string is supplied in the <elmah><errorLog> section of the web.config using a connection string name, and that while the name may the same in production as it is in development, chances are high that the connection string itself is different. The connection string property is readonly, so you can't change it at runtime. One solution is to create an elmah.config file, and use Finalbuilder or a web deployment project to change the path to that file when publishing, but if you like the AdvancedSettingsManager class I created and want to use that to set it you'll need to use a custom ErrorLog. Fortunately, Elmah is open source, so I simply downloaded the source, took a look at their SqlErrorLog class and then copied and pasted most of the code from that class into my own project, modifying it only slightly to suit my own needs.

In the end, the only changes I really needed to make were to pull the connectionstring by name from my AdvancedSettingsManager class and to copy a couple of helper functions locally into this class since they were marked as internal and therefore unavailable outside of the Elmah solution. I also removed the conditional compilation flags that only applied to .Net 1.x since this was a .Net 3.5 project.

namespace Williablog.Core.Providers

{

    #region Imports

 

    using System;

    using System.Configuration;

    using System.Data;

    using System.Data.SqlClient;

    using System.Diagnostics;

    using System.Threading;

    using System.Xml;

 

    using Elmah;

 

    using ApplicationException = System.ApplicationException;

    using IDictionary = System.Collections.IDictionary;

    using IList = System.Collections.IList;

 

    #endregion

 

    public class SqlErrorLog : ErrorLog

    {

        private readonly string _connectionString;

 

        private const int _maxAppNameLength = 60;

 

        private delegate RV Function<RV, A>(A a);

 

        /// <summary>

        /// Initializes a new instance of the <see cref="SqlErrorLog"/> class

        /// using a dictionary of configured settings.

        /// </summary>

 

        public SqlErrorLog(IDictionary config)

        {

            if (config == null)

                throw new ArgumentNullException("config");

 

// Start Williablog changes

 

            string connectionStringName = (string)config["connectionStringName"] ?? string.Empty;

 

            string connectionString = string.Empty;

 

            if (connectionStringName.Length > 0)

            {

 

            //

            // Write your code here to get the connection string as a ConnectionStringSettings object

 

            //

                ConnectionStringSettings settings = Williablog.Core.Configuration.AdvancedSettingsManager.SettingsFactory().ConnectionStrings["ErrorDB"];

                if (settings == null)

                    throw new ApplicationException("Connection string is missing for the SQL error log.");

 

                connectionString = settings.ConnectionString ?? string.Empty;

            }

 

// End Williablog changes

 

            //

            // If there is no connection string to use then throw an

            // exception to abort construction.

            //

 

            if (connectionString.Length == 0)

                throw new ApplicationException("Connection string is missing for the SQL error log.");

 

            _connectionString = connectionString;

 

            //

            // Set the application name as this implementation provides

            // per-application isolation over a single store.

            //

 

            string appName = NullString((string)config["applicationName"]);

 

            if (appName.Length > _maxAppNameLength)

            {

                throw new ApplicationException(string.Format(

                    "Application name is too long. Maximum length allowed is {0} characters.",

                    _maxAppNameLength.ToString("N0")));

            }

 

            ApplicationName = appName;

        }

 

        /// <summary>

        /// Initializes a new instance of the <see cref="SqlErrorLog"/> class

        /// to use a specific connection string for connecting to the database.

        /// </summary>

 

        public SqlErrorLog(string connectionString)

        {

            if (connectionString == null)

                throw new ArgumentNullException("connectionString");

 

            if (connectionString.Length == 0)

                throw new ArgumentException(null, "connectionString");

 

            _connectionString = connectionString;

        }

 

        /// <summary>

        /// Gets the name of this error log implementation.

        /// </summary>

 

        public override string Name

        {

            get { return "Microsoft SQL Server Error Log"; }

        }

 

        /// <summary>

        /// Gets the connection string used by the log to connect to the database.

        /// </summary>

 

        public virtual string ConnectionString

        {

            get { return _connectionString; }

        }

 

        /// <summary>

        /// Logs an error to the database.

        /// </summary>

        /// <remarks>

        /// Use the stored procedure called by this implementation to set a

        /// policy on how long errors are kept in the log. The default

        /// implementation stores all errors for an indefinite time.

        /// </remarks>

 

        public override string Log(Error error)

        {

            if (error == null)

                throw new ArgumentNullException("error");

 

            string errorXml = ErrorXml.EncodeString(error);

            Guid id = Guid.NewGuid();

 

            using (SqlConnection connection = new SqlConnection(this.ConnectionString))

            using (SqlCommand command = Commands.LogError(

                id, this.ApplicationName,

                error.HostName, error.Type, error.Source, error.Message, error.User,

                error.StatusCode, error.Time.ToUniversalTime(), errorXml))

            {

                command.Connection = connection;

                connection.Open();

                command.ExecuteNonQuery();

                return id.ToString();

            }

        }

 

        /// <summary>

        /// Returns a page of errors from the databse in descending order

        /// of logged time.

        /// </summary>

 

        public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)

        {

            if (pageIndex < 0)

                throw new ArgumentOutOfRangeException("pageIndex", pageIndex, null);

 

            if (pageSize < 0)

                throw new ArgumentOutOfRangeException("pageSize", pageSize, null);

 

            using (SqlConnection connection = new SqlConnection(this.ConnectionString))

            using (SqlCommand command = Commands.GetErrorsXml(this.ApplicationName, pageIndex, pageSize))

            {

                command.Connection = connection;

                connection.Open();

 

                XmlReader reader = command.ExecuteXmlReader();

 

                try

                {

                    ErrorsXmlToList(reader, errorEntryList);

                }

                finally

                {

                    reader.Close();

                }

 

                int total;

                Commands.GetErrorsXmlOutputs(command, out total);

                return total;

            }

        }

 

        /// <summary>

        /// Begins an asynchronous version of <see cref="GetErrors"/>.

        /// </summary>

 

        public override IAsyncResult BeginGetErrors(int pageIndex, int pageSize, IList errorEntryList,

            AsyncCallback asyncCallback, object asyncState)

        {

            if (pageIndex < 0)

                throw new ArgumentOutOfRangeException("pageIndex", pageIndex, null);

 

            if (pageSize < 0)

                throw new ArgumentOutOfRangeException("pageSize", pageSize, null);

 

            //

            // Modify the connection string on the fly to support async

            // processing otherwise the asynchronous methods on the

            // SqlCommand will throw an exception. This ensures the

            // right behavior regardless of whether configured

            // connection string sets the Async option to true or not.

            //

 

            SqlConnectionStringBuilder csb = new SqlConnectionStringBuilder(this.ConnectionString);

            csb.AsynchronousProcessing = true;

            SqlConnection connection = new SqlConnection(csb.ConnectionString);

 

            //

            // Create the command object with input parameters initialized

            // and setup to call the stored procedure.

            //

 

            SqlCommand command = Commands.GetErrorsXml(this.ApplicationName, pageIndex, pageSize);

            command.Connection = connection;

 

            //

            // Create a closure to handle the ending of the async operation

            // and retrieve results.

            //

 

            AsyncResultWrapper asyncResult = null;

 

            Function<int, IAsyncResult> endHandler = delegate

            {

                Debug.Assert(asyncResult != null);

 

                using (connection)

                using (command)

                {

                    using (XmlReader reader = command.EndExecuteXmlReader(asyncResult.InnerResult))

                        ErrorsXmlToList(reader, errorEntryList);

 

                    int total;

                    Commands.GetErrorsXmlOutputs(command, out total);

                    return total;

                }

            };

 

            //

            // Open the connenction and execute the command asynchronously,

            // returning an IAsyncResult that wrap the downstream one. This

            // is needed to be able to send our own AsyncState object to

            // the downstream IAsyncResult object. In order to preserve the

            // one sent by caller, we need to maintain and return it from

            // our wrapper.

            //

 

            try

            {

                connection.Open();

 

                asyncResult = new AsyncResultWrapper(

                    command.BeginExecuteXmlReader(

                        asyncCallback != null ? /* thunk */ delegate { asyncCallback(asyncResult); } : (AsyncCallback)null,

                        endHandler), asyncState);

 

                return asyncResult;

            }

            catch (Exception)

            {

                connection.Dispose();

                throw;

            }

        }

 

        /// <summary>

        /// Ends an asynchronous version of <see cref="ErrorLog.GetErrors"/>.

        /// </summary>

 

        public override int EndGetErrors(IAsyncResult asyncResult)

        {

            if (asyncResult == null)

                throw new ArgumentNullException("asyncResult");

 

            AsyncResultWrapper wrapper = asyncResult as AsyncResultWrapper;

 

            if (wrapper == null)

                throw new ArgumentException("Unexepcted IAsyncResult type.", "asyncResult");

 

            Function<int, IAsyncResult> endHandler = (Function<int, IAsyncResult>)wrapper.InnerResult.AsyncState;

            return endHandler(wrapper.InnerResult);

        }

 

        private void ErrorsXmlToList(XmlReader reader, IList errorEntryList)

        {

            Debug.Assert(reader != null);

 

            if (errorEntryList != null)

            {

                while (reader.IsStartElement("error"))

                {

                    string id = reader.GetAttribute("errorId");

                    Error error = ErrorXml.Decode(reader);

                    errorEntryList.Add(new ErrorLogEntry(this, id, error));

                }

            }

        }

 

        /// <summary>

        /// Returns the specified error from the database, or null

        /// if it does not exist.

        /// </summary>

        public override ErrorLogEntry GetError(string id)

        {

            if (id == null)

                throw new ArgumentNullException("id");

 

            if (id.Length == 0)

                throw new ArgumentException(null, "id");

 

            Guid errorGuid;

 

            try

            {

                errorGuid = new Guid(id);

            }

            catch (FormatException e)

            {

                throw new ArgumentException(e.Message, "id", e);

            }

 

            string errorXml;

 

            using (SqlConnection connection = new SqlConnection(this.ConnectionString))

            using (SqlCommand command = Commands.GetErrorXml(this.ApplicationName, errorGuid))

            {

                command.Connection = connection;

                connection.Open();

                errorXml = (string)command.ExecuteScalar();

            }

 

            if (errorXml == null)

                return null;

 

            Error error = ErrorXml.DecodeString(errorXml);

            return new ErrorLogEntry(this, id, error);

        }

 

// These utility functions were marked as internal, so I had to copy them locally

        public static string NullString(string s)

        {

            return s ?? string.Empty;

        }

 

        public static string EmptyString(string s, string filler)

        {

            return NullString(s).Length == 0 ? filler : s;

        }

 

// End

 

        private sealed class Commands

        {

            private Commands() { }

 

            public static SqlCommand LogError(

                Guid id,

                string appName,

                string hostName,

                string typeName,

                string source,

                string message,

                string user,

                int statusCode,

                DateTime time,

                string xml)

            {

                SqlCommand command = new SqlCommand("ELMAH_LogError");

                command.CommandType = CommandType.StoredProcedure;

 

                SqlParameterCollection parameters = command.Parameters;

 

                parameters.Add("@ErrorId", SqlDbType.UniqueIdentifier).Value = id;

                parameters.Add("@Application", SqlDbType.NVarChar, _maxAppNameLength).Value = appName;

                parameters.Add("@Host", SqlDbType.NVarChar, 30).Value = hostName;

                parameters.Add("@Type", SqlDbType.NVarChar, 100).Value = typeName;

                parameters.Add("@Source", SqlDbType.NVarChar, 60).Value = source;

                parameters.Add("@Message", SqlDbType.NVarChar, 500).Value = message;

                parameters.Add("@User", SqlDbType.NVarChar, 50).Value = user;

                parameters.Add("@AllXml", SqlDbType.NText).Value = xml;

                parameters.Add("@StatusCode", SqlDbType.Int).Value = statusCode;

                parameters.Add("@TimeUtc", SqlDbType.DateTime).Value = time;

 

                return command;

            }

 

            public static SqlCommand GetErrorXml(string appName, Guid id)

            {

                SqlCommand command = new SqlCommand("ELMAH_GetErrorXml");

                command.CommandType = CommandType.StoredProcedure;

 

                SqlParameterCollection parameters = command.Parameters;

                parameters.Add("@Application", SqlDbType.NVarChar, _maxAppNameLength).Value = appName;

                parameters.Add("@ErrorId", SqlDbType.UniqueIdentifier).Value = id;

 

                return command;

            }

 

            public static SqlCommand GetErrorsXml(string appName, int pageIndex, int pageSize)

            {

                SqlCommand command = new SqlCommand("ELMAH_GetErrorsXml");

                command.CommandType = CommandType.StoredProcedure;

 

                SqlParameterCollection parameters = command.Parameters;

 

                parameters.Add("@Application", SqlDbType.NVarChar, _maxAppNameLength).Value = appName;

                parameters.Add("@PageIndex", SqlDbType.Int).Value = pageIndex;

                parameters.Add("@PageSize", SqlDbType.Int).Value = pageSize;

                parameters.Add("@TotalCount", SqlDbType.Int).Direction = ParameterDirection.Output;

 

                return command;

            }

 

            public static void GetErrorsXmlOutputs(SqlCommand command, out int totalCount)

            {

                Debug.Assert(command != null);

 

                totalCount = (int)command.Parameters["@TotalCount"].Value;

            }

        }

 

        /// <summary>

        /// An <see cref="IAsyncResult"/> implementation that wraps another.

        /// </summary>

 

        private sealed class AsyncResultWrapper : IAsyncResult

        {

            private readonly IAsyncResult _inner;

            private readonly object _asyncState;

 

            public AsyncResultWrapper(IAsyncResult inner, object asyncState)

            {

                _inner = inner;

                _asyncState = asyncState;

            }

 

            public IAsyncResult InnerResult

            {

                get { return _inner; }

            }

 

            public bool IsCompleted

            {

                get { return _inner.IsCompleted; }

            }

 

            public WaitHandle AsyncWaitHandle

            {

                get { return _inner.AsyncWaitHandle; }

            }

 

            public object AsyncState

            {

                get { return _asyncState; }

            }

 

            public bool CompletedSynchronously

            {

                get { return _inner.CompletedSynchronously; }

            }

        }

    }

}

Finally all you need to do is modify the web.config file to use this SqlErrorlog instead of the built in one:

  <elmah>  

    <errorLog type="Williablog.Core.Providers.SqlErrorLog, Williablog.Core"

            connectionStringName="ErrorDB" />

<!--

            Other elmah settings ommitted for clarity

-->

  </elmah>

Note: You will still need to reference the Elmah dll in your project as all we have done here is subclass the ErrorLog type, all of the remaining Elmah goodness is still locked up inside the elmah dll. You could of course make these changes directly inside the elmah source code and recompile it to produce your own version of the elmah dll, but these changes were project specific and I didn't want to end up one day with dozens of project specific versions of the elmah dll. This way, the project specific code stays with the project and the elmah dll remains untouched.


Categories: ASP.Net | C# | CodeProject
Posted by Williarob on Thursday, March 18, 2010 12:10 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Auto detect the runtime environment and use the right app settings and connection strings

There are many ways to manage the problem of connection string and app settings substitution in the web.config / app.config files when publishing to different environments (e.g. QA and Production servers). In the past I have made use of the Web Deployment project's ability to replace the appsettings and connectionstrings sections, I have experimented with batch files, Build events, conditional compilation and used the extremely powerful FinalBuilder. However, my prefered solution is to have a single shared .config file with all the possible settings in it (so you only have to open one file to change any of the settings) then have the executing application automatically detect the environment and use the correct settings every time.

The technique dicussed below builds on that of an earlier article which described how to centralize your shared application settings and connection strings in a common class library. It also assumes that you know the machine names of your development, QA and production servers. Obviously servers get replaced from time to time and websites sometimes get moved from one server to another, but it has been my experience that there is usually some sort of common naming convention used on servers and web farms, and knowing that convention should be good enough. Even this is not the case, the Development, QA and Production server names are stored in an app setting so you can easily change them at any time if necessary. For this example, the assumption is that the development servers are all named something like "Squirrel01", "Squirrel02", the QA boxes are "Fox01", "Fox02", and the production (farm) boxes are "Rabbit01x", "Rabbit01y", "Rabbit02x", "Rabbit02y", etc. With this in mind, it is necessary only to look for the words "Rabbit", "Fox" or "Squirrel" in the machine name we are running on to identify the current environment and know which section of our config file to use. If none of these names is found, we shall assume the app is running on the localhost of a developer's computer, and use those settings. I should point out that it is possible to for a server to be configured in such a way as to prevent Environment.MachineName from returning a value, in which case this technique simply will not work, so before you start trying to integrate this code into your solution, I recommend you craete a quick test.aspx page or console app that simply does a Response.Write(Environment.MachineName)/Console.WriteLine(Environment.MachineName) and run it on your servers.

First, let's setup our .config file:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <configSections>

    <sectionGroup name="Localhost" type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">

      <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

      <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />

    </sectionGroup>

 

    <sectionGroup name="Dev" type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">

      <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

      <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />

    </sectionGroup>

 

    <sectionGroup name="Qa" type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">

      <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

      <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />

    </sectionGroup>

 

    <sectionGroup name="Production" type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">

      <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

      <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />

    </sectionGroup>

  </configSections>

 

  <Localhost>

    <appSettings>

      <add key="WebServiceUrl" value="http://webservices.squirrel01.yourserver.com/YourService.asmx"/>

      <add key="SmtpServer" value="smtp.yourlocalmailserver.com"/>

    </appSettings>

    <connectionStrings>

      <add name="AppData" connectionString="data source=Ford01;initial catalog=MyDB;User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>

      <add name="ElmahDB" connectionString="Database=ELMAH;Server=Ford02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>

    </connectionStrings>

  </Localhost>

 

  <Dev>

    <appSettings>

      <add key="WebServiceUrl" value="http://webservices.squirrel01.yourserver.com/YourService.asmx"/>

      <add key="SmtpServer" value="smtp.yourlocalmailserver.com"/>

    </appSettings>

    <connectionStrings>

      <add name="AppData" connectionString="data source=Ford01;initial catalog=MyDB;User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>

      <add name="ElmahDB" connectionString="Database=ELMAH;Server=Ford02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>

    </connectionStrings>

  </Dev>

 

  <Qa>

    <appSettings>

      <add key="WebServiceUrl" value="http://webservices.Fox01.yourserver.com/YourService.asmx"/>

      <add key="SmtpServer" value="smtp.yourlocalmailserver.com"/>

    </appSettings>

    <connectionStrings>

      <add name="AppData" connectionString="data source=BMW01;initial catalog=MyDB;User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>

      <add name="ElmahDB" connectionString="Database=ELMAH;Server=BMW02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>

    </connectionStrings>

  </Qa>

 

  <Production>

    <appSettings>

      <add key="WebServiceUrl" value="http://webservices.yourserver.com/YourService.asmx"/>

      <add key="SmtpServer" value="smtp.yourmailserver.com"/>

    </appSettings>

    <connectionStrings>

      <add name="AppData" connectionString="data source=Audi01;initial catalog=MyDB;User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>

      <add name="ElmahDB" connectionString="Database=ELMAH;Server=Audi02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>

    </connectionStrings>

  </Production>

 

  <appSettings>

    <!-- Global/common appsettings can go here -->

    <add key="Test" value="Hello World"/>

 

    <add key="DevelopmentNames" value="SQUIRREL"/>

    <add key="ProductionNames" value="RABBIT"/>

    <add key="QANames" value="FOX"/>

    <add key="EnvironmentOverride" value=""/>

    <!-- /Dev | /Localhost | /Production | (blank)-->

 

  </appSettings>

</configuration>

As you can see, the first thing we do in the config file is declare four section groups, "LocalHost", "Dev", "Qa" and "Production". I chose to create a custom SectionGroup since this allowed me to strongly type the expected sections within it, greatly simplifying the code required to access those sections. All the EnvironmentSectionGroup class does, is inherit ConfigurationSectionGroup and declare two properties:

namespace Williablog.Core.Configuration

{

    using System.Configuration;

 

    public class EnvironmentSectionGroup : ConfigurationSectionGroup

    {

 

        #region Properties

 

        [ConfigurationProperty("appSettings")]

        public AppSettingsSection AppSettings

        {

            get

            {

                return (AppSettingsSection)Sections["appSettings"];

            }

        }

 

        [ConfigurationProperty("connectionStrings")]

        public ConnectionStringsSection ConnectionStrings

        {

            get

            {

                return (ConnectionStringsSection)Sections["connectionStrings"];

            }

        }

 

        #endregion

 

    }

}

Next, we create the sections for localhost, development, qa and production, each of which has its own appSettings and connectionStrings sections. These are of the same type as the connectionStrings and appSettings found in any .config file, meaning we don't need to write any additional code to fully utilise these sections - no traversing of primitive xmlNodes or anything like that to get the connectionstrings from that section. Finally we add the expected, normal appsettings section which in this case will provide the global or common appsettings that are shared by all environments. It is here that we store the server names that will help us identify where the app is currently executing. The EnvironmentOverride setting is an added bonus -it allows you to use all of qa or production settings while running on localhost which helps you debug those "well it works on my machine" situations without having to manually change all of the settings for localhost.

Building on the BasicSettingsManager we built earlier we simply add some code to determine the machine name we are running on and return the appSettings and connectionStrings sections appropriate to that environment:

namespace Williablog.Core.Configuration

{

    using System;

    using System.Collections.Specialized;

    using System.Configuration;

    using System.IO;

    using System.Linq;

 

    public class AdvancedSettingsManager

    {

        #region fields

 

        private const string ConfigurationFileName = "Williablog.Core.config";

 

        /// <summary>

        /// default path to the config file that contains the settings we are using

        /// </summary>

        private static string configurationFile;

 

        /// <summary>

        /// Stores an instance of this class, to cut down on I/O: No need to keep re-loading that config file

        /// </summary>

        /// <remarks>Cannot use system.web.caching since agents will not have access to this by default, so use static member instead.</remarks>

        private static AdvancedSettingsManager instance;

 

        /// <summary>

        /// Settings Environment

        /// </summary>

        private static string settingsEnvironment;

 

        private static EnvironmentSectionGroup currentSettingsGroup;

 

        #endregion

 

        #region Constructors

 

        private AdvancedSettingsManager()

        {

            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();

 

            fileMap.ExeConfigFilename = configurationFile;

 

            Configuration config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);

 

            settingsEnvironment = "Localhost"; // default to localhost

 

            // get the name of the machine we are currently running on

            string machineName = Environment.MachineName.ToUpper();

 

            // compare to known environment machine names

            if (config.AppSettings.Settings["ProductionNames"].Value.Split(',').Where(x => machineName.Contains(x)).Count() > 0)

            {

                settingsEnvironment = "Production";

            }

            else if (config.AppSettings.Settings["QANames"].Value.Split(',').Where(x => machineName.Contains(x)).Count() > 0)

            {

                settingsEnvironment = "Qa";

            }

            else if (config.AppSettings.Settings["DevelopmentNames"].Value.Split(',').Where(x => machineName.Contains(x)).Count() > 0)

            {

                settingsEnvironment = "Dev";

            }

 

            // If there is a value in the EnvironmentOverride appsetting, ignore results of auto detection and set it here

            // This allows us to hit production data from localhost without monkeying with all the config settings.

            if (!string.IsNullOrEmpty(config.AppSettings.Settings["EnvironmentOverride"].Value))

            {

                settingsEnvironment = config.AppSettings.Settings["EnvironmentOverride"].Value;

            }

 

            // Get the name of the section we are using in this environment & load the appropriate section of the config file

            currentSettingsGroup = config.GetSectionGroup(SettingsEnvironment) as EnvironmentSectionGroup;

        }

 

        #endregion

 

        #region Properties

 

        /// <summary>

        /// Returns the name of the current environment

        /// </summary>

        public string SettingsEnvironment

        {

            get

            {

                return settingsEnvironment;

            }

        }

 

        /// <summary>

        /// Returns the ConnectionStrings section

        /// </summary>

        public ConnectionStringSettingsCollection ConnectionStrings

        {

            get

            {

                return currentSettingsGroup.ConnectionStrings.ConnectionStrings;

            }

        }

 

        /// <summary>

        /// Returns the AppSettings Section

        /// </summary>

        public NameValueCollection AppSettings

        {

            get

            {

                NameValueCollection settings = new NameValueCollection();

                foreach (KeyValueConfigurationElement element in currentSettingsGroup.AppSettings.Settings)

                {

                    settings.Add(element.Key, element.Value);

                }

 

                return settings;

            }

        }

 

        #endregion

 

        #region static factory methods

 

        /// <summary>

        /// Public factory method

        /// </summary>

        /// <returns></returns>

        public static AdvancedSettingsManager SettingsFactory()

        {

            // If there is a bin folder, such as in web projects look for the config file there first

            if (Directory.Exists(AppDomain.CurrentDomain.BaseDirectory + @"\bin"))

            {

                configurationFile = string.Format(@"{0}\bin\{1}", AppDomain.CurrentDomain.BaseDirectory, ConfigurationFileName);

            }

            else

            {

                // agents, for example, won't have a bin folder in production

                configurationFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigurationFileName);

            }

 

            // If we still cannot find it, quit now!

            if (!File.Exists(configurationFile))

            {

                throw new FileNotFoundException(configurationFile);

            }

 

            return CreateSettingsFactoryInternal();

        }

 

        /// <summary>

        /// Overload that allows you to pass in the full path and filename of the config file you want to use.

        /// </summary>

        /// <param name="fullPathToConfigFile"></param>

        /// <returns></returns>

        public static AdvancedSettingsManager SettingsFactory(string fullPathToConfigFile)

        {

            configurationFile = fullPathToConfigFile;

            return CreateSettingsFactoryInternal();

        }

 

        /// <summary>internal Factory Method

        /// </summary>

        /// <returns>ConfigurationSettings object

        /// </returns>

        internal static AdvancedSettingsManager CreateSettingsFactoryInternal()

        {

            // If we havent created an instance yet, do so now

            if (instance == null)

            {

                instance = new AdvancedSettingsManager();

            }

 

            return instance;

        }

 

        #endregion

    }

}

As before you can then access the appSettings of the Core.Config from any of your projects like so:

Console.WriteLine(Williablog.Core.Configuration.AdvancedSettingsManager.SettingsFactory().AppSettings["Test"]);

To make this work, you will need to add a reference to System.Configuration. If the config file and Settings manager code is to be part of a class library, you will need to set the "Copy to Output Directory" property of your .config file to "Copy always"and add a reference to System.Configuration to each of your projects.

Download the Williablog.Core project: Williablog.Core.zip (100.77 kb)


Posted by Williarob on Thursday, March 18, 2010 9:00 AM
Permalink | Comments (0) | Post RSSRSS comment feed

How to store shared app settings and connection strings with your class library

When working on enterprise level, multi-tiered .Net applications it is not uncommon to want to create a shared class library, that may be used in multiple related projects. For example, let's suppose you are building a public website, a separate private intranet website used by company staff to manage the public site, and one or more console applications that may run as scheduled tasks related to both sites. You may have an console application that creates and emails reports about sales and other data, and another app that encodes video or audio that is uploaded to your site. Finally, you probably have another project for unit tests.

Since all of these projects will be working with the same database you also have a class library in your solution acting as your datalayer, and perhaps another Core library that contains other shared components. Each of these projects has it's own web.config or app.config file, and you had to copy and paste your connection string, smtp server data, and various other appSettings required by all the projects into every .config file. You may be inspired to add a new .config file to your Core library, and store all of the shared appsettings and connection strings in that one central location. If you then delete all of these settings from the other .config files you'll quickly realize that everything breaks. Even setting the "Copy to Output Directory" property of your Core.config file to "Copy always" doesn't fix this. The reason for this of course is that .Net always looks to the host application for the settings.

The solution is to add some code to your Core project that explicitly loads the Core.config file, reads in the data and makes the results available to all the other projects. That code might look something like this:

namespace Williablog.Core.Configuration

{

    using System;

    using System.Collections.Specialized;

    using System.Configuration;

    using System.IO;

 

    public class BasicSettingsManager

    {

        #region fields

 

        private const string ConfigurationFileName = "Williablog.Core.config";

 

        /// <summary>

        /// default path to the config file that contains the settings we are using

        /// </summary>

        private static string configurationFile;

 

        /// <summary>

        /// Stores an instance of this class, to cut down on I/O: No need to keep re-loading that config file

        /// </summary>

        /// <remarks>Cannot use system.web.caching since agents will not have access to this by default, so use static member instead.</remarks>

        private static BasicSettingsManager instance;

 

        private static Configuration config;

 

        #endregion

 

        #region Constructors

 

        private BasicSettingsManager()

        {

            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();

            fileMap.ExeConfigFilename = configurationFile;

            config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);

        }

 

        #endregion

 

        #region Properties

 

        /// <summary>

        /// Returns the ConnectionStrings section

        /// </summary>

        public ConnectionStringSettingsCollection ConnectionStrings

        {

            get

            {

                return config.ConnectionStrings.ConnectionStrings;

            }

        }

 

        /// <summary>

        /// Returns the AppSettings Section

        /// </summary>

        public NameValueCollection AppSettings

        {

            get

            {

                NameValueCollection settings = new NameValueCollection();

                foreach (KeyValueConfigurationElement element in config.AppSettings.Settings)

                {

                    settings.Add(element.Key, element.Value);

                }

 

                return settings;

            }

        }

 

        #endregion

 

        #region static factory methods

 

        /// <summary>

        /// Public factory method

        /// </summary>

        /// <returns></returns>

        public static BasicSettingsManager SettingsFactory()

        {

            // If there is a bin folder, such as in web projects look for the config file there first

            if (Directory.Exists(AppDomain.CurrentDomain.BaseDirectory + @"\bin"))

            {

                configurationFile = string.Format(@"{0}\bin\{1}", AppDomain.CurrentDomain.BaseDirectory, ConfigurationFileName);

            }

            else

            {

                // agents, for example, won't have a bin folder in production

                configurationFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigurationFileName);

            }

 

            // If we still cannot find it, quit now!

            if (!File.Exists(configurationFile))

            {

                throw new FileNotFoundException(configurationFile);

            }

 

            return CreateSettingsFactoryInternal();

        }

 

        /// <summary>

        /// Overload that allows you to pass in the full path and filename of the config file you want to use.

        /// </summary>

        /// <param name="fullPathToConfigFile"></param>

        /// <returns></returns>

        public static BasicSettingsManager SettingsFactory(string fullPathToConfigFile)

        {

            configurationFile = fullPathToConfigFile;

            return CreateSettingsFactoryInternal();

        }

 

        /// <summary>internal Factory Method

        /// </summary>

        /// <returns>ConfigurationSettings object

        /// </returns>

        internal static BasicSettingsManager CreateSettingsFactoryInternal()

        {

            // If we havent created an instance yet, do so now

            if (instance == null)

            {

                instance = new BasicSettingsManager();

            }

 

            return instance;

        }

 

        #endregion

    }

}

You can then access the appSettings of Core.Config from any of your projects like so:

Console.WriteLine(Williablog.Core.Configuration.BasicSettingsManager.SettingsFactory().AppSettings["Key"]);

To make this work, you will need to set the "Copy to Output Directory" property of your Core.config file to "Copy always"and add a reference to System.Configuration to each of your projects.

We shall take this a step further next time and expand on this technique to enable your Core project to automatically detect wether it is running on localhost, a development environment, QA, or production, and to return the appropriate connection strings and settings for that environment.


Posted by Williarob on Thursday, March 18, 2010 8:08 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Use lambda expressions to aggregate values into a delimited string

Let's say you need to aggregate one value from each object in a list into a single string. For Example, you want to send an e-mail to a set of customers. This requires a string with the email addresses seperated by a semicolon (;). The following code will create a generic List of Books, and provide a method ListAllEmails() that will print the delimited list of emails to the console window:

 

namespace ConsoleApplication1

{

    using System;

    using System.Collections.Generic;

    using System.Linq;

 

    public class Lambdas

    {

        /// <summary>

        /// Define the Book Class

        /// </summary>

        public class Book

        {

            public string Title { get; set; }

            public string Author { get; set; }

            public double Price { get; set; }

            public string EmailAddress { get; set; }

        }

 

        public List<Book> Books { get; private set; }

 

        public Lambdas()

        {

            // Create a new list of Books

            Books = new List<Book> {

                new Book { Title = "Pro ASP.Net MVC Framework", Author = "Steven Sanderson", Price = 49.99, EmailAddress = "steve@nospam.com" },

                new Book{ Title = "Pro Silverlight 2 in C# 2008", Author = "Matthew MacDonald", Price = 49.99, EmailAddress = "Matthew@nospam.com" },

                new Book{ Title = "Pro VB 2008 and the .Net 3.5 Platform", Author = "Andrew Troelsen", Price = 59.99, EmailAddress = "Andrew@nospam.com" }

            };

        }

 

        /// <summary>

        /// Creates a semicolon (;) delimited list of email addresses

        /// </summary>

        public void ListAllEmails()

        {

            Console.WriteLine(this.Books.Select(b => b.EmailAddress).Aggregate((items, item) => items + "; " + item));

        }

    }

}

 

The Select Method selects the EmailAddress for each Book. The Aggregate method builds a list of the items based on the lambda expression. The result:

 

steve@nospam.com; Matthew@nospam.com; Andrew@nospam.com

 

Notice that this did not require any additional code to ensure there is no extra semi-colon at the beginning or end of the list, which is often required when using a loop to concatenate text.

 

Note: Be careful when using the Aggregate method because it is very inefficient on large numbers of strings. Consider using the String Join method instead.

 

In VB, the lambda would look like this:

 

   Console.WriteLine(Books.Select(Function(b) b.EmailAddress).Aggregate(Function(items, item) items & "; " & item))

 

You can also filter the list of email addresses. For Example, suppose you want to send an email to all the authors who sell their books for under $50, telling them that you think you can sell their next book for $59.99:

 

        /// <summary>

        /// Creates a semicolon (;) delimited list of email addresses where the price of the book is under $50

        /// </summary>

        public void ListSomeEmails()

        {

            Console.WriteLine(this.Books.Where(b => b.Price < 50).Select(b => b.EmailAddress).Aggregate((items, item) => items + ", " + item));

        }

 

 


Tags:
Categories: ASP.Net | C# | VB
Posted by Williarob on Friday, February 26, 2010 9:44 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Simplify Your Code with Lambda Expressions

Most applications retain lists of things, and a common task is to find an item in that list. The following class illustrates three ways to find an item in a generic list:

 

namespace ConsoleApplication1

{

    using System;

    using System.Collections.Generic;

    using System.Linq;

 

    public class Lambdas

    {

        public class Book

        {

            public string Title { get; set; }

            public string Author { get; set; }

            public double Price { get; set; }

        }

 

        public List<Book> Books { get; private set; }

 

        public Lambdas()

        {

            Books = new List<Book> {

                new Book { Title = "Pro ASP.Net MVC Framework", Author = "Steven Sanderson", Price = 49.99 },

                new Book{ Title = "Pro Silverlight 2 in C# 2008", Author = "Matthew MacDonald", Price = 49.99},

                new Book{ Title = "Pro VB 2008 and the .Net 3.5 Platform", Author = "Andrew Troelsen", Price = 59.99 }

            };

        }

 

        /// <summary>

        /// Returns a book using a traditional loop

        /// </summary>

        private Book FindUsingTraditionalLoop(string title)

        {

            Book foundBook = null;

 

            foreach (var b in this.Books)

            {

                if (b.Title == title)

                {

                    foundBook = b;

                    break;

                }

            }

 

            return foundBook;

        }

 

        /// <summary>

        /// Returns the book using a Linq expression

        /// </summary>

        private Book FindUsingLinq(string title)

        {

            var query = from b in this.Books

                        where b.Title == title

                        select b;

 

            return query.Count() > 0 ? query.ToList()[0] : null;

        }

 

        /// <summary>

        /// Returns the book using a Lambda expression

        /// </summary>

        private Book FindUsingLambda(string title)

        {

            return this.Books.FirstOrDefault(b => b.Title == title);

        }

 

        public void Test()

        {

            Console.WriteLine("Found: {0}", this.FindUsingTraditionalLoop("Pro Silverlight 2 in C# 2008").Author);

            Console.WriteLine("Found: {0}", this.FindUsingLinq("Pro Silverlight 2 in C# 2008").Author);

            Console.WriteLine("Found: {0}", this.FindUsingLambda("Pro Silverlight 2 in C# 2008").Author);

        }

    }

}

As these examples show, you can save time reading and writing your code by using Lambda expressions to find items in a list.

For VB programmers, the syntax of the Lambda expression looks like this:

return Me.Books.FirstOrDefault(Function(b) b.Title = title)


Categories: ASP.Net | C# | VB
Posted by Williarob on Wednesday, January 06, 2010 8:10 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Mock a database repository using Moq

The concept of unit testing my code is still fairly new to me and was introduced when I started writing applications with the Microsoft MVC Framework in Visual Studio 2008. Intimidated somewhat by the Moq library's heavy reliance on lambdas, my early tests used full Mock classes that I would write myself, and which implemented the same interface as my real database repositories. I'd only write the code for the methods I needed, all other methods would simply throw a "NotImplementedException". However, I quickly discovered that the problem with this approach is that whenever a new method was added to the interface, my test project would no longer build (since the new method was not implemented in my mock repository) and I would have to manually add a new method that threw another "NotImplementedException". After doing this for the 5th or 6th time I decided to face my fears and get to grips with using the Moq library instead. Here is a simple example, of how you can mock a database repository class using the Moq library.

Let's assume that your database contains a table called Product, and that either you or Linq, or LLBLGen, or something similar has created the following class to represent that table as an object in your class library:

The Product Class

namespace MoqRepositorySample

{

    using System;

 

    public class Product

    {

        public int ProductId { get; set; }

 

        public string Name { get; set; }

 

        public string Description { get; set; }

 

        public double Price { get; set; }

 

        public DateTime DateCreated { get; set; }

 

        public DateTime DateModified { get; set; }

    }

}

 

Your Product Repository class might implement an interface similar to the following, which offers basic database functionality such as retrieving a product by id, by name, fetching all products, and a save method that would handle inserting and updating products.

 

The IProductRepository Interface

 

namespace MoqRepositorySample

{

    using System.Collections.Generic;

 

    public interface IProductRepository

    {

        IList<Product> FindAll();

 

        Product FindByName(string productName);

 

        Product FindById(int productId);

 

        bool Save(Product target);

    }

}

 

The test class that follows demonstrates how to use Moq to set up a mock Products repository based on the interface above. The unit tests shown here focus primarily on testing the mock repository itself, rather than on testing how your application uses the repository, as they would in the real world.

 

Microsoft Unit Test Class

 

namespace TestProject1

{

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

 

    using Moq;

 

    using MoqRepositorySample;

 

    /// <summary>

    /// Summary description for UnitTest1

    /// </summary>

    [TestClass]

    public class UnitTest1

    {

        /// <summary>

        /// Constructor

        /// </summary>

        public UnitTest1()

        {

            // create some mock products to play with

            IList<Product> products = new List<Product>

                {

                    new Product { ProductId = 1, Name = "C# Unleashed", Description = "Short description here", Price = 49.99 },

                    new Product { ProductId = 2, Name = "ASP.Net Unleashed", Description = "Short description here", Price = 59.99 },

                    new Product { ProductId = 3, Name = "Silverlight Unleashed", Description = "Short description here", Price = 29.99 }

                };

 

            // Mock the Products Repository using Moq

            Mock<IProductRepository> mockProductRepository = new Mock<IProductRepository>();

 

            // Return all the products

            mockProductRepository.Setup(mr => mr.FindAll()).Returns(products);

 

            // return a product by Id

            mockProductRepository.Setup(mr => mr.FindById(It.IsAny<int>())).Returns((int i) => products.Where(x => x.ProductId == i).Single());

 

            // return a product by Name

            mockProductRepository.Setup(mr => mr.FindByName(It.IsAny<string>())).Returns((string s) => products.Where(x => x.Name == s).Single());

 

            // Allows us to test saving a product

            mockProductRepository.Setup(mr => mr.Save(It.IsAny<Product>())).Returns(

                (Product target) =>

                {

                    DateTime now = DateTime.Now;

 

                    if (target.ProductId.Equals(default(int)))

                    {

                        target.DateCreated = now;

                        target.DateModified = now;

                        target.ProductId = products.Count() + 1;

                        products.Add(target);

                    }

                    else

                    {

                        var original = products.Where(q => q.ProductId == target.ProductId).Single();

 

                        if (original == null)

                        {

                            return false;

                        }

 

                        original.Name = target.Name;

                        original.Price = target.Price;

                        original.Description = target.Description;

                        original.DateModified = now;

                    }

 

                    return true;

                });

 

            // Complete the setup of our Mock Product Repository

            this.MockProductsRepository = mockProductRepository.Object;

        }

 

        /// <summary>

        /// Gets or sets the test context which provides

        /// information about and functionality for the current test run.

        ///</summary>

        public TestContext TestContext { get; set; }

 

        /// <summary>

        /// Our Mock Products Repository for use in testing

        /// </summary>

        public readonly IProductRepository MockProductsRepository;

 

        /// <summary>

        /// Can we return a product By Id?

        /// </summary>

        [TestMethod]

        public void CanReturnProductById()

        {

            // Try finding a product by id

            Product testProduct = this.MockProductsRepository.FindById(2);

 

            Assert.IsNotNull(testProduct); // Test if null

            Assert.IsInstanceOfType(testProduct, typeof(Product)); // Test type

            Assert.AreEqual("ASP.Net Unleashed", testProduct.Name); // Verify it is the right product

        }

 

        /// <summary>

        /// Can we return a product By Name?

        /// </summary>

        [TestMethod]

        public void CanReturnProductByName()

        {

            // Try finding a product by Name

            Product testProduct = this.MockProductsRepository.FindByName("Silverlight Unleashed");

 

            Assert.IsNotNull(testProduct); // Test if null

            Assert.IsInstanceOfType(testProduct, typeof(Product)); // Test type

            Assert.AreEqual(3, testProduct.ProductId); // Verify it is the right product

        }

 

        /// <summary>

        /// Can we return all products?

        /// </summary>

        [TestMethod]

        public void CanReturnAllProducts()

        {

            // Try finding all products

            IList<Product> testProducts = this.MockProductsRepository.FindAll();

 

            Assert.IsNotNull(testProducts); // Test if null

            Assert.AreEqual(3, testProducts.Count); // Verify the correct Number

        }

 

        /// <summary>

        /// Can we insert a new product?

        /// </summary>

        [TestMethod]

        public void CanInsertProduct()

        {

            // Create a new product, not I do not supply an id

            Product newProduct = new Product

                { Name = "Pro C#", Description = "Short description here", Price = 39.99 };

 

            int productCount = this.MockProductsRepository.FindAll().Count;

            Assert.AreEqual(3, productCount); // Verify the expected Number pre-insert

 

            // try saving our new product

            this.MockProductsRepository.Save(newProduct);

 

            // demand a recount

            productCount = this.MockProductsRepository.FindAll().Count;

            Assert.AreEqual(4, productCount); // Verify the expected Number post-insert

 

            // verify that our new product has been saved

            Product testProduct = this.MockProductsRepository.FindByName("Pro C#");

            Assert.IsNotNull(testProduct); // Test if null

            Assert.IsInstanceOfType(testProduct, typeof(Product)); // Test type

            Assert.AreEqual(4, testProduct.ProductId); // Verify it has the expected productid

        }

 

        /// <summary>

        /// Can we update a prodict?

        /// </summary>

        [TestMethod]

        public void CanUpdateProduct()

        {

            // Find a product by id

            Product testProduct = this.MockProductsRepository.FindById(1);

 

            // Change one of its properties

            testProduct.Name = "C# 3.5 Unleashed";

 

            // Save our changes.

            this.MockProductsRepository.Save(testProduct);

 

            // Verify the change

            Assert.AreEqual("C# 3.5 Unleashed", this.MockProductsRepository.FindById(1).Name);

        }

    }

}

 

Download the Sample project and run the tests yourself:

MoqRepositorySample.zip (691.96 kb)


Categories: ASP.Net | C# | CodeProject | Moq | MVC | Unit Testing
Posted by Williarob on Tuesday, December 15, 2009 8:17 AM
Permalink | Comments (0) | Post RSSRSS comment feed

New and Improved Microsoft AntiXss 3.1.

Until now, the preferred way to selectively allow only certain HTML tags like <b> and <i> was to regex the input to ensure it contained only valid Unicode letter and number characters and those specified tags, something like this:

if (!Regex.IsMatch(input, @"^([\p{L}\p{N}'\s]|<b>|</b>|<i>|</i>){1,40}$")) throw new Exception();

This approach will prevent all unwanted tags, but it will also prevent all attributes on the allowed tags. Sometimes this is good – attackers can add malicious script to onmouseover attributes of <b> and <i> tags – but again, sometimes this is overkill and blocks the use of benign attributes like lang or title. It would be theoretically possible to extend the regular expression to allow these attributes, as well as other safe HTML tags and their attributes, but realistically that would be an incredibly difficult regex both to develop and maintain.

AntiXss 3.1 takes care of all of this logic for you, using the same whitelist approach: it filters the input using a list of known good tags and attributes and strips out all other text. Simply pass the untrusted input through the AntiXss.GetSafeHtml or GetSafeHtmlFragment method to sanitize it:

string output = AntiXss.GetSafeHtml(input);

I strongly encourage everyone to download the new AntiXss 3.1 and incorporate it into your applications starting today. It’s a very effective defense, especially when used in conjunction with the output encoding functionality that’s been a part of AntiXss from the beginning.

Read the Full Article Here.

Download AntiXss 3.1 from Microsoft.


Posted by Williarob on Tuesday, September 29, 2009 1:50 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Working with Buttons inside a GridView Control

This post is as much for my benefit as anyone elses. Every few months I find myself scratching my head, wondering how I can find all the pieces of information I need inside the handler for a button click on a grid view. Since I just had to do this again yesterday, this time, I'm going to record my findings here, so that next time I can come right to this page.

So here is the scenario: there is a GridView control on a page, bound to data in a database. It has multiple columns of read only data, but the last column contains a textbox and a button. When that button is pushed, I need to get the value of the text box, the orderid and the primary key of the current row in order to update a record in the database. I have found at least three ways to do this without getting too fancy.

      <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ID" DataSourceID="SqlDataSource1">
<Columns>
<asp:BoundField DataField="ID" HeaderText="ID" InsertVisible="False" ReadOnly="True" SortExpression="ID" />
<asp:BoundField DataField="NAME" HeaderText="NAME" SortExpression="NAME" />
<asp:BoundField DataField="ORDERID" HeaderText="ORDERID" SortExpression="ORDERID" />
<asp:BoundField DataField="CITY" HeaderText="CITY" SortExpression="CITY" />
<asp:TemplateField>
<ItemTemplate>
<asp:TextBox" ID="TextBox1" runat="server" Text='<%# Eval("PHONE") %>'
<asp:LinkButton ID="LinkButton1" runat="server" OnClick="LinkButton1_Click">Get Row Info</asp:LinkButton>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>

The first way I'm going to explore may be considered rather "ghetto" in the sense that while it works beautifully it does bend some rules so anyone who tries to adhere to common patterns and practices would probably frown upon it. However, as you'll see, it does have a number of advantages over some of the more official techniques so don't rule it out right away! To use this ghetto code, add a RowDataBound event to your gridview and use it to find each instance of your button control and add some new attributes to it containing the information you'll need for this row:

protected void gv_RowDataBound(object sender, System.Web.UI.WebControls.GridViewRowEventArgs e)

    {

        GridViewRow gvr = (GridViewRow)e.Row;

 

        if (e.Row.RowType == DataControlRowType.DataRow)

        {

            // Find your controls

            LinkButton LinkButton1 = (LinkButton)gvr.FindControl("LinkButton1");

 

            // this is ghetto, but you can, add orderid and other data to attributes so we can grab them later           

            int orderId = Convert.ToInt32(DataBinder.Eval(e.Row.DataItem, "OrderID"));

 

            LinkButton1.Attributes.Add("OrderID", orderId);          

        } // end if this is a real row

    }

 

Obviously, if you choose to do this your linkbutton will be rendered with an OrderID attribute <a href="..." OrderID="123" ... /> which you can grab later when posting back. For example, if you add OnClick="LinkButton_Click" to your linkbutton inside the gridview, then inside LinkButton_Click you can get the orderID like so:

 

    protected void LinkButton1_Click(object sender, EventArgs e)

    {

        LinkButton lb = (LinkButton)sender;

        int orderID = int.Parse(lb.Attributes["OrderID"]);

        // ...

    }

 

It's quick, it works and while it doesn't seem like the "Microsoft approved" method to me, it is more scalable than the counting the cells (If your OrderID is rendered as a label in the third column, you might use Convert.ToInt32(row.Cells(2).Text); to obtain the orderid). But what happens when you or someone else adds an additional column to the gridview in front of orderid? Suddenly your orderid might be something else entirely and if it still compiles and runs perhaps no one will even notice!


Let's explore two more quick examples that adhere to what Microsoft had in mind. The first, again assumes you add OnClick="LinkButton_Click" to your linkbutton inside the gridview and that you need not only the orderid, but also the Primary Key and the value of TextBox1:

 

    protected void LinkButton1_Click(object sender, EventArgs e)

    {

        LinkButton lb = (LinkButton)sender;

        GridViewRow row = (GridViewRow)lb.NamingContainer;

 

        // get the value of the textbox

        TextBox txt1 = row.Cells[4].FindControl("TextBox1") as TextBox;

        string phoneNumber = txt1.Text;

 

        // get the Primary Key Value

        int ID = GridView1.DataKeys[row.RowIndex].Value;

 

        // ... Do something with these values like update a row in a database

    }

 

It is unfortunate that you cannot address a cell by anything other than its Index as if you later need to add another column in front of this one, your application will break. You might think this is no big deal, but consider what happens if you have this sort of code applied to two or three columns, and each one addresses multiple cells. Then your boss asks you to add a new column. Trust me, you will groan, as you now have to recalculate the index of all cells addressed in this fashion. It can turn a 2 minute task into a 20 minute task.


As an alternative to the OnClick Event applied to an individual button, you can instead use the RowCommand Event of the GridView. This allows you to store some data inside the ComandArgument property of your button, which is arguably similar to the custom attribute method discussed at the beginning of this article, but since you can only set one command argument, if you need three pieces of information from it you are faced with a choice of either combining the information through concatenation - setting your command argument up as a string you plan to split apart later e.g. "OrderID=123|ProductID=456" or you are back to counting cells. And what's more, despite what this MSDN article implies, the CommandArgument does NOT contain the RowIndex by default, you would need to add it there yourself which can be done declaratively like so:

<asp:LinkButton ID="LinkButton1" runat="server" CommandArgument='<%# gv.Rows.Count.ToString() %>' CommandName="UpdatePhone">Get Row Info</asp:LinkButton>

Then your RowCommand Handler would look something like this:

 

       protected void GridView1_RowCommand(Object sender, GridViewCommandEventArgs e)

       {

            if (e.CommandName == "UpdatePhone")

            {

 

 

                // Convert the row index stored in the CommandArgument

                // property to an Integer.

                int index = Convert.ToInt32(e.CommandArgument);

 

 

                // Retrieve the row that contains the button clicked

                // by the user from the Rows collection.

                GridViewRow row = GridView1.Rows[index];

 

                // get the value of the textbox

                TextBox txt1 = row.Cells[4].FindControl("TextBox1") as TextBox;

                string phoneNumber = txt1.Text;

 

                // get the Primary Key Value

                int ID = GridView1.DataKeys[row.RowIndex].Value;

 

                // ... Do something with these values like update a row in a database

            }

        }

 

Conclusions


So there you have it. Three ways to Handle the onClick event of a button in a grid view. In my opinion, the OnRowCommand Event of the GridView is the least useful of them when trying to fulfil this brief. Sure it gives you the CommandArgument property, but you immediately need to use it for storing the row index. Whenever possible, I usually choose a Microsft approved approach so this time I opted for the OnClick event of the button but if I have to come back and add another column again, I'll probably use the custom attributes approach, because I do not want to have to increment/decrement all the cell index values again.


Categories: ASP.Net | C#
Posted by Williarob on Thursday, June 18, 2009 6:19 AM
Permalink | Comments (1) | Post RSSRSS comment feed

Asynchronous Programming with ASP.Net MVC Futures

Using the AsyncController

Introduction

The AsyncController is an experimental class offered inside the latest MVC Futures dll to allow developers to write asynchronous action methods.  The usage scenario for this is for action methods that have to make long-running requests, such as going out over the network or to a database, and don’t want to block the web server from performing useful work while the request is ongoing.

In general, the pattern is that the web server schedules Thread A to handle some incoming request, and Thread A is responsible for everything up to launching the action method, then Thread A goes back to the available pool to service another request.  When the asynchronous operation has completed, the web server retrieves a Thread B (which might be the same as Thread A) from the thread pool to process the remainder of the request, including rendering the response.  The diagram below illustrates this point.

Configuration

The asynchronous types are declared in the Microsoft.Web.Mvc namespace in the assembly Microsoft.Web.Mvc.dll.  The first change a developer needs to make is to declare a route as asynchronous.  There are MapAsyncRoute() extension methods to assist with this; these are analogs of the normal MapRoute() extension methods already provided by the MVC framework.  In Global.asax:

routes.MapAsyncRoute(

    "Default",

    "{controller}/{action}/{id}",

    new { controller = "Home", action = "Index", id = "" }

);

A route declared with MapAsyncRoute() can correctly handle both synchronous and asynchronous controllers, so there is no need to create complex routing logic such that sync controllers are serviced by the normal MapRoute() handler and async controllers are serviced by the MapAsyncRoute() handler.  However, a sync route [MapRoute()] cannot in general correctly execute async controllers and methods.

Secondly, if you are using IIS6 or IIS7 classic mode, you need to replace the standard *.mvc handler with its asynchronous counterpart.  Replace these lines in Global.asax (they don’t necessarily appear adjacent to one another):

<add verb="*" path="*.mvc" validate="false" type="System.Web.Mvc.MvcHttpHandler, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>

<add name="MvcHttpHandler" preCondition="integratedMode" verb="*" path="*.mvc" type="System.Web.Mvc.MvcHttpHandler, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>

With these:

<add verb="*" path="*.mvc" validate="false" type="Microsoft.Web.Mvc.MvcHttpAsyncHandler, Microsoft.Web.Mvc"/>

<add name="MvcHttpHandler" preCondition="integratedMode" verb="*" path="*.mvc" type="Microsoft.Web.Mvc.MvcHttpAsyncHandler, Microsoft.Web.Mvc"/>

Writing asynchronous action methods

In general, any asynchronous controllers that you author should subclass the AsyncController type.  Remember to import the Microsoft.Web.Mvc namespace.

public class MyAsyncController : AsyncController {

    // ...

}

The default constructor of the MyAsyncController sets the ActionInvoker property to an instance of the AsyncControllerActionInvoker.  This specialized invoker can understand several asynchronous patterns.  You can mix and match these patterns inside of your controllers.  If an ambiguity is found, we do our best to throw a detailed exception.

The AsyncController understands three async patterns.  These patterns can be mixed and matched within a single AsyncController.  Additionally, an AsyncController can contain normal (synchronous) action methods.  This makes it easy to change the base class of your controller from Controller to AsyncController in order to add async methods while allowing your original sync methods to run correctly.

The IAsyncResult pattern

The IAsyncResult pattern is a well-known pattern in the .NET Framework and is documented heavily.  A method pair signature which implements the IAsyncResult pattern is shown below.

public IAsyncResult BeginFoo(int id, AsyncCallback callback, object state);

public ActionResult EndFoo(IAsyncResult asyncResult);

This is the asynchronous analog of a synchronous method with the signature public ActionResult Foo(int id).  Note that the BeginFoo() method takes the same parameters as the Foo() method plus two extra – an AsyncCallback and a state object – and returns an IAsyncResult.  The EndFoo() method takes a single parameter – an IAsyncResult – and has the same return type as the Foo() method.

Standard model binding takes place for the normal parameters of the BeginFoo() method.  The invoker will pass a callback and state object for the BeginFoo() method to consume when its asynchronous task has finished.  When the callback is called, we automatically invoke the EndFoo() method and capture the result, then execute the result just as we would have in a synchronous request.

Only filter attributes placed on the BeginFoo() method are honored.  If a filter attribute is placed on EndFoo(), it will be ignored.  If an [ActionName] attribute is placed on the BeginFoo() method in order to alias it, we will look for an EndFoo() method based on the method name of BeginFoo(), not the aliased action name.  For example:

[ActionName("Bar")]

public IAsyncResult BeginFoo(int id, AsyncCallback callback, object state);

public ActionResult EndFoo(IAsyncResult asyncResult);

This will cause the BeginFoo() method to match requests for Bar rather than Foo.  Note that the completion method is still called EndFoo() instead of EndBar() since it matches the name of the entry method, not the entry alias.

The event pattern

In this pattern, the action method is divided into a setup method and a completion method.  The signatures are below:

public void Foo(int id);

public ActionResult FooCompleted(...);

When a request comes for Foo, we execute the Foo() method.  When the asynchronous operations are completed, we invoke the FooCompleted() method and execute the returned ActionResult.

The invoker will model bind parameters to the Foo() method in the standard way.  Parameters to FooCompleted() are not provided using model binders.  Rather, they come from the AsyncController.AsyncManager.Parameters dictionary, which can be populated as part of the asynchronous setup.  Keys in the dictionary correspond to parameter names of the FooCompleted() method, and any parameters which do not have corresponding keys in the dictionary are given a value of default(T).

The invoker must keep track of the number of outstanding asynchronous operations so that it does not invoke the FooCompleted() method prematurely.  To do this, a counter has been provided, accessible from AsyncController.AsyncManager.OutstandingOperations.  This counter can be incremented or decremented to signal that an operation has kicked off or concluded, and when the counter hits zero the invoker will invoke the completion routine.  For example:

public void Foo(int id) {

    AsyncManager.Parameters["p"] = new Person();

    AsyncManager.OutstandingOperations.Increment();

    ThreadPool.QueueUserWorkItem(o => {

        Thread.Sleep(2000); // simulate some work

        AsyncManager.OutstandingOperations.Decrement();

    }, null);

}

public ActionResult FooCompleted(Person p) {

    // consume 'p'

}

There is also an AsyncController.AsyncManager.RegisterTask() method that is helpful for wrapping calls to IAsyncResult pattern methods from within an event pattern method.  The RegisterTask() method also handles incrementing and decrementing the counter correctly.  For example:

public void Foo(int id) {

    AsyncManager.RegisterTask(

        callback => BeginGetPersonById(id, callback, null),

        ar => {

            AsyncManager.Parameters["p"] = EndGetPersonById(ar);

        });

    AsyncManager.RegisterTask(

        callback => BeginGetTotalUsersOnline(callback, null),

        ar => {

            AsyncManager.Parameters["numOnline"] = EndGetTotalUsersOnline(ar);

        });

}

public ActionResult FooCompleted(Person p, int numOnline) {

    // ...

}

This will kick off two asynchronous tasks and wait for both to finish before executing the FooCompleted() method with the values that were returned.

Only filter attributes placed on the Foo() method are honored.  If a filter attribute is placed on FooCompleted(), it will be ignored.  If an [ActionName] attribute is placed on the Foo() method in order to alias it, we will look for an FooCompleted() method based on the method name of Foo(), not the aliased action name.  For example:

[ActionName("Bar")]

public void Foo(int id);

public ActionResult FooCompleted(...);

This will cause the Foo() method to match requests for Bar rather than Foo.  Note that the completion method is still called FooCompleted() instead of BarCompleted() since it matches the name of the entry method, not the entry alias.

The Foo() method is allowed to return anything.  The invoker ignores the return value of this method; it only cares about the return value of the FooCompleted() method.  This is to allow writing Foo() methods that are easier to unit test.

The delegate pattern

This pattern is very similar to the event pattern, except that the method Foo() returns a parameterless delegate type and there is no FooCompleted() method.  For example:

public Func<ActionResult> Foo(int id) {

    Person p = null;

    int numOnline = 0;

    AsyncManager.RegisterTask(

        callback => BeginGetPersonById(id, callback, null),

        ar => {

            p = EndGetPersonById(ar);

        });

    AsyncManager.RegisterTask(

        callback => BeginGetTotalUsersOnline(callback, null),

        ar => {

            numOnline = EndGetTotalUsersOnline(ar);

        });

    return () => {

        ViewData["p"] = p;

        ViewData["numOnline"] = numOnline;

        return View();

    };

}

Or, more succinctly:

public Func<ActionResult> Foo(int id) {

    AsyncManager.RegisterTask(

        callback => BeginGetPersonById(id, callback, null),

        ar => {

            ViewData["p"] = EndGetPersonById(ar);

        });

    AsyncManager.RegisterTask(

        callback => BeginGetTotalUsersOnline(callback, null),

        ar => {

            ViewData["numOnline"] = EndGetTotalUsersOnline(ar);

        });

    return View;

}

Since this pattern supports only parameterless delegates instead of parameterful delegates, the earlier discussion about AsyncController.AsyncManager.Parameters is not applicable.

Timeouts

There is a Timeout property accessible from AsyncController.AsyncManager.Timeout that specifies the number of milliseconds to wait for a response from the action method before canceling the request.  The default value is 30000 (equal to 30 seconds).  If the action method has not returned by the specified time, we throw a TimeoutException.  Action filters and exception filters may handle this particular exception type if they wish.  Setting the Timeout property to System.Threading.Timeout.Infinite signifies that we will never throw this exception.

The timeout duration can be specified on a per-controller or per-action basis by attributing a class or method with [AsyncTimeout] or [NoAsyncTimeout].

Known issues

-          Asynchronous actions generally cannot be called by synchronous invokers or handlers.  If you receive an exception message about an action being unable to be executed synchronously, ensure that you’re using the MapAsyncRoute() extension method in your Global.asax and that your controller subclasses AsyncController.

-          The asynchronous invoker will not match any method beginning with Begin or End or ending with Completed.  This is to prevent web calls to the BeginFoo(), EndFoo(), and FooCompleted() methods directly.  If you need to make an action with this name accessible to web users, use the [ActionName] attribute to alias the method:

[ActionName("Begin")]

public ActionResult DoSomething();

The above is an example of a normal synchronous method that has been renamed to Begin to work around the invoker’s blocking of this name.

-          If the route that normally handles requests for the application root (/) is an asynchronous route, the Default.aspx file should be removed from the web application.  The Default.aspx file included in the template only works with synchronous requests.


Categories: ASP.Net | Asynchronous | C# | MVC
Posted by Williarob on Thursday, June 18, 2009 6:03 AM
Permalink | Comments (0) | Post RSSRSS comment feed