I have a web application that is using the absolute latest version (3.3) and is using session-by-request session management within a HttpModule, so there are no problems with multiple session conflicts. Unfortunately, I am finding that the session is getting automatically closed immediately after I perform a Transaction.Commit which I only do when I am actually performing a Create, Update or Delete. I am finding this within my NHibernate log.
I know for a fact that I am not doing it, because the only call to the ISession.Close function is being done within my HttpModule.
Yes, of course I can put code in my SessionManager to check for the IsClosed parameter and then use the OpenSession function instead of GetCurrentSession, but should this even be happening? Is there some way that I can prevent this either through my configuration or some attribute that I could set on the Session or Transaction object or is this just one of the new features that I can't find any documentation anywhere on?
Please help.
Brian
I was asked to provide some code, so here is the code for the HttpModule:
public class NhibernateModule : IHttpModule
{
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
context.EndRequest += new EventHandler(context_EndRequest);
}
public void context_BeginRequest(Object sender, EventArgs e)
{
WebSessionContext.Bind(NhibernateSessionManager.GetContextSession());
}
public void context_EndRequest(Object sender, EventArgs e)
{
ISession session = WebSessionContext.Unbind(NhibernateSessionManager.SessionFactory);
if (session != null)
{
if (session.Transaction != null && session.Transaction.IsActive)
{
session.Transaction.Rollback();
}
else
session.Flush();
session.Close();
}
}
}
}
Next, you will find the original code that I am using within my SessionManager:
public sealed class NhibernateSessionManager
{
private readonly ISessionFactory sessionFactory;
public static ISessionFactory SessionFactory
{
get { return Instance.sessionFactory; }
}
private ISessionFactory GetSessionFactory()
{
return sessionFactory;
}
public static NhibernateSessionManager Instance
{
get { return NestedSessionManager.sessionManager; }
}
public static ISession GetContextSession()
{
ISession session;
if (CurrentSessionContext.HasBind(SessionFactory))
{
session = SessionFactory.GetCurrentSession();
}
else
{
session = SessionFactory.OpenSession();
CurrentSessionContext.Bind(session);
}
return session;
}
private NhibernateSessionManager()
{
if (sessionFactory == null)
{
Configuration configuration;
configuration = new Configuration().Configure(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web.config"));
log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web.config")));
//Configuration configuration = new Configuration().Configure();
if (configuration == null)
{
throw new InvalidOperationException("NHibernate configuration is null.");
}
else
{
sessionFactory = configuration.BuildSessionFactory();
if (sessionFactory == null)
throw new InvalidOperationException("Call to BuildSessionFactory() returned null.");
}
}
}
class NestedSessionManager
{
internal static readonly NhibernateSessionManager sessionManager = new NhibernateSessionManager();
}
}
Last, here is a function that is currently causing the session to close immediately after the Transaction.Commit(). Each of the inner functions retrieve the current session and then process a Save call.
public static Int32 AddVideo(VideoFile Video, Int32 UserID, Int16 InstID)
{
log.Debug("Begin AddVideo");
Int32 FileID = 0;
using (ISession Session = NhibernateSessionManager.GetContextSession())
{
using (ITransaction Transaction = Session.BeginTransaction())
{
Video.Created = DateTime.Now;
Video.Modified = DateTime.Now;
FileID = (Int32)Session.Save(Video);
Video.FileID = FileID;
// Need to process through all the categories and insert records into the ivxFileCategories table
// to associate the newly created file with the chosen categories
if (Video.CategoryAssociations != null)
{
log.Info("Number of categories to be associated with the video: " + Video.CategoryAssociations.Count);
for (int i = 0; i < Video.CategoryAssociations.Count; i++)
{
CategoryFileAssociation Assoc = (CategoryFileAssociation)Video.CategoryAssociations[i];
Assoc.FileID = FileID;
AssociationManager.AddCategoryFileTransaction(Assoc);
}
}
// Need to add the default file access for the UserDetail that is creating the new video which will always
// be Admin because the UserDetail creating a file should always have admin access over the file, no matter
// what their default role is.
AssociationManager.AddFileAccessTransaction(FileID, UserID, UserClassConstants.IVXUSERCLASS_ADMIN);
// Need to add the institutional association based on whether the new video was created by a librarian
// or one of the iVidix admins
AssociationManager.AddInstitutionFileTransaction(InstID, FileID);
Transaction.Commit();
}
}
log.Debug("End AddVideo");
return FileID;
}
The session will by disposed in the AddVideo method because you are using the using Statement for the session.
using (ISession Session = NhibernateSessionManager.GetContextSession())
{
}
I would totally recommend striping away the transaction stuff
using (ISession Session = NhibernateSessionManager.GetContextSession())
{
using (ITransaction Transaction = Session.BeginTransaction())
{
...
}
}
and move it into the begin/end request. This way you have a UOW for each request. The reason you session is closing IS because of the using statement.
Your begin request code can then be something along the lines of:-
var session = sessionFactory.OpenSession();
CurrentSessionContext.Bind(session);
session.BeginTransaction();
and your end request:-
var session = CurrentSessionContext.Unbind(sessionFactory);
if (session != null)
{
if (session.Transaction.IsActive)
{
try
{
session.Transaction.Commit();
}
catch
{
session.Transaction.Rollback();
}
}
session.Close();
}
I have this in my global.asax
public static ISessionFactory SessionFactory { get; set; }
and this in my repositories
public ISession Session
{
get
{
return SessionFactory.GetCurrentSession();
}
}
Now I use IOC to pass my sessionFactory to my repository layers, else you will need to pass this in yourself manually.
Committing transaction will end that session.
Move your transaction start to context_BeginRequest and the commit/cleanup in context_EndRequest
I actually don't like session in view pattern and prefer to keep my transactions open as short as possible and rather inject session into the controller. I then perform the transaction in the action or service. I prefer this fine grained control of the transactions and keeps them short lived avoiding any locking issues.
Related
I have a SignalR hub in which I'm injecting service classes which persist data to a local SQL Server instance via Castle Windsor.
The hub looks like:
[Authorize]
public class MyHub : Hub
{
private readonly IHubService _hubService;
private readonly IHubUserService _hubUserService;
private readonly IUserService _userService;
public MyHub(IHubService hubService, IHubUserService hubUserService, IUserService userService)
{
_hubService = hubService;
_hubUserService = hubUserService;
_userService = userService;
}
public async Task JoinHub(Guid hubId)
{
var hub = _hubService.GetHubById(hubId);
if (hub == null)
throw new NotFoundException(String.Format("Hub ({0}) was not found.", hubId.ToString()));
var userName = Context.User.Identity.Name;
var user = _userService.GetUserByUserName(userName);
if (user == null)
throw new NotFoundException(String.Format("User ({0}) was not found.", userName));
var hubUser = new HubUser
{
User = user,
Hub = hub,
ConnectionId = Context.ConnectionId
};
// Persist a new HubUser to the DB
hubUser = _hubUserService.InsertHubUser(hubUser);
await Groups.Add(Context.ConnectionId, hub.Id.ToString());
Clients.Group(hub.Id.ToString()).addChatMessage(userName + " has joined.");
}
public async Task LeaveHub()
{
var userName = Context.User.Identity.Name;
var hubUser = _hubUserService.GetHubUserByUserName(userName);
// Removes HubUser from the DB
_hubUserService.RemoveHubUser(hubUser);
await Groups.Remove(Context.ConnectionId, hubUser.Hub.Id.ToString());
Clients.Group(hubUser.Hub.Id.ToString()).addChatMessage(userName + " has left.");
}
public override Task OnDisconnected(bool stopCalled)
{
var userName = Context.User.Identity.Name;
var hubUser = _hubUserService.GetHubUserByUserName(userName);
// Removes HubUser from the DB
_hubUserService.RemoveHubUser(hubUser); // This line executes but does not persist anything to DB
Groups.Remove(Context.ConnectionId, hubUser.Hub.Id.ToString());
Clients.Group(hubUser.Hub.Id.ToString()).addChatMessage(userName + " has left.");
return base.OnDisconnected(stopCalled);
}
}
When calling JoinHub and LeaveHub methods from the client, everything works fine. However, when the OnDisconnected method fires, nothing is deleted from the database. I can see that the code does indeed execute, but the record remains in the DB and does not get deleted.
I'm wondering if perhaps my nhibernate session is not committing the transaction to the database due to castle windsor's dependency lifetimes or something, however, it's odd that LeaveHub executes as expected but the same code does not in the OnDisconnected method.
My dependencies are registered with the following configuration as per this blog post.
Kernel.Register(
//Nhibernate session factory
Component.For<ISessionFactory>().UsingFactoryMethod(CreateNhSessionFactory).LifeStyle.Singleton,
//Nhibernate session
Component.For<ISession>().UsingFactoryMethod(kernel => kernel.Resolve<ISessionFactory>().OpenSession()).LifeStyle.HybridPerWebRequestTransient()
);
and I also register an interceptor to implement a unit of work pattern:
// Unitofwork interceptor
Component.For<NhUnitOfWorkInterceptor>().LifeStyle.HybridPerWebRequestTransient()
If anyone can give any input on why the method LeaveHub works correctly and why it fails to persist anything in the OnDisconnected method, that'd be greatly appreciated.
Just an FYI Nhibernate Sessions don't do so well using async as they are not threadsafe at all. Try running things synchronously and see what you get.
Is Nhibernate set to flush on transaction commit? I can't comment becasue I am a newbie but I ran into this issue some time ago. I am not using FluentNhibernate but I am sure there is a config option to set flush on transaction commit. This is assuming you are wrapping all open session calls in a transaction. I use something like this for sessions. Also go get Nhibernate Profiler it is a godsend.
public class SessionManager : ISessionManager
{
private readonly ISessionFactory _sessionFactory;
private ISession _currentSession;
private ITransaction _currentTransaction;
public SessionManager(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
public ISession OpenSession()
{
if (CurrentSessionContext.HasBind(_sessionFactory))
{
_currentSession = _sessionFactory.GetCurrentSession();
}
else
{
_currentSession = _sessionFactory.OpenSession();
CurrentSessionContext.Bind(_currentSession);
}
CurrentSessionContext.Bind(_currentSession);
_currentTransaction = _currentSession.BeginTransaction();
return _currentSession;
}
public void Dispose()
{
try
{
if (_currentTransaction != null && _currentTransaction.IsActive)
_currentTransaction.Commit();
}
catch (Exception)
{
if (_currentTransaction != null) _currentTransaction.Rollback();
throw;
}
finally
{
if (_currentSession != null)
{
if (_currentTransaction != null) _currentTransaction.Dispose();
_currentSession.Close();
}
}
}
}
Here is my configuration, I am using it on several apps. On a side not there is a reason I don't use FluentNhibernate, The mapping by code built in is awesome and flexible. Let me know I can send you some sample mappings.
public class SessionFactoryBuilder
{
public static ISessionFactory BuildSessionFactory(string connectionString)
{
var cfg = new Configuration();
cfg.DataBaseIntegration(db =>
{
db.Dialect<MsSql2012Dialect>();
db.Driver<Sql2008ClientDriver>();
db.ConnectionString = connectionString;
db.BatchSize = 1500;
db.LogSqlInConsole = false;
db.PrepareCommands = true;
db.ConnectionReleaseMode = ConnectionReleaseMode.AfterTransaction;
db.IsolationLevel = IsolationLevel.ReadCommitted;
})
.SetProperty(Environment.CurrentSessionContextClass, "web")
.SetProperty(Environment.UseSecondLevelCache, "true")
.SetProperty(Environment.ShowSql, "true")
.SetProperty(Environment.PrepareSql, "true")
.Cache(c =>
{
c.UseQueryCache = true;
c.Provider<RtMemoryCacheProvider>();
c.DefaultExpiration = 1440;
}).SessionFactory().GenerateStatistics();
HbmMapping mapping = GetMappings();
cfg.AddDeserializedMapping(mapping, "AppName");
SchemaMetadataUpdater.QuoteTableAndColumns(cfg);
return cfg.BuildSessionFactory();
}
private static HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.AddMappings(typeof (UserMap).Assembly.GetTypes());
HbmMapping mapping = mapper.CompileMappingForAllExplicitlyAddedEntities();
return mapping;
}
}
Here is a neat little bit for managing SignalR dependencies with Castle. You may want to give this a try just for giggles.
public class SignalRDependencyResolver : Microsoft.AspNet.SignalR.DefaultDependencyResolver
{
private readonly IWindsorContainer _container;
public SignalRDependencyResolver(IWindsorContainer container)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
_container = container;
}
public override object GetService(Type serviceType)
{
return TryGet(serviceType) ?? base.GetService(serviceType);
}
public override IEnumerable<object> GetServices(Type serviceType)
{
return TryGetAll(serviceType).Concat(base.GetServices(serviceType));
}
[DebuggerStepThrough]
private object TryGet(Type serviceType)
{
try
{
return _container.Resolve(serviceType);
}
catch (Exception)
{
return null;
}
}
private IEnumerable<object> TryGetAll(Type serviceType)
{
try
{
Array array = _container.ResolveAll(serviceType);
return array.Cast<object>().ToList();
}
catch (Exception)
{
return null;
}
}
}
Put this in global asax before you set your controller factory
// SignalR
_container.Register(Classes.FromThisAssembly().BasedOn(typeof(IHub)).LifestyleTransient());
SignalRDependencyResolver signalRDependencyResolver = new SignalRDependencyResolver(_container);
Microsoft.AspNet.SignalR.GlobalHost.DependencyResolver = signalRDependencyResolver;
I have an ASP.Net Web API project. I am using NHibernate in this project; Fluent NHibernate to be specific. I am handling NHib session management using a custom ActionFilterAttribute. It looks like this:
public class SessionManagement : ActionFilterAttribute
{
public SessionManagement()
{
SessionFactory = WebApiApplication.SessionFactory;
}
private ISessionFactory SessionFactory { get; set; }
public override void OnActionExecuting(HttpActionContext actionContext)
{
var session = SessionFactory.OpenSession();
CurrentSessionContext.Bind(session);
session.BeginTransaction();
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
var session = SessionFactory.GetCurrentSession();
var transaction = session.Transaction;
if (transaction != null && transaction.IsActive)
{
transaction.Commit();
}
session = CurrentSessionContext.Unbind(SessionFactory);
session.Close();
}
This was working well for my needs. However, I have recently added a custom JSON.NET MediaTypeFormatter to format the my action's resulting JSON. The problem I am having is that my ActionFilter OnActionExecuted() method is called before the MediaTypeFormatter's WriteToStreamAsync can do it's job. The result is that lazily loaded (the problem) collections are now not available because the session is closed. What is the best way to handle this? Should I remove the ActionFilter's OnActionExecuted method and just close my session in the MediaTypeFormatter?
Thanks!!
The MediaTypeFormatter is the wrong layer to close the session at because this behavior really has nothing to do with the particular formatter you're using. Here's what I recommend doing:
Derive from HttpContent and create a class that derives from ObjectContent. Override the SerializeToStreamAsync implementation to await the base implementation's SerializeToStreamAsync then close the session:
public class SessionClosingObjectContent : ObjectContent
{
private Session _session;
public SessionClosingObjectContent(Type type, object value, MediaTypeFormatter formatter, Session session)
: base(type, value, formatter)
{
_session = session;
}
protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
await base.SerializeToStreamAsync(stream, context);
// Close the session and anything else you need to do
_session.Close();
}
}
Now in your action filter, instead of closing the session, you want to replace the response Content with your new class that closes the session:
public class SessionManagement : ActionFilterAttribute
{
public SessionManagement()
{
SessionFactory = WebApiApplication.SessionFactory;
}
private ISessionFactory SessionFactory { get; set; }
public override void OnActionExecuting(HttpActionContext actionContext)
{
var session = SessionFactory.OpenSession();
CurrentSessionContext.Bind(session);
session.BeginTransaction();
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
var session = SessionFactory.GetCurrentSession();
var response = actionExecutedContext.Response;
if (response.Content != null)
{
ObjectContent objectContent = response.Content as ObjectContent;
if (objectContent != null)
{
response.Content = new SessionClosingObjectContent(objectContent.ObjectType, objectContent.Value, objectContent.Formatter, session);
foreach (KeyValuePair<string, IEnumerable<string>> header in objectContent.Headers)
{
response.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
}
}
}
You could also choose instead to return an HttpResponseMessage with your new Content directly from your controller code wherever you need it.
I'm trying to use a session-per-request pattern and I'm having trouble with getting a record right after it's been saved. The reason for doing that being that I need to get the records that the foreign keys relate to.
Some (simplified) code:
// UnitOfWork Attribute
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
SessionFactory.Begin();
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
if (filterContext.Exception == null) {
try {
SessionFactory.Commit();
} catch {
SessionFactory.Rollback();
throw;
}
} else {
SessionFactory.Rollback();
throw filterContext.Exception;
}
}
// Service layer
public void Save(Form form)
{
_repository.Save(form);
var savedForm = _repository.Get(form.Id);
SendEmail(savedForm);
}
// Repository
public void Save(Form form)
{
var session = SessionFactory.CurrentSession;
session.SaveOrUpdate(form);
}
The problem is that when I try to get the record, the transaction hasn't yet been committed, so it just gives me what's already in the session. Am I just going to have to commit the transaction after saving, and open a new transaction for getting it?
Thanks
Update:
I've implemented the Agathas Storefront way of doing things, giving the service layer control over the transactions, i.e.:
public class UnitOfWork : IUnitOfWork
{
public void Commit()
{
var session = SessionFactory.CurrentSession;
using (ITransaction transaction = session.BeginTransaction())
{
try {
transaction.Commit();
} catch {
transaction.Rollback();
throw;
}
}
}
public void Clear()
{
var session = SessionFactory.CurrentSession;
session.Clear();
}
}
Then in the service layer:
public void SaveForm(Form form)
{
_repository.Save(form);
_uow.Commit();
_uow.Clear();
var savedForm = _repository.Get(form.Id);
SendEmail(savedForm);
}
Update 2
OK, I think I've found a suitable solution. I've gone back to the transaction-per-request pattern, and after saving the form I'm now flushing it and then evicting the form from the session in order to force NH to get it from the DB.
// Service layer
public void SaveForm(Form form)
{
_repository.Save(form);
var savedForm = _repository.Get(form.Id);
SendEmail(savedForm);
}
// Repository
public void Save(Form form)
{
var session = SessionFactory.CurrentSession;
session.SaveOrUpdate(form);
session.Flush();
session.Evict(form);
}
Until you flush the session or reduce your transaction scope you won't have an ID because nhibernate will not have inserted the record.
One option, change your id strategy from an identity generated in the database to one that is managed by NHibernate (see options at http://nhibernate.info/doc/nh/en/index.html#mapping-declaration-id-generator) or another option change your Session's flushmode to FlushMode.Auto.
I have a ASP.NET MVC application for which I want to write some stress tests in sense of concurrent database access from multiple threads. I wrote it as a unit test using Parallel.ForEach(), but was not able to make it work since I was getting the following exception most of the time:
There is already an open DataReader associated with this Command which must be closed first
So I simplified the test as much as possible and here it is
[Test]
public void Can_Access_DB_Concurrently()
{
Parallel.ForEach(Enumerable.Range(0, 9), x =>
{
try
{
var sessionBuilder = new HybridSessionBuilder();
var session = sessionBuilder.GetSession();
using (ITransaction transaction = session.BeginTransaction())
{
var job = session.Query<Job>().Where(y => y.Name == "TestProject").SingleOrDefault().Name;
Trace.WriteLine(Thread.CurrentThread.ManagedThreadId + ": Job name is " + job);
transaction.Commit();
}
}
catch (Exception e)
{
Trace.WriteLine(Thread.CurrentThread.ManagedThreadId + ": Exception: " + e.Message);
}
});
}
Typical output:
13: Exception: Object reference not set to an instance of an object.
16: Exception: There is already an open DataReader associated with this Command which must be closed first.
9: Exception: There is already an open DataReader associated with this Command which must be closed first.
16: Exception: There is already an open DataReader associated with this Command which must be closed first.
14: Exception: There is already an open DataReader associated with this Command which must be closed first.
The HybridSessionBuilder looks like this:
public class HybridSessionBuilder : ISessionBuilder
{
private static ISessionFactory _sessionFactory;
private static ISession _currentSession;
public ISession GetSession()
{
ISessionFactory factory = getSessionFactory();
ISession session = getExistingOrNewSession(factory);
return session;
}
private ISessionFactory getSessionFactory()
{
lock (this)
{
if (_sessionFactory == null)
{
Configuration configuration = GetConfiguration();
_sessionFactory = configuration.BuildSessionFactory();
}
return _sessionFactory;
}
}
public Configuration GetConfiguration()
{
string connectionString = WebConfigurationManager.ConnectionStrings["StagingDatabase"].ConnectionString;
Configuration configuration = new Configuration();
configuration = PostgreSQLConfiguration.PostgreSQL82
.ConnectionString(connectionString)
.Dialect("NHibernate.Dialect.PostgreSQL82Dialect")
.UseReflectionOptimizer()
.AdoNetBatchSize(50)
.ConfigureProperties(new Configuration());
configuration.AddMappingsFromAssembly(typeof(Job).Assembly);
Fluently.Configure(configuration);
return configuration;
}
private ISession getExistingOrNewSession(ISessionFactory factory)
{
{
if (HttpContext.Current != null)
{
ISession session = GetExistingWebSession();
if (session == null)
{
session = openSessionAndAddToContext(factory);
}
else if (!session.IsOpen)
{
session = openSessionAndAddToContext(factory);
}
return session;
}
if (_currentSession == null)
{
_currentSession = factory.OpenSession();
}
else if (!_currentSession.IsOpen)
{
_currentSession = factory.OpenSession();
}
}
return _currentSession;
}
public ISession GetExistingWebSession()
{
return HttpContext.Current.Items[GetType().FullName] as ISession;
}
private ISession openSessionAndAddToContext(ISessionFactory factory)
{
ISession session = factory.OpenSession();
HttpContext.Current.Items.Remove(GetType().FullName);
HttpContext.Current.Items.Add(GetType().FullName, session);
return session;
}
}
Apparently I am doing something wrong with this concurrent access, but I am unable to spot the error. Thank you for any advice.
HybridSessionBuilder is storing the ISession in a static member and therefore reusing it for each thread. The simplest solution to fix your tests would be to remove the static keyword from _currentSession. Each instance of HybridSessionBuilder would then provide a different ISession.
I'm getting a sporadic error that is difficult to reproduce. My first guess is that somehow I have a leaking nhibernate session, however when I ran the nhibernate profiler, I didn't see much out of the ordinary.
MVC 2.0
Fluent version 1.1.0.685
NHibernate version 2.1.2.4000
Exception: System.ArgumentException :
An item with the same key has already
been added.
Stack Trace: at
System.Collections.Generic.Dictionary2.Insert(TKey
key, TValue value, Boolean add) at
NHibernate.Util.ThreadSafeDictionary2.Add(TKey
key, TValue value) at
NHibernate.SqlTypes.SqlTypeFactory.GetTypeWithLen[T](Int32
length, TypeWithLenCreateDelegate
createDelegate) at
NHibernate.Type.EnumStringType..ctor(Type
enumClass, Int32 length)
I am using a repository model. Here's my repository class.
public sealed class Repository<T> : IRepository<T> where T : CoreObjectBase
{
#region IRepository<T> Members
private ISession Session
{
get
{
return new SessionHelper().GetSession();
}
}
public IQueryable<T> GetAll()
{
return (from entity in Session.Linq<T>() select entity);
}
public T GetById(int id)
{
return Session.Get<T>(id);
}
public void Save(params T[] entities)
{
using (ITransaction tx = Session.BeginTransaction())
{
for (int x = 0; x < entities.Count(); x++)
{
var entity = entities[x];
entity.Validate();
Session.SaveOrUpdate(entities[x]);
if (x == entities.Count() - 1 || (x != 0 && x % 20 == 0)) //20 is the batch size
{
Session.Flush();
Session.Clear();
}
}
tx.Commit();
}
}
public void SaveWithDependence<K>(T entity, K dependant) where K : CoreObjectBase
{
entity.Validate();
dependant.Validate();
using (ITransaction tx = Session.BeginTransaction())
{
Session.SaveOrUpdate(entity);
Session.SaveOrUpdate(dependant);
tx.Commit();
}
}
public void Save(T entity)
{
entity.Validate();
using (ITransaction tx = Session.BeginTransaction())
{
Session.SaveOrUpdate(entity);
tx.Commit();
}
}
public void Delete(T entity)
{
using (ITransaction tx = Session.BeginTransaction())
{
Session.Delete(entity);
tx.Commit();
}
}
public T GetOne(QueryBase<T> query)
{
var result = query.SatisfyingElementFrom(Session.Linq<T>());
return result;
//return query.SatisfyingElementFrom(Session.Linq<T>());
}
public IQueryable<T> GetList(QueryBase<T> query)
{
return query.SatisfyingElementsFrom(Session.Linq<T>());
}
/// <summary>
/// remove the sepcific object from level 1 cache so it can be refreshed from the database
/// </summary>
/// <param name="entity"></param>
public void Evict(T entity)
{
Session.Evict(entity);
}
#endregion
}
And here is my session helper, adapted from this.
public sealed class SessionHelper
{
private static ISessionFactory _sessionFactory;
private static ISession _currentSession;
public ISession GetSession()
{
ISessionFactory factory = getSessionFactory();
ISession session = getExistingOrNewSession(factory);
return session;
}
private ISessionFactory getSessionFactory()
{
if (_sessionFactory == null)
{
_sessionFactory = BuildSessionFactory();
}
return _sessionFactory;
}
private ISessionFactory BuildSessionFactory()
{
return Fluently.Configure().Database(
FluentNHibernate.Cfg.Db.MsSqlConfiguration.MsSql2005
.ConnectionString(c => c
.FromConnectionStringWithKey("MyDatabase"))
.AdoNetBatchSize(20))
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<SessionHelper>())
.BuildSessionFactory();
}
private ISession getExistingOrNewSession(ISessionFactory factory)
{
if (HttpContext.Current != null)
{
ISession session = GetExistingWebSession();
if (session == null)
{
session = openSessionAndAddToContext(factory);
}
else if (!session.IsOpen)
{
session = openSessionAndAddToContext(factory);
}
return session;
}
if (_currentSession == null)
{
_currentSession = factory.OpenSession();
}
else if (!_currentSession.IsOpen)
{
_currentSession = factory.OpenSession();
}
return _currentSession;
}
public ISession GetExistingWebSession()
{
return HttpContext.Current.Items[GetType().FullName] as ISession;
}
private ISession openSessionAndAddToContext(ISessionFactory factory)
{
ISession session = factory.OpenSession();
HttpContext.Current.Items.Remove(GetType().FullName);
HttpContext.Current.Items.Add(GetType().FullName, session);
return session;
}
}
Any ideas or suggestions to avoid this issue?
Problem is, that SessionHelper isn't thread-safe. It will potentially build several session factories (it's a bad implementation of Singleton), which in turn probably causes the error you're seeing.
I recommend using SharpArchitecture as guidance instead.
I ran into the same issue, "An item with the same key has already been added" while constructing the nhibernate configuration.
What was happening for me was that two threads were programmatically constructing different configurations, intended to connect to different databases, at the same time.
I added a lock around the entire configuration-maker, and the problem went away.
So I guess the configuration object depends on some internal global state, i.e. assumes that the configuration itself is a singleton (as i guess it would be, if it were totally file-driven, as opposed to programmatically constructed).
I realize that this is an old question but I had a similar error just a few days ago, using NHibernate 3.0.
For readers that may stumble upon this issue: this is the result of a known thread-safety problem in older versions of NHibernate. This was fixed in version 3.2 but older versions will not have the fix and may produce this problem. This bug entry describes the issue: https://nhibernate.jira.com/browse/NH-3271
I went the route that #joel truher wrote about. But, I wanted to put the code here on how to do it.
public class NHibernateBootstrapper
{
private static readonly object _sessionFactoryLock = new object();
private static ISessionFactory _sessionFactory;
public static ISessionFactory CreateThreadStaticSessionFactory(string connectionString, bool exportSchema)
{
lock (_sessionFactoryLock)
{
if (_sessionFactory == null)
{
_sessionFactory = Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(connectionString)
.AdoNetBatchSize(16))
.CurrentSessionContext<ThreadStaticSessionContext>()
.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<NHibernateBootstrapper>()
.Conventions.AddFromAssemblyOf<NHibernateBootstrapper>();
m.HbmMappings.AddFromAssemblyOf<NHibernateBootstrapper>();
})
.ExposeConfiguration(cfg => BuildSchema(cfg, exportSchema))
.BuildSessionFactory();
}
return _sessionFactory;
}
}
}
Obviously, you can configure your database however you'd like.