I've spent the last couple days moving a project from MVC 4 RC to MVC 4 RTM. It's gone well, but I encountered a couple trouble spots related to MVC 4 authentication and membership that I want to share.
I'm using a MVC 4 application that was created pre-RTM, so I'm working through some pain that won't cause trouble for people creating new MVC 4 applications. Microsoft provides some information on how to upgrade a project from MVC 3 to MVC 4 on their website. But that's not this.
I wanted to upgrade an MVC 4 RC project to MVC 4 RTM. One of the "truths" I give to my interns is that "I'm lazy" (the other is "Users are Stupid", but that's another topic). To that end, I created a brand new MVC 4 RTM project and started copying my files from the RC to the RTM. I don't have better guidance. Sorry. And I'm lazy. (I'm bummed about losing TFS bindings, but I’m on a schedule here and I’m no TFS expert).
Is New MVC 4 Authentication Like New Math?
In RTM, when you create a blank MVC 4 app using the Internet Application template, it uses the brand spanking new WebMatrix WebSecurity system for simple authentication, membership, etc. (maybe it's not brand new, but it's new to MVC 4) My project had its authentication schema configured under the RC and by default, the systems don't mesh well.
Because OAuth is now in all the built-in templates, the new authentication system nicely merges OAuth accounts with local accounts. I want to do Twitter authentication in the future, so I took the time to change the schema before my app went live.
I just wanted to see what happened if I tried to run my app with the old auth schema on a new project. It wasn't pretty. Here's an example of the error:
Account Login Exception has been thrown by the target of an invocation. -----
at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache) at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipVisibilityChecks, Boolean skipCheckThis, Boolean fillCache) at System.Activator.CreateInstance(Type type, Boolean nonPublic) at System.Threading.LazyInitializer.LazyHelpers
1.ActivatorFactorySelector() at System.Threading.LazyInitializer.EnsureInitializedCore[T](T& target, Boolean& initialized, Object& syncLock, Func1 valueFactory) at System.Threading.LazyInitializer.EnsureInitializedT
Oh man. You are so screwed. This nastiness is happening inside the InitializeSimpleMembershipAttribute.cs file. The new authentication system fires this filter because it declares the attribute [InitializeSimpleMembership] on the AccountController. This filter executes and checks to make sure the auth database is created and ready to go. Here's what the new schema looks like:
And here's the CREATE TABLE SQL scripts for them:
The filter also opens up a connection to the auth database using this line of code:
[code lang="csharp"]WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);[/code]
We'll see this again later.
Ditch the Zero. Get With the Hero.
I updated the auth schema in my app's database by removing all of the auth tables from the previous version and replacing it with all of the new ones (using the SQL Scripts above). Again, if you're upgrading from an MVC 3 app (which uses a completely different schema), you'll have to seek out help on how to do that.
That exception message is exactly what needs to be done in order to keep the app working.
I'm almost done. While the work above creates the auth and membership mechanism, it only gets me halfway. Since I link to the UserId column of the UserProfile table in my app's tables, I often want to retrieve information based on the UserId.
Note that referencing a UserId is better than referencing a UserName if you want to use OAuth. The webpages_OAuthMembership table links a provider's username to a UserId in our database. Thus, if you want a consistent reference to the user logged in, you must use UserId. The alternative is a lot of username lookups and possible username collisions across providers. Cough Don't do it! Cough
I wanted to use the WebSecurity.CurrentUserId property to retrieve that UserId and use it in a separate WebApi database query. I just stuck it in there and went with it. Oh the hubris! Here's the error I got:
"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."
From what I can tell, the problem I had was due to separate launches of the application while debugging it. Any activity that's called on the AccountController (register, login, logoff, etc.) will ensure this database connection is initialized. However, since the auth cookie persisted in my browser between debugging sessions, I was hitting my WebApi without using AccountController actions.
Put Down Some Roots (Files that is)
I'm so used to the exception information provided being useless that I searached for the term InitializeDatabaseConnection to see what this was about. Maybe you even searched that term to get to this page. But mark this day, that exception message is exactly what needs to be done in order to keep the app working.
Create a file in the root of your web app called _AppStart.cshtml and put this in it:
See that? It's the same line as is in the InitializeSimpleMembership filter. Honestly, I tried just setting the InitializeSimpleMembership attribute on my WebApi controller, but it didn’t work, so I went with this method. See earlier references to laziness if you've got any problems with this.
Note that, in this case, it's not actually creating the auth database. It's simply making sure that there is a database connection made to enable WebSecurity to do all its work.
I Feel So Secure
Whether you feel secure or not is another issue. Once I got all these changes moved over, my app is back in tip-top working order and I'm authenticating and membershipping (?) till the cows come home. And I live in the middle of farms, so that's more literal than figurative.
Have you found anything else related to upgrading? Is this totally messed up? Let me know!
[endcall]Image Credit: Robert Scoble[/endcall]