Motivation
The default ASP.NET MVC Internet Application comes with forms authentication. It has all layers and classes in a single project. This is good for a simple and small application, but for an enterprise application, I would like to have a better architecture. So, I changed the default Internet Application to have more layers and put the layers in separate projects.
Analysis
In order to re-architecture this default application, I need to use the DDD (Domain Driven Design) concept to find the domain and services first. Because the core functionality in this application is authenticating users with forms authentication, the domain is membership management. The membership management is actually implemented in
AspNetSqlMembershipProvider
which is the realization of Microsoft’s Provider pattern, so the Domain is actually hiding behind the scenes. What are ChangePasswordModel
, LogOnModel
, and RegisterModel
, you may ask? First of all, they are Models, but they are View Models instead of Domain Models, from my perspective. Domain model is the core of your application and should contain both the data and behavior to reflect the specific domain.ChangePasswordModel
, LogOnModel
, and RegisterModel
really just help move data from the View to the Domain and vice versa. After identifying the domain, I am able to decide what should stay in the default project and what should be moved to a different project to have a better separation of concern, reusability, and testability.Change
The below steps are used to change the default application to a different architecture.
Step 1: Create View Models
Like I mentioned above,
ChangePasswordModel
, LogOnModel
, and RegisterModel
are actually View Models from my perspective. So, I created a ViewModels folder in the default project and createdChangePasswordViewModel
, LogOnViewModel
, and RegisterViewModel
in it. All the places that usedChangePasswordModel
, LogOnModel
, and RegisterModel
originally are refactored to useChangePasswordViewModel
, LogOnViewModel
, and RegisterViewModel
.Step 2: Create Utilities
There is an
AccountValidation
class in the default application that is used to convertMembershipCreateStatus
to a description for displaying in the view. I created a Utilities folder to contain it. This folder will contain all the common classes and helper classes. Therefore, ValidatePasswordLengthAttributes
is moved into it also.Step 3: Create Server Interfaces
The default application already has a good decoupling for Membership and Forms Authentication functions to allow switching implementation in different environments, like in Tests project,
MockMembershipService
is used instead of AspNetSqlMembershipProvider
to allow testing logon, register, etc., functions without connecting to the membership repository. However, I like to only keep the service interface in the web project. A ServiceInterfacesfolder is created and IFormAuthenticationService
and IMembershipService
are moved into it.Step 4: Create Security Project
From step 3, you will know I like the implementation to stay in a separate project (assembly) to better decouple them from the contract and the caller. The Demo.Web.Secruity project is created and
AccountMembershipService
andFormAuthenticationService
are moved into it. There is a problem. The AccountController
createsAccountMembershipService
and FormAuthenticationSerrvice
instances directly to provide for using in the controller actions. I don’t want to let the Web project reference the Security project and seeing the concrete classes because this ruins the separation of concern. This problem will be fixed in the following steps with Dependency Injection.Step 5: Create Library
Before I can have Dependency Injection in place, I need to have some cornerstone first. The IoC container I use in here is Unity. It’s not the best IoC container on the market, but it has all the features I need, like lifetime management, configuration file, etc. I created a Library folder in the folder of the solution file and copied all the Unity assembly files into it.
In order to let the solution manage all the Unity files, I also created a Library solution folder and made references to all files in the Library folder.
Step 6: Create Base Project
For an enterprise application, there are always some utility and helper classes that need to be used in all projects. The Demo.Base project is created to contain those classes. So far, I only have one interface
IDependencyLocator
in it that is implemented in the project that has a dependency injection access point.Step 7: Create Dependency Injection
ASP.NET MVC3 has built-in Dependency Injection support (
DependencyResolver
), but I prefer to manage Dependency Injection via a Registry class. A WebRegistry
class is created in the Web project. It hasDependencyLocator
to wrap up the IoC container. The FormsService
property and MembershipService
property will query the IoC container to get the instance object.The
UnityDependencyLocator
binds with WebRegistry
in Global.asax.cs. Collapse | Copy Code
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
RegisterUnityContainer();
}
private void RegisterUnityContainer()
{
var container = new UnityContainer();
UnityConfigurationSection section = (UnityConfigurationSection)
ConfigurationManager.GetSection("unity");
section.Configure(container);
WebRegistry.DependencyLocator = new UnityDependencyLocator(container);
}
The dependency configuration is in the web.config:
Collapse | Copy Code
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration" />
</configSections>
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<alias alias="IMembershipService"
type="Demo.Web.ServiceInterfaces.IMembershipService, Demo.Web" />
<alias alias="MembershipService"
type="Demo.Web.Security.AccountMembershipService, Demo.Web.Security" />
<alias alias="IFormsAuthenticationService"
type="Demo.Web.ServiceInterfaces.IFormsAuthenticationService, Demo.Web" />
<alias alias="FormsAuthenticationService"
type="Demo.Web.Security.FormsAuthenticationService, Demo.Web.Security" />
<container>
<register type="IMembershipService" mapTo="MembershipService">
<constructor />
</register>
<register type="IFormsAuthenticationService" mapTo="FormsAuthenticationService" />
</container>
</unity>
The
AccountController
is changed to initialize the FormsService
and MembershipService
variables withWebRegistry
. Collapse | Copy Code
protected override void Initialize(RequestContext requestContext)
{
if (FormsService == null) { FormsService = WebRegistry.FormsService; }
if (MembershipService == null) { MembershipService = WebRegistry.MembershipService; }
base.Initialize(requestContext);
}
One more thing needs to be done here, which is to add:
Collapse | Copy Code
xcopy /Y /F "$(SolutionDir)Demo.Web.Security\$(OutDir)\
"Demo.Web.Security.dll "$(SolutionDir)Demo.Web\bin"
in the Security project “Build Events -> Post-build event command line” to copy Demo.Web.Security.dll into the Web application’s bin folder because there is no direct reference between the Web project and the Security project. The IoC container needs the assembly file to bind the implementation to the interface.
Step 8: Clean up
Finally, I have all the pieces here, I just need to remove the original Models folder from the Web project and refactor the entire solution to replace the old classes with the new classes and layers.
Comparison
After the change, the View, ViewModel, Controller, and Service interface are still in the Web project. The Model layer is in
AspNetSqlMembershipProvider
. The Service implementation is in the Security project.Conclusion
What I did in this article is just to reflect my thoughts on how a web application with Forms Authentication should be organized. There is no big innovation. It’s just a different idea on developing an application with ASP.NET MVC that has Forms Authentication for entitlement management.
Using the Code
The code is developed in Visual Studio 2010. SQL Server database aspnetdb is needed for membership that can be created with aspnet_regsql.exe.
No comments:
Post a Comment