Try fast search NHibernate

16 March 2010

ConfORM: “Mapping” Asp.Net Membership


In an application we are going to use ConfORM as way to map our domain for NHibernate. The application is composed by various modules and the first module where we want substitute “the other way to map” with ConfORM, is our implementation of Asp.NET Membership Provider (the application Expo Blox).
This is a good proof of ConfORM because we can’t take decisions about Table/Column naming nor anything else strictly related with the DB. In addition I have asked to Michele don’t send me the source but only assemblies. The result is that I can’t touch neither the DB nor the entities implementation.


Introduction

ConfORM does not have the limitation of define anything concerning a class in a single place and you can organize the mapping by concern. As basic template, for each module, we will use this interface :
public interface IModuleMapping
{
void DomainDefinition(ObjectRelationalMapper orm);
void RegisterPatterns(Mapper mapper);
void Customize(Mapper mapper);
IEnumerable<Type> GetEntities();
}

Initial Questions
  1. Which are the Root entities of this module and which is the strategy to represent the hierarchy ?
  2. Which are many-to-many, one-to-one ?
  3. Cascades… aggregate-root ?
  4. Something to say about the POID ?
  5. Something to say about the collection ? I mean… are you specifying only ICollection<T>, for property and field, even when the collection is an ISet<T> ?
With his answers the implementation of IModuleMapping become:
public class MembershipModuleMapping: IModuleMapping
{
private readonly Type[] tablePerClassRootEntities = new[] { typeof(User), typeof(Role), typeof(Application) };

#region Implementation of IModuleMapping

public void DomainDefinition(ObjectRelationalMapper orm)
{
orm.TablePerClass(tablePerClassRootEntities);
orm.ManyToMany<User, Role>();

orm.Patterns.PoidStrategies.Add(new GuidPoidPattern());
orm.Patterns.Sets.Add(new SetWhenGenericCollectionPattern());
}

public void RegisterPatterns(Mapper mapper)
{
}

public void Customize(Mapper mapper)
{
}

public IEnumerable<Type> GetEntities()
{
return typeof(User).Assembly.GetTypes().Where(t => typeof(BaseEntity).IsAssignableFrom(t));
}

#endregion
}
The Michele’s surprise is that with four lines we have a correct mapping but… “Fabio, We can’t follow the NHibernate’s convention for columns-naming , nor tables-naming, nor columns size, and nor for the type of DateTime.”

Follow Questions

  1. Which is the matter with DateTime ?
  2. Which are columns/table naming convention and specifications ?
Answers
  1. For all DateTime of this module we are using a custom type (SqlServerDateTimeUserType).
  2. Have a look to the DB defined by Microsoft, there you can find what you need.

To solve the answer-1

public void RegisterPatterns(Mapper mapper)
{
PersistentTypesPatterns(mapper);
}

private void PersistentTypesPatterns(Mapper mapper)
{
mapper.AddPropertyPattern(
mi => IsMemberOfClassOfMembershipProvider(mi) && mi.GetPropertyOrFieldType() == typeof(DateTime),
pm => pm.Type<SqlServerDateTimeUserType>());
}

private bool IsMemberOfClassOfMembershipProvider(MemberInfo mi)
{
return mi.ReflectedType.Assembly == typeof(User).Assembly;
}
For the answer-2
First of all some methods to group mappings by concern
public void RegisterPatterns(Mapper mapper)
{
PersistentTypesPatterns(mapper);
ColumnsNamingPatterns(mapper);
DDLPatterns(mapper);
}

public void Customize(Mapper mapper)
{
CustomizeTables(mapper);
CustomizeColumns(mapper);
}
I saw that most of columns follows the NHibernate’s convention where the mapped property-name is the name of the column. The columns where the NHibernate’s convention does not work are few and alls follows another convention that will be valid only for this module (in the others we will use the clear NHibernate’s convention):
private void ColumnsNamingPatterns(Mapper mapper)
{
mapper.AddPoidPattern(mi => IsMemberOfClassOfMembershipProvider(mi) && mi.Name == "Id",
(mi, idm) => idm.Column(mi.ReflectedType.Name + "Id"));
mapper.AddCollectionPattern(IsMemberOfClassOfMembershipProvider,
(mi, cm) => cm.Key(km => km.Column(mi.DeclaringType.Name + "Id")));
mapper.AddManyToOnePattern(IsMemberOfClassOfMembershipProvider,
(mi, idm) => idm.Column(mi.GetPropertyOrFieldType().Name + "Id"));
}

Another fact is that most of string-properties are represented with VARCHAR(256) (old school string sizing):

private void DDLPatterns(Mapper mapper)
{
mapper.AddPropertyPattern(
mi => mi.GetPropertyOrFieldType() == typeof(string) && IsMemberOfClassOfMembershipProvider(mi),
pm => pm.Length(256));
}

I can’t see any other pattern in the DB.
Now I can implements the method to customize the name of all tables involved as required by Asp.NET Membership:

private void CustomizeTables(Mapper mapper)
{
mapper.Class<User>(cm => cm.Table("aspnet_Users"));
mapper.Class<Role>(cm => cm.Table("aspnet_Roles"));
mapper.Class<Application>(cm => cm.Table("aspnet_Applications"));
mapper.JoinedSubclass<Entities.Membership>(cm => cm.Table("aspnet_Membership"));

mapper.Customize<User>(pcc => pcc.Collection(role => role.Roles, cpm => cpm.Table("aspnet_UsersInRoles")));
mapper.Customize<Role>(pcc => pcc.Collection(role => role.Users, cpm => cpm.Table("aspnet_UsersInRoles")));
}
As last I can map the other columns stuff :
private void CustomizeColumns(Mapper mapper)
{
mapper.Customize<User>(pcc =>
{
pcc.Property(role => role.UserName, pm => pm.NotNullable(true));
pcc.Property(role => role.LoweredUserName, pm => pm.NotNullable(true));
pcc.Property(role => role.MobileAlias, pm => pm.Length(16));

pcc.ManyToOne(role => role.Application, mtom => mtom.NotNullable(true));
});
mapper.Customize<Role>(pcc =>
{
pcc.Property(role => role.RoleName, pm => pm.NotNullable(true));
pcc.Property(role => role.LoweredRoleName, pm => pm.NotNullable(true));
pcc.ManyToOne(role => role.Application, mtom => mtom.NotNullable(true));
});
mapper.Customize<Application>(pcc =>
{
pcc.Property(app => app.ApplicationName, pm => pm.NotNullable(true));
pcc.Property(app => app.LoweredApplicationName, pm => pm.NotNullable(true));
});
mapper.Customize<Payoff.Membership.Entities.Membership>(pcc =>
{
pcc.Property(membership => membership.Password, pm =>
{
pm.Length(128);
pm.NotNullable(true);
});
pcc.Property(membership => membership.PasswordSalt, pm =>
{
pm.Length(128);
pm.NotNullable(true);
});
pcc.Property(membership => membership.MobilePIN, pm => pm.Length(16));
pcc.Property(membership => membership.PasswordAnswer, pm => pm.Length(128));
pcc.Property(membership => membership.Comment, pm => pm.Length(3000));
});

mapper.JoinedSubclass<Payoff.Membership.Entities.Membership>(cm => cm.Key(km => km.Column("UserId")));
}

Conclusion

Even when the DB was defined by somebody else and the classes implementations was defined by somebody else, ConfORM pass the test and allow me to add chunks of mapping with new discovered/needed matters without modify existing mappings and, over all, I can map by concern and not class by class.

7 comments:

  1. Good Post!,

    It's a showroom of the capabilities and extensibility points of ConfORM.

    Great work Fabio!

    ReplyDelete
  2. can u share the implementation of the membership provider? :)
    i'm just begin to implement it by myself using nhibernate

    ReplyDelete
  3. I can't. I don't have the sources, only DLLs and the source is not mine.

    ReplyDelete
  4. ok, thanks for the reply and congratulations for confORM ;)

    ReplyDelete
  5. ConfORM Seems like a really good approach.
    I like the idea, moreover was able to adopt in 2 days in my most recent project. (vs. fluentnhibernate)

    But I have a problem with a Version column handling.

    (actually what I'm looking for is an equivalent of this fluent statement for MSSQL timestamp: )

    public class VersionColumnConvention : IVersionConvention
    {
    public void Apply(IVersionInstance instance)
    {
    instance.Generated.Always();
    instance.CustomSqlType("timestamp");
    instance.CustomType("BinaryBlob");
    instance.UnsavedValue("null");
    }
    }
    //where versioned entity have a byte[] Version {get ; protected set; }

    As per the latest trunk I'm getting Invalid cast exception: where

    public abstract class Entity
    {
    //...
    public virtual byte[] Version { get; protected set; }
    //...
    }
    (entities are derived from Entity class (and mapped correctly as the factory is initializable))

    within:
    public static ObjectRelationalMapper GetMappedDomain()
    {
    var orm = new ObjectRelationalMapper();
    //...
    // setting the Version property
    orm.VersionProperty(x => x.Version);
    //...

    Please tell me how to handle custom Version Property
    if the property type is byte[]
    generated
    contains timestamp (generated by the SQL server)



    At last:
    A really nice work from you!

    Easy, (if you know the concept behind NHibernate)



    So I would say I'm became confORM-ed :)

    ReplyDelete
  6. So far it is available only through class customizer
    mapper.Class(x => x.Version(myclass => myclass.Version, vm => {here});

    Monday, probably, through aplliers.

    ReplyDelete
  7. Csizmazia,
    If you don't want wait you can try the polymorphism of customizers.
    Customize the base class and have a look ;) (should work)

    ReplyDelete