7

I am developing a code first web app in Visual Studio 2012 Express.

I use this connection string in the web.config:

<add name="myContext" connectionString="Data Source=.;Integrated Security=True;Initial Catalog=cityKingMVC4" providerName="System.Data.SqlClient" />

I am using SimpleMembership.

I am trying to seed 1 administrator from Filters/InitializeSimpleMembershipAttribute.cs: ...

WebSecurity.InitializeDatabaseConnection("myContext", "Users", "UserId", "Email", autoCreateTables: true);

// A: Create Admin user
if (!WebSecurity.ConfirmAccount("admin@mydom.com"))
{
   WebSecurity.CreateUserAndAccount("admin@mydom.com", "password");
}

// B: Create admin role if not exist
if (!Roles.RoleExists("Administrator"))
{
   Roles.CreateRole("Administrator");
   Roles.AddUserToRole("admin@mydom.com", "Administrator");
}

If I comment A & B it doesn't crash. If I don't I get this: The "WebSecurity.InitializeDatabaseConnection" method can be called only once.

If I debug and put a breakpoint on 'WebSecurity.InitializeDatabaseConnection' - it only calls it once and there is no other code calling WebSecurity.InitializeDatabaseConnection anywhere.

If I debug - it crashes on a different line higher up in the file (standard SimpleAuthentication file): LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);

with this error: Exception has been thrown by the target of an invocation.

Stack Trace:

[InvalidOperationException: The "WebSecurity.InitializeDatabaseConnection" method can be called only once.]
   WebMatrix.WebData.WebSecurity.InitializeMembershipProvider(SimpleMembershipProvider simpleMembership, DatabaseConnectionInfo connect, String userTableName, String userIdColumn, String userNameColumn, Boolean createTables) +87978
   WebMatrix.WebData.WebSecurity.InitializeProviders(DatabaseConnectionInfo connect, String userTableName, String userIdColumn, String userNameColumn, Boolean autoCreateTables) +86
   myapPMVC4.Filters.SimpleMembershipInitializer..ctor() in c:\Users\name\Documents\Visual Studio 2012\Projects\myapPMVC4\myapPMVC4\Filters\InitializeSimpleMembershipAttribute.cs:43

[InvalidOperationException: The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588]
   myapPMVC4.Filters.SimpleMembershipInitializer..ctor() in c:\Users\name\Documents\Visual Studio 2012\Projects\myapPMVC4\myapPMVC4\Filters\InitializeSimpleMembershipAttribute.cs:88

[TargetInvocationException: Exception has been thrown by the target of an invocation.]
   System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) +0
   System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +159
   System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +256
   System.Activator.CreateInstance(Type type, Boolean nonPublic) +127
   System.Activator.CreateInstance(Type type) +11
   System.Threading.LazyHelpers`1.ActivatorFactorySelector() +72
   System.Threading.LazyInitializer.EnsureInitializedCore(T& target, Boolean& initialized, Object& syncLock, Func`1 valueFactory) +241
   System.Threading.LazyInitializer.EnsureInitialized(T& target, Boolean& initialized, Object& syncLock) +139
   myapPMVC4.Filters.InitializeSimpleMembershipAttribute.OnActionExecuting(ActionExecutingContext filterContext) in c:\Users\name\Documents\Visual Studio 2012\Projects\myapPMVC4\myapPMVC4\Filters\InitializeSimpleMembershipAttribute.cs:22
   System.Web.Mvc.Async.AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(IActionFilter filter, ActionExecutingContext preContext, Func`1 nextInChain) +145
   System.Web.Mvc.Async.AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(IActionFilter filter, ActionExecutingContext preContext, Func`1 nextInChain) +840201
   System.Web.Mvc.Async.<>c__DisplayClass37.<BeginInvokeActionMethodWithFilters>b__31(AsyncCallback asyncCallback, Object asyncState) +266
   System.Web.Mvc.Async.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout) +146
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate`1 endDelegate, Object tag, Int32 timeout) +202
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate`1 endDelegate, Object tag) +112
   System.Web.Mvc.Async.<>c__DisplayClass25.<BeginInvokeAction>b__1e(AsyncCallback asyncCallback, Object asyncState) +839055
   System.Web.Mvc.Async.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout) +146
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate`1 endDelegate, Object tag, Int32 timeout) +166
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate`1 endDelegate, Object tag) +27
   System.Web.Mvc.<>c__DisplayClass1d.<BeginExecuteCore>b__17(AsyncCallback asyncCallback, Object asyncState) +50
   System.Web.Mvc.Async.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout) +146
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate`1 endDelegate, Object tag, Int32 timeout) +166
   System.Web.Mvc.Controller.BeginExecuteCore(AsyncCallback callback, Object state) +826145
   System.Web.Mvc.Async.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout) +146
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate`1 endDelegate, Object tag, Int32 timeout) +166
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate endDelegate, Object tag) +27
   System.Web.Mvc.Controller.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state) +401
   System.Web.Mvc.<>c__DisplayClass8.<BeginProcessRequest>b__2(AsyncCallback asyncCallback, Object asyncState) +786250
   System.Web.Mvc.Async.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout) +146
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate`1 endDelegate, Object tag, Int32 timeout) +166
   System.Web.Mvc.Async.AsyncResultWrapper.Begin(AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate endDelegate, Object tag) +27
   System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state) +343
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +12550291
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +288

What's going on?

Thx

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Threading;
using System.Web.Mvc;
using WebMatrix.WebData;
using System.Web.Security;
using myapPMVC4.Models;

namespace myapPMVC4.Filters
{
    [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 override void OnActionExecuting(ActionExecutingContext filterContext)
        {
           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("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);

                    if (!WebSecurity.Initialized)
                    {
                       WebSecurity.InitializeDatabaseConnection("myapPMVC4DBContext", "Users", "UserId", "Email", autoCreateTables: true);
                    }


                    // Create Admin user
                    if (!WebSecurity.ConfirmAccount("admin@myapp.com"))
                    {
                       //WebSecurity.CreateUserAndAccount("admin", "pass", new { email = "a@b.com" });
                       WebSecurity.CreateUserAndAccount("admin@myapp.com", "pass");
                    }

                    // Create admin role if not exist
                    if (!Roles.RoleExists("Administrator"))
                    {
                       Roles.CreateRole("Administrator");
                       Roles.AddUserToRole("admin@myapp.com", "Administrator");
                    }


                }
                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);
                }
            }
        }
    }
}
niico
  • 11,206
  • 23
  • 78
  • 161

5 Answers5

14

The problem ...

is that InitializeDatabaseConnection calls WebSecurity.InitializeProviders internally, and this method is not thread-safe, then combine this with the fact that WebSecurity typically needs initializing from various places. This has implications as web applications are inherently multi-threaded environments ... and WebSecurity.Initialized and WebSecurity.InitializeDatabaseConnection are not thread-safe when used together - they create a typical race condition.

Seeding (for migrations) means that your WebSecurity can be initialized more than once as you may also need to initialise it in Global.asax.cs for deployments with seeding turned off, and your InitializeSimpleMembershipAttribute can potentially be called multiple times simultaneusly by http requests in live deployments etc.

Putting the initialisation code in multiple places also breaks your DRYness

The solution ...

Make sure your init calls are thread-safe, and only occur once per instance of an AppDomain. Use a thread-safe singleton class to do this; and reduce duplication of code.

Call the singleton's EnsureInitialize method from any/all of the following, as appropriate for your application:

  • Global.asax.cs (Application_Start method before anything else)
  • Migrations\Configuration.cs Seed method (before creating the users)
  • Filters\InitializeSimpleMembershipAttribute.cs (in the SimpleMembershipInitializer constructor after the context is initialised)

Here is a simple example singleton:

// Call this with WebSecurityInitializer.Instance.EnsureInitialize()
public class WebSecurityInitializer {
    private WebSecurityInitializer() { }
    public static readonly WebSecurityInitializer Instance = new WebSecurityInitializer();
    private bool isNotInit = true;
    private readonly object SyncRoot = new object();
    public void EnsureInitialize() {
        if (isNotInit) {
            lock (this.SyncRoot) {
                if (isNotInit) {
                    isNotInit = false;
                    WebSecurity.InitializeDatabaseConnection("MyContextName",
                        userTableName: "UserProfile", userIdColumn: "UserId", userNameColumn: "UserName",
                        autoCreateTables: true);
                }
            }
        }
    }
}

Once I had done this, my case of the error you mention disappeared, not to be seen again.


Footnotes

The singleton class also keeps your code DRY, which is especially useful during early stage app development if you need to later change the configuration of your WebSecurity.InitializeDatabaseConnection as it will only be in one place (end edit)

I also keep the SimpleMembershipInitializer clean, and instead seed my users along with the common seeding in Migrations\Configuration.cs Seed method. This helps with testability of seeding through my migrations by keeping everything in one place. I use unit testing to make sure we can always go up and down the migrations tree, so this makes it easier to do that.

However the location of your seeding code won't matter, it is more just making sure that, globally, you have initialised WebSecurity only once within your AppDomain, and that any call to InitializeDatabaseConnection is thread-safe.

Community
  • 1
  • 1
Andy Brown
  • 18,961
  • 3
  • 52
  • 62
  • Thanks - this looks really interesting. Will report back ASAP (offline for a day) – niico May 13 '13 at 20:02
  • Implementing this now - can you recommend any sample applications I can download that incorporate best practice - such as this. I am new to MVC and it would be very helpful. MVCMusicStore doesn't go nearly far enough... – niico May 14 '13 at 05:11
  • @user2254951. As its own class, it can go anywhere, as long as it is reachable by all the points that I mentioned that need to call it. I'm not sure about sample applications - the template ones provided by VS are a good start, but I haven't found any good, large, examples of best practice yet. – Andy Brown May 14 '13 at 06:30
  • To try and understand the situation better. I have this in first line of Application_Start(): 'if (!WebSecurity.Initialized) { System.Data.Entity.Database.SetInitializer(new MyAppMVC4.Models.SampleData()); }' I also have this, within InitializeSimpleMembershipAttribute.cs: if (!WebSecurity.Initialized) { WebSecurity.InitializeDatabaseConnection("MyAppMVC4DBContext", "Users", "UserId", "Email", autoCreateTables: true); } What you're saying is, they conflict & need moving to the same place in the code thats executed only once? – niico May 14 '13 at 07:12
  • I think there's a gap in the market there - I would certainly pay $5? $20? or more for a comprehensive clear sample app.... – niico May 14 '13 at 10:47
  • @user2254951. Your first line looks odd to me, I've just never done it like that, or linked it to `WebSecurity` (see [Place to put Database.SetInitializer](http://stackoverflow.com/questions/8679562/place-to-put-database-setinitializer/8679662#8679662) and [shotgun surgery](http://stackoverflow.com/questions/11460594/avoiding-shotgun-surgery-with-database-setinitializer)). Quite separately I use my `EnsureInitialize` in both those places without any "if WebSec...Init.." tests to allow for various deployment scenarios. – Andy Brown May 14 '13 at 18:18
  • Thanks - I also found this post very useful: http://blog.longle.net/2012/09/25/seeding-users-and-roles-with-mvc4-simplemembershipprovider-simpleroleprovider-ef5-codefirst-and-custom-user-properties – niico May 17 '13 at 11:47
  • Thanks so much for this. I could get my site to run and seed locally, but I kept running into the issue when publishing to Azure. I still need to fully grok the concepts, but Saturday afternoon saved! :) – vitaminjeff Feb 08 '15 at 01:35
13

Add this code to Global.asax.cs. This will makes sure that your database is always Initialized before any other executions. Also make sure its the first registration in Application_Start()

if (!WebSecurity.Initialized)
                WebSecurity.InitializeDatabaseConnection("DefaultConnection",
"UserProfile", "UserId", "UserName", autoCreateTables: true);

Get rid of Filters/InitializeSimpleMembershipAttribute.cs or just comment the code inside, in case you would like to go back to it.

Remove [InitializeSimpleMembership] at the top of AccountController.cs

Also if you haven't already enabled migrations, i would encourage you do so. That way, you can do your seeds in Configuration.cs created inside the Migration folder when you run Enable-Migrations

Komengem
  • 3,662
  • 7
  • 33
  • 57
2

If it is already initialized then make sure your first call:

if (!WebSecurity.Initialized)
{
    // Do the initialization first.
}

// The rest of the code

This way you'll be sure that you don't repeat the initialization process.

Spongman
  • 9,665
  • 8
  • 39
  • 58
Huske
  • 9,186
  • 2
  • 36
  • 53
  • This doesn't seem to help - see above. FYI, my SampleData.cs seed method derives from 'dropcreatedatabasealways): public class SampleData : DropCreateDatabaseAlways – niico May 12 '13 at 20:18
  • I tried wrapping LazyLoader (see AccountController.cs above) in this - didn't help. – niico May 12 '13 at 20:41
0

Do you also have the InitializeSimpleMembership attribute on your AccountController class? (this is the default). If so, you are initializing twice.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • 1
    "You must call the "WebSecurity.InitializeDatabaseConnection" method before you call any other method of the "WebSecurity" class. This call should be placed in an _AppStart.cshtml file in the root of your site." – niico May 12 '13 at 19:00
  • @user2254951 - you're absolutely sure this is the only place where the InitalizeDatabaseConnection method is? You're sure you don't have the attribute in more than one place? – Erik Funkenbusch May 12 '13 at 19:15
  • No, as I say above - If I remove [IntializeSimpleMembership] in AccountController.cs - I get the above error (which implies it hasn't been initialized at all – niico May 12 '13 at 20:17
  • @user2254951 - No? You're not sure? – Erik Funkenbusch May 12 '13 at 20:18
  • @user2254951 - you need to provide the entire source code of you InitializeSimpleMembershipAttribute class, by only providing a small part we can't see what else you might be doing. – Erik Funkenbusch May 12 '13 at 20:21
  • @user2254951 - there's something going on here that you're not showing us.. You have to be calling this twice somehow. – Erik Funkenbusch May 12 '13 at 20:56
  • @user2254951 - you don't have an _AppStart.cshtml file do you? Does that have an InitializeDatabaseConnection call in it? Do you have `` in your web.config? – Erik Funkenbusch May 12 '13 at 21:00
  • I don't have any code in _AppStart.cshtml, only layout info (a hang over from old WebMatrix I think?) - and no web.config reference either – niico May 12 '13 at 21:19
  • I think I've narrowed it down - it seems to work if I comment these lines out: [code]( // Create Admin user if (!WebSecurity.ConfirmAccount("admin@mydom.com")) { //WebSecurity.CreateUserAndAccount("admin", "pass", new { email = "a@b.com" }); WebSecurity.CreateUserAndAccount("admin@mydom.com", "pass"); } // Create admin role if not exist if (!Roles.RoleExists("Administrator")) { Roles.CreateRole("Administrator"); Roles.AddUserToRole("admin@mydom.com", "Administrator"); } ) Is something wrong with them? If so whats best way to seed admins? – niico May 12 '13 at 21:41
  • formatted code: I think I've narrowed it down - it seems to work if I comment these lines out: ' // Create Admin user if (!WebSecurity.ConfirmAccount("admin@mydom.com")) { //WebSecurity.CreateUserAndAccount("admin", "pass", new { email = "a@b.com" }); WebSecurity.CreateUserAndAccount("admin@mydom.com", "pass"); } // Create admin role if not exist if (!Roles.RoleExists("Administrator")) { Roles.CreateRole("Administrator"); Roles.AddUserToRole("admin@mydom.com", "Administrator"); } ' – niico May 12 '13 at 21:51
0

To make sure that the WebSecurity.InitializeDatabaseConnection is not called twice just use the WebSecurity.Initialized to check if it was already called. This blog post provides detailed instructions on seeding and customizing SimpleMembership. There is a series in this blog on using SimpleMembership and I would also recommend looking at decoupling SimpleMembership from your ASP.NET MVC Application. You can get the complete source code for these examples here.

Kevin Junghans
  • 17,475
  • 4
  • 45
  • 62