24

I think my understanding on SimpleMembershipProvider is almost 60% and the rest is getting to know how it internally work.

You can quickly found some issue when using [InitializeSimpleMembership] filter only in AccountController (the default template). I think anywhere you use Memberhsip API or WebMatrix.WebSecurity, you need to make sure this filter should be called first.

Later, If you use User.IsInRole in my _Layout.cshtml. You need to apply the filter to all controllers, then you start registering it in globally.

However I just realize there is LazyInitializer.EnsureInitialized which make the initialization performed only once per app start.

So why the SimpleMembershipInitializer (in the filter) is not directly in Application_Start? Is there any reason to use the filter?

CallMeLaNN
  • 8,328
  • 7
  • 59
  • 74

7 Answers7

21

I believe the template used an attribute for database initialization so that the non-authenticated portions of the site would still work if the initialization failed.

For most practical purposes, it's best to just have this done in the App_Start.

Matt Magpayo
  • 714
  • 5
  • 7
18

If you were to merge the InitializeSimpleMembershipAttribute into the Global.asax.cs Application_Start so that the SimpleMembershipProvider would be initialized without any AccountController routes being called...

...it could look something like this: http://aaron-hoffman.blogspot.com/2013/02/aspnet-mvc-4-membership-users-passwords.html

// The using below is needed for "UsersContext" - it will be relative to your project namespace
using MvcApplication1.Models;

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Threading;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using WebMatrix.WebData;

namespace MvcApplication1
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth();

            // Ensure ASP.NET Simple Membership is initialized only once per app start
            LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
        }

        private static SimpleMembershipInitializer _initializer;
        private static object _initializerLock = new object();
        private static bool _isInitialized;

        private class SimpleMembershipInitializer
        {
            public SimpleMembershipInitializer()
            {
                Database.SetInitializer<UsersContext>(null);

                try
                {
                    using (var context = new UsersContext())
                    {
                        if (!context.Database.Exists())
                        {
                            // Create the SimpleMembership database without Entity Framework migration schema
                            ((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
                        }
                    }

                    WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex);
                }
            }
        }
    }
}
Aaron Hoffman
  • 6,604
  • 8
  • 56
  • 61
  • in your answer this line is confusing `((IObjectContextAdapter)context).ObjectContext.CreateDatabase();` I don't know what to replace with this line. i have replaced `UsersContext` with `ApplicationDbContext`. I am using asp.net identity and i am using data first approach. – Irfan Y Jan 28 '16 at 21:56
  • This code was specifically for ASP.NET MVC 4. If you're using a newer version, it likely will not work. The specific line you're talking about can safely be removed if the Database and DB Tables already exist. The important line was the call to `WebSecurity.InitializeDatabaseConnection()`. – Aaron Hoffman Feb 03 '16 at 18:43
5

If plan on making sure the InitializeSimpleMembershipAttribute runs globally, it would be best practice to use the MVC 4 way in the App_Start\FilterConfig.cs;

public class FilterConfig
{
  public static void RegisterGlobalFilters(GlobalFilterCollection filters)
  {
    filters.Add(new HandleErrorAttribute());
    filters.Add(new InitializeMembershipAttribute());
  }
}

Keeps the Global.asax.cs clean from code that should probably be encapsulated the way MVC 4 is over previous versions. Leaves a nice clean:

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        AuthConfig.RegisterAuth();
    }
}

I also recommend changing the type to a AuthorizeAttribute (which is really what it does) because AuthorizeAttribute methods are executed before ActionFilterAttribute methods. (This should produce less problems if other ActionFilters are checking security, and allows derived custom AuthorizeAttributes).

[AttributeUsage(AttributeTargets.Class | 
                AttributeTargets.Method, 
                AllowMultiple = false, 
                Inherited = true)]
public class InitializeMembershipAttribute : AuthorizeAttribute
{
    private static SimpleMembershipInitializer _initializer;
    private static object _initializerLock = new object();
    private static bool _isInitialized;

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        // Ensure ASP.NET Simple Membership is initialized only once per app start
        LazyInitializer.EnsureInitialized(ref _initializer, 
          ref _isInitialized, 
          ref _initializerLock);
        base.OnAuthorization(filterContext);
    }

    private class SimpleMembershipInitializer ...
    }
}
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • 1
    The existence of `InitializeMembershipAttribute` is not really necessary. I just put 2 line of code `Database.SetInitializer(...)` and `WebSecurity.InitializeDatabaseConnection(...)` in Global.asax.cs or create another App_Start\DbConfig.cs. Anyway, while I am using SimpleMembership, I am looking for the new ASP.NET Identity that might replace WebMatrix assembly for new membership and the API is not much different. – CallMeLaNN Jul 21 '13 at 08:49
  • You could say that about; routes, bundling, filters, etc.. Encapsulating the code in a filter and leaving the global.asax.cs file the way it exists in MVC 4 looks much cleaner IMHO. And it's only 1 line of code. – Erik Philips Jul 22 '13 at 00:22
  • encapsulating code that is only meant to run one time is unnecessary and confusing. the whole reason for objects is for them to be instantiated multiple times, the whole reason for methods is for them to be called multiple times. if you have some code that needs to be called only once just put it inline. see http://number-none.com/blow/john_carmack_on_inlined_code.html – Spongman Apr 27 '16 at 18:51
  • Although I agreed with the notion, I personally don't agree with the usage in this scenario. *What* the initialization is doing, is not part of the HttpApplication, it's intended output is side affect. As for actual inlining, msbuild or the jit will most likely inline it for performance reasons anyway. – Erik Philips Apr 27 '16 at 19:05
3

Inspired on Aaron's answer I've implemented a solution that keeps Global.asax clean and reuses the code that comes with the template.

  1. Add one line to RegisterGlobalFilters method in RegisterApp_Satrt/FilterConfig.cs

    filters.Add(new InitializeSimpleMembershipAttribute());
  2. Add a default constructor to InitializeMembershipAttribute class that is found in Filters folder. The content of this constructor is going to be the same line that is in the override of OnActionExecuting method. (Here is how the constructor looks like)

    public InitializeSimpleMembershipAttribute()
        {
            // Ensure ASP.NET Simple Membership is initialized only once per app start
            LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
        }
  3. Comment out (or remove) the override of OnActionExecuting method.

And that's it. This solution is giving me two main benefits:

  1. The flexibility to check things related to membership and roles immediately after FilterConfig.RegisterGlobalFilters(GlbalFilters.Filters) line gets executed on global.asax.

  2. Ensures that WebSecurity database initialization is executed just once.


EDIT: The InitializeSimpleMembershipAttribute that I'm using.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class InitializeSimpleMembershipAttribute : ActionFilterAttribute
{
    private static SimpleMembershipInitializer _initializer;
    private static object _initializerLock = new object();
    private static bool _isInitialized;

    public InitializeSimpleMembershipAttribute()
    {
        // Ensure ASP.NET Simple Membership is initialized only once per app start
        LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
    }

    //public override void OnActionExecuting(ActionExecutingContext filterContext)
    //{
    //    // Ensure ASP.NET Simple Membership is initialized only once per app start
    //    LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
    //}

    private class SimpleMembershipInitializer
    {
        public SimpleMembershipInitializer()
        {
            Database.SetInitializer<UsersContext>(null);

            try
            {
                using (var context = new UsersContext())
                {
                    if (!context.Database.Exists())
                    {
                        // Create the SimpleMembership database without Entity Framework migration schema
                        ((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
                    }
                }

                WebSecurity.InitializeDatabaseConnection("Database_Connection_String_Name", "Users", "UserId", "UserName", autoCreateTables: true);
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex);
            }
        }
    }
}
Miguel
  • 1,575
  • 1
  • 27
  • 31
  • I see a FilterConfig.cs in App_Start but I do not see any InitializeMembershipAttribute class in any "Filters folder". Not sure where this is. – Nathan McKaskle Jun 06 '17 at 15:43
  • @NathanMcKaskle please check Erik's answer for more details on the attribute. "Filters" folder is generated by MVC 4 template. – Miguel Jun 06 '17 at 18:44
  • Yeah I'm MVC 6 in VS 2017 here, I have no idea what he's talking about. Do I just create that class at the end separately or ? I have no idea. – Nathan McKaskle Jun 06 '17 at 19:02
  • He has this class at the end that's incomplete, just assuming I know what's after the ... when I don't. – Nathan McKaskle Jun 06 '17 at 19:06
  • @NathanMcKaskle just added the class. Don't know how if that will work in MVC6 – Miguel Jun 06 '17 at 22:48
  • Not if I don't know what's in the class. He doesn't put anything about that class. It's just assumed. Dunno what is supposed to be in there. A lot of things are just left unsaid in this thing. Trying to resolve this problem here: https://stackoverflow.com/questions/44395991/sql-server-error-after-web-deploy-of-app-to-separate-web-server/44408032#44408032 – Nathan McKaskle Jun 07 '17 at 15:14
  • @NathanMcKaskle are you using SimpleMembership auth provider in your project? If not all that we have in this post is not relevant to what you are doing. My understanding is that MVC 6 uses ASP.Net.Identity by default. – Miguel Jun 07 '17 at 21:17
2

I knocked my head on the walls for a day trying to figure out why my Role.GetRoleForUser failed. It was because of the LazyInitializer not getting called.

So, like Matt said, just put it in App_Start to make sure you have no issues.

2

I spent many hours on this very problem. but I ended up with just this change:

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new InitializeSimpleMembershipAttribute());

    }
}

I had been randomly seeing the following error

System.Web.HttpException (0x80004005): Unable to connect to SQL Server database. ---> System.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)

I noticed that whenever I would see the error I would also see:

at ASP._Page_Views_Shared__Layout_cshtml.Execute() in h:\root\home\btournoux-001\www\site7\Views\Shared_Layout.cshtml:line 5

This happens to be the following line in my _Layout.cshtml:

if (User != null && User.Identity != null && (User.IsInRole("publisher") || User.IsInRole("admin")))

So in order to test my simple solution, I put a breakpoint in my InitializeSmpleMembershipAttribute class at the EnsureInitialized call and another one at the first line in the SimpleMembershipInitializer

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // Ensure ASP.NET Simple Membership is initialized only once per app start
        LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
    }

    private class SimpleMembershipInitializer
    {
        public SimpleMembershipInitializer()
        {
            Database.SetInitializer<DataContext>(null);

In addition to those 2 breakpoints I also put a breakpoint in my _Layout.cshtml (I put the test for User in a code section so I could add the breakpoint.

@{
    var maintenanceAccess = false;
    if (User != null && User.Identity != null && (User.IsInRole("publisher") || User.IsInRole("admin")))
   {
       maintenanceAccess = true;
   }
}

After putting in the breakpoints what I did was to comment out the filters.Add( new InitializSimpleMembershipAttribute() and then start up the app in Visual Studio. I could see that I hit the breakpoint in the _Layout.cshtml before any other breakpoint. Then I uncommented that line and ran the app again. This time I saw the breakpoints inside the InitializeSimpleMembershipAttribute class occur prior to the breakpoint in the _Layout.cshtml. And to be sure it was working correctly, I logged in on my website and then saw the first breakpoint in the InitializeSimpleMembershipAttribute class (EnsureInitialized) but not the second one - which was what I expected.

So it all seems to work.

Thanks to all who discovered this!

N8NT
  • 21
  • 1
0

The reason for the InitializeSimpleMembership filter and its overly complex code is for the case when a developer might decide not to use forms authentication, then the template generated code will still work correctly. If you will always use forms authentication you can initialize SimpleMembership in the Application_Start method of the Global.asax. There are detailed instructions on how to do this here.

Kevin Junghans
  • 17,475
  • 4
  • 45
  • 62
  • I will agreed on the first sentence but once decided to use form authentication, I will not recommend to use `InitializeSimpleMembership` filter as a new practice for MVC4 rather than setting in Application_Start because of the issue mentioned in the question. Anyway, nice blog. – CallMeLaNN Feb 23 '13 at 09:30