Introduction
The intent of this article is to provide an implementation of Microsoft’s Membership (and Role) provider using Fluent Nhibernate. Fluent Nhibernate offers a simplified implementation of Nhibernate without using the XML(hbm) mapping files. Click here for more information on Fluent NHibernate.
I am new to Fluent Nhibernate (and to Nhibernate for that matter) so if you find any glaring mistakes in the code, please let me know and I will try to fix those.
Background
For background on Fluent hibernate, please read the Fluent NHibernate Wiki.
Environment
This application was developed using VS2008 and SQL server 2005 and the code is written in C#. This application should also work in VS2005 although I have not tested it in VS2005.
Basics
Microsoft’s Membership Provider provides an easy way to integrate user management in ASP.NET applications. However if you are not using MS-SQL server or don't want to deal with scores of tables and stored procedures that the default provides creates, you would have to provide a custom implementation of the provider to suit your needs.
This article explains a basic Fluent Nhibernate implementation of Membership and Role provider. We are going to implement a simplified provider that only uses a subset of tables that the actual provider is using which is sufficient for most of the applications.
Database
In this implementation, I am using only three tables Users
, Roles
and UsersInRoles
. The structure and definition of the tables is shown below:
“Users
” table has a listing of all the users in your system, and “Role
” table has all the roles. “UsersInRoles
” is a Many to Many table which has Users
and role
s associated. In Microsoft’s implementation of the provider, the primary keys are GUIDs, but in our case I have stuck with integers which sound simpler to me.
Database scripts are included as part of the source code.
Fluent NHibernate Implementation
If you are not familiar with Fluent Nhibernate, I would suggest going through the basic sample project to understand this section.This is how the Fluent NHibernate solution looks and some explanations follow:
Entities
Entities contain our business domain representation of the Users and Roles. Users has a constructor and methods for adding and removing the Roles. These methods also add/remove from the inverse relationships. Users hold a reference to the roles it has, a role holds reference to how many users are associated with the role. Utils.MinDate()
is used here to set a default min date for the database (SQL server in this case).
public class Users
{
public virtual int Id { get; private set; }
public virtual string Username { get; set; }
public virtual string ApplicationName { get; set; }
public virtual string Email { get; set; }
public virtual string Comment { get; set; }
public virtual string Password { get; set; }
public virtual string PasswordQuestion { get; set; }
public virtual string PasswordAnswer { get; set; }
public virtual bool IsApproved { get; set; }
public virtual DateTime LastActivityDate { get; set; }
public virtual DateTime LastLoginDate { get; set; }
public virtual DateTime LastPasswordChangedDate { get; set; }
public virtual DateTime CreationDate { get; set; }
public virtual bool IsOnLine { get; set; }
public virtual bool IsLockedOut { get; set; }
public virtual DateTime LastLockedOutDate { get; set; }
public virtual int FailedPasswordAttemptCount { get; set; }
public virtual int FailedPasswordAnswerAttemptCount { get; set; }
public virtual DateTime FailedPasswordAttemptWindowStart { get; set; }
public virtual DateTime FailedPasswordAnswerAttemptWindowStart { get; set; }
public virtual IList<roles> Roles { get; set; }
public Users()
{
this.CreationDate = Utils.MinDate();
this.LastPasswordChangedDate = Utils.MinDate();
this.LastActivityDate = Utils.MinDate();
this.LastLockedOutDate = Utils.MinDate();
this.FailedPasswordAnswerAttemptWindowStart = Utils.MinDate();
this.FailedPasswordAttemptWindowStart = Utils.MinDate();
this.LastLoginDate = Utils.MinDate();
}
public virtual void AddRole(Roles role)
{
role.UsersInRole.Add(this);
Roles.Add(role);
}
public virtual void RemoveRole(Roles role)
{
role.UsersInRole.Remove(this);
Roles.Remove(role);
}
}
Similar entity classes exist for Role
(see the project file for details).
Mappings
Mappings folder contains the Fluent Nhibernate equivalent of the XML mappings. In UsersMap
“Has ManyToMany” identifies the relationship to roles via the “UsersInRoles
” table. Conversely in RolesMap HasManyToMany
has an inverse relationship to “UsersInRoles
”.
public class UsersMap: ClassMap<users>
{
public UsersMap()
{
Id(x => x.Id);
Map(x => x.Username);
Map(x => x.ApplicationName);
Map(x => x.Email);
Map(x => x.Comment);
Map(x => x.Password);
Map(x => x.PasswordQuestion);
Map(x => x.PasswordAnswer);
Map(x => x.IsApproved);
Map(x => x.LastActivityDate);
Map(x => x.LastLoginDate);
Map(x => x.LastPasswordChangedDate);
Map(x => x.CreationDate);
Map(x => x.IsOnLine);
Map(x => x.IsLockedOut);
Map(x => x.LastLockedOutDate);
Map(x => x.FailedPasswordAttemptCount);
Map(x => x.FailedPasswordAnswerAttemptCount);
Map(x => x.FailedPasswordAttemptWindowStart);
Map(x => x.FailedPasswordAnswerAttemptWindowStart);
HasManyToMany(x => x.Roles )
.Cascade.All()
.Table("UsersInRoles");
}
}
Similar mapping classes exist for Role
(see the solution file for details).
Membership Provider
The actual implementation of the membership provider is in the class FNHMembershipProvider
. This class inherits from the Security.MembershipProvider
class. It provides overrides for all the methods available in the base class. In this class, I have replaced all the data access code with Fluent Data access code via the NHibernate session factory. There are several other methods that are to be implemented in the class, here we will look at two of them. In the GetUserNameByEmail
, we see an example of how we query by criteria using Nhibernate, returning strongly typed Entity Users:
public override string GetUserNameByEmail(string email)
{
Entities.Users usr = null;
using (ISession session = SessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
try
{
usr = session.CreateCriteria(typeof(Entities.Users))
.Add(NHibernate.Criterion.Restrictions.Eq("Email", email))
.UniqueResult < Entities.Users>();
}
catch (Exception e)
{
if(WriteExceptionsToEventLog)
WriteToEventLog(e, "GetUserNameByEmail");
throw new ProviderException(exceptionMessage);
}
}
}
if (usr == null)
return string.Empty;
else
return usr.Username; ;
}
CreateUser
procedure creates a new membership user, it uses Session.Save
to save the user information to the database:
public override MembershipUser CreateUser(string username,
string password,
string email,
string passwordQuestion,
string passwordAnswer,
bool isApproved,
object providerUserKey,
out MembershipCreateStatus status)
{
ValidatePasswordEventArgs args =
new ValidatePasswordEventArgs(username, password,true);
OnValidatingPassword(args);
if (args.Cancel)
{
status = MembershipCreateStatus.InvalidPassword;
return null;
}
if (RequiresUniqueEmail && GetUserNameByEmail(email) != "")
{
status = MembershipCreateStatus.DuplicateEmail;
return null;
}
MembershipUser u = GetUser(username, false);
if (u == null)
{
DateTime createDate = DateTime.Now;
using (ISession session = SessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Entities.Users user = new Entities.Users();
user.Username = username;
user.Password = EncodePassword(password);
user.Email = email;
user.PasswordQuestion = passwordQuestion;
user.PasswordAnswer = EncodePassword(passwordAnswer);
user.IsApproved = isApproved;
user.Comment = "";
user.CreationDate = createDate;
user.LastPasswordChangedDate = createDate;
user.LastActivityDate = createDate;
user.ApplicationName = _applicationName;
user.IsLockedOut = false;
user.LastLockedOutDate = createDate;
user.FailedPasswordAttemptCount = 0;
user.FailedPasswordAttemptWindowStart = createDate;
user.FailedPasswordAnswerAttemptCount = 0;
user.FailedPasswordAnswerAttemptWindowStart = createDate;
try
{
int retId = (int)session.Save(user);
transaction.Commit();
if ((retId <1))
status = MembershipCreateStatus.UserRejected;
else
status = MembershipCreateStatus.Success;
}
catch(Exception e)
{
status = MembershipCreateStatus.ProviderError;
if(WriteExceptionsToEventLog)
WriteToEventLog(e, "CreateUser");
}
}
}
return GetUser(username, false);
}
else
status = MembershipCreateStatus.DuplicateUserName;
return null;
}
RoleProvider
The implementation of Role provider FNHRoleProvider
involves overriding the Security.RoleProvider
class. It has methods for creating/deleting roles, for adding/removing users to/from roles and querying roles. The method shown below creates a role based on a given role name.
public override void CreateRole(string rolename)
{
if (rolename.Contains(","))
throw new ArgumentException("Role names cannot contain commas.");
if (RoleExists(rolename))
throw new ProviderException("Role name already exists.");
using (ISession session = SessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
try
{
Entities.Roles role = new Entities.Roles();
role.ApplicationName = this.ApplicationName;
role.RoleName = rolename;
session.Save(role);
transaction.Commit();
}
catch (OdbcException e)
{
if (WriteExceptionsToEventLog)
WriteToEventLog(e, "CreateRole");
else
throw e;
}
}
}
}
Each of the providers has an initialize
method which reads the configuration values from the web.config file that initializes each provider with the default settings.
SessionHelper
This
static
class is used to configure the connection to the database and create a session factory. I am using a connection to a SQL Server 2005, but you can easily change the database to any other supported database (MySql, Oracle, Jet, MsSqlCe, PostGre, SqlLite) without any other code changes. That’s the beauty of Nhibernate. You can read more about the database configurations in Fluent Nhibernate
here.
public static ISessionFactory CreateSessionFactory(string connstr)
{
return Fluently.Configure()
.Database(FluentNHibernate.Cfg.Db.MsSqlConfiguration.MsSql2005
.ConnectionString(connstr)
)
.Mappings(m =>
m.FluentMappings.AddFromAssemblyOf
<inct.fnhproviders.membership.fnhmembershipprovider>())
.BuildSessionFactory();
}
Sample Application
This is a sample application (FNHCustomProviders.SampleApp
) which demonstrates the use of the Fluent Nhibernate Membership provider. The application contains simple aspx pages which are used to:
- Create User
- Manage password/forgot password
- Manage users and roles
The Manage User and Roles page allows you to add/delete a role and assign/remove users to/from roles.
Putting It All Together
- Database: Creates the database and tables using the script
- FNH Membership Provider: Compiles as INCT.FNHProviders.dll. You should have the following supporting assemblies present in your bin folder for a successful build.
- Antlr3.Runtime.dll
- Castle.Core.dll
- Castle.Core.xml
- Castle.DynamicProxy2.dll
- Castle.DynamicProxy2.xml
- FluentNHibernate.dll
- Iesi.Collections.dll
- Iesi.Collections.xml
- INCT.FNHProviders.dll
- log4net.dll
- log4net.xml
- nhibernate-mapping.xsd
- NHibernate.ByteCode.Castle.dll
- NHibernate.ByteCode.Castle.xml
- NHibernate.dll
- NHibernate.xml
FNHCustomProviders.SampleApp
: Needs a reference to FluentNHibernate
and FNHCustomProviders
. You should have the following sections in the webconfig set up for the app to run. This should go inside system.web section. - If you want to use the email password feature, then mail settings section needs to be there:
- Lastly, you need to change the connection string to point to your own database.
Updates
I have added a profile provider to the existing project. The profile I have implemented is simple but suffices for most needs. It is added to the project as a separate zip file MembershipWithPorfileProvider-Part2.zip and includes the membership, role and profile provider. This also contains the web-config section to be modified and the additional table for profile. Look for other settings.txt in the zip. It contains the web-config changes and the table scripts.
Updates Profile provider
I realized there were some questions about usage of profile provider. I added a zip file with usage sample.
Two things need to be added a)In your project add UserProfileBase class (usage profile.zip) (b)In web.config use the inherits attribute of Profile provider to point to the location of UserProfileBase
For usage something like this will work :
UserProfileBase profile = UserProfileBase.GetUserProfile(Page.User.Identity.Name);
string i = profile.UserName;
profile.City = "stl";
profile.BirthDate = System.DateTime.Now.AddYears(-30);
profile.FirstName = "Suhel";
profile.Gender = "M";
profile.Language = "English";
profile.LastName = "s";
profile.Occupation = "Director";
profile.State = "mo";
profile.Street = "123 main";
profile.Subscription = "yes";
profile.Website = "www.incedeit.com";
profile.Zip = "1234";
profile.Country = "US";
profile.Save();
References
History
- 29th January, 2010: Initial post
- 7th June, 2010: Article updated