[button link="http://sdrv.ms/178k4no" color="primary" target="_blank" size="large" title="Code Download" icon_before="download"]Code Download[/button]
I’m having lots of fun with ASP.NET MVC these days. One of the cool new features in ASP.NET MVC4 and the Visual Studio MVC application template is the SimpleMembershipProvider which provides a more concise membership provider to the ASP.NET framework, and supports OAuth additionally. If you’re new to the SimpleMembershipProvider, check out Jon Galloway’s post SimpleMembership, Membership Providers, Universal Providers and the new ASP.NET 4.5 Web Forms and ASP.NET MVC 4 templates. Obviously, if you work with MS technologies, you probably are also using SQL Server or SQL Express, and the SimpleMembershipProvider is all you really need. But doing MVC on top of other databases is also very common practice. The out-of-the-box SimpleMembershipProvider leverages Entity Framework as it’s data access strategy. Built-in as well, are lots of custom SQL commands that are built for SQL Server. It is therefore not compatible at this time with other database and persistence providers. But, luckily, the ASP.NET team has provided a way to enhance this new membership provider by extending 2 abstract classes: WebMatrix.WebData.ExtendedMembershipProvider, and the standard System.Web.Security.RoleProvider which was moved to the System.Web.ApplicationServices.dll assembly. LLBLGen generates 1 DAL that supports a dozen or more database providers out-of-the-box. So supporting the new MVC4 membership provider on top of multiple database is a breeze. Here’s a sample scenario.Step 1 - Create an out-of-the-box ASP.NET MVC 4 internet application
Once I create the internet Visual Studio solution, I fired it up and registered a user, and validated that I could add a google, yahoo, and twitter account. All is good.

Step 2 - Model something out in LLBLGen (or use an existing model within your application)
In many cases, one’s application would have it’s own set of tables for users and roles, but in this case, we want to make sure that our implementation is at minimum compatible with the out-of-the-box framework so that when using SQL Server, we can switch back and forth between the default SimpleMembershipProvider or our new LLBLGen based provider without any issues. So I just imported the model into the LLBLGen designer, added other database providers (Oracle, MySql, MS Access), and here’s what you get.
Step 3 - Generate the LLBLGen DAL code
After generating the code, my project now looks like this:
<connectionstrings> <add name="DefaultConnection" connectionstring="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\testdb-20121109102135.mdf;Initial Catalog=testdb-20121109102135;Integrated Security=True;User Instance=True" providername="System.Data.SqlClient" /> <add name="ConnectionString.SQL Server (SqlClient)" connectionstring="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\testdb-20121109102135.mdf;Initial Catalog=testdb-20121109102135;Integrated Security=True;User Instance=True" providername="System.Data.SqlClient" /> <add name="ConnectionString.MySql (MySqlDirect)" connectionstring="Server=localhost;Database=testdb;User ID=testuser;Password=testuser;" providername="Devart.Data.MySql" /> <add name="ConnectionString.MS Access (OleDb)" connectionstring="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\testdb.mdb;User Id=;Password=;Jet OLEDB:System Database=;Jet OLEDB:Database password=;OLE DB Services=-4" providername="System.Data.OleDb" /> </connectionstrings>Then I create simple DataAccessAdapterFactory class that will easily load the appropriate data access layer depending on which connection string I chose. In this implementation, it uses the "providerName" attribute of the connection string to know which DataAccessAdapter to create. I do this so that I don't have to have any explicit references to the actual DAL classes, all we need is to put the *.dll in the bin and let our web.config instruct the application on which database it wants to use.
using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Text; using System.Threading.Tasks; using SD.LLBLGen.Pro.ORMSupportClasses; namespace MyCorp.ExtendedAdapterMembershipProviders.Providers { public class DataAccessAdapterFactory: IDataAccessAdapterFactory { private static Func creator = null; public IDataAccessAdapter CreateDataAccessAdapter() { return creator.Invoke(); } public void Initialize(string connectionStringName) { if(creator == null) { var connectionString = ConfigurationManager.ConnectionStrings[connectionStringName]; if (connectionString == null) throw new ArgumentException(string.Format("ConnectionString {0} does not exist!", connectionStringName)); Type dataAccessAdapterType; switch (connectionString.ProviderName) { case "System.Data.SqlClient": dataAccessAdapterType = Type.GetType("MyCorp.ExtendedAdapterMembershipProviders.SqlServer.DatabaseSpecific.DataAccessAdapter, MyCorp.ExtendedAdapterMembershipProviders.SqlServerDBSpecific"); break; case "Devart.Data.MySql": case "MySql.Data.MySqlClient": dataAccessAdapterType = Type.GetType("MyCorp.ExtendedAdapterMembershipProviders.MySql.DatabaseSpecific.DataAccessAdapter, MyCorp.ExtendedAdapterMembershipProviders.MySqlDBSpecific"); break; case "System.Data.OleDb": dataAccessAdapterType = Type.GetType("MyCorp.ExtendedAdapterMembershipProviders.MsAccess.DatabaseSpecific.DataAccessAdapter, MyCorp.ExtendedAdapterMembershipProviders.MsAccessDBSpecific"); break; case "Oracle.DataAccess.Client": dataAccessAdapterType = Type.GetType("MyCorp.ExtendedAdapterMembershipProviders.Oracle.DatabaseSpecific.DataAccessAdapter, MyCorp.ExtendedAdapterMembershipProviders.OracleDBSpecific"); break; default: throw new Exception("Unrecognized provider name on connection string!"); } creator = () => { return (IDataAccessAdapter)Activator.CreateInstance(dataAccessAdapterType, new object[] { connectionString.ConnectionString }); }; } } } }
Step 4 - Create the extended Membership and Role providers
I create the providers in the generic class library that LLBLGen conveniently gives me.
The Role Provider
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration.Provider; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Security; using MyCorp.ExtendedAdapterMembershipProviders.EntityClasses; using MyCorp.ExtendedAdapterMembershipProviders.FactoryClasses; using MyCorp.ExtendedAdapterMembershipProviders.HelperClasses; using SD.LLBLGen.Pro.ORMSupportClasses; namespace MyCorp.ExtendedAdapterMembershipProviders.Providers { public partial class ExtendedAdapterRoleProvider: RoleProvider { #region Constructor(s) private IDataAccessAdapterFactory adapterFactory; public ExtendedAdapterRoleProvider(): this(new DataAccessAdapterFactory()) { } public ExtendedAdapterRoleProvider(IDataAccessAdapterFactory dataAccessAdapterFactory) { this.adapterFactory = dataAccessAdapterFactory; } #endregion #region Initialize public override void Initialize(string name, NameValueCollection config) { if (config == null) throw new ArgumentNullException("config"); if (string.IsNullOrEmpty(name)) name = "ExtendedAdapterRoleProvider"; if (string.IsNullOrEmpty(config["description"])) { config.Remove("description"); config.Add("description", "Adapter Extended Role Provider"); } base.Initialize(name, config); ApplicationName = GetValueOrDefault(config, "applicationName", o => o.ToString(), "MySampleApp"); this.ConnectionStringName = GetValueOrDefault(config, "connectionStringName", o => o.ToString(), string.Empty); adapterFactory.Initialize(ConnectionStringName); config.Remove("name"); config.Remove("description"); config.Remove("applicationName"); config.Remove("connectionStringName"); if (config.Count <= 0) return; var key = config.GetKey(0); if (string.IsNullOrEmpty(key)) return; throw new ProviderException(string.Format(CultureInfo.CurrentCulture, "The role provider does not recognize the configuration attribute {0}.", key)); } public string ConnectionStringName { get; set; } #endregion #region Abstract Property Overrides public override string ApplicationName { get; set; } #endregion #region Abstract Method Overrides public override void AddUsersToRoles(string[] usernames, string[] roleNames) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var users = GetUsers(adapter, usernames); var roles = GetRoles(adapter, roleNames); var userIds = users.Select(u => u.UserId).ToArray(); var roleIds = roles.Select(r => r.RoleId).ToArray(); var bucket = new RelationPredicateBucket(); bucket.PredicateExpression.Add(WebpagesUsersInRoleFields.UserId == userIds); bucket.PredicateExpression.AddWithAnd(WebpagesUsersInRoleFields.RoleId == roleIds); var userroles = new EntityCollection<webpagesusersinroleentity>(new WebpagesUsersInRoleEntityFactory()); adapter.FetchEntityCollection(userroles, bucket); var userroleIds = userroles.Select(u => new { UserId = u.UserId, RoleId = u.RoleId }).ToArray(); foreach (var userId in userIds) { foreach (var roleId in roleIds) { if (!userroleIds.Any(ur => ur.UserId == userId && ur.RoleId == roleId)) userroles.Add(new WebpagesUsersInRoleEntity { UserId = userId, RoleId = roleId }); } } adapter.SaveEntityCollection(userroles); } } public override void CreateRole(string roleName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var role = new WebpagesRoleEntity { RoleName = roleName }; if (adapter.FetchEntityUsingUniqueConstraint(role, role.ConstructFilterForUCRoleName())) throw new ProviderException(string.Format("Role {0} already exists!", roleName)); adapter.SaveEntity(role); } } public override bool DeleteRole(string roleName, bool throwOnPopulatedRole) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var role = GetRoles(adapter, new[] { roleName }).FirstOrDefault(); if (role == null) throw new ProviderException(string.Format("Role {0} does not exist!", roleName)); var bucket = new RelationPredicateBucket(WebpagesUsersInRoleFields.RoleId == role.RoleId); if (throwOnPopulatedRole && adapter.GetDbCount(new EntityCollection<webpagesusersinroleentity>(new WebpagesUsersInRoleEntityFactory()), bucket) > 0) throw new ProviderException(string.Format("Role {0} is not empty!", roleName)); adapter.DeleteEntitiesDirectly(typeof(WebpagesUsersInRoleEntity), bucket); return adapter.DeleteEntity(role); } } public override string[] FindUsersInRole(string roleName, string usernameToMatch) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var users = new EntityCollection<userprofileentity>(new UserProfileEntityFactory()); var prefetchPath = new PrefetchPath2(EntityType.UserProfileEntity); prefetchPath.Add(UserProfileEntity.PrefetchPathRoleMemberships); var filter = new RelationPredicateBucket(); filter.PredicateExpression.AddWithAnd(new FieldLikePredicate(UserProfileFields.UserName, null, usernameToMatch)); filter.PredicateExpression.AddWithAnd(WebpagesRoleFields.RoleName == roleName); adapter.FetchEntityCollection(users, filter, prefetchPath); return users.Select(u => u.UserName).OrderBy(u => u).ToArray(); } } public override string[] GetAllRoles() { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var roles = new EntityCollection<webpagesroleentity>(new WebpagesRoleEntityFactory()); adapter.FetchEntityCollection(roles, null); return roles.Select(r => r.RoleName).OrderBy(r => r).ToArray(); } } public override string[] GetRolesForUser(string username) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = new UserProfileEntity { UserName = username }; if (!adapter.FetchEntity(user, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathRoleMemberships })) throw new ProviderException(string.Format("User {0} does not exist!", username)); return user.RoleMemberships.Select(r => r.RoleName).OrderBy(r => r).ToArray(); } } public override string[] GetUsersInRole(string roleName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var role = new WebpagesRoleEntity { RoleName = roleName }; var prefetchPath = new PrefetchPath2(EntityType.WebpagesRoleEntity); prefetchPath.Add(WebpagesRoleEntity.PrefetchPathUserMembers); if (!adapter.FetchEntityUsingUniqueConstraint(role, role.ConstructFilterForUCRoleName(), prefetchPath)) throw new ProviderException(string.Format("Role {0} does not exist!", roleName)); return role.UserMembers.Select(u => u.UserName).OrderBy(u => u).ToArray(); } } public override bool IsUserInRole(string username, string roleName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] { username }).FirstOrDefault(); var role = GetRoles(adapter, new[] { roleName }).FirstOrDefault(); if (user == null) throw new ProviderException(string.Format("User {0} does not exist!", username)); if (role == null) throw new ProviderException(string.Format("Role {0} does not exist!", roleName)); var bucket = new RelationPredicateBucket(); bucket.PredicateExpression.AddWithAnd(WebpagesUsersInRoleFields.UserId == user.UserId); bucket.PredicateExpression.AddWithAnd(WebpagesUsersInRoleFields.RoleId == role.RoleId); return adapter.GetDbCount(new EntityCollection<webpagesusersinroleentity>(new WebpagesUsersInRoleEntityFactory()), bucket) > 0; } } public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var users = GetUsers(adapter, usernames); var roles = GetRoles(adapter, roleNames); var bucket = new RelationPredicateBucket(); bucket.PredicateExpression.AddWithAnd(WebpagesUsersInRoleFields.UserId == users.Select(u => u.UserId).ToArray()); bucket.PredicateExpression.AddWithAnd(WebpagesUsersInRoleFields.RoleId == roles.Select(u => u.RoleId).ToArray()); adapter.DeleteEntitiesDirectly(typeof(WebpagesUsersInRoleEntity), bucket); } } public override bool RoleExists(string roleName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { return adapter.GetDbCount(new EntityCollection<webpagesusersinroleentity>(new WebpagesUsersInRoleEntityFactory()), new RelationPredicateBucket(WebpagesRoleFields.RoleName == roleName)) > 0; } } #endregion #region Helper Methods private static T GetValueOrDefault<t>(NameValueCollection nvc, string key, Func<object, T> converter, T defaultIfNull) { var val = nvc[key]; if (val == null) return defaultIfNull; try { return converter(val); } catch { return defaultIfNull; } } private EntityCollection<UserProfileEntity> GetUsers(IDataAccessAdapter adapter, string[] usernames, PrefetchPath2 prefetchPath = null) { var users = new EntityCollection<UserProfileEntity>(new UserProfileEntityFactory()); adapter.FetchEntityCollection(users, new RelationPredicateBucket(UserProfileFields.UserName == usernames), prefetchPath); return users; } private EntityCollection<WebpagesRoleEntity> GetRoles(IDataAccessAdapter adapter, string[] rolenames, PrefetchPath2 prefetchPath = null) { var roles = new EntityCollection<WebpagesRoleEntity>(new WebpagesRoleEntityFactory()); adapter.FetchEntityCollection(roles, new RelationPredicateBucket(WebpagesRoleFields.RoleName == rolenames), prefetchPath); return roles; } #endregion } }
The Membership Provider
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration.Provider; using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Web; using System.Web.Helpers; using System.Web.Security; using MyCorp.ExtendedAdapterMembershipProviders.EntityClasses; using MyCorp.ExtendedAdapterMembershipProviders.FactoryClasses; using MyCorp.ExtendedAdapterMembershipProviders.HelperClasses; using SD.LLBLGen.Pro.ORMSupportClasses; using WebMatrix.WebData; namespace MyCorp.ExtendedAdapterMembershipProviders.Providers { public class ExtendedAdapterMembershipProvider: ExtendedMembershipProvider { private const int TokenSizeInBytes = 0x10; #region Constructor(s) private IDataAccessAdapterFactory adapterFactory; public ExtendedAdapterMembershipProvider(): this(new DataAccessAdapterFactory()) { } public ExtendedAdapterMembershipProvider(IDataAccessAdapterFactory dataAccessAdapterFactory) { this.adapterFactory = dataAccessAdapterFactory; } #endregion #region Initialize public override void Initialize(string name, NameValueCollection config) { if (config == null) throw new ArgumentNullException("config"); if (string.IsNullOrEmpty(name)) { name = "ExtendedAdapterMembershipProvider"; } if (string.IsNullOrEmpty(config["description"])) { config.Remove("description"); config.Add("description", "Adapter Extended Membership Provider"); } base.Initialize(name, config); ApplicationName = GetValueOrDefault(config, "applicationName", o => o.ToString(), "MySampleApp"); this.ConnectionStringName = GetValueOrDefault(config, "connectionStringName", o => o.ToString(), string.Empty); adapterFactory.Initialize(ConnectionStringName); this.EnablePasswordRetrievalInternal = GetValueOrDefault(config, "enablePasswordRetrieval", Convert.ToBoolean, false); this.EnablePasswordResetInternal = GetValueOrDefault(config, "enablePasswordReset", Convert.ToBoolean, true); this.RequiresQuestionAndAnswerInternal = GetValueOrDefault(config, "requiresQuestionAndAnswer", Convert.ToBoolean, false); this.RequiresUniqueEmailInternal = GetValueOrDefault(config, "requiresUniqueEmail", Convert.ToBoolean, true); this.MaxInvalidPasswordAttemptsInternal = GetValueOrDefault(config, "maxInvalidPasswordAttempts", Convert.ToInt32, 3); this.PasswordAttemptWindowInternal = GetValueOrDefault(config, "passwordAttemptWindow", Convert.ToInt32, 10); this.PasswordFormatInternal = GetValueOrDefault(config, "passwordFormat", o => { MembershipPasswordFormat format; return Enum.TryParse(o.ToString(), true, out format) ? format : MembershipPasswordFormat.Hashed; }, MembershipPasswordFormat.Hashed); this.MinRequiredPasswordLengthInternal = GetValueOrDefault(config, "minRequiredPasswordLength", Convert.ToInt32, 6); this.MinRequiredNonAlphanumericCharactersInternal = GetValueOrDefault(config, "minRequiredNonalphanumericCharacters", Convert.ToInt32, 1); this.PasswordStrengthRegularExpressionInternal = GetValueOrDefault(config, "passwordStrengthRegularExpression", o => o.ToString(), string.Empty); this.HashAlgorithmType = GetValueOrDefault(config, "hashAlgorithmType", o => o.ToString(), "SHA1"); config.Remove("name"); config.Remove("description"); config.Remove("applicationName"); config.Remove("connectionStringName"); config.Remove("enablePasswordRetrieval"); config.Remove("enablePasswordReset"); config.Remove("requiresQuestionAndAnswer"); config.Remove("requiresUniqueEmail"); config.Remove("maxInvalidPasswordAttempts"); config.Remove("passwordAttemptWindow"); config.Remove("passwordFormat"); config.Remove("minRequiredPasswordLength"); config.Remove("minRequiredNonalphanumericCharacters"); config.Remove("passwordStrengthRegularExpression"); config.Remove("hashAlgorithmType"); if (config.Count <= 0) return; var key = config.GetKey(0); if (string.IsNullOrEmpty(key)) return; throw new ProviderException(string.Format(CultureInfo.CurrentCulture, "The membership provider does not recognize the configuration attribute {0}.", key)); } public string ConnectionStringName { get; set; } public string HashAlgorithmType { get; set; } #endregion #region Abstract Property Overrides public override string ApplicationName { get; set; } public override bool EnablePasswordReset { get { return EnablePasswordResetInternal; } } private bool EnablePasswordResetInternal { get; set; } public override bool EnablePasswordRetrieval { get { return EnablePasswordRetrievalInternal; } } private bool EnablePasswordRetrievalInternal { get; set; } public override int MaxInvalidPasswordAttempts { get { return MaxInvalidPasswordAttemptsInternal; } } private int MaxInvalidPasswordAttemptsInternal { get; set; } public override int MinRequiredNonAlphanumericCharacters { get { return MinRequiredNonAlphanumericCharactersInternal; } } private int MinRequiredNonAlphanumericCharactersInternal { get; set; } public override int MinRequiredPasswordLength { get { return MinRequiredPasswordLengthInternal; } } private int MinRequiredPasswordLengthInternal { get; set; } public override int PasswordAttemptWindow { get { return PasswordAttemptWindowInternal; } } private int PasswordAttemptWindowInternal { get; set; } public override MembershipPasswordFormat PasswordFormat { get { return PasswordFormatInternal; } } private MembershipPasswordFormat PasswordFormatInternal { get; set; } public override string PasswordStrengthRegularExpression { get { return PasswordStrengthRegularExpressionInternal; } } private string PasswordStrengthRegularExpressionInternal { get; set; } public override bool RequiresQuestionAndAnswer { get { return RequiresQuestionAndAnswerInternal; } } private bool RequiresQuestionAndAnswerInternal { get; set; } public override bool RequiresUniqueEmail { get { return RequiresUniqueEmailInternal; } } private bool RequiresUniqueEmailInternal { get; set; } #endregion #region Abstract Method Overrides public override bool ConfirmAccount(string accountConfirmationToken) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var memberships = new EntityCollection(new WebpagesMembershipEntityFactory()); adapter.FetchEntityCollection(memberships, new RelationPredicateBucket(WebpagesMembershipFields.ConfirmationToken == accountConfirmationToken)); var membership = memberships.Where(m => m.ConfirmationToken.Equals(accountConfirmationToken, StringComparison.Ordinal)).FirstOrDefault(); if(membership == null) { return false; } membership.IsConfirmed= true; return adapter.SaveEntity(membership); } } public override bool ConfirmAccount(string userName, string accountConfirmationToken) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] { userName }, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesMembership }).FirstOrDefault(); if (user == null) return false; if (user.WebpagesMembership == null) return false; if (user.WebpagesMembership.ConfirmationToken.Equals(accountConfirmationToken, StringComparison.Ordinal)) { user.WebpagesMembership.IsConfirmed = true; return adapter.SaveEntity(user.WebpagesMembership); } return false; } } public override string CreateAccount(string userName, string password, bool requireConfirmationToken) { if (string.IsNullOrEmpty(password)) { throw new MembershipCreateUserException(MembershipCreateStatus.InvalidPassword); } string hashedPassword = Crypto.HashPassword(password); if (hashedPassword.Length > 0x80) { throw new MembershipCreateUserException(MembershipCreateStatus.InvalidPassword); } if (string.IsNullOrEmpty(userName)) { throw new MembershipCreateUserException(MembershipCreateStatus.InvalidUserName); } using (var adapter = adapterFactory.CreateDataAccessAdapter()) { return this.CreateAccount(adapter, userName, password, requireConfirmationToken); } } public override string CreateUserAndAccount(string userName, string password, bool requireConfirmation, IDictionary<string, object> values) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { this.CreateUser(adapter, userName, values); return this.CreateAccount(adapter, userName, password, requireConfirmation); } } public override bool DeleteAccount(string userName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] { userName }).FirstOrDefault(); if (user == null) return false; return 1 == adapter.DeleteEntitiesDirectly(typeof(WebpagesMembershipEntity), new RelationPredicateBucket(WebpagesMembershipFields.UserId == user.UserId)); } } public override string GeneratePasswordResetToken(string userName, int tokenExpirationInMinutesFromNow) { if (string.IsNullOrEmpty(userName)) { throw new ArgumentException("Username cannot be empty", "username"); } using (var adapter = adapterFactory.CreateDataAccessAdapter()) { bool throwException = true; var userId = this.VerifyUserNameHasConfirmedAccount(adapter, userName, throwException); var user = new UserProfileEntity(userId); adapter.FetchEntity(user, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesMembership }); var membership = user.WebpagesMembership; if (membership.PasswordVerificationTokenExpirationDate.HasValue && membership.PasswordVerificationTokenExpirationDate.Value > DateTime.UtcNow) { return membership.PasswordVerificationToken; } var token = GenerateToken(); membership.PasswordVerificationToken = token; membership.PasswordVerificationTokenExpirationDate = DateTime.UtcNow.AddMinutes((double)tokenExpirationInMinutesFromNow); if (!adapter.SaveEntity(membership)) { throw new ProviderException("Unable to generate password reset token"); } return token; } } public override ICollection GetAccountsForUser(string userName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] { userName }, new PrefetchPath2(EntityType.UserProfileEntity){ UserProfileEntity.PrefetchPathWebpagesOauthMemberships }).FirstOrDefault(); if (user != null) { var list = new List(); foreach (var oauth in user.WebpagesOauthMemberships) { list.Add(new OAuthAccountData(oauth.Provider, oauth.ProviderUserId)); } return list; } } return new OAuthAccountData[0]; } public override DateTime GetCreateDate(string userName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] { userName }, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesMembership }).FirstOrDefault(); if (user != null && user.WebpagesMembership != null && user.WebpagesMembership.CreateDate.HasValue) { return user.WebpagesMembership.CreateDate.Value; } } return DateTime.MinValue; } public override DateTime GetLastPasswordFailureDate(string userName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] { userName }, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesMembership }).FirstOrDefault(); if (user == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "User {0} does not exist!", userName)); } if (user.WebpagesMembership != null && user.WebpagesMembership.LastPasswordFailureDate.HasValue) { return user.WebpagesMembership.LastPasswordFailureDate.Value; } } return DateTime.MinValue; } public override DateTime GetPasswordChangedDate(string userName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] { userName }, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesMembership }).FirstOrDefault(); if (user == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "User {0} does not exist!", userName)); } if (user.WebpagesMembership != null && user.WebpagesMembership.PasswordChangedDate.HasValue) { return user.WebpagesMembership.PasswordChangedDate.Value; } } return DateTime.MinValue; } public override int GetPasswordFailuresSinceLastSuccess(string userName) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[]{userName}).FirstOrDefault(); if (user == null) { throw new InvalidOperationException(string.Format("User {0} does not exist!", userName)); } return GetPasswordFailuresSinceLastSuccess(adapter, user.UserId); } } public override int GetUserIdFromPasswordResetToken(string token) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var users = new EntityCollection(new WebpagesMembershipEntityFactory()); var bucket = new RelationPredicateBucket(WebpagesMembershipFields.PasswordVerificationToken == token); adapter.FetchEntityCollection(users, bucket); if(users.Count == 0) return -1; return users.First().UserId; } } public override bool IsConfirmed(string userName) { if (string.IsNullOrEmpty(userName)) { throw new ArgumentException("Username cannot be empty", "username"); } using (var adapter = adapterFactory.CreateDataAccessAdapter()) { bool throwException = false; return (this.VerifyUserNameHasConfirmedAccount(adapter, userName, throwException) != -1); } } public override bool ResetPasswordWithToken(string token, string newPassword) { if (string.IsNullOrEmpty(newPassword)) { throw new ArgumentException("NewPassword cannot be empty", "newPassword"); } using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var memberships = new EntityCollection(new WebpagesMembershipEntityFactory()); var bucket = new RelationPredicateBucket(); bucket.PredicateExpression.AddWithAnd(WebpagesMembershipFields.PasswordVerificationToken == token); bucket.PredicateExpression.AddWithAnd(new FieldCompareValuePredicate(WebpagesMembershipFields.PasswordVerificationTokenExpirationDate, null, ComparisonOperator.GreaterThan, DateTime.UtcNow)); adapter.FetchEntityCollection(memberships, bucket); if (memberships.Count == 1) { var membership = memberships[0]; var passwordSet = SetPassword(adapter, memberships[0].UserId, newPassword); if (passwordSet) { membership.PasswordVerificationToken = null; membership.PasswordVerificationTokenExpirationDate = null; if (!adapter.SaveEntity(membership)) throw new ProviderException("Unable to reset password with token!"); } return passwordSet; } return false; } } public override bool ChangePassword(string username, string oldPassword, string newPassword) { if (string.IsNullOrEmpty(username)) { throw new ArgumentException("Username cannot be empty", "username"); } if (string.IsNullOrEmpty(oldPassword)) { throw new ArgumentException("OldPassword cannot be empty", "oldPassword"); } if (string.IsNullOrEmpty(newPassword)) { throw new ArgumentException("NewPassword cannot be empty", "newPassword"); } using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[]{username}).FirstOrDefault(); if (user == null) { return false; } if (!this.CheckPassword(adapter, user.UserId, oldPassword)) { return false; } return SetPassword(adapter, user.UserId, newPassword); } } public override bool DeleteUser(string username, bool deleteAllRelatedData) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] { username }).FirstOrDefault(); if(user == null) { return false; } if (deleteAllRelatedData) { //TODO: delete some stuff here } var userId = user.UserId; adapter.DeleteEntitiesDirectly(typeof(WebpagesOauthMembershipEntity), new RelationPredicateBucket(WebpagesOauthMembershipFields.UserId == userId)); adapter.DeleteEntitiesDirectly(typeof(WebpagesMembershipEntity), new RelationPredicateBucket(WebpagesMembershipFields.UserId == userId)); return 1 == adapter.DeleteEntitiesDirectly(typeof(UserProfileEntity), new RelationPredicateBucket(UserProfileFields.UserId == userId)); } } public override System.Web.Security.MembershipUser GetUser(string username, bool userIsOnline) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = GetUsers(adapter, new[] {username}).FirstOrDefault(); if (user == null) { return null; } return new MembershipUser(Membership.Provider.Name, username, user.UserId, null, null, null, true, false, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue); } } public override bool ValidateUser(string username, string password) { if (string.IsNullOrEmpty(username)) { throw new ArgumentException("Username cannot be empty", "username"); } if (string.IsNullOrEmpty(password)) { throw new ArgumentException("Password cannot be empty", "password"); } using (var adapter = adapterFactory.CreateDataAccessAdapter()) { bool throwException = false; int userId = this.VerifyUserNameHasConfirmedAccount(adapter, username, throwException); if (userId == -1) { return false; } return this.CheckPassword(adapter, userId, password); } } #endregion #region Required ExtendedMembershipProvider Overrides public override string CreateAccount(string userName, string password) { // let the base class handle this one return base.CreateAccount(userName, password); } public override void CreateOrUpdateOAuthAccount(string provider, string providerUserId, string userName) { if (string.IsNullOrEmpty(userName)) { throw new ArgumentException("Username cannot be empty", "username"); } using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var user = this.GetUsers(adapter, new[] { userName }, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesOauthMemberships }).FirstOrDefault(); if (user == null) { user = new UserProfileEntity { UserName = userName }; adapter.SaveEntity(user); user = this.GetUsers(adapter, new[] { userName }, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesOauthMemberships }).FirstOrDefault(); //throw new MembershipCreateUserException(MembershipCreateStatus.InvalidUserName); } var oAuth = user.WebpagesOauthMemberships.FirstOrDefault(o => o.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase) && o.ProviderUserId.Equals(providerUserId, StringComparison.OrdinalIgnoreCase)); if (oAuth == null) { oAuth = new WebpagesOauthMembershipEntity { ProviderUserId = providerUserId, Provider = provider, UserId = user.UserId }; if(!adapter.SaveEntity(oAuth)) { throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError); } } else { oAuth.UserId = user.UserId; if(!adapter.SaveEntity(oAuth)) { throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError); } } } } public override string CreateUserAndAccount(string userName, string password) { // let the base class handle this one return base.CreateUserAndAccount(userName, password); } public override string CreateUserAndAccount(string userName, string password, bool requireConfirmation) { // let the base class handle this one return base.CreateUserAndAccount(userName, password, requireConfirmation); } public override string CreateUserAndAccount(string userName, string password, IDictionary<string, object> values) { // let the base class handle this one return base.CreateUserAndAccount(userName, password, values); } public override void DeleteOAuthAccount(string provider, string providerUserId) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var filter = new RelationPredicateBucket(); var compare1 = new FieldCompareValuePredicate(WebpagesOauthMembershipFields.Provider, null, ComparisonOperator.Equal, provider); compare1.CaseSensitiveCollation = true; // makes it case-insensitive var compare2 = new FieldCompareValuePredicate(WebpagesOauthMembershipFields.ProviderUserId, null, ComparisonOperator.Equal, providerUserId); compare2.CaseSensitiveCollation = true; // makes it case-insensitive filter.PredicateExpression.AddWithAnd(compare1); filter.PredicateExpression.AddWithAnd(compare2); if(1 != adapter.DeleteEntitiesDirectly(typeof(WebpagesOauthMembershipEntity), filter)) { throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError); } } } public override void DeleteOAuthToken(string token) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { adapter.DeleteEntitiesDirectly(typeof(WebpagesOauthTokenEntity), new RelationPredicateBucket(WebpagesOauthTokenFields.Token == token)); } } public override string GeneratePasswordResetToken(string userName) { // let the base class handle this one return base.GeneratePasswordResetToken(userName); } public override string GetOAuthTokenSecret(string token) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var oauthToken = new WebpagesOauthTokenEntity(token); if (adapter.FetchEntity(oauthToken)) return oauthToken.Secret; return null; } } public override int GetUserIdFromOAuth(string provider, string providerUserId) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var filter = new RelationPredicateBucket(); var compare1 = new FieldCompareValuePredicate(WebpagesOauthMembershipFields.Provider, null, ComparisonOperator.Equal, provider); compare1.CaseSensitiveCollation = true; // makes it case-insensitive var compare2 = new FieldCompareValuePredicate(WebpagesOauthMembershipFields.ProviderUserId, null, ComparisonOperator.Equal, providerUserId); compare2.CaseSensitiveCollation = true; // makes it case-insensitive filter.PredicateExpression.AddWithAnd(compare1); filter.PredicateExpression.AddWithAnd(compare2); var oauthMemberships = new EntityCollection(new WebpagesOauthMembershipEntityFactory()); adapter.FetchEntityCollection(oauthMemberships, filter); if(oauthMemberships.Count != 1) return -1; return oauthMemberships.Single().UserId; } } public override string GetUserNameFromId(int userId) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var userProfile = new UserProfileEntity(userId); if (adapter.FetchEntity(userProfile)) return userProfile.UserName; return null; } } public override bool HasLocalAccount(int userId) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var userProfile = new UserProfileEntity(userId); if (adapter.FetchEntity(userProfile, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesMembership })) return userProfile.WebpagesMembership != null; return false; } } public override void ReplaceOAuthRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { adapter.DeleteEntitiesDirectly(typeof(WebpagesOauthTokenEntity), new RelationPredicateBucket(WebpagesOauthTokenFields.Token == requestToken)); this.StoreOAuthRequestToken(accessToken, accessTokenSecret); } } public override void StoreOAuthRequestToken(string requestToken, string requestTokenSecret) { using (var adapter = adapterFactory.CreateDataAccessAdapter()) { var tokenEntity = new WebpagesOauthTokenEntity(requestToken); if (adapter.FetchEntity(tokenEntity) && tokenEntity.Secret == requestTokenSecret) { return; } tokenEntity.Secret = requestTokenSecret; if (!adapter.SaveEntity(tokenEntity)) { throw new ProviderException("Unable to store OAuth token"); } return; } } #endregion #region Helper Methods private EntityCollection GetUsers(IDataAccessAdapter adapter, string[] usernames, PrefetchPath2 prefetchPath = null) { var users = new EntityCollection(new UserProfileEntityFactory()); adapter.FetchEntityCollection(users, new RelationPredicateBucket(UserProfileFields.UserName == usernames), prefetchPath); return users; } private void CreateUser(IDataAccessAdapter adapter, string userName, IDictionary<string, object> values) { if (GetUsers(adapter, new[] { userName }) != null) { throw new MembershipCreateUserException(MembershipCreateStatus.DuplicateUserName); } var user = new UserProfileEntity { UserName = userName }; foreach (var value in values) { if (value.Key.Equals("username", StringComparison.OrdinalIgnoreCase)) continue; var field = user.Fields.SingleOrDefault(f => f.Name.Equals(value.Key, StringComparison.OrdinalIgnoreCase)); if (field != null) { user.Fields[field.FieldIndex].CurrentValue = value.Value; } } if (!adapter.SaveEntity(user)) { throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError); } } private string CreateAccount(IDataAccessAdapter adapter, string userName, string password, bool requireConfirmationToken) { if (string.IsNullOrEmpty(password)) { throw new MembershipCreateUserException(MembershipCreateStatus.InvalidPassword); } string hashedPassword = Crypto.HashPassword(password); if (hashedPassword.Length > 0x80) { throw new MembershipCreateUserException(MembershipCreateStatus.InvalidPassword); } if (string.IsNullOrEmpty(userName)) { throw new MembershipCreateUserException(MembershipCreateStatus.InvalidUserName); } var user = GetUsers(adapter, new[] { userName }, new PrefetchPath2(EntityType.UserProfileEntity) { UserProfileEntity.PrefetchPathWebpagesMembership }).FirstOrDefault(); if (user == null) { throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError); } if (user.WebpagesMembership != null) { throw new MembershipCreateUserException(MembershipCreateStatus.DuplicateUserName); } string token = null; if (requireConfirmationToken) { token = GenerateToken(); } var membership = new WebpagesMembershipEntity { UserId = user.UserId, Password = hashedPassword, PasswordSalt = string.Empty, IsConfirmed = !requireConfirmationToken, ConfirmationToken = token, CreateDate = DateTime.UtcNow, PasswordChangedDate = DateTime.UtcNow, PasswordFailuresSinceLastSuccess = 0 }; if (!adapter.SaveEntity(membership)) { throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError); } return token; } private bool SetPassword(IDataAccessAdapter adapter,int userId,string newPassword) { var hashedPassword = Crypto.HashPassword(newPassword); if (hashedPassword.Length > 0x80) { throw new ArgumentException("Password is too long!"); } var membership = new WebpagesMembershipEntity { Password = hashedPassword, PasswordSalt = string.Empty, PasswordChangedDate = DateTime.UtcNow }; return adapter.UpdateEntitiesDirectly(membership, new RelationPredicateBucket(WebpagesMembershipFields.UserId == userId)) > 0; } private bool CheckPassword(IDataAccessAdapter adapter,int userId,string password) { var hashedPassword = this.GetHashedPassword(adapter, userId); var matches = (hashedPassword != null) && Crypto.VerifyHashedPassword(hashedPassword, password); if (matches) { var membership = new WebpagesMembershipEntity { PasswordFailuresSinceLastSuccess = 0 }; adapter.UpdateEntitiesDirectly(membership, new RelationPredicateBucket(WebpagesMembershipFields.UserId == userId)); return matches; } int passwordFailuresSinceLastSuccess = GetPasswordFailuresSinceLastSuccess(adapter, userId); if (passwordFailuresSinceLastSuccess != -1) { var membership = new WebpagesMembershipEntity { PasswordFailuresSinceLastSuccess = passwordFailuresSinceLastSuccess + 1, LastPasswordFailureDate = DateTime.UtcNow }; adapter.UpdateEntitiesDirectly(membership, new RelationPredicateBucket(WebpagesMembershipFields.UserId == userId)); } return matches; } private int GetPasswordFailuresSinceLastSuccess(IDataAccessAdapter adapter,int userId) { var membership = new WebpagesMembershipEntity(userId); if(!adapter.FetchEntity(membership)) return -1; return membership.PasswordFailuresSinceLastSuccess; } private string GetHashedPassword(IDataAccessAdapter adapter, int userId) { var membership = new WebpagesMembershipEntity(userId); if(!adapter.FetchEntity(membership)) return null; return membership.Password; } private static T GetValueOrDefault(NameValueCollection nvc, string key, Func < object , T> converter, T defaultIfNull) { var val = nvc[key]; if (val == null) return defaultIfNull; try { return converter(val); } catch { return defaultIfNull; } } private string GenerateToken() { using (var provider = new RNGCryptoServiceProvider()) { return GenerateToken(provider); } } internal static string GenerateToken(RandomNumberGenerator generator) { var data = new byte[0x10]; generator.GetBytes(data); return HttpServerUtility.UrlTokenEncode(data); } private int VerifyUserNameHasConfirmedAccount(IDataAccessAdapter adapter, string username, bool throwException) { var user = GetUsers(adapter, new[] { username }).FirstOrDefault(); if (user == null) { if (throwException) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "User {0} does not exist!", new object[] { username })); } return -1; } var bucket = new RelationPredicateBucket(WebpagesMembershipFields.UserId == user.UserId); bucket.PredicateExpression.AddWithAnd(WebpagesMembershipFields.IsConfirmed == true); var count = adapter.GetDbCount(new EntityCollection(new WebpagesMembershipEntityFactory()), bucket); if (count != 0) { return user.UserId; } if (throwException) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "User {0} does not exist!", new object[] { username })); } return -1; } #endregion #region Unsupported methods in the SimpleMembershipProvider model public override System.Web.Security.MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out System.Web.Security.MembershipCreateStatus status) { throw new NotSupportedException(); } public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { throw new NotSupportedException(); } public override System.Web.Security.MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { throw new NotSupportedException(); } public override System.Web.Security.MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { throw new NotSupportedException(); } public override System.Web.Security.MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { throw new NotSupportedException(); } public override int GetNumberOfUsersOnline() { throw new NotSupportedException(); } public override string GetPassword(string username, string answer) { throw new NotSupportedException(); } public override System.Web.Security.MembershipUser GetUser(object providerUserKey, bool userIsOnline) { throw new NotSupportedException(); } public override string GetUserNameByEmail(string email) { throw new NotSupportedException(); } public override string ResetPassword(string username, string answer) { throw new NotSupportedException(); } public override bool UnlockUser(string userName) { throw new NotSupportedException(); } public override void UpdateUser(System.Web.Security.MembershipUser user) { throw new NotSupportedException(); } #endregion } }
Step 5 - Register the providers in web.config
All we have to do now is register the new providers in web.config.<membership defaultProvider="ExtendedAdapterMembershipProvider"> <providers> <add name="ExtendedAdapterMembershipProvider" type="MyCorp.ExtendedAdapterMembershipProviders.Providers.ExtendedAdapterMembershipProvider, MyCorp.ExtendedAdapterMembershipProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" connectionStringName="ConnectionString.SQL Server (SqlClient)" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="true" maxInvalidPasswordAttempts="3" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="1" passwordAttemptWindow="10" applicationName="/" /> </providers> </membership> <roleManager defaultProvider="ExtendedAdapterRoleProvider"> <providers> <add name="ExtendedAdapterRoleProvider" type="MyCorp.ExtendedAdapterMembershipProviders.Providers.ExtendedAdapterRoleProvider, MyCorp.ExtendedAdapterMembershipProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" connectionStringName="ConnectionString.SQL Server (SqlClient)" applicationName="/" /> </providers> </roleManager>The connectionStringName attribute is what the custom providers use to find the appropriate connection string in the connectionstrings section of the config file, which in turn have the providerName attribute discussed earlier. The provider uses the exact same connection string datasource as the initial database used by the out-of-the-box simple membership provider and does everything the default provider does. One caviat! The out-of-the-box implementation creates the tables for you (not something I'm particularly fond of). This implementation described in this post has you pre-generate the database tables with your own SQL. The SQL scripts to create the SQL Server, MySQL tables are included in the download. I also provide an access database with the tables as well in the download. I also had to make 2 changes to the AccountController.cs file that is shipped with the MVC project template. First, I comment out the [InitializeSimpleMembership] attribute on the controller, which creates the database using EntityFramework (we don't want that anymore), and there's a section of code on line 266 through 286 that also uses the EntityFramework "UsersContext" which does not apply and was refactored away in our custom provider implementation. Check out the code for more details on this.
Step 6 - Enjoy
Enjoy your application built-on top of whichever database you are using. I hope this was interesting, it certainly was a lot of fun prototyping this last night.[button link="http://sdrv.ms/178k4no" color="primary" target="_blank" size="large" title="Code Download" icon_before="download"]Code Download[/button]
Cheers!
Thanks, Matt!
Following this article, I was able to create an extended SimpleMembership provider for my Oracle-backed site! This was a huge help, as I’ve not been able to find any other resources explaining how to create a provider for Oracle.
However, there were a few caveats I found along the way that I thought I might pass on to other readers trying to do the same:
– Had to rename the following columns on the WebpagesMembership table and update mappings in “MyCorp.ExtendedAdapterMembershipProviders.Oracle.DatabaseSpecific.PersistenceInfoProvider” due to a 30-character limit on field identifiers in Oracle
– PasswordFailuresSinceLastSuccess : PwdFailsSinceLastSuccess
– PasswordVerificationTokenExpirationDate : PwdVerifyTokenExprDate
– Had to manually create the triggers for inserting the Sequenced Primary Key values on the UserProfile and WebpagesRole tables because LLBLGen did not generate these scripts for me. (I assume this was not a problem when generating the scripts for other database solutions)
– Had to generate a custom PasswordSalt and re-factor the CreateAccount, SetPassword, and CheckPassword methods in “MyCorp.ExtendedAdapterMembershipProviders.Providers.ExtendedAdapterMembershipProvider” to use this salt value, as the included code was inserting an empty string into the PasswordSalt column, even though this column was marked with a NOT NULL constraint. The code was also not salting the password before hashing it.
Great feedback Dan, thanks!
Can you upload your final working LLBL Project?
[…] https://www.mattjcowan.com/funcoding/2012/11/10/simplemembershipprovider-in-mvc4-for-mysql-oracle-and… […]
Just added a new version of the code that uses the free version of LLBLGen – LLBLGen v4 Lite.
Great article!
I am using VS2012 and LLBL 2.6 and get load errors with the MySQL & SS projects. Are you using the newer version of LLBL? The errors I am getting:
C:\Downloads\SimpleMembershipProvider-LLBL\llblgen_extendedproviders\MyCorp.ExtendedAdapterMembershipProviders.Generated\DbSpecific\MySql (MySqlDirect)\MyCorp.ExtendedAdapterMembershipProviders.MySqlDBSpecific.csproj : error : Cannot evaluate the item metadata “%(FullPath)”. The item metadata “%(FullPath)” cannot be applied to the path “MySql (MySqlDirect)\DatabaseSpecific\bin\Debug\MyCorp.ExtendedAdapterMembershipProviders.MySqlDBSpecific.pdb”. C:\Downloads\SimpleMembershipProvider-LLBL\llblgen_extendedproviders\MyCorp.ExtendedAdapterMembershipProviders.Generated\DbSpecific\MySql (MySqlDirect)\MySql (MySqlDirect)\DatabaseSpecific\bin\Debug\MyCorp.ExtendedAdapterMembershipProviders.MySqlDBSpecific.pdb C:\Windows\Microsoft.NET\Framework\v4.0.30319\Microsoft.Common.targets
C:\Downloads\SimpleMembershipProvider-LLBL\llblgen_extendedproviders\MyCorp.ExtendedAdapterMembershipProviders.Generated\DbSpecific\SQL Server (SqlClient)\MyCorp.ExtendedAdapterMembershipProviders.SqlServerDBSpecific.csproj : error : Cannot evaluate the item metadata “%(FullPath)”. The item metadata “%(FullPath)” cannot be applied to the path “SQL Server (SqlClient)\DatabaseSpecific\bin\Debug\MyCorp.ExtendedAdapterMembershipProviders.SqlServerDBSpecific.pdb”. C:\Downloads\SimpleMembershipProvider-LLBL\llblgen_extendedproviders\MyCorp.ExtendedAdapterMembershipProviders.Generated\DbSpecific\SQL Server (SqlClient)\SQL Server (SqlClient)\DatabaseSpecific\bin\Debug\MyCorp.ExtendedAdapterMembershipProviders.SqlServerDBSpecific.pdb C:\Windows\Microsoft.NET\Framework\v4.0.30319\Microsoft.Common.targets
Can’t figure out what I am doing wrong. Any thoughts?
Thanks,
Raman
Not sure Raman. I’m using llblgen 3.5, the latest. I just re-downloaded the code to make sure, updated the packages and it compiled on the first go. Is this after you regenerated the code from LLBLGen, or using the downloaded code without alteration? Maybe your *.csproj file got messed up somehow 🙁
hi Matt.
very useful and well written article!
I am an MVC 4 Entity Framework developer with experience implementing simplemembership on sql server. But my company is switching from SQL Server to Oracle and so I need to implement SimpleMembership on Oracle for some existing projects and some new ones.
I’ll need to convince my boss I need a license to buy LLBLGEN. It seems to me I need this license to provide the level of abstraction I need in order to implement SimpleMembship on Oracle because I find no other way. I search Google but not find provider example for Oracle simplemembership.
Is this the case or do I need to look and research more carefully?
Any information you can provide will be useful. I’m hopeful of persuading my boss to buy license because to me it seems to make my life easy. As I understand it license is $299 right? Is this annual license or forever license?
kind regards
Rob
Hi Rob,
You can’t go wrong with getting an LLBLGen Pro license (in fact get licenses for all your devs if you can). As a consultant, I’ve helped several companies adopt LLBLGen and none of them have regretted it. It will give you a leap in productivity, consistency in your APIs, and you’ll find you’ll be able (IMHO) to deliver on new business requirements so much faster (when the requirements relate to connecting to a database especially). I use LLBLGen Pro for much more then just ORM, I use LLBLGen Pro to generate web apis, user interfaces in SharePoint and pure HTML interfaces for web apps, there’s a lot you can do once you learn the templating system (which is easy btw). Using LLBLGen Pro for SimpleMembership support in Oracle is a good way to get LLBLGen in the door at your company (or even just in your toolbox), but there are so many other selling points.
As far as SimpleMembership with Oracle, if that’s all you need, you could also look into the DevArt dotConnect provider for Oracle, which recently added built-in support for the new membership API (see: http://forums.devart.com/viewtopic.php?f=1&t=26101).
As far as LLBLGen Pro pricing, their prices are in Euros, so the actual cost will be slightly higher (see here). It’s a forever license as far as I know for the major release you are purchasing. If you buy now, based on their website, you’ll get the v4.0 for free which will be integrated into Visual Studio and is due out shortly.
Hope that helps!
Matt
Matt Cowan
It looks like Kellerman Software has a MySQL LINQ Provider:
https://www.kellermansoftware.com/p-47-net-data-access-layer.aspx
I’ve used some of your code to quickly develop SimpleMembershipProvider for MongoDB. I’ve added reference to your site.
Thanks.
Very nice 🙂
it’s doesn’t’ work too many compilation errors
Hey Luis, sorry you couldn’t get it to compile. I also emailed you directly to see if I could help.
2 step process:
1. Make sure you have the LLBLGen binaries on the machine you are trying to compile this on. Typically, these would be in your GAC, otherwise, move them to your box and reference them in the “MyCorp” projects.
2. Install all the missing packages using Nuget. You can get Nuget here. Once installed, in VS.NET, go to Tools > Library Package Manager > Package Manager Console.
Then type the following commands (with the solution open) at the PM> prompt:
nuget update -self
mkdir packages
nuget install .\MyMvcApplication\packages.config -OutputDirectory .\packages
You should now be able to compile the project.
I like it!
Nice 🙂
excellent article, as always! 🙂
Thanks Frans!
[…] SimpleMembershipProvider in MVC4 for MySql, Oracle, and more with LLBLGen (Matt Cowan) […]