ArgumentOutOfRangeException : Index was out of range - nhibernate

I'm getting this weird ArgumentOutOfRangeException whenever I use the
PersitenceSpecification class for verifying an entity that has a
reference to a value object.
public class CatalogItem : DomainEntity
{
internal virtual Manufacturer Manufacturer { get; private
set; }
internal virtual String Name { get; private set; }
protected CatalogItem()
{}
public CatalogItem(String name, String manufacturer)
{
Name = name;
Manufacturer = new Manufacturer(manufacturer);
}
}
public class CatalogItemMapping : ClassMap<CatalogItem>
{
public CatalogItemMapping()
{
Id(catalogItem => catalogItem.Id);
Component<Manufacturer>(category => category.Manufacturer,
m => m.Map(manufacturer =>
manufacturer.Name));
Map(catalogItem => catalogItem.Name);
Map(Reveal.Property<CatalogItem>("Price"));
}
}
[TestFixture]
public class When_verifying_the_class_mapping_of_a_catalog_item
: NHibernateSpecification
{
[Test]
public void Then_a_catalog_object_should_be_persistable()
{
new PersistenceSpecification<CatalogItem>(Session)
.VerifyTheMappings();
}
}
[TestFixture]
public class NHibernateSpecification
: Specification
{
protected ISession Session { get; private set; }
protected override void Establish_context()
{
var configuration = new SQLiteConfiguration()
.InMemory()
.ShowSql()
.ToProperties();
var sessionSource = new SessionSource(configuration, new
RetailerPersistenceModel());
Session = sessionSource.CreateSession();
sessionSource.BuildSchema(Session);
ProvideInitialData(Session);
Session.Flush();
Session.Clear();
}
protected override void Dispose_context()
{
Session.Dispose();
Session = null;
}
protected virtual void ProvideInitialData(ISession session)
{}
}
Here's the error I'm getting:
TestCase
'Then_a_catalog_object_should_be_persistable' not executed:
System.ArgumentOutOfRangeException :
Index was out of range. Must be
non-negative and less than the size of
the collection. Parameter name: index
at System.ThrowHelper.ThrowArgumentOutOfRangeException
(ExceptionArgument argument,
ExceptionResource resource)
at System.ThrowHelper.ThrowArgumentOutOfRangeException()
at System.Collections.Generic.List1.get_Item(Int32
index)
at System.Data.SQLite.SQLiteParameterCollection.GetParameter(Int32
index)
at System.Data.Common.DbParameterCollection.System.Collections.IList.get_Item
(Int32 index)
at NHibernate.Type.GuidType.Set(IDbCommand
cmd, Object value, Int32 index)
at NHibernate.Type.NullableType.NullSafeSet(IDbCommand
cmd, Object value, Int32 index)
at NHibernate.Type.NullableType.NullSafeSet(IDbCommand
st, Object value, Int32 index,
ISessionImplementor session)
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.Insert(Object
id, Object[] fields, Boolean[]
notNull, Int32 j, SqlCommandInfo sql,
Object obj, ISessionImplementor
session)
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object
id, Object[] fields, Object obj,
ISessionImplementor session)
at NHibernate.Action.EntityInsertAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable
executable)
at NHibernate.Engine.ActionQueue.ExecuteActions(IList
list)
at NHibernate.Engine.ActionQueue.ExecuteActions()
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions
(IEventSource session)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush
(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at NHibernate.Transaction.AdoTransaction.Commit()
d:\Builds\FluentNH\src\FluentNHibernate\Testing
\PersistenceSpecification.cs(127,0):
at
FluentNHibernate.Testing.PersistenceSpecification1.TransactionalSave
(Object propertyValue)
d:\Builds\FluentNH\src\FluentNHibernate\Testing
\PersistenceSpecification.cs(105,0):
at
FluentNHibernate.Testing.PersistenceSpecification`1.VerifyTheMappings
()
C:\Source\SupplyChain\Tests\Retailer.IntegrationTests\Mappings
\CatalogItemMappingSpecifications.cs(14,0):
at
SupplyChain.Retailer.IntegrationTests.Mappings.When_verifying_the_class_mapping_of_a_catalog_item.Then_a_catalog_object_should_be_persistable
()
Sorry for the long post, but this one got me busy for a couple of
hours now. This might not be caused by FNH as I found this JIRA ticket
of NH itself that mentions something similar:
http://forum.hibernate.org/viewtopic.php?p=2395409
I'm still hoping that I'm doing something wrong in my code :-). Any
thought?
Thanks in advance

I found the solution to this problem which resulted from my own
stupidity in the first place. It all became clear to me as soon as I
generated the hbm files from the fluent NH mapping.
<class name="CatalogItem" table="`CatalogItem`" xmlns="urn:nhibernate-
mapping-2.2" optimistic-lock="version">
...
<property name="Name" length="100" type="String">
<column name="Name" />
</property>
...
<component name="Manufacturer" insert="false" update="true">
<property name="Name" length="100" type="String">
<column name="Name" />
</property>
</component>
</class>
Notice that the column for the Name property and the column for the
Manufacturer component are both mapped to the same column. That's why
this resulted into an ArgumentOutOfRangeException, because there were
more arguments than there were column names. I solved this by
explicitely specifying a column name for the component mapping:
Component(catalogItem => catalogItem.Manufacturer,
m => m.Map(manufacturer => manufacturer.Name,
"Manufacturer"));
Another lesson learned.

In my case, I was mapping two properties to the same column with Fluent NHibernate.

Your CatalogItem doesn't seem to have a Price property, which seems odd when you're using the Reveal helper.

Yes, I removed that one for reducing some of the noise. I guess I forgot to remove it from the mapping as well. After doing some more investigation, I noticed that it has something to do with Manufacturer being mapped as a component. When I used a plain-old string instead of a separate class, everything works fine.

In my particular case I was adding property as well as ID (using attributes) over the same .NET property. This resulted in the same error.

Old question, but if someone runs into the same problem as I did, it may help to know that this Fluent Nhibernate issue (ColumnPrefix only applied to first Component mapping inside a ComponentMap) can give the same exception since the columns prefixes are not always applied.

The other answers for this question are correct. But there is another case where NHibernate outputs the same message with different stack trace. The stack trace can be found in this question.
As this is one of the top questions that popup when I search online with exact error message, I thought it would be helpful for others to know how I fixed it. There is a documented issue for this error on NHibernate's GitHub repository. Here is the link - https://github.com/nhibernate/nhibernate-core/issues/1319.
The error has been fixed in latest NHibernate at the time of writing this answer. For older versions such as 3.1.0 (the version I was working with when facing the error), setting format_sql property to false in nhibernate.config file fixes the issue. More details about the error and workaround can be found in the GitHub issue and NHibernate doc.

Related

database column to constant value without the need for a property in the entity class with mapping by code

this is almost the same as this question except using NH's mapping-by-code.
I really need the virtual properties because i also want to use SchemaExport to create the database for different rdbms without the need to create/maintain scripts for each.
Maybe there is a MbC Guru who knows how to do it with MbC
Update: the obvious simple code
Property("dummyProperty", c =>
{
c.Column("legacyColumn");
c.Access(typeof(MyPropertyAccessor));
});
does not work
NHibernate.MappingException: Unable to instantiate mapping class (see InnerException): Test.MbC.GroupMap ---> System.Reflection.TargetInvocationException: Ein Aufrufziel hat einen Ausnahmefehler verursacht. ---> NHibernate.MappingException: Member not found. The member 'dummyProperty' does not exists in type Test.Data.Group
bei NHibernate.Mapping.ByCode.Impl.CustomizersImpl.PropertyContainerCustomizer`1.GetPropertyOrFieldMatchingNameOrThrow(String memberName)
bei NHibernate.Mapping.ByCode.Impl.CustomizersImpl.PropertyContainerCustomizer`1.RegisterNoVisiblePropertyMapping(String notVisiblePropertyOrFieldName, Action`1 mapping)
bei
...
neither does this, because god knows why MbC checks internally with reflection, that the property does exist on the class.
var parameter = Expression.Parameter(typeof(T), "x");
Expression body = Expression.Property(parameter, new GetterPropertyInfo(typeof(T), defaultgetter));
body = Expression.Convert(body, typeof(object));
var lambda = Expression.Lambda<Func<T, object>>(body, parameter);
Property(lambda, m =>
{
m.Column(defaultgetter.PropertyName);
m.Access(propertyAccessorType);
});
and even disabling the test with reflection through overriding RegisterProperty() in ClassMapping it still throws while building the hbm complaining:
System.ArgumentOutOfRangeException: Can't add a property of another graph
Parametername: property
bei NHibernate.Mapping.ByCode.Impl.AbstractBasePropertyContainerMapper.Proper
ty(MemberInfo property, Action`1 mapping)
bei NHibernate.Mapping.ByCode.ModelMapper.MapProperty(MemberInfo member, Prop
ertyPath propertyPath, IMinimalPlainPropertyContainerMapper propertiesContainer)
bei NHibernate.Mapping.ByCode.ModelMapper.MapProperties(Type propertiesContai
nerType, IEnumerable`1 propertiesToMap, IPropertyContainerMapper propertiesConta
iner, PropertyPath path)
bei NHibernate.Mapping.ByCode.ModelMapper.MapProperties(Type propertiesContai
nerType, IEnumerable`1 propertiesToMap, IPropertyContainerMapper propertiesConta
iner)
bei NHibernate.Mapping.ByCode.ModelMapper.MapRootClass(Type type, HbmMapping
mapping)
bei NHibernate.Mapping.ByCode.ModelMapper.CompileMappingFor(IEnumerable`1 typ
es)
Mapping by code should be more flexible than FNH? Where?
After investing a lot of time trying to do this rather simple mapping in MbC i concede and throw aboard MbC again.
Even simple Mapping like this is not remotely possible with the oh so flexible MbC
public class MyClassMap : ClassMap<MyClass>
{
public MyClassMap()
{
Map(x => this.VirtualProp, "legacyColumn").Default("123").Access.None();
}
public long VirtualProp { get; set; }
}
the advantage here is that i can use SchemaExport to create a compatible schema for the legacy app without polluting my domain classes
You can do it pretty much in the same way as in the linked question. Implementation of PropertyAccessor stays the same. To use it within mapping-by-code, map the column using string overload (the name of the property is required but not really used in this case) and attach the accessor:
Property("dummyPropertyNameForConstant", c =>
{
c.Column("ConstantColumn");
c.Access(typeof(CustomAccessor));
});

NHibernate gets crashed by a magic string

Here is my trivial program:
public class Entity
{
public virtual long Id { get; set; }
public virtual string Payload { get; set; }
}
class Program
{
static void Main( string[] args )
{
var config = new Configuration().Configure();
var sessionFactory = config.BuildSessionFactory();
using ( var session = sessionFactory.OpenSession() )
{
var entity = new Entity { Payload = "'))" };
session.Save( entity );
}
}
}
As long as Payload property is assigned any innocent string such as 'Hi there' everything works as expected. However this particular magic string ')) makes NHibernate throw an exception: 'Index was out of range. Must be non-negative and less than the size of the collection' when it tries to save the entity.
I wonder why NHibernate cares of the parameter content. It really shouldn't.
Or maybe something is very wrong with this sample?
NHibernate version is 2.1
SQL script:
create table tblEntity
(
EntityId BIGINT NOT NULL IDENTITY PRIMARY KEY
,Payload VARCHAR(10) NOT NULL
)
Mapping:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<class name="MyNamespace.Entity, MyAssembly"
table="tblEntity">
<id name="Id" column="EntityId" unsaved-value="0">
<generator class="identity"/>
</id>
<property name="Payload"/>
</class>
</hibernate-mapping>
NHibernate settings:
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
<property name="connection.connection_string_name">Main</property>
<property name="connection.isolation">ReadCommitted</property>
<property name="default_schema">dbo</property>
<property name="format_sql">true</property>
<property name="query.substitutions">true=1;false=0</property>
<property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
<mapping assembly="MyAssembly"/>
</session-factory>
</hibernate-configuration>
Nothing special as you can see, not too much room for mistakes.
Stack Trace
System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
System.ThrowHelper.ThrowArgumentOutOfRangeException()
System.Collections.Generic.List`1.get_Item(Int32 index)
NHibernate.AdoNet.Util.BasicFormatter.FormatProcess.CloseParen()
NHibernate.AdoNet.Util.BasicFormatter.FormatProcess.Perform()
NHibernate.AdoNet.Util.BasicFormatter.Format(String source)
NHibernate.AdoNet.Util.SqlStatementLogger.LogCommand(String message, IDbCommand command, FormatStyle style)
NHibernate.AdoNet.Util.SqlStatementLogger.LogCommand(IDbCommand command, FormatStyle style)
NHibernate.AdoNet.AbstractBatcher.LogCommand(IDbCommand command)
NHibernate.AdoNet.AbstractBatcher.Prepare(IDbCommand cmd)
NHibernate.AdoNet.AbstractBatcher.ExecuteReader(IDbCommand cmd)
NHibernate.Id.IdentityGenerator.InsertSelectDelegate.ExecuteAndExtract(IDbCommand insert, ISessionImplementor session)
NHibernate.Id.Insert.AbstractReturningDelegate.PerformInsert(SqlCommandInfo insertSQL, ISessionImplementor session, IBinder binder)
NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object[] fields, Boolean[] notNull, SqlCommandInfo sql, Object obj, ISessionImplementor session)
NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object[] fields, Object obj, ISessionImplementor session)
NHibernate.Action.EntityIdentityInsertAction.Execute()
NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
NHibernate.Event.Default.AbstractSaveEventListener.PerformSaveOrReplicate(Object entity, EntityKey key, IEntityPersister persister, Boolean useIdentityColumn, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
NHibernate.Event.Default.AbstractSaveEventListener.PerformSave(Object entity, Object id, IEntityPersister persister, Boolean useIdentityColumn, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
NHibernate.Event.Default.AbstractSaveEventListener.SaveWithGeneratedId(Object entity, String entityName, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.SaveWithGeneratedOrRequestedId(SaveOrUpdateEvent event)
NHibernate.Event.Default.DefaultSaveEventListener.SaveWithGeneratedOrRequestedId(SaveOrUpdateEvent event)
NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.EntityIsTransient(SaveOrUpdateEvent event)
NHibernate.Event.Default.DefaultSaveEventListener.PerformSaveOrUpdate(SaveOrUpdateEvent event)
NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(SaveOrUpdateEvent event)
NHibernate.Impl.SessionImpl.FireSave(SaveOrUpdateEvent event)
NHibernate.Impl.SessionImpl.Save(Object obj)
I have looked up the changes made to
src/NHibernate/AdoNet/Util/BasicFormatter.cs
there was an issue in NH 2.1 that caused this behavior, this was fixed in 2.1.1.GA.
Bugtracker: NH-1992
Changeset on github: Commit
I believe it might be a bug with NHibernate. When a command is logged (because you have show_sql set to true) it will write out your command and it will look something like this:
insert into tblEntity (Payload) VALUES(#p0); #p0 = ''(('
The logger then tries to tokenize the string and gets confused by the parentheses. Try submitting a bug to the NHibernate JIRA. In the meantime you can try turning off show_sql
I am not familiar with NHibernate, however what you are describing looks as if it might be the result of inadvertant SQL Injection Hopefully the linked wiki page might help.

How do I change a child's parent in NHibernate when cascade is delete-all-orphan?

I have two entities in a bi-directional one-to-many relationship:
public class Storage
{
public IList<Box> Boxes { get; set; }
}
public class Box
{
public Storage CurrentStorage { get; set; }
}
And the mapping:
<class name="Storage">
<bag name="Boxes" cascade="all-delete-orphan" inverse="true">
<key column="Storage_Id" />
<one-to-many class="Box" />
</bag>
</class>
<class name="Box">
<many-to-one name="CurrentStorage" column="Storage_Id" />
</class>
A Storage can have many Boxes, but a Box can only belong to one Storage.
I have them mapped so that the one-to-many has a cascade of all-delete-orphan.
My problem arises when I try to change a Box's Storage. Assuming I already ran this code:
var storage1 = new Storage();
var storage2 = new Storage();
storage1.Boxes.Add(new Box());
Session.Create(storage1);
Session.Create(storage2);
The following code will give me an exception:
// get the first and only box in the DB
var existingBox = Database.GetBox().First();
// remove the box from storage1
existingBox.CurrentStorage.Boxes.Remove(existingBox);
// add the box to storage2 after it's been removed from storage1
var storage2 = Database.GetStorage().Second();
storage2.Boxes.Add(existingBox);
Session.Flush(); // commit changes to DB
I get the following exception:
NHibernate.ObjectDeletedException : deleted object would be re-saved by cascade (remove deleted object from associations)
This exception occurs because I have the cascade set to all-delete-orphan. The first Storage detected that I removed the Box from its collection and marks it for deletion. However, when I added it to the second Storage (in the same session), it attempts to save the box again and the ObjectDeletedException is thrown.
My question is, how do I get the Box to change its parent Storage without encountering this exception? I know one possible solution is to change the cascade to just all, but then I lose the ability to have NHibernate automatically delete a Box by simply removing it from a Storage and not re-associating it with another one. Or is this the only way to do it and I have to manually call Session.Delete on the box in order to remove it?
See http://fabiomaulo.blogspot.com/2009/09/nhibernate-tree-re-parenting.html
Basically, it boils down to this... You need to define a custom collection type for NHibernate that re-defines what it means to be an orphan. NHibernate's default behavior is to do just as you discovered - to consider a child to be orphaned if it has been removed from the parent. Instead, you need NHibernate to test the child to see if it has been assigned to a new parent. NHibernate does not do this by default because it would require additional information on the one-to-many mapping - it would need to know the name of the corresponding many-to-one property on the child.
Change your Storage mapping to look like this:
<class name="Storage">
<bag name="Boxes" cascade="all-delete-orphan" inverse="true" collection-type="StorageBoxBag">
<key column="Storage_Id" />
<one-to-many class="Box" />
</bag>
</class>
Define a new type named StorageBoxBag (note - this code is written against NHibernate 2.1 - if you are using NH3 you may have to tweak this a bit):
public class StorageBoxBag : IUserCollectionType
{
public object Instantiate(int anticipatedSize)
{
return new List<Box>();
}
public IPersistentCollection Instantiate(ISessionImplementor session, ICollectionPersister persister)
{
return new PersistentStorageBoxBag(session);
}
public IPersistentCollection Wrap(ISessionImplementor session, object collection)
{
return new PersistentStorageBoxBag(session, (IList<Box>)collection);
}
public IEnumerable GetElements(object collection)
{
return (IEnumerable)collection;
}
public bool Contains(object collection, object entity)
{
return ((IList<Box>)collection).Contains((Box)entity);
}
public object IndexOf(object collection, object entity)
{
return ((IList<Box>) collection).IndexOf((Box) entity);
}
public object ReplaceElements(object original, object target, ICollectionPersister persister, object owner, IDictionary copyCache, ISessionImplementor session)
{
var result = (IList<Box>)target;
result.Clear();
foreach (var box in (IEnumerable)original)
result.Add((Box)box);
return result;
}
}
... and a new type named PersistentStorageBoxBag:
public class PersistentStorageBoxBag: PersistentGenericBag<Box>
{
public PersistentStorageBoxBag(ISessionImplementor session)
: base(session)
{
}
public PersistentStorageBoxBag(ISessionImplementor session, ICollection<Box> original)
: base(session, original)
{
}
public override ICollection GetOrphans(object snapshot, string entityName)
{
var orphans = base.GetOrphans(snapshot, entityName)
.Cast<Box>()
.Where(b => ReferenceEquals(null, b.CurrentStorage))
.ToArray();
return orphans;
}
}
The GetOrphans method is where the magic happens. We ask NHibernate for the list of Boxes that it thinks are orphans, and then filter that down to only the set of Boxes that actually are orphans.

DateTime precision in NHibernate and support for DateTime2 in NHibernate SchemeExport

I am then using Fluent NHibernate and its automapping feature to map the the following simplified POCO class:
public class Foo
{
public virtual int Id { get; set; }
public virtual datetime CreatedDateTime { get; set; }
}
The CreatedDateTime field will map to a SQL DateTime by default. However if I do a test to check that the entity is being created correctly it fails. This is because the precision of the DateTime field is not maintained through to the SQL database. I undersatnd the reason behind this to be that a MS SQL Server DateTime can only hold milisecond precision by rounded to increments of .000, .003, or .007 (see http://msdn.microsoft.com/en-us/library/ms187819.aspx). For this reason NHibernate truncates the miliseconds when saving to the store. This results in my test failing when checking that the fields where persisted correctly as my .NET DateTime holds its miliseconds but the DateTime retrived after the save has lost its miliseconds and therefore the two are not truely equal.
To overcome this problem I have added the following mapping to the Foo object:
public class FooMap : IAutoMappingOverride<Foo>
{
public void Override(AutoMapping<Foo> mapping)
{
mapping.Map(f => f.CreatedDateTime).CustomType("datetime2");
}
}
I understand that this mapping makes NHibernate persist the CreatedDateTime to a SQL type of datetime2, which can store the full precision that a .NET DateTime can.
This works a treat and the test now passes.
However with one pass comes another fail: My test that checks the schema export now fails with the following error:
System.ArgumentException : Dialect does not support DbType.DateTime2
Parameter name: typecode
with a stack trace of:
at NHibernate.Dialect.TypeNames.Get(DbType typecode)
at NHibernate.Dialect.Dialect.GetTypeName(SqlType sqlType)
at NHibernate.Mapping.Column.GetDialectTypeName(Dialect dialect, IMapping mapping)
at NHibernate.Mapping.Table.SqlCreateString(Dialect dialect, IMapping p, String defaultCatalog, String defaultSchema)
at NHibernate.Cfg.Configuration.GenerateSchemaCreationScript(Dialect dialect)
at NHibernate.Tool.hbm2ddl.SchemaExport..ctor(Configuration cfg, IDictionary`2 configProperties)
at NHibernate.Tool.hbm2ddl.SchemaExport..ctor(Configuration cfg)
The code uses the NHibernate.Tool.hbm2ddl.SchemaExport object to call the Execute method.
I am using Fluent v1 and NHibernate v2.1.
I have also tried mapping my DateTime to a TimeStamp but couldn't even get the mapping working as the insert fails stating:
Cannot insert an explicit value into a timestamp column. Use INSERT with a column list to exclude the timestamp column, or insert a DEFAULT into the timestamp column.
Does anyone know either how to get the SchemeExport working with a datetime2 OR how to get timestamp mapping working for a datetime property?
Actually the NHibernate reference states that the DateTime nhibernate type will store the .NET DateTime as an SQL datetime truncated at the second level (no millisecond granularity)
As such it provides the Timestamp NHibernate type (type="Timestamp" in the mapping) which will store a .NET DateTime as an SQL datetime without truncation. Note here that an SQL timestamp datatype is not needed and will infact break if you have more than one timestamp column in one table. It's thus important to differentiate between the sql-type and type attributes in the NHibernate mapping.
Additionally, note that if you are working with filters, the same rule applies at the filter definition: If you specify a DateTime parameter, the parameter's value will be truncated without milliseconds.
Check out chapter 5.2.2. Basic value types, Table 5.3 System.ValueType Mapping Types.
For anyone looking to actually keep the nanosecond part of the date, you'll have to use DateTime2 as the sql-column type as well as the Nhibernate DateTime2 type.
Here's my convention for setting this up (using fluent)
public class DateTimeConvention : IPropertyConvention, IPropertyConventionAcceptance
{
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => x.Type == typeof(DateTime) || x.Type == typeof(DateTime?));
}
public void Apply(IPropertyInstance instance)
{
instance.CustomSqlType("DateTime2"); //specify that the sql column is DateTime2
instance.CustomType("DateTime2"); //set the nhib type as well
}
}
And to activate the convention:
var v = Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008
.ConnectionString(d => d.FromConnectionStringWithKey("connstring"))
.ShowSql())
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<IRepository>()
.Conventions.AddFromAssemblyOf<IRepository>()) //this adds your convention
.BuildSessionFactory();
Using this you'll get to keep nanoseconds when storing your datetimes.
I ran into the same problem with a CreatedDate audit field on my business classes. I worked around it by setting the time using the value from a utility method. Hope this helps.
/// <summary>
/// Return a DateTime with millisecond resolution to be used as the timestamp. This is needed so that DateTime of an existing instance
/// will equal one that has been persisted and returned from the database. Without this, the times differ due to different resolutions.
/// </summary>
/// <returns></returns>
private DateTime GetTime()
{
var now = DateTime.Now;
var ts = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, now.Millisecond, DateTimeKind.Local);
return ts;
}
In my domain it is acceptable to lose the milliseconds from datetimes in SQL Server. I therefore allow a tolerance in my persistance testers using this static helper (nunit implementation):
public static class AssertDateTime
{
/// <summary>
/// Checks that the DateTimes are no more than second apart
/// </summary>
/// <param name="Expected"></param>
/// <param name="Actual"></param>
public static void AreWithinOneSecondOfEachOther(DateTime Expected, DateTime Actual)
{
var timespanBetween = Actual.Subtract(Expected);
if (timespanBetween > TimeSpan.FromSeconds(1))
Assert.Fail(string.Format("The times were more than a second appart. They were out by {0}. Expected {1}, Actual {2}.", timespanBetween, Expected, Actual));
}
}
I was able to get my optimistic locking worked out using the below:
(using datetime2).
Note, I used the name (and case of the datatype-name) from here:
http://msdn.microsoft.com/en-us/library/system.data.dbtype.aspx
"DateTime2" is in my mapping code (under CustomType) and not the Sql Server data-type-case ("datetime2"). I'm not sure if that makes a difference but I wanted to point it out.
Fluent Mapping:
public class DogBreedMap : ClassMap<DogBreed>
{
public DogBreedMap()
{
Id(x => x.DogBreedUUID).GeneratedBy.GuidComb();
OptimisticLock.Version();
Version(x => x.Version)
.Column("MyTimestamp").CustomType("DateTime2");
}
}
public partial class DogBreed
{
public DogBreed()
{
CommonConstructor();
}
private void CommonConstructor()
{
this.Version = DateTime.MinValue; /*I don't think this is necessary*/
}
public virtual Guid? DogBreedUUID { get; set; }
public virtual DateTime Version { get; set; }
}
The Sql Server column is created at:
[MyTimestamp] [datetime2](7) NOT NULL
And my basic tests work and I (correctly) receive an exception like this (when someone else has updted the row)
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [DogBreed#abcabc1d-abc4-abc9-abcb-abca01140a27]
at NHibernate.Persister.Entity.AbstractEntityPersister.Check(Int32 rows, Object id, Int32 tableNumber, IExpectation expectation, IDbCommand statement)
at NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)
at NHibernate.Persister.Entity.AbstractEntityPersister.UpdateOrInsert(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)
at NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Int32[] dirtyFields, Boolean hasDirtyCollection, Object[] oldFields, Object oldVersion, Object obj, Object rowId, ISessionImplementor session)
at NHibernate.Action.EntityUpdateAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
at NHibernate.Engine.ActionQueue.ExecuteActions()
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at NHibernate.Transaction.AdoTransaction.Commit()

NHibernate Interceptor Auditing Inserted Object Id

I am using NHibernate interceptors to log information about Updates/Inserts/Deletes to my various entities.
Included in the information logged is the Entity Type and the Unique Id of the entity modified. The unique Id is marked as a <generator class="identity"> in the NHibernate mapping file.
The obvious problem is when logging an Insert operation using IInterceptor.OnSave() the Id of the entity has not yet been assigned.
How can I obtain the Id of the inserted entity before logging the audit information?
(I have looked into NHibernate Listeners PostSave event but can't get them working with the Spring.net configuration being used, so I would like to stick with interceptors if at all possible)
Code:
// object id parameter is null...
public override bool OnSave(object entity, object id, object[] state,
string[] propertyNames, IType[] types)
{
AddAuditItem(entity, INSERT);
return false;
}
I've worked around this problem by adding a list to my interceptor class which is populated with objects during the OnSave implementation.
In the PostFlush implementation the list is iterated over and each element is audited as an insert. The objects in this list have been persisted in PostFlush() and thus have generated IDs.
This seems to work OK but I'd be grateful if any potential pitfalls were pointed out :-)
public class AuditInterceptor : EmptyInterceptor
{
// To hold inserted items to be audited after insert has been flushed
private IList<object> insertItems = new List<object>();
public override void PostFlush(System.Collections.ICollection entities)
{
foreach (var entity in insertItems)
{
AddAuditItem(entity, INSERT);
}
insertItems.Clear();
base.PostFlush(entities);
}
public override bool OnSave(object entity, object id, object[] state,
string[] propertyNames, IType[] types)
{
var auditable = entity as IAuditable;
if (auditable != null)
insertItems.Add(entity);
return false;
}
}
try the OnFlushDirty method.. or maybe PostFlush
edit: also, can you post your code? Don't you get the Id as a parameter to OnSave?