In a previous article I mentioned that Provider model can be used as a business rules layer or as a repository layer. I will show how this has been done as a portion of a real project and show all the relevant code for that portion of the project to help you fully understand how this works. This article will assume you have read the earlier article, Provider Model Enhanced which describes the provider model pattern and how I enhanced it.
Update Feb 8,2013: I have a revised version of much of this code on a github project called candor-common. The namespace, model class, and methods on the business provider have evolved, but the concepts shown in the code below still represent how I would develop many aspects of an application.
After creating some screen mockups to figure out exactly what it is I am going to create; One of the first bits of code I always create is the model. I segment the portions of what is on the screen mockups into properties and then organize each of those properties into classes. For my models I just create POCOs (Plain Ordinary Class Objects), and I use automatic properties in them where I don’t have a strong reason otherwise. I also put on a serializable attribute so that the framework can easily store them in a cache, session, or other custom store requiring serialization.
using System; namespace CandorCircle.Security { [Serializable] public class User { public User() { } /// <summary> /// Gets or sets the unique identity of the user. /// </summary> public Guid UserID { get; set; } /// <summary> /// Gets or sets the unique identity of the master linked /// user record. Useful when you want to combine user /// accounts into one. /// </summary> public Guid MasterUserID { get; set; } /// <summary> /// Gets or sets the sign in name of the user. /// This may be in the format of an email address. /// </summary> public String Name { get; set; } /// <summary> /// Gets or sets the password of the user. /// This will be empty most of the time, when not required. /// </summary> public String Password { get; set; } /// <summary> /// Gets or sets if this user has been deleted. /// </summary> public Boolean IsDeleted { get; set; } /// <summary> /// Gets or sets the date when this user was created/saved. /// </summary> public DateTime CreatedDate { get; set; } /// <summary> /// Gets or sets the date when this user was last saved. /// </summary> public DateTime UpdatedDate { get; set; } /// <summary> /// Gets or sets the date when this user's password was last updated. /// </summary> public DateTime PasswordUpdatedDate { get; set; } } } //CandorCircle.Security.dll
The term Repository is used here to refer to the pattern. The repository is where the interaction with the datasource takes place, a database (via ADO.Net or an ORM), a webservice, a cache, a mock or however else you store your data. This layer does not have any business logic. This layer is purely for persistence.
I typically place my repository abstractions in a sub-namespace of the model assembly, wheras the business logic layer I put in the root of the model namespace. This segmentation makes it more likely that the application code using this library will use the business logic, and less likely they will call the repository directly. I don’t define these in a separate assembly since the business layer needs to call the repository, but yet the repository needs a reference to the model entities. Generally for ‘normal’ sized applications this is easily understood and the dependency tree is easily followed.
I use the provider model pattern for all of my repositories. So first define the base abstraction that all the repository implementors must fulfill.
Aside: You would still follow this step with a dependency injection framework also, except that you would define this as an interface instead of an abstract class.
using System; using System.Collections.Generic; using System.Configuration.Provider; using Common.Logging; namespace CandorCircle.Security.Repository { /// <summary> /// The base contract that must be fullfilled by any UserRepository provider. /// </summary> public abstract class UserRepositoryProvider : ProviderBase { /// <summary> /// Gets a user by the unique identity. /// </summary> /// <param name="userID">The unique identity.</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> public abstract User GetUser( Guid userID, ExecutionResults result ); /// <summary> /// Gets a user by the user name field /// (which may be an email address). /// </summary> /// <param name="userName">The user name (or email address)</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> public abstract User GetUserByName( String userName, ExecutionResults result ); /// <summary> /// Get the salt by the user name field (which may be an email address). /// </summary> /// <param name="userName">The user name (or email address)</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> public abstract String GetUserSaltByName(String userName, ExecutionResults result); /// <summary> /// Gets all user accounts linked to the specified master user ID. /// </summary> /// <param name="masterUserID">The identity of the master user account.</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> /// <returns></returns> public abstract List<User> GetUsersByMasterID( Guid masterUserID, ExecutionResults result ); /// <summary> /// Saves a user. /// </summary> /// <param name="item">The user details to be saved.</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> /// <returns>A boolean indicating success (true) /// or failure (false).</returns> public abstract bool SaveUser( User item, ExecutionResults result ); private ILog logProvider_ = null; /// <summary> /// Gets or sets the log destination for this provider instance. /// If not set, it will be automatically loaded when needed. /// </summary> public ILog LogProvider { get { if (logProvider_ == null) logProvider_ = LogManager.GetLogger(typeof(UserRepositoryProvider)); return logProvider_; } set { logProvider_ = value; } } } } //CandorCircle.Security.dll, Repository folder
Aside: These method signatures use a class called ExecutionResults which I have not shared yet., you can find it on candor-common. It is just a container I use as an extra input in many method calls so that deep down in the code I can define error messages that end up getting displayed to a user. I use it more frequently in the business layer to give meaningful error messages about violating some business rule and give the user a tip as how to resolve the error. It is still needed sometimes in the data layer such as when saving a field and that field is too long for the repository implementation. The repository layer ‘can’ have some data integrity or max field length rules, however, you should try to duplicate them in your UI layer as well.
Next you need the static API class as an access point to select/resolve the configured implementation. Microsoft names their provider model static API classes by various names such as:
Aside: This is where dependency injection frameworks and the provider model go in different directions. If you are working with a dependency injection framework you don’t need the static class, instead you’ll need special constructors on the classes that use the repository abstraction above. Also if you need a stateful repository class then you should use a dependency injection framework instead of provider model. However, I can’t think of a situation where a stateful repository would be better than a stateless one.
using System; using System.Collections.Generic; using prov = CandorCircle.Configuration.Provider; namespace CandorCircle.Security.Repository { public static class UserRepositoryManager { private static prov.ProviderCollection<UserRepositoryProvider> _providers; /// <summary> /// Gets the default provider instance. /// </summary> public static UserRepositoryProvider Provider { get { return Providers.ActiveProvider; } } /// <summary> /// Gets all the configured UserRepository providers. /// </summary> public static prov.ProviderCollection<UserRepositoryProvider> Providers { get { if (_providers == null) _providers = new prov.ProviderCollection <UserRepositoryProvider>(typeof(UserRepositoryManager)); return _providers; } } /// <summary> /// Gets a user by the unique identity. /// </summary> /// <param name="userID">The unique identity.</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> public static User GetUser( Guid userID, ExecutionResults result ) { return Provider.GetUser(userID, result); } /// <summary> /// Gets a user by the user name field (which may be an email address). /// </summary> /// <param name="userName">The user name (or email address)</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> public static User GetUserByName( String userName, ExecutionResults result ) { return Provider.GetUserByName(userName, result); } /// <summary> /// Get the salt by the user name field (which may be an email address). /// </summary> /// <param name="userName">The user name (or email address)</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> public static String GetUserSaltByName(String userName, ExecutionResults result) { return Provider.GetUserSaltByName(userName, result); } /// <summary> /// Gets all user accounts linked to the specified master user ID. /// </summary> /// <param name="masterUserID">The identity of the master user account.</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> /// <returns></returns> public static List<User> GetUsersByMasterID( Guid masterUserID, ExecutionResults result ) { return Provider.GetUsersByMasterID(masterUserID, result); } /// <summary> /// Saves a user. /// </summary> /// <param name="item">The user details to be saved.</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> /// <returns>A boolean indicating success (true) /// or failure (false).</returns> public static bool SaveUser( User item, ExecutionResults result ) { return Provider.SaveUser(item, result); } } }
To understand how the Providers property works, see the previous article Provider Model Enhanced.
Usually I don’t start off with an implementation that hits the database, and instead I start with a mock implementation. This gets me up and running as soon as possible without waiting for a database to be provisioned or taking time to ‘gel’ my model design. But I’ll show the SQL implementation instead of the mock because it is more interesting.
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Data.SqlClient; using CandorCircle.Security.Cryptography; namespace CandorCircle.Security.Repository { public class SQLUserRepositoryProvider : UserRepositoryProvider { private string connectionName_ = string.Empty; /// <summary> /// Gets the connection name of the Sql database. /// </summary> public string ConnectionName { get { return connectionName_; } } /// <summary> /// Gets the connection string to the Sql database. /// </summary> public string ConnectionString { get { return System.Configuration.ConfigurationManager .ConnectionStrings[ConnectionName].ConnectionString; } } /// <summary> /// Initializes the provider from the configuration values. /// </summary> /// <param name="name">The name of this provider instance.</param> /// <param name="config">The configuration values.</param> public override void Initialize( string name, NameValueCollection config ) { base.Initialize(name, config); if (config["connectionName"] != null) connectionName_ = config["connectionName"]; } /// <summary> /// Gets a user by the unique identity. /// </summary> /// <param name="userID">The unique identity.</param> /// <param name="result">A ExecutionResults instance to add /// applicable warning and error messages to.</param> public override User GetUser( Guid userID, ExecutionResults result ) { using (SqlConnection cn = new SqlConnection(ConnectionString)) { cn.Open(); using (SqlCommand cmd = cn.CreateCommand()) { cmd.CommandType = System.Data.CommandType.StoredProcedure; cmd.CommandText = "GetUser"; cmd.Parameters.AddWithValue("UserID", userID); using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader.Read()) return BuildUser(reader); else result.AppendError("User not found."); } } } return null; } private User BuildUser( SqlDataReader reader ) { User item = new User(); item.UserID = reader.GetGuid("UserID"); item.Name = reader.GetString("Name", ""); item.MasterUserID = reader.GetGuid("MasterUserID"); item.PasswordUpdatedDate = reader.GetUTCDateTime("PasswordUpdatedDate", DateTime.MinValue); item.IsDeleted = reader.GetBoolean("IsDeleted", false); item.CreatedDate = reader.GetUTCDateTime("CreatedDate", DateTime.MinValue); item.UpdatedDate = reader.GetUTCDateTime("UpdatedDate", item.CreatedDate); return item; } //snipped other methods for this sample. } }
The BuildUser method above is using extenstion methods I have added to SqlDataReader. It is rather simple, but let me show you a few of them.
Update 1/11/2013: These can now be found in candor-common on github
public static String GetString( this SqlDataReader reader, String columnName, String defaultValue ) { Int32 index = reader.GetOrdinal(columnName); return GetString(reader, index, defaultValue); } public static String GetString( this SqlDataReader reader, Int32 columnIndex, String defaultValue ) { if (reader.IsDBNull(columnIndex)) return defaultValue; return reader.GetString(columnIndex); } public static DateTime GetUTCDateTime( this SqlDataReader reader, String columnName, DateTime defaultValue ) { Int32 index = reader.GetOrdinal(columnName); return GetUTCDateTime(reader, index, defaultValue); } public static DateTime GetUTCDateTime( this SqlDataReader reader, Int32 columnIndex, DateTime defaultValue ) { if (reader.IsDBNull(columnIndex)) return defaultValue; DateTime utc = reader.GetDateTime(columnIndex); //already stored in UTC if (utc == DateTime.MinValue || utc == DateTime.MaxValue) return utc; return new DateTime(utc.Year, utc.Month, utc.Day, utc.Hour, utc.Minute, utc.Second, utc.Millisecond, DateTimeKind.Utc); } public static Guid GetGuid( this SqlDataReader reader, String columnName ) { Int32 index = reader.GetOrdinal(columnName); return GetGuid(reader, index); } public static Guid GetGuid( this SqlDataReader reader, Int32 columnIndex ) { if (reader.IsDBNull(columnIndex)) return Guid.Empty; return reader.GetGuid(columnIndex); } public static Boolean GetBoolean( this SqlDataReader reader, String columnName, Boolean defaultValue ) { Int32 index = reader.GetOrdinal(columnName); return GetBoolean(reader, index, defaultValue); } public static Boolean GetBoolean( this SqlDataReader reader, Int32 columnIndex, Boolean defaultValue ) { if (reader.IsDBNull(columnIndex)) return defaultValue; return reader.GetBoolean(columnIndex); }
This provider can be instantiated in two ways. It has an implicit default constructor that takes no parameters. The static manager class will instantiate it with the default constructor and then call the Initialize method to load other values from the attributes of the node in the configuration that added the provider. Here is the provider configuration if you let the manager class do the instantiation.
<configuration> <configSections> <sectionGroup name="CandorCircle.Security.Repository"> <section name="UserRepositoryManager" type="CandorCircle.Configuration.Provider.ProviderConfigurationSection, CandorCircle" /> </sectionGroup> </configSections> <!-- ... --> <CandorCircle.Security.Repository> <UserRepositoryManager defaultProvider="SQL"> <providers> <add name="SQL" type="CandorCircle.Security.Repository.SQLUserRepositoryProvider, CandorCircle.Security" connectionName="DefaultConnection" /> </providers> </UserRepositoryManager> </CandorCircle.Security.Repository> <configuration>
Alternatively, you can instantiate the providers using code configuration. The following code does the same thing as the xml configuration above. This code should be placed in your application start-up code, which only runs once.
UserRepositoryManager.Providers = new ProviderCollection(); UserRepositoryManager.Providers.SetActiveProvider( new SQLUserRepositoryProvider("db"){ ConnectionName = "DefaultConnection" }) });
You should only do one of the above options, not both. In both cases you still need the connectionStrings configuration section defined. You can, of coarse, hard code a connection string in code, but I don’t recommend it. The connectionStrings configuration section can be encrypted.
<connectionStrings> <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=LawnKeeper;Persist Security Info=True;Integrated Security=SSPI" /> </connectionStrings>
With the above complete you now have a functional repository layer using the provider model pattern.
If you have a simple CRUD (Create, Read, Update, Delete) application with no more than basic validation, then you may be able to stop at the repository layer. The business layer is an optional layer in the cases where you have other rules you want to enforce besides basic length and range validations. You can have a business layer for one repository or for all your repositories. This is not an all or nothing decision. You may have a business layer provider that calls multiple repository layer provider types. Or you may have one business layer provider type for each repository layer provider type. You may have some repository provider types that are not called by any business layer repositories that instead are accessed directly from the consuming application.
The technical implementation of a business layer provider works the same as the repository layer provider.
In my example I have some business rules I need to enforce when saving a user. Passwords must meet minimal complexity requirements, email addresses must be valid, a user name must not be in use already, and a user record can only be edited by the user themselves or a site administrator. Here is an example of business layer user provider. This code may have logical bugs in it still as this is a work in progress for an application I am working on. I will update it as logical errors are fixed. Or if you spot a logical error, please let me know.
Update: Feb 8, 2013. This logic has been updated on github project candor-common, and will continue to evolve there. Check it out there or just reference the NuGet package in your project to stay on top of the latest version. See the next article on candor security for explanations on the new business logic.
using System; using System.Configuration.Provider; using Common.Logging; namespace CandorCircle.Security { /// <summary> /// The base contract that must be fullfilled by any User provider. /// </summary> public class UserProvider : ProviderBase { private ILog logProvider_ = null; private string nameRegexExpression_ = "^([a-zA-Z0-9\\-_\\s])*$"; private System.Text.RegularExpressions.Regex nameRegex_; private string emailRegexExpression_ = "^([0-9a-zA-Z]+[-\\._+&amp;]*)*[0-9a-zA-Z]+@([-0-9a-zA-Z]+[.])+[a-zA-Z]{2,6}$"; private System.Text.RegularExpressions.Regex emailRegex_; private string passwordRegexExpression_ = "^([a-zA-Z0-9@*#]{6,32})$"; private System.Text.RegularExpressions.Regex passwordRegex_; private string passwordErrorMessage_ = "The password must be between 6 and 32 characters long; and can only contain letters, numbers, and these special symbols(@, *, #)"; /// <summary> /// Gets or sets the log destination for this provider instance. If not set, it will be automatically loaded when needed. /// </summary> public ILog LogProvider { get { if (logProvider_ == null) logProvider_ = LogManager.GetLogger(typeof(UserProvider)); return logProvider_; } set { logProvider_ = value; } } /// <summary> /// A regular expression to validate names. /// </summary> public virtual string NameRegexExpression { get { return nameRegexExpression_; } set { nameRegexExpression_ = value; nameRegex_ = null; } } /// <summary> /// Gets the name regular expression instance for the configured expression. /// </summary> public virtual System.Text.RegularExpressions.Regex NameRegex { get { if (nameRegex_ == null) nameRegex_ = new System.Text.RegularExpressions.Regex( NameRegexExpression, System.Text.RegularExpressions.RegexOptions.Compiled); return nameRegex_; } } /// <summary> /// A regular expression to validate emails. /// </summary> public virtual string EmailRegexExpression { get { return emailRegexExpression_; } set { emailRegexExpression_ = value; emailRegex_ = null; } } /// <summary> /// Gets the email regular expression instance for the configured expression. /// </summary> public virtual System.Text.RegularExpressions.Regex EmailRegex { get { if (emailRegex_ == null) emailRegex_ = new System.Text.RegularExpressions.Regex( EmailRegexExpression, System.Text.RegularExpressions.RegexOptions.Compiled); return emailRegex_; } } /// <summary> /// A regular expression to validate passwords. /// </summary> public virtual string PasswordRegexExpression { get { return passwordRegexExpression_; } set { passwordRegexExpression_ = value; passwordRegex_ = null; } } /// <summary> /// Gets the password regular expression instance for the configured expression. /// </summary> public virtual System.Text.RegularExpressions.Regex PasswordRegex { get { if (passwordRegex_ == null) passwordRegex_ = new System.Text.RegularExpressions.Regex( PasswordRegexExpression, System.Text.RegularExpressions.RegexOptions.Compiled); return passwordRegex_; } } /// <summary> /// An error message shown when the password does not match the required format. /// </summary> public virtual string PasswordErrorMessage { get { return passwordErrorMessage_; } } /// <summary> /// Validates that a password meets minimum requirements. /// </summary> /// <param name="password"></param> /// <param name="results"></param> /// <returns></returns> public virtual bool ValidatePassword( string password, ExecutionResults results ) { if (!PasswordRegex.IsMatch(password)) { results.AppendError(passwordErrorMessage_); return false; } return true; } /// <summary> /// Validates that a string is a valid email address format. /// </summary> /// <returns></returns> public virtual bool ValidateEmailAddressFormat( string emailAddress ) { return System.Text.RegularExpressions.Regex.IsMatch(emailAddress, EmailRegexExpression); } /// <summary> /// Validates that the specified name meets minimum requirements. /// </summary> /// <param name="name">The desired name/alias.</param> /// <param name="result">Any error messages about the desired name.</param> /// <returns></returns> public virtual bool ValidateName( string name, ExecutionResults result ) { if (!NameRegex.IsMatch(name) &amp;&amp; !EmailRegex.IsMatch(name)) { //if this message is changed, do the same anywhere else it is used result.AppendError("The name contains invalid characters. The name must be an email address OR contain only letters, numbers, dashes, underscores, and spaces, are allowed."); return false; } return true; } /// <summary> /// Saves a user. /// </summary> /// <param name="item">The user details to be saved.</param> /// <param name="result">A ExecutionResults instance to add applicable /// warning and error messages to.</param> /// <returns>A boolean indicating success (true) or failure (false).</returns> public virtual bool SaveUser( User item, ExecutionResults result ) { if (result == null) throw new ArgumentNullException("result"); if (item == null) throw new ArgumentNullException("item"); ExecutionResults checkResult = new ExecutionResults(); User existingUser = null; if (!String.IsNullOrWhiteSpace(item.Name)) existingUser = Repository.UserRepositoryManager.GetUserByName(item.Name, checkResult); else if (!item.UserID.Equals(Guid.Empty)) existingUser = Repository.UserRepositoryManager.GetUser(item.UserID, checkResult); bool valid = true; //check all entity validations on first pass. if (item.Name.Trim().Length == 0) { result.AppendError("The user name was not specified or was blank."); valid = false; } else { if (existingUser != null &amp;&amp; !existingUser.UserID.Equals(item.UserID)) { result.AppendError("The requested Name cannot be used."); valid = false; } if (item.Password.Length == 0) { result.AppendError("To change your name, you must also update or re-enter your password."); valid = false; } } if (item.Password.Length > 0 &amp;&amp; PasswordRegexExpression.Length > 0) { if (!PasswordRegex.IsMatch(item.Password) &amp;&amp; existingUser != null &amp;&amp; existingUser.Password != item.Password) { //changing password, and new password does not meet rules. result.AppendError(PasswordErrorMessage); valid = false; } } if (item.UserID.Equals(Guid.Empty) || item.CreatedDate == DateTime.MinValue) { if (item.Password.Length == 0) { result.AppendError("The user password cannot be empty for a new user."); valid = false; } } if (!ValidateName(item.Name, result)) { valid = false; } if (!valid) return false; //an entitiy validation failure //security checks UserPrincipal actor = SecurityContextManager.CurrentUser; if (actor.IsAnonymous) { //can create new user only, or reset password if (existingUser != null) //!item.ID.Equals(Guid.Empty) &amp;&amp; string.IsNullOrWhiteSpace(item.Password)) { result.AppendError("An Anonymous user cannot update an existing user."); return false; } } else if (!actor.Identity.UserID.Equals(item.UserID)) { //not self if (true) //TODO: check admin role to allow admins to edit user { result.AppendError("You do not have access to update another user."); LogProvider.WarnFormat("User '{0}' attempted to save details for user '{1}', " + "and was denied because they do not have the 'SaveUser' permission.", actor.Identity.Name, item.Name); return false; } } return Repository.UserRepositoryManager.SaveUser(item, result); } } } //CandorCircle.Security.dll
Ultimately this SaveUser method calls the repository layer method to perform the saving operation. This sample code uses a SecurityContextManager that I have not shown yet to access the currently signed in user. Just imagine your current role validation code in place of that for now. This sample is where you can see the value of the ExecutionResults class. This business layer class gets to decide what all the extended validation error messages are that the user will see. Your view layer should still catch the common type and range validation errors for inputs, but your business layer will likely have some case that may not be caught until it hits the business layer validation rules.
The static Manager class, and the configuration for the business layer provider structurally looks the same as the same for the repository layer. For a single project I normally do not create derived / alternate implementation providers for business layer providers. But the provider is a nice container to store the business logic, instead of it being spread around all layers of the application. Notice how this UserProvider is not abstract, but all the methods are virtual. I would normally work on an entire project just with this single implementation. What this does offer is a way to override business rules for another client implementation of the same project.
It is easy to use a provider model abstraction anywhere in your end user application. Just add a using statement for the namespace of your business layer and call methods on the static API (Manager) classes. I typically have the same root namespace in the web project as I am using for the business layer, then add a sub-namespace for the web project. In that case you don’t need a using statement. But in either case don’t forget to reference the business layer assembly (dll) in the web project.
In this sample I started with the ‘ASP.Net MVC4 Web Application’ template provided by Microsoft, and picked “Internet application” in the options. This creates a project with an AccountController that uses forms authentication. I used the supplied view models for Login and register with a minor modifications, and moved them to folder Models/Account. I’d prefer if the MVC4 project template didn’t create 3 classes in a single file. I think only a single class, enum, or interface should be in any single .cs file. I also prefer to segment view models into a folder/namespace of the name of the controller, which is how views are segmented by default.
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Security; using CandorCircle.ProjectName.Web.Models.Account; using CandorCircle.Security; using CandorCircle.Security.Repository; using Common.Logging; namespace CandorCircle.ProjectName.Web.Controllers { [Authorize] public class AccountController : Controller { [AllowAnonymous] public ActionResult Login( string returnUrl ){/*...snipped...*/} [AllowAnonymous] [HttpPost] public ActionResult Login( LoginModel model, string returnUrl ) {/*...snipped...*/} public ActionResult LogOff(){/*...snipped...*/} // // GET: /Account/Register [AllowAnonymous] public ActionResult Register() { return View(); } // // POST: /Account/Register [AllowAnonymous] [HttpPost] public ActionResult Register( RegisterModel model ) { if (ModelState.IsValid) { ExecutionResults results = new ExecutionResults(); User user = new User() { Name = model.Email, UserID = Guid.NewGuid(), Password = model.Password }; if (UserManager.SaveUser(user, results)) { UserIdentity identity = AuthenticationManager.AuthenticateUser( user.Name, model.Password, Request.UserHostAddress, 60 * 24 * 14); if (identity.IsAuthenticated) { SecurityContextManager.CurrentUser = new UserPrincipal(identity); return RedirectToAction("Index", "Home"); } else results.AppendError(identity.Ticket.Message); } else results.AppendError("Failed to create user."); for (int e = 0; e < results.Messages.Count; e++) ModelState.AddModelError(e.ToString(), results.Messages[e].Message); } // If we got this far, something failed, redisplay form return View(model); } // // GET: /Account/ChangePassword public ActionResult ChangePassword() { return View(); } // // POST: /Account/ChangePassword [HttpPost] public ActionResult ChangePassword( ChangePasswordModel model ) { if (ModelState.IsValid) { //some of this logic probably belongs in the UserProvider also. I'm still working to refactor this code. ExecutionResults result = new ExecutionResults(); UserIdentity identity = AuthenticationManager.AuthenticateUser( model.Email, model.OldPassword, Request.UserHostAddress, 60 * 24 * 14); if (identity.IsAuthenticated) { //old password was correct User user = UserRepositoryManager.GetUserByName(model.Email, result); if (user != null) { user.Password = model.NewPassword; user.PasswordUpdatedDate = DateTime.UtcNow; if (UserManager.SaveUser(user, result)) { SecurityContextManager.CurrentUser = new UserPrincipal(identity); return RedirectToAction("ChangePasswordSuccess"); } } else { LogProvider.ErrorFormat("Authenticated user, but could not load user. Details: {0}", result); result.AppendError("System error loading user details."); } } else { ModelState.AddModelError("", "The specified email and current password combination is not correct."); } for (int e = 0; e < result.Messages.Count; e++) ModelState.AddModelError(e.ToString(), result.Messages[e].Message); } // If we got this far, something failed, redisplay form return View(model); } // // GET: /Account/ChangePasswordSuccess public ActionResult ChangePasswordSuccess() { return View(); } private ILog logProvider_ = null; /// <summary> /// Gets or sets the log destination for this provider instance. /// If not set, it will be automatically loaded when needed. /// </summary> public ILog LogProvider { get { if (logProvider_ == null) logProvider_ = LogManager.GetLogger(typeof(AccountController)); return logProvider_; } set { logProvider_ = value; } } } }
For now pay little attention to the use of SecurityContextManager and AuthenticationManager. I use these implementations instead of the Membership of Roles APIs provided by Microsoft because I don’t like the API for those capabilities. I may drill into what these are in a future article. For now you can imagine using Membership instead, which is what comes with the MVC4 project template.
Update Feb 8, 2013: I have written a followup article about an MVC4 bootstrap project using candor security.
The primary difference in the consumer code from dependency injection (DI) is that I just use the static API class. I can let MVC instantiate the controllers and manage lifetime of them itself. If you were using a DI framework, you would need a constructor taking in those dependencies and fields to store them within. So to clarify you also have the constrained construction problem when using dependency injection. Except that instead of having a constraint that you need a default contructor, which all controllers have by default (implicitely), you now need a constructor taking dependencies.
I’ve heard complaints from DI experts and users in that the Manager code is not needed with DI, and hence provider model takes more code to implement overall in your solution. I’ll continue to counter that by saying that DI requires more code in the consumer of the abstraction that you don’t need with provider model. The more consumers you have of an abstraction in a DI solution, the more superfluous code you need to store dependency instances and constructors to inject the values for them. If you have a chain of dependencies it gets even more complicated. In the end I think this argument is a wash either way. Some applications may be simple enough where an abstraction is only used in one consumer (a controller class) and it may be less code that having a manager class. Other applications may have more consumers of a given abstraction and result in more code for the DI alternative over the provider model static manager class.
Update 1/11/2013: Much of the code above has been added to candor-common on Github
https://github.com/michael-lang/candor-common
5 Comments