How do I implement ChangeTime and ChangeUser columns using NHibernate? - nhibernate

I'm trying to use NHibernate with an existing database. In the data-model there is columns in each table that contains the time and username of the last update made to a row. How do I do this using NHibernate?
I tried to implement a interceptor that sets ChangeTime and ChangeUser in the entities before it gets saved using the IInterceptor.OnSave method. This didn't work because setting these properties triggers an update to the row even if no other properties has been modified.
It could have worked if there was any way to tell NHibernate to exclude the ChangeTime and ChangeUser properties then it does it's dirty-checking. But i haven't found any way to accomplish this.
Thanks for any help.

You should register a listener to the pre insert and pre update events. You can do it through your configuration like so:
<hibernate-configuration>
...
<event type="pre-update">
<listener class="MyListener, MyAssembly"/>
</event>
<event type="pre-insert">
<listener class="MyListener, MyAssembly"/>
</event>
</hibernate-configuration>
and then implement a listener - something like this (might not be entirely accurate - written off my memory):
public class MyListener : IPreUpdateEventListener, IPreInsertEventListener
{
public bool OnPreUpdate(PreUpdateEvent evt)
{
if (evt.Entity is IHasLastModified)
UpdateLastModified(evt.State, evt.Persister.PropertyNames);
return false;
}
public bool OnPreInsert(PreInsertEvent evt)
{
if (evt.Entity is IHasLastModified)
UpdateLastModified(evt.State, evt.Persister.PropertyNames);
return false;
}
void UpdateLastModified(object[] state, string[] names)
{
var index = Array.FindIndex(names, n => n == "LastModified");
state[index] = DateTime.Now;
}
}
and do the same thing with the pre update event.
EDIT: This one takes care of insert as well as update and it seems to work.

Hey I just had to solve this on a project I am working on, here is my answer
public interface IDateModified
{
DateTime Created { get; set; }
DateTime Modified { get; set; }
}
public class CustomDefaultSaveOrUpdateEventListener
: DefaultSaveOrUpdateEventListener
{
protected override object EntityIsPersistent(SaveOrUpdateEvent evt)
{
var entity = evt.Entity as IDateModified;
if (entity != null)
{
entity.Modified = DateTime.Now;
}
return base.EntityIsPersistent(evt);
}
protected override object EntityIsTransient(SaveOrUpdateEvent evt)
{
var entity = evt.Entity as IDateModified;
if (entity != null)
{
entity.Created = entity.Modified = DateTime.Now;
}
return base.EntityIsTransient(evt);
}
}
Then in my configuration (I am using Fluent NHibernate to configure my unit tests in code)
configuration.EventListeners.SaveOrUpdateEventListeners
= new ISaveOrUpdateEventListener[]
{
new CustomDefaultSaveOrUpdateEventListener()
};
AWESOMENESSSSSSSS!

Mookid's answer is correct although I would like to point out that if one is using S#arp Architecture, the NHib configuration should be set up as follows:
<hibernate-configuration>
<session-factory>
...
<event type="pre-update">
<listener class="MyListener, MyAssembly"/>
</event>
<event type="pre-insert">
<listener class="MyListener, MyAssembly"/>
</event>
</session-factory>
</hibernate-configuration>
The event elements go into the session-factory element.

Instead of using LIsteners, you can also use Interceptors:
Audit changes using interceptor

Related

Save all string values in lowercase

I would like to lower case all string values before saving them do db.
Is there any way NHibernate can do this and how ? Also are there any performance implications that I should be aware of ?
One way to achieve it would be introducing custom type for conversion. Something like:
[Serializable]
public class LowerCaseStringType : AbstractStringType, ILiteralType
{
public LowerCaseStringType() : base(new StringSqlType())
{
//To avoid NHibernate to issue update on flush when the same string is assigned with different casing
Comparer = StringComparer.OrdinalIgnoreCase;
}
public override string Name { get; } = "LowerCaseString";
public override void Set(DbCommand cmd, object value, int index, ISessionImplementor session)
{
base.Set(cmd, ((string) value)?.ToLowerInvariant(), index, session);
}
//Called when NHibernate needs to inline non parameterized string right into SQL. Not sure if you need it
string ILiteralType.ObjectToSQLString(object value, Dialect.Dialect dialect)
{
return "'" + ((string) value).ToLowerInvariant() + "'";
}
//If you also want to retrieve all values in lowercase than also override Get method
}
Than you can either map required properties with this type like:
<property name="Name" type="YourNamespace.LowerCaseStringType, YourAssemblyName">
Or even register it as default type for all string mappings (at least it's true for latest NHibernate 5.2):
//Somewhere before SessionFactory is created
TypeFactory.RegisterType(typeof(string), new LowerCaseStringType(), new[] {"string", "String"});

NHibernate New item added to bag collection does not get saved when updating parent entity

I'm using NHibernate version 3.1.0.4000 with hbm mapping files.
When I have an entity which contains a bag collection and i save the entity with all the items added to the bag then it saves all the collection items fine. However, when i then subsequently (after the initial save) add another item to the collection and save the entity then it does not save the new item in the collection. I had a look at the SQL being generated by NHibernate and it creates the initial Insert SQL statement, but it does not create the Update SQL statement to update the foreign key value. This issue occurs for all bags in the solution.
Here is a mapping extract:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
default-lazy="false"
namespace="some_namespace"
assembly="some_assembly">
<class name="Landing"
table="[Landing]"
select-before-update="true"
optimistic-lock="version">
<id name="Id"
column="[Id]"
unsaved-value="null">
<generator class="assigned" />
</id>
<version name="Version"
column="[Version]"
unsaved-value="null" />
<bag name="LandingPermits"
cascade="all-delete-orphan"
access="field.camelcase">
<key column="[LandingId]" />
<one-to-many class="LandingPermit" />
</bag>
</class>
</hibernate-mapping>
Here's the Save method on my NHibernate Repository:
public NHibernateRepository(ISessionFactory factory, string queriesAssemblyName = null, string clientName = null)
{
this.sessionFactory = factory;
this.queriesAssemblyName = queriesAssemblyName;
this.clientName = clientName;
if (!string.IsNullOrEmpty(this.queriesAssemblyName))
LoadQueries();
}
public virtual void Save(IAggregateRoot entity)
{
Save(new IAggregateRoot[] { entity });
}
public virtual void Save(IAggregateRoot[] entities)
{
try
{
using (var session = OpenSession())
{
using (var transaction = session.BeginTransaction())
{
try
{
foreach (var entity in entities)
{
if (entity.IsNew)
entity.AddedDateTime = DateTime.Now;
else
entity.UpdatedDateTime = DateTime.Now;
session.SaveOrUpdate(entity.GetType().Name, entity);
}
transaction.Commit();
}
catch (Exception e)
{
transaction.Rollback();
throw e;
}
finally
{
if (session.Connection.State == ConnectionState.Open)
session.Connection.Close();
session.Connection.Dispose();
session.Close();
}
}
}
}
catch (GenericADOException e)
{
var sqlException = e.InnerException as SqlException;
if ((sqlException != null) && (sqlException.Number == ForeignKeyExceptionNumber))
throw new EntityInUseException("Save", sqlException);
else
throw new RepositoryException("Save", e.InnerException);
}
catch (StaleObjectStateException e)
{
throw new ConcurrencyException("Save", e, new Identity((Guid?)e.Identifier));
}
catch (Exception e)
{
throw new RepositoryException("Save", e);
}
}
I have tried a few things, including setting the inverse property to true, but no success.
Hope this is enough information for anyone to asssist.
Thanks
If your collection is inverse=false (the default), that means that the owner of the collection (the parent) is responsible for the relationship.
It's not clear in your code above if both parent and child are IAggregateRoot, but if they both are it means that, when you save the child in a session not aware of the parent, that the child will be saved fine but will not be added to the collection.
If you would use inverse=true, the child would be responsible for the relationship. In this case the model should be bidirectional and you should map both ends.
You have a few options here...
Is the child really an aggregate root? Maybe it should be persisted together with its parent?
Use a bidirectional relationship and use inverse=true

Hibernate3 --> Hibernate 4 and issues (Lazy...)

I'm trying to update the libraries of my project (from Hibernate 3.2.1 GA to Hibernate 4.2.8)
This (complex) application use LAZY loading and get the object later only when we need it.
-->it seems to work differently now because I get some org.hibernate.LazyInitializationException: could not initialize proxy - no Session
#Entity
#Table(name = "CLIENTS")
public class Clients {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "INFOIDT", insertable = true, updatable = false)
private Information info;
//...
}
and when I need to know more about the product before using it :
cli.getInfo();
Note that in my persistence.xml I also have the property
hibernate.cache.provider_class set to org.hibernate.cache.EhCacheProvider for a second level cache.
QUESTION : what is the simple way to migrate my existing code with Hibernate4?
(the class given for example above is a fake example to illustrate the many cases using the LAZY loading)
Thank you.
As requested, see my DAO below :
public class MyAppJpaDAO extends GenericJpaDAO implements IMyAppDAO {
protected static Log log = LogFactory.getLog(MyAppJpaDAO.class);
// Entity Manager of the project
#PersistenceContext(unitName = "MyApp.hibernate")
private EntityManager em;
public News readLastNews() {
StringBuffer sql = new StringBuffer("");
sql.append(" select object(n) ");
sql.append(" from News n ");
sql.append(" Where n.flagLastStatus = 'V' ");
sql.append(" order by n.pk.date desc ");
Query aQuery = em.createQuery(sql.toString());
List<News> res = (List<News>) aQuery.getResultList();
if (res != null && res.size() != 0) {
return res.get(0);
}
return null;
}
//...
}
/////////////
public class GenericJpaDAO implements IGenericDAO {
protected static Log log = LogFactory.getLog(GenericJpaDAO.class);
#PersistenceContext(unitName = "MyApp.hibernate")
EntityManager em;
public Object getReference(Class _class, Object _object) {
return em.getReference(_class, _object);
}
public void createObject(Object object) {
try {
em.persist(object);
} catch (LazyInitializationException lie) {
em.merge(em.merge(object));
}
}
public void deleteObject(Object object) {
try {
em.remove(object);
} catch (Exception e) {
em.remove(em.merge(object));
}
}
public void updateObject(Object object) {
em.merge(em.merge(object));
}
//...
}
If you want to use LazyLoading, you need to have the session opened and connected at the time when you calls .getInfo(). org.hibernate.LazyInitializationException occures if you tries to get an entity but the session is disconnected or closed.
I think you have problems with session handling. There is nothing to do with the entities.
If the SessionFactory is configured in a Spring context file, we can use the OpenSessionInViewFilter to keep the session open.
<filter>
<filter-name>Hibernate Session In View Filter</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Hibernate Session In View Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Unfortunately, my application is not configured like this...
Interesting...but still not helping
http://www.javacodegeeks.com/2012/07/four-solutions-to-lazyinitializationexc_05.html
But I find something :
1)Hibernate 3.2.1 GA and Spring 2.0
I used to put a Person having a LAZY bag in a Group and when I wanted to get some pencil from the bag of any person of the group, I was able to get it.
2)Hibernate 4.2.8 et Spring 3.2.5.
If I don't explicitely ask to know the content of the bag just after getting the Person and before putting it into the group, I will have the lazy exception.
If someone could explain me why...

AppFabric: Could not contact the cache service

Update: I have now implemented this properly. For more information see my blog post about it.
I'm trying to use AppFabric with NHibernate as my second level cache provider but I'm getting the following error: ErrorCode:Initialization: Could not contact the cache service. Contact administrator and refer to product help documentation for possible reasons.
I presume that the problem is with my configuration in web.config:
<section name="dcacheClient"
type="Microsoft.ApplicationServer.Caching.DataCacheClientSection, Microsoft.ApplicationServer.Caching.Core"
allowLocation="true"
allowDefinition="Everywhere"/>
...
<dcacheClient deployment="routing" localCache="False">
<localCache isEnabled="false" sync="TimeoutBased" ttlValue="300" />
<hosts>
<host name="localhost" cachePort="22233" cacheHostName="AppFabricCachingService" />
</hosts>
</dcacheClient>
I've downloaded the NHibernate.Caches source code to try and discover where the problem lies and the exception is being thrown in the VelocityClient constructor when the GetCache method is called:
public VelocityClient(string regionName, IDictionary<string, string> properties)
{
region = regionName.GetHashCode().ToString(); //because the region name length is limited
var cacheCluster = new CacheFactory();
cache = cacheCluster.GetCache(CacheName);
try
{
cache.CreateRegion(region, true);
}
catch (CacheException) {}
}
If I add a watch to the cacheCluster variable, I can find a _servers private variable which has one System.Data.Caching.EndpointID which has the MyURI property set to net.tcp://localhost:22234/AppFabricCachingServive which I presume has come from the configuration in web.config.
If you don't know the exact cause of the problem but have some ideas on how to go about troubleshooting this problem, that would be much appreciated as well.
Additional Info
I get the following results from the command, Get-CacheHostConfig -HostName tn-staylor-02 -CachePort 22233:
HostName : tn-staylor-02
ClusterPort : 22234
CachePort : 22233
ArbitrationPort : 22235
ReplicationPort : 22236
Size : 3001 MB
ServiceName : AppFabricCachingService
HighWatermark : 90%
LowWatermark : 70%
IsLeadHost : True
So I think the values I've got configured in web.config are OK.
Googling this problem and investigating how to set up AppFabric in the first place, I have come across two slightly different ways of how to configure the cache in web.config. The way I have described above and the way Hanselman has it in his AppFabric blog post
I actually started with it like this however, I got the following error which is how I came to have it configured how I have it now:
ErrorCode:"dcacheClient" tag not specified in the application configuration file. Specify valid tag in configuration file.
Full stack trace of the exception that gets thrown in VelocityClient:
System.Data.Caching.CacheException occurred
Message="ErrorCode:\"dcacheClient\" tag not specified in the application configuration file. Specify valid tag in configuration file."
Source="CacheBaseLibrary"
ErrorCode="ERRCMC0004"
StackTrace:
at System.Data.Caching.ClientConfigFile.ThrowException(String errorCode, String param)
at System.Data.Caching.ClientConfigReader.GetDeployementMode()
at System.Data.Caching.ClientConfigurationManager.InitializeDepMode(ClientConfigReader cfr)
at System.Data.Caching.ClientConfigurationManager.Initialize(String path)
at System.Data.Caching.ClientConfigurationManager..ctor()
at System.Data.Caching.CacheFactory.InitCacheFactory()
at System.Data.Caching.CacheFactory.GetCache(String cacheName)
at NHibernate.Caches.Velocity.VelocityClient..ctor(String regionName, IDictionary`2 properties) in C:\Source\Projects\NHibernate.contrib\trunk\src\NHibernate.Caches\Velocity\NHibernate.Caches.Velocity\VelocityClient.cs:line 67
InnerException:
EDIT: Added output from get-cachehost as requested by #PhilPursglove
Output from get-cachehost:
HostName : CachePort Service Name Service Status Version Info
-------------------- ------------ -------------- ------------
tn-staylor-02:22233 AppFabricCachingService UP 1 [1,1][1,1]
SOLUTION: #PhilPursglove was spot on. The NHibernate velocity provider was using old dll's so upgrading them and making a few code changes resolved my problems. I thought I would include my complete solution here.
Downloaded the NHibernate.contrib source from the SVN repository at https://nhcontrib.svn.sourceforge.net/svnroot/nhcontrib/trunk
Opened up the NHibernate.Caches.Everything solution and removed the references to the old velocity dll's from the NHibernate.Caches.Velocity project.
Added references to the App Fabric dll's which were installed when I installed App Fabric. This isn't the normal case of adding a reference to an assembly in the GAC, but this article describes how to do it.
Adding the new references meant that the VelocityClient class no longer compiled. With a little bit of help from this I came up with the version of VelocityClient.cs below.
I added a reference to the new version of NHibernate.Caches.Velocity to my project, made the changes below to my configuration and everything worked.
VelocityClient.cs
using System;
using System.Collections.Generic;
using Microsoft.ApplicationServer.Caching;
using log4net;
using NHibernate.Cache;
using CacheException = Microsoft.ApplicationServer.Caching.DataCacheException;
using CacheFactory = Microsoft.ApplicationServer.Caching.DataCacheFactory;
namespace NHibernate.Caches.Velocity
{
public class VelocityClient : ICache
{
private const string CacheName = "nhibernate";
private static readonly ILog log;
private readonly DataCache cache;
private readonly string region;
private Dictionary<string, DataCacheLockHandle> locks = new Dictionary<string, DataCacheLockHandle>();
static VelocityClient()
{
log = LogManager.GetLogger(typeof (VelocityClient));
}
public VelocityClient() : this("nhibernate", null) {}
public VelocityClient(string regionName) : this(regionName, null) {}
public VelocityClient(string regionName, IDictionary<string, string> properties)
{
region = regionName.GetHashCode().ToString(); //because the region name length is limited
var cacheCluster = new CacheFactory();
cache = cacheCluster.GetCache(CacheName);
try
{
cache.CreateRegion(region);
}
catch (CacheException) {}
}
#region ICache Members
public object Get(object key)
{
if (key == null)
{
return null;
}
if (log.IsDebugEnabled)
{
log.DebugFormat("fetching object {0} from the cache", key);
}
DataCacheItemVersion version = null;
return cache.Get(key.ToString(), out version, region);
}
public void Put(object key, object value)
{
if (key == null)
{
throw new ArgumentNullException("key", "null key not allowed");
}
if (value == null)
{
throw new ArgumentNullException("value", "null value not allowed");
}
if (log.IsDebugEnabled)
{
log.DebugFormat("setting value for item {0}", key);
}
cache.Put(key.ToString(), value, region);
}
public void Remove(object key)
{
if (key == null)
{
throw new ArgumentNullException("key");
}
if (log.IsDebugEnabled)
{
log.DebugFormat("removing item {0}", key);
}
if (Get(key.ToString()) != null)
{
cache.Remove(region, key.ToString());
}
}
public void Clear()
{
cache.ClearRegion(region);
}
public void Destroy()
{
Clear();
}
public void Lock(object key)
{
DataCacheLockHandle lockHandle = null;
if (Get(key.ToString()) != null)
{
try
{
cache.GetAndLock(key.ToString(), TimeSpan.FromMilliseconds(Timeout), out lockHandle, region);
locks.Add(key.ToString(), lockHandle);
}
catch (CacheException) {}
}
}
public void Unlock(object key)
{
DataCacheLockHandle lockHandle = null;
if (Get(key.ToString()) != null)
{
try
{
if (locks.ContainsKey(key.ToString()))
{
cache.Unlock(key.ToString(), locks[key.ToString()], region);
locks.Remove(key.ToString());
}
}
catch (CacheException) {}
}
}
public long NextTimestamp()
{
return Timestamper.Next();
}
public int Timeout
{
get { return Timestamper.OneMs * 60000; } // 60 seconds
}
public string RegionName
{
get { return region; }
}
#endregion
}
}
NHibernate.config:
...
<property name="cache.provider_class">NHibernate.Caches.Velocity.VelocityProvider, NHibernate.Caches.Velocity</property>
<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache">true</property>
...
web.config
...
<section name="dataCacheClient"
type="Microsoft.ApplicationServer.Caching.DataCacheClientSection, Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
allowLocation="true"
allowDefinition="Everywhere"/>
...
<dataCacheClient>
<!-- cache host(s) -->
<hosts>
<host
name="localhost"
cachePort="22233"/>
</hosts>
</dataCacheClient>
...
I didn't make any further changes to my App Fabric configuration or anything.
I think there are two possible culprits here:
In your web.config under the hosts element, you're listing localhost - I'd try swapping that out for the actual server name tn-staylor-02
That exception stack trace refers to CacheBaseLibrary - I don't know a great deal (read: anything!) about NHibernate but I would hazard a guess that that cache might not be built with the release version of AppFabric - CacheBaseLibrary was an assembly that appeared in the CTPs and betas but I didn't think it was used in the RTM version. Note that in the section element for dcacheclient, it refers to the Microsoft.ApplicationServer.Caching.Core assembly.

JSON.NET and nHibernate Lazy Loading of Collections

Is anybody using JSON.NET with nHibernate? I notice that I am getting errors when I try to load a class with child collections.
I was facing the same problem so I tried to use #Liedman's code but the GetSerializableMembers() was never get called for the proxied reference.
I found another method to override:
public class NHibernateContractResolver : DefaultContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
if (typeof(NHibernate.Proxy.INHibernateProxy).IsAssignableFrom(objectType))
return base.CreateContract(objectType.BaseType);
else
return base.CreateContract(objectType);
}
}
We had this exact problem, which was solved with inspiration from Handcraftsman's response here.
The problem arises from JSON.NET being confused about how to serialize NHibernate's proxy classes. Solution: serialize the proxy instances like their base class.
A simplified version of Handcraftsman's code goes like this:
public class NHibernateContractResolver : DefaultContractResolver {
protected override List<MemberInfo> GetSerializableMembers(Type objectType) {
if (typeof(INHibernateProxy).IsAssignableFrom(objectType)) {
return base.GetSerializableMembers(objectType.BaseType);
} else {
return base.GetSerializableMembers(objectType);
}
}
}
IMHO, this code has the advantage of still relying on JSON.NET's default behaviour regarding custom attributes, etc. (and the code is a lot shorter!).
It is used like this
var serializer = new JsonSerializer{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new NHibernateContractResolver()
};
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new Newtonsoft.Json.JsonTextWriter(stringWriter);
serializer.Serialize(jsonWriter, objectToSerialize);
string serializedObject = stringWriter.ToString();
Note: This code was written for and used with NHibernate 2.1. As some commenters have pointed out, it doesn't work out of the box with later versions of NHibernate, you will have to make some adjustments. I will try to update the code if I ever have to do it with later versions of NHibernate.
I use NHibernate with Json.NET and noticed that I was getting inexplicable "__interceptors" properties in my serialized objects. A google search turned up this excellent solution by Lee Henson which I adapted to work with Json.NET 3.5 Release 5 as follows.
public class NHibernateContractResolver : DefaultContractResolver
{
private static readonly MemberInfo[] NHibernateProxyInterfaceMembers = typeof(INHibernateProxy).GetMembers();
protected override List<MemberInfo> GetSerializableMembers(Type objectType)
{
var members = base.GetSerializableMembers(objectType);
members.RemoveAll(memberInfo =>
(IsMemberPartOfNHibernateProxyInterface(memberInfo)) ||
(IsMemberDynamicProxyMixin(memberInfo)) ||
(IsMemberMarkedWithIgnoreAttribute(memberInfo, objectType)) ||
(IsMemberInheritedFromProxySuperclass(memberInfo, objectType)));
var actualMemberInfos = new List<MemberInfo>();
foreach (var memberInfo in members)
{
var infos = memberInfo.DeclaringType.BaseType.GetMember(memberInfo.Name);
actualMemberInfos.Add(infos.Length == 0 ? memberInfo : infos[0]);
}
return actualMemberInfos;
}
private static bool IsMemberDynamicProxyMixin(MemberInfo memberInfo)
{
return memberInfo.Name == "__interceptors";
}
private static bool IsMemberInheritedFromProxySuperclass(MemberInfo memberInfo, Type objectType)
{
return memberInfo.DeclaringType.Assembly == typeof(INHibernateProxy).Assembly;
}
private static bool IsMemberMarkedWithIgnoreAttribute(MemberInfo memberInfo, Type objectType)
{
var infos = typeof(INHibernateProxy).IsAssignableFrom(objectType)
? objectType.BaseType.GetMember(memberInfo.Name)
: objectType.GetMember(memberInfo.Name);
return infos[0].GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length > 0;
}
private static bool IsMemberPartOfNHibernateProxyInterface(MemberInfo memberInfo)
{
return Array.Exists(NHibernateProxyInterfaceMembers, mi => memberInfo.Name == mi.Name);
}
}
To use it just put an instance in the ContractResolver property of your JsonSerializer. The circular dependency problem noted by jishi can be resolved by setting the ReferenceLoopHandling property to ReferenceLoopHandling.Ignore . Here's an extension method that can be used to serialize objects using Json.Net
public static void SerializeToJsonFile<T>(this T itemToSerialize, string filePath)
{
using (StreamWriter streamWriter = new StreamWriter(filePath))
{
using (JsonWriter jsonWriter = new JsonTextWriter(streamWriter))
{
jsonWriter.Formatting = Formatting.Indented;
JsonSerializer serializer = new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new NHibernateContractResolver(),
};
serializer.Serialize(jsonWriter, itemToSerialize);
}
}
}
Are you getting a circular dependancy-error? How do you ignore objects from serialization?
Since lazy loading generates a proxy-objects, any attributes your class-members have will be lost. I ran into the same issue with Newtonsoft JSON-serializer, since the proxy-object didn't have the [JsonIgnore] attributes anymore.
You will probably want to eager load most of the object so that it can be serialized:
ICriteria ic = _session.CreateCriteria(typeof(Person));
ic.Add(Restrictions.Eq("Id", id));
if (fetchEager)
{
ic.SetFetchMode("Person", FetchMode.Eager);
}
A nice way to do this is to add a bool to the constructor (bool isFetchEager) of your data provider method.
I'd say this is a design problem in my opinion. Because NH makes connections to the database underneath all and has proxies in the middle, it is not good for the transparency of your application to serialize them directly (and as you can see Json.NET does not like them at all).
You should not serialize the entities themselves, but you should convert them into "view" objects or POCO or DTO objects (whatever you want to call them) and then serialize these.
The difference is that while NH entity may have proxies, lazy attributes, etc. View objects are simple objects with only primitives which are serializable by default.
How to manage FKs?
My personal rule is:
Entity level: Person class and with a Gender class associated
View level: Person view with GenderId and GenderName properties.
This means that you need to expand your properties into primitives when converting to view objects. This way also your json objects are simpler and easier to handle.
When you need to push the changes to the DB, in my case I use AutoMapper and do a ValueResolver class which can convert your new Guid to the Gender object.
UPDATE: Check http://blog.andrewawhitaker.com/blog/2014/06/19/queryover-series-part-4-transforming/ for a way to get the view directly (AliasToBean) from NH. This would be a boost in the DB side.
The problem can happen when NHibernate wraps the nested collection properties in a PersistentGenericBag<> type.
The GetSerializableMembers and CreateContract overrides cannot detect that these nested collection properties are "proxied". One way to resolve this is to override the CreateProperty method. The trick is to get the value from the property using reflection and test whether the type is of PersistentGenericBag. This method also has the ability to filter any properties that generated exceptions.
public class NHibernateContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
property.ShouldSerialize = instance =>
{
try
{
PropertyInfo prop = (PropertyInfo)member;
if (prop.CanRead)
{
var value = prop.GetValue(instance, null);
if (value != null && typeof(NHibernate.Collection.Generic.PersistentGenericBag<>).IsSubclassOfRawGeneric(value.GetType()))
return false;
return true;
}
}
catch
{ }
return false;
};
return property;
}
}
The IsSubclassOfRawGeneric extension used above:
public static class TypeExtensions
{
public static bool IsSubclassOfRawGeneric(this Type generic, Type? toCheck)
{
while (toCheck != null && toCheck != typeof(object))
{
var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
if (generic == cur)
{
return true;
}
toCheck = toCheck?.BaseType;
}
return false;
}
}
If you serialize objects that contain NHibernate proxy classes you might end up downloading the whole database, because once the property is accessed NHibernate would trigger a request to the database.
I've just implemented a Unit of Work for NHibernate: NHUnit that fixes two of the most annoying issues from NHibernate: proxy classes and cartesian product when using fetch.
How would you use this?
var customer = await _dbContext.Customers.Get(customerId) //returns a wrapper to configure the query
.Include(c => c.Addresses.Single().Country, //include Addresses and Country
c => c.PhoneNumbers.Single().PhoneNumberType) //include all PhoneNumbers with PhoneNumberType
.Unproxy() //instructs the framework to strip all the proxy classes when the Value is returned
.Deferred() //instructs the framework to delay execution (future)
.ValueAsync(token); //this is where all deferred queries get executed
The above code is basically configuring a query: return a customer by id with multiple child objects which should be executed with other queries (futures) and the returned result should be stripped of NHibernate proxies. The query gets executed when ValueAsync is called.
NHUnit determines if it should do join with the main query, create new future queries or make use of batch fetch.
There is a simple example project on Github to show you how to use NHUnit package. If others are interested in this project I will invest more time to make it better.
This is what I use:
Have a marker interface and inherit it on your entities, e.g. in my case empty IEntity.
We will use the marker interface to detect NHibernate entity types in the contract resolver.
public class CustomerEntity : IEntity { ... }
Create a custom contract resolver for JSON.NET
public class NHibernateProxyJsonValueProvider : IValueProvider {
private readonly IValueProvider _valueProvider;
public NHibernateProxyJsonValueProvider(IValueProvider valueProvider)
{
_valueProvider = valueProvider;
}
public void SetValue(object target, object value)
{
_valueProvider.SetValue(target, value);
}
private static (bool isProxy, bool isInitialized) GetProxy(object proxy)
{
// this is pretty much what NHibernateUtil.IsInitialized() does.
switch (proxy)
{
case INHibernateProxy hibernateProxy:
return (true, !hibernateProxy.HibernateLazyInitializer.IsUninitialized);
case ILazyInitializedCollection initializedCollection:
return (true, initializedCollection.WasInitialized);
case IPersistentCollection persistentCollection:
return (true, persistentCollection.WasInitialized);
default:
return (false, false);
}
}
public object GetValue(object target)
{
object value = _valueProvider.GetValue(target);
(bool isProxy, bool isInitialized) = GetProxy(value);
if (isProxy)
{
if (isInitialized)
{
return value;
}
if (value is IEnumerable)
{
return Enumerable.Empty<object>();
}
return null;
}
return value;
}
}
public class NHibernateContractResolver : CamelCasePropertyNamesContractResolver {
protected override JsonContract CreateContract(Type objectType)
{
if (objectType.IsAssignableTo(typeof(IEntity)))
{
return base.CreateObjectContract(objectType);
}
return base.CreateContract(objectType);
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
property.ValueProvider = new NHibernateProxyJsonValueProvider(property.ValueProvider);
return property;
}
}
Normal uninitialized lazy loaded properties will result in null in the json output.
Collection uninitialized lazy loaded properties will result in an [] empty array in json.
So for a lazy loaded property to appear in the json output you need to eagerly load it in the query or in code before serialization.
Usage:
JsonConvert.SerializeObject(entityToSerialize, new JsonSerializerSettings() {
ContractResolver = new NHibernateContractResolver()
});
Or globally in in ASP.NET Core Startup class
services.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new NHibernateContractResolver();
});
Using:
NET 5.0
NHibernate 5.3.8
JSON.NET latest via ASP.NET Core