I've reduced the test code down to this, it has zero dependencies on anything outside NHibernate except some of the ASP.NET Membership code which is a simple SQL INSERT done with the System.Data.SqlClient classes.
Code:
using System;
using System.Data.SqlClient;
using W3b.MortCal.Biz;
using W3b.MortCal.Provider;
using NUnit.Framework;
using NHibernate;
using NHibernate.Cfg;
using System.Web.Security;
using Cult = System.Globalization.CultureInfo;
using KVPSO = System.Collections.Generic.KeyValuePair<string,object>;
namespace W3b.MortCal.Tests {
internal static class NHibernateHelper {
private const string _currentSessionKey = "nhibernate.current_session";
private static ISessionFactory _sessionFactory;
private static ISession _currentSessionIfNoContext; // used because this isn't in ASP.NET with HttpContext
/// <summary>Creates the Session Factory</summary>
private static void Initialise() {
if( _sessionFactory == null ) {
_sessionFactory = new Configuration().Configure().BuildSessionFactory();
}
}
public static void CloseSessionFactory() {
if( _sessionFactory != null ) {
_sessionFactory.Close();
_sessionFactory = null;
}
}
[System.Diagnostics.DebuggerStepThrough()]
public static ISession GetCurrentSession() {
Initialise();
ISession currentSession = _currentSessionIfNoContext;
if( currentSession == null ) {
currentSession = _sessionFactory.OpenSession();
_currentSessionIfNoContext = currentSession;
}
return currentSession;
}
public static void CloseSession() {
ISession currentSession = _currentSessionIfNoContext;
if( currentSession == null ) return;
currentSession.Close();
_currentSessionIfNoContext = null;
}
}
[TestFixture]
public class NonUniqueTest2 {
// now import all the called code into this file so there are no non-hibernate/membership dependencies
private MortCalUser _currentUser;
private MortCalUser _calendarUser;
[SetUp]
public void ClearDatabase() {
using(SqlConnection c = new SqlConnection( System.Configuration.ConfigurationManager.ConnectionStrings["mortCalSqlServerConnection"].ConnectionString ) )
using(SqlCommand cmd = c.CreateCommand() ) {
c.Open();
cmd.CommandText = "DELETE FROM Appointments";
cmd.ExecuteNonQuery();
cmd.CommandText = "DELETE FROM Clients";
cmd.ExecuteNonQuery();
cmd.CommandText = "DELETE FROM Sources";
cmd.ExecuteNonQuery();
cmd.CommandText = "DELETE FROM Users";
cmd.ExecuteNonQuery();
cmd.CommandText = "DELETE FROM aspnet_UsersInRoles";
cmd.ExecuteNonQuery();
cmd.CommandText = "DELETE FROM aspnet_Roles";
cmd.ExecuteNonQuery();
cmd.CommandText = "DELETE FROM aspnet_Membership";
cmd.ExecuteNonQuery();
cmd.CommandText = "DELETE FROM aspnet_Users";
cmd.ExecuteNonQuery();
}
}
[Test]
public void TheAcidTest() {
//////////////////////////////////////////
// Stage 1: Insertion of user, client, and appointment
// i.e. Application begin, AppointmentNew.aspx
//////////////////////////////////////////
Application_Start();
Global_EndRequest();
BasePage_Init();
Int32 id = AppointmentNew_CreateAppointment();
Global_EndRequest();
//////////////////////////////////////////
// Stage 2: Intermediary
// i.e. ApplicationView.aspx
//////////////////////////////////////////
BasePage_Init();
CalendarPage_GetAppointment(id);
Global_EndRequest();
//////////////////////////////////////////
// Stage 3: Kaboom
// i.e. ApplicationEdit.aspx
//////////////////////////////////////////
BasePage_Init();
CalendarPage_GetAppointment(id);
AppointmentEdit_Load();
Global_EndRequest();
//////////////////////////////////////////
// Stage 3: Done
//////////////////////////////////////////
Application_End();
Assert.IsTrue(true); // w00t
}
private void Application_Start() {
MembershipCreateStatus status;
MortCalUser administrator = (MortCalUser)Membership.CreateUser("Administrator",
"str0ngpassw0rd##",
"email@example.com",
"Question",
"Answer",
true,
out status
);
administrator.Name.FirstName = "Administrator";
Membership.UpdateUser( administrator );
}
private void BasePage_Init() {
// doing this manually here, it SHOULD be the same as if ASP.NET did it
_currentUser = (MortCalUser)Membership.GetUser("Administrator");
_calendarUser = _currentUser; // CalendarPage assigns CalendarUser to CurrentUser if own calendar
}
private Int32 AppointmentNew_CreateAppointment() {
Client c = ClientForm_CreateClient();
String subject = null, notes = "Notes go here";
DateTime startDate = DateTime.Parse( "15/02/08 21:10" );
Appointment app = new Appointment( startDate, _currentUser, _calendarUser, c, TimeAvailability.ClientMeeting, subject, notes);
Calendar.AddAppointment( app );
return app.AppointmentId;
}
private Client ClientForm_CreateClient() {
Source source = new Source( "SourceName" );
DateTime dateAdded = DateTime.Now;
Name name = new Name( "Mr", "First", "M", "Last" );
Address address = new Address( "123 Fake Street", "", "City", "County", "AA11 6AA" );
Phone phone = new Phone( "00000 000000", "", "00000 000000", false );
Client c = new Client( dateAdded, name, address, phone, 0, 0, 0, 0, false, false, false, false, source);
return c;
}
private void CalendarPage_GetAppointment(Int32 id) {
KVPSO argId = new KVPSO("id", id);
Appointment appointment = null;
ITransaction tx = Session.BeginTransaction();
try {
IQuery query = _session.CreateQuery("SELECT app FROM Appointment AS app WHERE app.id = :id");
query.SetParameter("id", id);
AppointmentCollection appCollection = new AppointmentCollection( query.Enumerable<Appointment>() );
appointment = appCollection[0]; // in the actual source I do perform checks for .Count
tx.Commit();
} catch (HibernateException) {
tx.Rollback();
throw;
}
MortCalUser user = appointment.User;
MortCalUser clerk = appointment.Clerk;
if(user == null || clerk == null) {
throw new CalendarException( "User null" );
} else if( user.ProviderUserKey == null || clerk.ProviderUserKey == null ) {
throw new CalendarException( "Key null" );
}
user = (MortCalUser)System.Web.Security.Membership.GetUser( user.ProviderUserKey );
clerk = (MortCalUser)System.Web.Security.Membership.GetUser( clerk.ProviderUserKey );
// hacking readonly properties
Type t = typeof(Appointment);
System.Reflection.FieldInfo fi;
fi = t.GetField("_user", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
fi.SetValue( appointment, user );
fi = t.GetField("_clerk", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
fi.SetValue( appointment, clerk );
}
private void AppointmentEdit_Load() {
System.Web.UI.WebControls.ListItem li;
/////////////////////////////////////
// Clients
/////////////////////////////////////
ClientCollection clients = Calendar.GetAllClients();
clients.Sort(); // sort by name
foreach(Client c in clients) {
li = new System.Web.UI.WebControls.ListItem( c.Name.ToString(Name.NameFormat.LastFirst), c.ClientId.ToString(Cult.InvariantCulture) );
//Clients.Items.Add( li );
}
//////////////////////////////////////
// Consultants
//////////////////////////////////////
MembershipUserCollection users = Membership.GetAllUsers();
foreach(MembershipUser muser in users) {
MortCalUser user = muser as MortCalUser;
li = new System.Web.UI.WebControls.ListItem( user.Name.ToString( Name.NameFormat.LastFirst), user.UserName);
/* if( _app.Clerk == user ) {
li.Selected = true;
} */
//AppConsultants.Items.Add( li );
}
}
private void Global_EndRequest() {
W3b.MortCal.Provider.Calendar.CloseSession();
}
private void Application_End() {
W3b.MortCal.Provider.Calendar.CloseEntirely();
}
private ISession _session;
private ISession Session {
get {
InitialiseSession();
return _session;
}
}
/// <summary>Initialises the ISession. Don't call from Initialise (some stuff with HttpContext).</summary>
[System.Diagnostics.DebuggerStepThrough()]
private void InitialiseSession() {
// When Calendar (a static class) is initialised, it loads the provider into a static field
// this instantiated field is probably persisted between requests
// ...along with this provider's "IsInitialized" field
// even though the ISession is closed after every request
// so don't use IsInitialised and just ensure the ISession state is valid
if( _session == null || !_session.IsOpen ) {
_session = NHibernateHelper.GetCurrentSession();
}
}
}
}
Interestingly, when I run this the exception is shifted from GetAllClients. It now appears at:
Code:
W3b.MortCal.Tests.NonUniqueTest2.TheAcidTest : NHibernate.NonUniqueObjectException : a different object with the same identifier value was already associated with the session: b8dfa0d5-259b-479a-825c-b8098a7c4c99, of class: W3b.MortCal.Biz.MortCalUser
at NHibernate.Impl.SessionImpl.CheckUniqueness(EntityKey key, Object obj)
at NHibernate.Impl.SessionImpl.DoUpdateMutable(Object obj, Object id, IEntityPersister persister)
at NHibernate.Impl.SessionImpl.DoUpdate(Object obj, Object id, IEntityPersister persister)
at NHibernate.Impl.SessionImpl.SaveOrUpdate(Object obj)
at NHibernate.Engine.Cascades.CascadingAction.ActionSaveUpdateClass.Cascade(ISessionImplementor session, Object child, Object anything)
at NHibernate.Engine.Cascades.Cascade(ISessionImplementor session, Object child, IType type, CascadingAction action, CascadeStyle style, CascadePoint cascadeTo, Object anything)
at NHibernate.Engine.Cascades.Cascade(ISessionImplementor session, IEntityPersister persister, Object parent, CascadingAction action, CascadePoint cascadeTo, Object anything)
at NHibernate.Impl.SessionImpl.PreFlushEntities()
at NHibernate.Impl.SessionImpl.FlushEverything()
at NHibernate.Impl.SessionImpl.AutoFlushIfRequired(ISet querySpaces)
at NHibernate.Impl.SessionImpl.GetQueries(String query, Boolean scalar)
at NHibernate.Impl.SessionImpl.Enumerable[T](String query, QueryParameters parameters)
at NHibernate.Impl.QueryImpl.Enumerable[T]()
at W3b.MortCal.Tests.NonUniqueTest2.CalendarPage_GetAppointment(Int32 id) in D:\Users\David\My Documents\Visual Studio 2005\Projects\W3b\W3b.MortCal\W3b.MortCal.Tests\NonUniqueTest2.cs:line 238
at W3b.MortCal.Tests.NonUniqueTest2.TheAcidTest() in D:\Users\David\My Documents\Visual Studio 2005\Projects\W3b\W3b.MortCal\W3b.MortCal.Tests\NonUniqueTest2.cs:line 147