0 Comments
Here's the list of my favorite tools with explanation:

  • Free tools:

    • StopOnFirstBuildError

      When you build a large solution or project, you don’t have to wait until all errors are reported.
      Early cancelation of the build and revealing the very first error allows you to tackle one error at a time (which is often the most important one) and save you the wait of the long build.

      Screenshot


    • Target Framework Migrator

      Update all projects at one click.
       


      The drawback is that it won’t upgrade your EDMX files accordingly. This means you will have many errors the next time you run an EDMX update.


    • Nest In

      Nest any type of file on any other type of file within your solution explorer. Keep your code more organized.

      Nest

      As simple as Ctrl Click to select a few files. Right Click to nest them.

    • VSColorOutput

      Color output for build and debug windows.

      VSColorOutput



  • Paid Tools:

    • Resharper

      This is a must have tool. It shows errors, suggestions, provides quick code navigations, completions.
      It does require lots of memory though. If your computer has 10 GB or more of free memory, then it’s for you.

0 Comments
Google Analytics and other tools could help you monitor your application in a generic way. If there has been a few users or clients who keep complaining about performance of your application, you'll need to monitor their activities and the system response time for these users in particular.

With the help of MiniProfiler and Log4Net, this task can be done within an hour.

Before we start, here are my assumptions:

  1. Your application is an ASP .NET MVC or Web Forms application.
  2. You want to know how long it takes the server to serve the HTTP requests from the moment your application receive the request until a response has been sent out.
    The metric of choice is milliseconds.
  3. You have your client or user ID saved in Session right after they login to the application.

After you have done all the hard work, you should see a similar result:

image

 

Let’s do it.

  1. Right click your application in the Solution Explorer to Manage NuGet Packages. Then search and install MiniProfiler.
    MiniProfilerNuget
  2. You should see a new reference added to your project.
    MiniProfilerCommit
    Commit or check-in your code to source control now if you have one.
  3. Next, use NuGet again to install Log4Net.
    Log4NetNuGet
  4. And you’ll see new reference:
    Log4NetReference
    Commit or check-in your code to source control now if you have one.
  5. If you have a multi-tiered architect, you need to consult your team about which tier, project or folder you should place the new code that I will give to you in next step. If you don’t have multi-tiered architect (meaning, separate projects for MVC, Logic, Data Access,…), you should create the special App_Code folder:
    AppCode
  6. Copy and Paste the Profiler class to under the App_Code or your folder of choice:
    Profiler

    // -------------------------------------------------------------------------------------------------------------------- // <copyright file="Profiler.cs" company="Believe2014"> // WhenYouBelieve2014@gmail.com // </copyright> // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Web; using StackExchange.Profiling; namespace Mvc4Application { /// <summary> /// The profiler. /// </summary> public class Profiler { /// <summary> /// The key to obtain current user's login ID. /// </summary> public const string SESSION_KEY_CURRENT_USER = "CurrentUser.LoginID"; /// <summary> /// The initialize. /// </summary> internal static void Initialize() { MiniProfiler.Settings.Storage = new Log4NetStorage(); if (MiniProfiler.Settings.IgnoredPaths == null) return; // add partials of the URLs that you don't want to profile List<string> ignored = MiniProfiler.Settings.IgnoredPaths.ToList(); ignored.Add("images"); ignored.Add("style"); ignored.Add("include"); ignored.Add("jsdebug"); ignored.Add(".axd"); ignored.Add(".js"); ignored.Add(".png"); ignored.Add(".ashx"); MiniProfiler.Settings.IgnoredPaths = ignored.ToArray(); } /// <summary> /// Start profiling the current request. /// </summary> /// <param name="context"> /// The request context. /// </param> /// <remarks> /// The current user may not belong to the profiler target which is set in the web.config file. /// In that case, don't start Miniprofiler. /// </remarks> internal static void Start(HttpContext context) { if (context == null || context.Session == null) return; var user = context.Session[SESSION_KEY_CURRENT_USER] as string; if (string.IsNullOrEmpty(user)) return; if (!ProfilerSetting.CheckUser(user)) return; MiniProfiler.Start(); if (MiniProfiler.Current == null) return; MiniProfiler.Current.User = user; } /// <summary> /// stop profiling the current request. /// </summary> /// <remarks> /// MiniProfiler internally decide when to write messages to Log4Net. /// Log4Net is configured in the web.config to write messages that it receives from MiniProfiler to the disk at its own /// timing. /// </remarks> internal static void Stop() { MiniProfiler.Stop(); if (MiniProfiler.Settings.Storage != null && MiniProfiler.Current != null) MiniProfiler.Settings.Storage.Save(MiniProfiler.Current); } /// <summary> /// The profiler setting. /// </summary> internal class ProfilerSetting { /// <summary> /// The AppSetting key for list of user login IDs to run profile. /// </summary> private const string CONFIG_KEY_PROFILER_TARGET_LOGIN_LIST = "ProfilerTargetLoginCommaSeparatedList"; /// <summary> /// Initializes static members of the <see cref="ProfilerSetting" /> class. /// </summary> /// <remarks> /// Load the ProfilerTargetLoginCommaSeparatedList setting from web.config. /// </remarks> static ProfilerSetting() { string appSettingValue; try { appSettingValue = ConfigurationManager.AppSettings[CONFIG_KEY_PROFILER_TARGET_LOGIN_LIST]; } catch (Exception anyError) { throw new ConfigurationErrorsException(string.Format("Cannot read AppSettings for key<{0}>", CONFIG_KEY_PROFILER_TARGET_LOGIN_LIST), anyError); } if (appSettingValue != null) ProfilerTargetLoginCommaSeparatedList = appSettingValue.Split(','); } /// <summary> /// Gets or sets the profiler target login comma separated list. /// </summary> public static string[] ProfilerTargetLoginCommaSeparatedList { get; set; } /// <summary> /// Check the user against the ProfilerTargetLoginCommaSeparatedList setting in web.config. /// </summary> /// <param name="user"> /// The user. /// </param> /// <returns> /// True if the list contains the user; Otherwise, false. /// </returns> public static bool CheckUser(string user) { if (user == null || ProfilerTargetLoginCommaSeparatedList == null) return false; return ProfilerTargetLoginCommaSeparatedList.Any( loginIdToCheck => string.Compare(loginIdToCheck, user, StringComparison.CurrentCultureIgnoreCase) == 0); } } } }
  7. This code has 2 classes: Profiler and ProfilerSetting (this is a nested class).
    The Profiler class reads the current user’s login ID from the Session using the key “CurrentUser.LoginID”.
    The ProfilerSetting class reads a list of user login IDs from web.config file.
  8. Create the config entry for indicating whose requests should be profiled.
    Config
    In this example, I presume usernames are email addresses.
  9. Copy and paste Log4NetStorage class to the same above:

    // -------------------------------------------------------------------------------------------------------------------- // <copyright file="Log4NetStorage.cs" company="Believe2014"> // WhenYouBelieve2014@gmail.com // </copyright> // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using log4net; using log4net.Config; using StackExchange.Profiling; using StackExchange.Profiling.Storage; namespace Mvc4Application { /// <summary> /// User Log4Net as storage. /// </summary> internal class Log4NetStorage : IStorage { /// <summary> /// The logger by Log4Net. /// </summary> private static readonly ILog Log4NetLogger; /// <summary> /// Initializes static members of the <see cref="Log4NetStorage" /> class. /// </summary> static Log4NetStorage() { XmlConfigurator.Configure(); Log4NetLogger = LogManager.GetLogger(typeof (Log4NetStorage)); } /// <summary> /// Returns a list of <see cref="P:StackExchange.Profiling.MiniProfiler.Id"/>s that haven't been seen by /// <paramref name="user"/>. /// </summary> /// <param name="user"> /// User identified by the current <c>MiniProfiler.Settings.UserProvider</c> /// </param> /// <returns> /// the list of key values. /// </returns> public List<Guid> GetUnviewedIds(string user) { throw new NotSupportedException("This method should never run"); } /// <summary> /// list the result keys. /// </summary> /// <param name="maxResults"> /// The max results. /// </param> /// <param name="start"> /// The start. /// </param> /// <param name="finish"> /// The finish. /// </param> /// <param name="orderBy"> /// order by. /// </param> /// <returns> /// the list of keys in the result. /// </returns> public IEnumerable<Guid> List(int maxResults, DateTime? start = null, DateTime? finish = null, ListResultsOrder orderBy = ListResultsOrder.Descending) { throw new NotSupportedException("This method should never run"); } /// <summary> /// Returns a <see cref="T:StackExchange.Profiling.MiniProfiler"/> from storage based on <paramref name="id"/>, which /// should map to <see cref="P:StackExchange.Profiling.MiniProfiler.Id"/>. /// </summary> /// <param name="id"> /// The id. /// </param> /// <remarks> /// Should also update that the resulting profiler has been marked as viewed by its profiling /// <see cref="P:StackExchange.Profiling.MiniProfiler.User"/>. /// </remarks> /// <returns> /// The <see cref="T:StackExchange.Profiling.MiniProfiler"/>. /// </returns> public MiniProfiler Load(Guid id) { throw new NotSupportedException("This method should never run"); } /// <summary> /// Stores <paramref name="profiler"/> under its <see cref="P:StackExchange.Profiling.MiniProfiler.Id"/>. /// </summary> /// <param name="profiler"> /// The results of a profiling session. /// </param> /// <remarks> /// Should also ensure the profiler is stored as being un-viewed by its profiling /// <see cref="P:StackExchange.Profiling.MiniProfiler.User"/>. /// </remarks> public void Save(MiniProfiler profiler) { if (profiler == null || Log4NetLogger == null) return; Log4NetLogger.Info(string.Format("User<{0}>: {1}", profiler.User, profiler)); } /// <summary> /// Sets a particular profiler session so it is considered "un-viewed" /// </summary> /// <param name="user"> /// The user. /// </param> /// <param name="id"> /// The id. /// </param> public void SetUnviewed(string user, Guid id) { // do nothing } /// <summary> /// Sets a particular profiler session to "viewed" /// </summary> /// <param name="user"> /// The user. /// </param> /// <param name="id"> /// The id. /// </param> public void SetViewed(string user, Guid id) { throw new NotSupportedException("This method should never run"); } } }
  10. This class saves the state of a MiniProfiler instance to a Log4NetLogger at information level.
  11. Right click the 2 class files you have just added, look at their properties, change Build Action to Compile to make sure other code behind or controller classes can call them.
    AppCodeCompile
  12. Check and make sure that you save the current user’s login ID to the Session using the same key as the Profiler class expects.
    SessionWrite

  13. // POST: /Account/Login /// <summary> /// The login. /// </summary> /// <param name="model"> /// The model. /// </param> /// <param name="returnUrl"> /// The return url. /// </param> /// <returns> /// The <see cref="ActionResult"/>. /// </returns> [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Login(LoginModel model, string returnUrl) { if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, model.RememberMe)) { Session[Profiler.SESSION_KEY_CURRENT_USER] = model.UserName; return RedirectToLocal(returnUrl); } // If we got this far, something failed, redisplay form ModelState.AddModelError(string.Empty, "The user name or password provided is incorrect."); return View(model); }


     

  14. Specify the folder for Log4Net to create and write log files.
    LogConfig1

    And

    LogConfig2

    For your convenience, you can copy the config file content:
    <?xml version="1.0" encoding="utf-8"?> <!-- For more information on how to configure your ASP.NET application, please visit http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /> </configSections> <connectionStrings> <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-Mvc4Application-20140604154121;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-Mvc4Application-20140604154121.mdf" providerName="System.Data.SqlClient" /> </connectionStrings> <appSettings> <add key="webpages:Version" value="2.0.0.0" /> <add key="webpages:Enabled" value="false" /> <add key="PreserveLoginUrl" value="true" /> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" /> <!-- All 3rd party config keys go above this line --> <add key="ProfilerTargetLoginCommaSeparatedList" value="AgentSmith@gmail.com,WhenYouBelieve2014@gmail.com" /> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" /> <authentication mode="Forms"> <forms loginUrl="~/Account/Login" timeout="2880" /> </authentication> <pages> <namespaces> <add namespace="System.Web.Helpers" /> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Optimization" /> <add namespace="System.Web.Routing" /> <add namespace="System.Web.WebPages" /> </namespaces> </pages> </system.web> <system.webServer> <validation validateIntegratedModeConfiguration="false" /> <handlers> <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" /> <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" /> <remove name="ExtensionlessUrlHandler-Integrated-4.0" /> <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> </handlers> </system.webServer> <log4net> <root> <level value="ALL" /> <appender-ref ref="RollingLogFileAppender" /> </root> <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender"> <file value="C:\Codeplex\miniprofilerlog4net\MiniExample\Mvc4Application\App_Data\rolling.log" /> <appendToFile value="true" /> <rollingStyle value="Date" /> <datePattern value="yyyyMMdd-HHmm" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" /> </layout> </appender> </log4net> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="DotNetOpenAuth.Core" publicKeyToken="2780ccd10d57b246" /> <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="DotNetOpenAuth.AspNet" publicKeyToken="2780ccd10d57b246" /> <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="2.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="2.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="0.0.0.0-1.3.0.0" newVersion="1.3.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-1.2.13.0" newVersion="1.2.13.0" /> </dependentAssembly> </assemblyBinding> </runtime> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="v12.0" /> </parameters> </defaultConnectionFactory> </entityFramework> </configuration>
  15. Modify the Global class to initialize the storage, start and stop profilers.
    // -------------------------------------------------------------------------------------------------------------------- // <copyright file="Global.asax.cs" company="Believe2014"> // WhenYouBelieve2014@gmail.com // </copyright> // <summary> // The mvc application. // </summary> // -------------------------------------------------------------------------------------------------------------------- using System; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace Mvc4Application { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 /// <summary> /// The mvc application. /// </summary> public class MvcApplication : HttpApplication { /// <summary> /// The application_ start. /// </summary> protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); AuthConfig.RegisterAuth(); Profiler.Initialize(); } /// <summary> /// The event when the application acquires request state. /// </summary> /// <param name="sender"> /// The sender. /// </param> /// <param name="e"> /// The event argument.. /// </param> protected void Application_AcquireRequestState(object sender, EventArgs e) { Profiler.Start(HttpContext.Current); } /// <summary> /// This function is called by ASP .NET at the end of every http request. /// </summary> /// <param name="sender"> /// The sender. /// </param> /// <param name="e"> /// The event argument. /// </param> protected void Application_EndRequest(object sender, EventArgs e) { Profiler.Stop(); } } }

     

     

0 Comments
 
There are 2 timeout values in the web.config file that are important for making the "Remember Me" functionality.
 
The first one is the FormsAuthentication.Timeout. This timeout is used to set expiration timestamp of the .ASPXAUTH cookie. the .ASPXAUTH cookie allow you to bypass the login page without entering username and password. When authentication timeout, the system “forgets” you. But the system won’t kick you out or give you a Session Timeout Error because it has nothing to do with the timeline of the session object on the server.
<authentication mode="Forms">
  <forms loginUrl="~/Account/Login" timeout="2880" />
</authentication>
 
 
The second one is the HttpSessionState.Timeout. This timeout is kept in the session manager or SQL table depending on which session storage mode is configured. After session timeout, the system kicks you out and give you a Session Timeout Error.
<configuration>
  <system.web>
    <sessionState mode="InProc" cookieless="true" timeout="30" />
  </system.web>
</configuration>
When a user visits the login page for the very first time, ASP.NET creates the ASP.NET_SessionId cookie. This cookie will always have expiration set to “Session”.
 

 
If a cookie has expiration timestamp "Session", that cookie is stored only in the memory of the browser as described on MSDN about Writing Cookies.
If you do not set the cookie's expiration, the cookie is created but it is not stored on the user's hard disk. Instead, the cookie is maintained as part of the user's session information. When the user closes the browser or if the session times out, the cookie is discarded. A non-persistent cookie like this is handy for information that needs to be stored for only a short time or that for security reasons should not be written to disk on the client computer. For example, non-persistent cookies are useful if the user is working on a public computer, where you do not want to write the cookie to disk.
The ASP.NET_SessionId cookie is used by ASP.NET to map subsequent HTTP requests to the same HttpContext.Session object on the server side. Indeed, this cookie is discarded and re-created for every HTTP request in Cookieless Sessions mode.
When the cookieless attribute setting is false, the session-state module actually creates a cookie named ASP.NET_SessionId and stores the session ID in it.
A session cookie is given a very short expiration term and is renewed at the end of each successful request.
However, I could not confirm that this behavior still holds. In other words, the exact same cookie is re-used for every HTTP requests.
 
A much more important point is that developers have no control over the expiration of the ASP.NET_SessionId cookie. ASP.NET always set its expiration to “Session” so that you have to login again if you close the browser, clear the cookies, or log out. The logout page is supposed to do 2 things:
Session.Abandon(); // start a new session on server side and replace the existing ASP.NET_SessionId cookie with a new one
FormsAuthentication.SignOut(); // expires the .ASPXAUTH cookie

When “Remember Me” is not selected

If the session expires on the server side, or Session.Abandon is called, a new session is created on the server side. The new session will have new ID and won’t match the ID in the ASP.NET_SessionId cookie stored in the browser’s memory. This mismatch is originated from the server. The mismatch may be originated from the client as the user closes the browser or tell the browser to clear cookies.
In either case, the mismatch results in session not found. ASP.NET applications generally throw Session Timeout Error because it has nowhere else to look.
 

When "Remember Me" is selected

 
If session expires, renewed, or the ASP.NET_SessionId cookie is lost or cleared, ASP .NET can still see the .ASPXAUTH cookie with an expiration timestamp explicitly set.
 

 
If a user selects "Remember Me" option, then leave the system for an extended period of time. When he or she gets back to the system, the .ASPXAUTH cookie will NOT resume a previous session. Instead, a new session is created and the user is redirected to the login page. It is the login page which will extract information (FormsAutheticationTicket) from the the .ASPXAUTH cookie and redirect the user to home page or back to the originally requested url as if the user enters the correct username and password again.
 

“Remember Me” in complex systems

The "Remember Me" functionality provided by Microsoft will not be adequate if the application needs to do more than matching username and password. For example: a multi tenant application needs the user to identify the client, database or table to log in. In this case, the application has to put extra information into the .ASPXAUTH cookie.
 
If you need to put extra information into this cookie, you need to take the following steps:
  1. When you serve the login page, try to read the .ASPXAUTH cookie. If the extra information could be read from the cookie, you can redirect the user away from login page to home page or other pages.
  2. When the user submits login information, you need to put the extra information into the FormsAuthenticationTicket. See How to Add Custom Info to Authentication Cookies.
 
The login flow:
 

0 Comments

Here are my open source projects hosted on CodePlex:

Entity Framework Interface Generator
This project contains customized T4 templates which can generate interfaces and attributes for the DbContext class and entity classes.

Entity Framework 5.0 View Generator
It contains a single T4 file which is a clone of ASP.NET Web Forms Application Using Entity Framework 4.0 Database First. The T4 template also contains fixes to work with .NET 4.5  regarding the following error: Argument 'xmlReaders' is not valid.

Uri.Combine
This project has one class Uri.cs which allows combining multiple parts to make a safe (encoded) Uri.

FxCop Installer
I extracted the setup file and uploaded it to this project for easy access.