I have the following entities:
public abstract class User : IIdentity
{
private readonly UserType _userType;
public virtual int EntitySK { get; set; }
public virtual int TenantSK { get; set; }
public abstract string Name { get; set; }
public virtual PublicKey PublicKey { get; set; }
public abstract string AuthenticationType { get; set; }
public virtual bool IsAuthenticated { get; protected internal set; }
public virtual bool LoginEnabled { get; set; }
public virtual bool LockedOut { get; set; }
public virtual int NumberOfFailedLoginAttempts { get; set; }
// Hibernate requires this constructor
protected User()
{
this._userType = this is PersonUser ? Models.UserType.Person : Models.UserType.Client;
this.LoginEnabled = true;
}
protected User(UserType userType)
{
this._userType = userType;
this.LoginEnabled = true;
}
public virtual UserType UserType
{
get { return this._userType; }
set
{
if(value != this._userType)
throw new InvalidOperationException("Attempted to load " + value + " into " + this._userType + "User.");
}
}
}
public class PersonUser : User
{
public virtual string Domain { get; set; }
public override string Name { get; set; }
public virtual byte[] Password { get; set; }
public virtual byte[] Pepper { get; set; }
public virtual string EmailAddress { get; set; }
public virtual int PersonSK { get; set; }
public override string AuthenticationType { get; set; }
public PersonUser() : base(UserType.Person) { }
}
public class ClientUser : User
{
public override string Name { get; set; }
public virtual string SharedSecret { get; set; }
public virtual ISet<string> Scopes { get; set; }
public virtual ISet<GrantType> AuthorizedGrantTypes { get; set; }
public virtual ISet<Uri> RegisteredRedirectUris { get; set; }
public virtual int AuthorizationCodeValiditySeconds { get; set; }
public virtual int AccessTokenValiditySeconds { get; set; }
public ClientUser() : base(UserType.Client) { }
}
I map these entities using the following Hibernate Conformist mapping:
public class UserMapping : ClassMapping<User>
{
public UserMapping()
{
LogManager.GetLogger().Info("Initialized User mapping.");
this.Table("Authentication_Users");
this.Id(u => u.EntitySK,
m => {
m.Column("UserSK");
m.Generator(Generators.Identity);
m.UnsavedValue(0);
});
this.Property(u => u.TenantSK,
m => {
m.Column("TenantSK");
m.NotNullable(true);
});
this.Property(u => u.PublicKey,
m => {
m.Column("PublicKey");
m.Type<PublicKeyCustomType>();
m.NotNullable(false);
m.Lazy(true);
});
this.Property(u => u.UserType,
m => {
m.Column("UserType");
m.NotNullable(true);
m.Type<EnumCustomType<UserType>>();
});
this.Property(u => u.LoginEnabled,
m => {
m.Column("LoginEnabled");
m.NotNullable(true);
});
this.Property(u => u.LockedOut,
m => {
m.Column("LockedOut");
m.NotNullable(true);
});
this.Property(u => u.NumberOfFailedLoginAttempts,
m => {
m.Column("NumberOfFailedLoginAttempts");
m.NotNullable(true);
});
this.Discriminator(d => d.Column("UserType"));
}
}
public class PersonUserMapping : SubclassMapping<PersonUser>
{
public PersonUserMapping()
{
LogManager.GetLogger().Info("Initialized PersonUser mapping.");
this.DiscriminatorValue((int)UserType.Person);
this.Join(
"PersonUser",
j =>
{
j.Table("Authentication_Users_PersonUsers");
j.Key(m => {
m.Column("UserSK");
m.NotNullable(true);
m.OnDelete(OnDeleteAction.Cascade);
m.Unique(true);
m.Update(false);
});
j.Property(u => u.Domain,
m => {
m.Column("DomainName");
m.NotNullable(false);
});
j.Property(u => u.Name,
m => {
m.Column("Username");
m.NotNullable(true);
});
j.Property(u => u.Password,
m => {
m.Column("Password");
m.NotNullable(false);
m.Lazy(true);
});
j.Property(u => u.Pepper,
m => {
m.Column("Pepper");
m.NotNullable(false);
m.Lazy(true);
});
j.Property(u => u.EmailAddress,
m => {
m.Column("EmailAddress");
m.NotNullable(false);
});
j.Property(u => u.PersonSK,
m => {
m.Column("PersonSK");
m.NotNullable(false);
});
j.Property(u => u.AuthenticationType,
m => {
m.Column("AuthenticationType");
m.NotNullable(true);
});
}
);
}
}
public class ClientUserMapping : SubclassMapping<ClientUser>
{
public ClientUserMapping()
{
LogManager.GetLogger().Info("Initialized ClientUser mapping.");
this.DiscriminatorValue((int)UserType.Client);
this.Join(
"ClientUser",
j =>
{
j.Table("Authentication_Users_ClientUsers");
j.Key(m => {
m.Column("UserSK");
m.NotNullable(true);
m.OnDelete(OnDeleteAction.Cascade);
m.Unique(true);
m.Update(false);
});
j.Property(u => u.Name,
m => {
m.Column("DisplayName");
m.NotNullable(true);
});
j.Property(u => u.SharedSecret,
m => {
m.Column("SharedSecret");
m.NotNullable(true);
});
j.Property(u => u.AuthorizationCodeValiditySeconds,
m => {
m.Column("AuthorizationCodeValiditySeconds");
m.NotNullable(true);
});
j.Property(u => u.AccessTokenValiditySeconds,
m => {
m.Column("AccessTokenValiditySeconds");
m.NotNullable(true);
});
j.Set(u => u.Scopes,
s => {
s.Fetch(CollectionFetchMode.Join);
s.Lazy(CollectionLazy.Lazy);
s.Table("Authentication_Users_ClientUsers_Scopes");
s.Key(m => {
m.Column("UserSK");
m.NotNullable(true);
});
},
r => r.Element(m => {
m.Column("Scope");
m.NotNullable(true);
m.Unique(true);
}));
j.Set(u => u.AuthorizedGrantTypes,
s => {
s.Fetch(CollectionFetchMode.Join);
s.Lazy(CollectionLazy.Lazy);
s.Table("Authentication_Users_ClientUsers_AuthorizedGrantTypes");
s.Key(m => {
m.Column("UserSK");
m.NotNullable(true);
});
},
r => r.Element(m => {
m.Column("GrantType");
m.NotNullable(true);
m.Unique(true);
m.Type<EnumCustomType<GrantType>>();
}));
j.Set(u => u.RegisteredRedirectUris,
s => {
s.Fetch(CollectionFetchMode.Join);
s.Lazy(CollectionLazy.Lazy);
s.Table("Authentication_Users_ClientUsers_RegisteredRedirectUris");
s.Key(m => {
m.Column("UserSK");
m.NotNullable(true);
});
},
r => r.Element(m => {
m.Column("Uri");
m.NotNullable(true);
m.Unique(true);
m.Type<UriCustomType>();
}));
}
);
}
}
The EnumCustomType
is an IUserType
that maps C# enums to integer columns.
I formulated this design and mapping after much research and consultation with the NHibernate reference documentation and this blog (specific page) (summary). I am sure this is the entity design I want, but of course it's possible (likely?) I got the mapping wrong.
When I start up and configure NHibernate, it loads the mappings and does not complain. The log output has no warnings about the mappings. However, when I create a PersonUser
, assign values to all of its properties, and Add
it to the ISession
, the most peculiar thing happens:
2014-01-16 00:58:34,465 DEBUG NHibernate.AdoNet.AbstractBatcher.Generate() - Building an IDbCommand object for the SqlString: INSERT INTO Authentication_Users (TenantSK, DisplayName, PublicKey, LoginEnabled, LockedOut, NumberOfFailedLoginAttempts, Username, AuthenticationType, UserType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '1'); select SCOPE_IDENTITY()
2014-01-16 00:58:34,472 DEBUG NHibernate.Persister.Entity.AbstractEntityPersister.Dehydrate() - Dehydrating entity: [Models.PersonUser#<null>]
2014-01-16 00:58:34,475 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding '0' to parameter: 0
2014-01-16 00:58:34,478 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'nick.williams' to parameter: 1
2014-01-16 00:58:34,482 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'PublicKey' to parameter: 3
2014-01-16 00:58:34,485 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'True' to parameter: 4
2014-01-16 00:58:34,486 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'False' to parameter: 5
2014-01-16 00:58:34,487 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding '0' to parameter: 6
2014-01-16 00:58:34,488 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'nick.williams' to parameter: 8
NHibernate.PropertyValueException : Error dehydrating property value for Models.PersonUser.Name
----> System.IndexOutOfRangeException : Invalid index 8 for this SqlParameterCollection with Count=8.
at NHibernate.Persister.Entity.AbstractEntityPersister.Dehydrate(Object id, Object[] fields, Object rowId, Boolean[] includeProperty, Boolean[][] includeColumns, Int32 table, IDbCommand statement, ISessionImplementor session, Int32 index)
at NHibernate.Persister.Entity.AbstractEntityPersister.GeneratedIdentifierBinder.BindValues(IDbCommand ps)
at NHibernate.Id.Insert.AbstractReturningDelegate.PerformInsert(SqlCommandInfo insertSQL, ISessionImplementor session, IBinder binder)
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object[] fields, Object obj, ISessionImplementor session)
at NHibernate.Action.EntityIdentityInsertAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
at NHibernate.Event.Default.AbstractSaveEventListener.PerformSaveOrReplicate(Object entity, EntityKey key, IEntityPersister persister, Boolean useIdentityColumn, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
at NHibernate.Event.Default.AbstractSaveEventListener.SaveWithGeneratedId(Object entity, String entityName, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.SaveWithGeneratedOrRequestedId(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.EntityIsTransient(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.FireSave(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.Save(Object obj)
Importantly, take a look at that SQL generated:
INSERT INTO Authentication_Users (TenantSK, DisplayName, PublicKey, LoginEnabled, LockedOut, NumberOfFailedLoginAttempts, Username, AuthenticationType, UserType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '1')
That makes no sense. It in no way corresponds to the mapping at all. It's got columns from three different tables in an insert statement for one table. It has columns from PersonUser
AND from ClientUser
(which shouldn't be possible), it's binding parameters with nonexistent parameter indexes, and it's not even including all the properties I set!
I've been playing around with this for several hours now without any progress. I'm at a complete loss here. It just doesn't make any sense. Anyone seen this before? Any idea what's going on?
EDIT I forgot to mention: I'm using the discriminator here because I want to be able to get a generic User
by its ID and it return the proper PersonUser
or ClientUser
depending on which type it is.
There are some issues with the mapping
a) You are mapping the UserType as property but you are using it as discriminator, too. But the user type value is generated by nhibernate, because of the discriminator usage... Therefore you can define the property to be generated.
You should also not throw an exception on the setter... to prevent
UserType
being set from somewhere, simply mark the auto property setter protected internalb) You are mapping the
Name
attribute of your base class in your sub classes only and you are trying to map that property to different columns within your sub classes' tables. Don't know if this is even possible, usually you have to map all properties of your base class to the base class table, or move the property into the subclass...To fix this, simply remove the mappings to
Name
from your subclass mappings and move it to your base class mapping.