-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 13 posts ] 
Author Message
 Post subject: NonUniqueObjectException - Frustrating - $100 reward?
PostPosted: Sat Feb 09, 2008 2:43 pm 
Newbie

Joined: Wed Dec 26, 2007 10:32 pm
Posts: 10
Hibernate version:
Build 1.2.0.GA

Mapping documents:
http://www.w3bdevil.com/misc/private/Hib/User.hbm.xml
http://www.w3bdevil.com/misc/private/Hib/Source.hbm.xml
http://www.w3bdevil.com/misc/private/Hib/Client.hbm.xml
http://www.w3bdevil.com/misc/private/Hi ... nt.hbm.xml

Code between sessionFactory.openSession() and session.close():

Here is the complete test class source. The test method (TheAcidTest) is a combination of everything that happens in the ASP.NET code.

The actions that appear on each page are in the private functions below. It fails on Line 238: "Calendar.GetAllClients()".

Code:
Code removed, updated version below


The relevant code of the abstract calendar class implementation is such:

It fails on query.Enumerable()

Code:
public override ClientCollection GetAllClients() {
         
         ITransaction tx = Session.BeginTransaction();
         try {
            
            IQuery query = _session.CreateQuery("SELECT cli FROM Client AS cli");
         
            ClientCollection cliCollection = new ClientCollection( query.Enumerable<Client>() );
            
            tx.Commit();
            
            return cliCollection;
            
         } catch (HibernateException) {
            tx.Rollback();
            throw;
         }
         
      }


Full stack trace of any exception that occurs:

W3b.MortCal.Tests.CalendarProviderTests.TestAddClientGetClient : NHibernate.NonUniqueObjectException : a different object with the same identifier value was already associated with the session: 2097fa56-396d-4a18-af0d-fb55648a3b21, of class: W3b.MortCal.Biz.MortCalUser

Code:
   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.Provider.NHibernateCalendarProvider.GetAllClients() in D:\Users\David\My Documents\Visual Studio 2005\Projects\W3b\W3b.MortCal\W3b.MortCal\Provider\NHibernateCalendarProvider.cs:line 234
   at W3b.MortCal.Provider.Calendar.GetAllClients() in D:\Users\David\My Documents\Visual Studio 2005\Projects\W3b\W3b.MortCal\W3b.MortCal\Provider\Calendar.cs:line 149
   at W3b.MortCal.Tests.NonUniqueTest.AppointmentEdit_Load() in D:\Users\David\My Documents\Visual Studio 2005\Projects\W3b\W3b.MortCal\W3b.MortCal.Tests\NonUniqueTest.cs:line 260
   at W3b.MortCal.Tests.NonUniqueTest.TheAcidTest() in D:\Users\David\My Documents\Visual Studio 2005\Projects\W3b\W3b.MortCal\W3b.MortCal.Tests\NonUniqueTest.cs:line 93



Name and version of the database you are using:

SQL Server 2000

The generated SQL (show_sql=true):

N/A. NHibernate throws the exception before hitting the DB

Debug level Hibernate log excerpt:

I can't get log4net to work

Thanks, this is really pulling my hair out, and I need it working by Monday the 11th

EDIT: Seeming as I'm in a rush, assuming it doesn't go against the forum rules, I'll give $100USD to anyone who can help me solve it (via PayPal).


Last edited by W3bbo on Sat Feb 09, 2008 9:48 pm, edited 3 times in total.

Top
 Profile  
 
 Post subject:
PostPosted: Sat Feb 09, 2008 3:55 pm 
Contributor
Contributor

Joined: Sat Sep 24, 2005 11:25 am
Posts: 198
Looks pretty obvious.
You are associating two different instances with the same id ( 2097fa56-396d-4a18-af0d-fb55648a3b21).
Check where you are doing it.

Likely you are reusing the same session over more than a single test.

You should call BuildSessionFactory ONCE, and create a new session per each test.


Top
 Profile  
 
 Post subject:
PostPosted: Sat Feb 09, 2008 5:24 pm 
Newbie

Joined: Wed Dec 26, 2007 10:32 pm
Posts: 10
Ayende Rahien wrote:
Looks pretty obvious.
You are associating two different instances with the same id ( 2097fa56-396d-4a18-af0d-fb55648a3b21).
Check where you are doing it.

Likely you are reusing the same session over more than a single test.

You should call BuildSessionFactory ONCE, and create a new session per each test.


Thanks, I've since gone through the test code and reduced it down to a single test where the error is constantly reproducable and updated the GP post in the thread.

Any ideas?


Top
 Profile  
 
 Post subject:
PostPosted: Sat Feb 09, 2008 6:43 pm 
Regular
Regular

Joined: Tue Dec 02, 2003 6:25 pm
Posts: 61
Location: Dallas, TX
It's hard to be sure what's happening when you haven't posted all the code that is performing operations on the ISession during this test. I suspect that you still haven't posted the code that is the cuprit.

My guess is that the problem is related to you mixing your Membership Provider (which looks like the standard ASP.NET SQL provider for membership and roles) with NHibernate.

The error is not happening in GetAllClients. This just happens to be the NEXT thing that you did after you had already caused the error. The call to make an NH query just tells NH to auto-flush any pending changes. It looks to me like the only other place the problem could be is inside Calendar.GetAppointment( id ). If that's the case, the reason it worked in Stage 2 is that you didn't do anything that flushed the session, which is what triggers the error. Try flushing the session at the end of Stage 2 and see if that produces your error. Can you post the code for Calendar.GetAppointment()?

The fact that it is processing any pending changes during flush implies that you made a change that you did NOT intend to actually persist to the database. Perhaps you are setting one of your Membership or Role provided objects on a property of an entity after NH loads it. Is that possible?

Again, as Ayende said, the only way for you to get this error is that somehow/somewhere you are giving NH a MortCalUser that is a different instance yet has the same ID as the one NH has already loaded itself.


Top
 Profile  
 
 Post subject:
PostPosted: Sat Feb 09, 2008 8:18 pm 
Newbie

Joined: Wed Dec 26, 2007 10:32 pm
Posts: 10
wfassett wrote:
The error is not happening in GetAllClients. This just happens to be the NEXT thing that you did after you had already caused the error. The call to make an NH query just tells NH to auto-flush any pending changes.


Yes, that sounds about right. When I was stepping through NHibernate's code it went through some functions to do with flushing, but I can't say I fully comprehended it though.

wfassett wrote:
It looks to me like the only other place the problem could be is inside Calendar.GetAppointment( id ). If that's the case, the reason it worked in Stage 2 is that you didn't do anything that flushed the session, which is what triggers the error. Try flushing the session at the end of Stage 2 and see if that produces your error. Can you post the code for Calendar.GetAppointment()?


The Calendar class uses the Provider design pattern, here's the NHibernate implementation for the .GetAppointment method:

Code:

using KVPSO = System.Collections.Generic.KeyValuePair<string,object>;

public override Appointment GetAppointment(int appointmentId) {
   
   KVPSO argId = new KVPSO("id", appointmentId);
   
   AppointmentCollection appointments = GetAppointmentsByQuery("SELECT app FROM Appointment AS app WHERE app.id = :id", new KVPSO[] { argId } );
   
   if(appointments.Count == 1) return appointments[0];
   else return null;
   
}


private AppointmentCollection GetAppointmentsByQuery(String hqlQuery, params KVPSO[] parameters) {
   
   ITransaction tx = Session.BeginTransaction();
   try {
      
      IQuery query = _session.CreateQuery(hqlQuery);
      
      foreach( KVPSO param in parameters ) {
         query.SetParameter(param.Key, param.Value);
      }
   
      AppointmentCollection appCollection = new AppointmentCollection( query.Enumerable<Appointment>() );
   
      tx.Commit();
      
      SetAppointmentUsers( appCollection );
      
      return appCollection;
      
   } catch (HibernateException) {
      tx.Rollback();
      throw;
   }
   
}


wfassett wrote:
The fact that it is processing any pending changes during flush implies that you made a change that you did NOT intend to actually persist to the database. Perhaps you are setting one of your Membership or Role provided objects on a property of an entity after NH loads it. Is that possible?

Again, as Ayende said, the only way for you to get this error is that somehow/somewhere you are giving NH a MortCalUser that is a different instance yet has the same ID as the one NH has already loaded itself.


Possibly, but since all MortCalUser instances are created by ASP.NET's Membership API and not NHibernate it gets confusing.

Interestingly, when I was stepping through the NHibernate.Impl.SessionImpl.CheckUniqueness(EntityKey key, Object obj) method, one of the locals in the method was an instantiated MortCalUser instance, but all its members were null; though I don't know what NHibernate was doing, exactly.

Right now I'm just simplifying my test-case code, reducing dependencies by pulling everything in there. It should help with demystifying the problem. I'll post any updated testcase code I make.


Top
 Profile  
 
 Post subject:
PostPosted: Sat Feb 09, 2008 9:50 pm 
Newbie

Joined: Wed Dec 26, 2007 10:32 pm
Posts: 10
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


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 10, 2008 1:04 am 
Regular
Regular

Joined: Tue Dec 02, 2003 6:25 pm
Posts: 61
Location: Dallas, TX
Saw your other post but I just wanted to make a quick comment on GetAppointment. Assuming Id is the primary key for Appointment, you should be able to reduce the method to one line:

Code:
public override Appointment GetAppointment(int appointmentId){
    return Session.Load<Appointment>(id);
}


I am curious what SetAppointmentUsers does.

Quote:
Possibly, but since all MortCalUser instances are created by ASP.NET's Membership API and not NHibernate it gets confusing.

So MortCalUser is not a mapped class? Instead you've mapped some super-class or interface? It might help if you posted your mapping document.


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 10, 2008 1:38 am 
Regular
Regular

Joined: Tue Dec 02, 2003 6:25 pm
Posts: 61
Location: Dallas, TX
If your stack trace has an auto-flush in it, it means that the error happened earlier.

Did you try manually flushing to see if you could produce the error?

What's very interesting is that the stack trace is at CalendarPage_GetAppointment, which is pretty early on in stage 3. You should try flushing on line 135, before you invoke this method:

Code:
Session.Flush()


If that reproduces the error, you should keep moving the Session.Flush() up in your TheAcidTest() method until you are at the earliest point where you can reproduce the error.

The other weird thing is that if I'm right and the error was created earlier, then it probably happened the first time you ran CalendarPage_GetAppointment. And that's weird. The whole thing is weird because you appear to be closing the session in Global_EndRequest, but then again I can't see the code for W3b.MortCal.Provider.Calendar.CloseSession().

Aha.

Ok I know what the problem is with your test, but I'm not sure how it translates to your actual Provider. Because the problem is probably in the test. You ported your NHibernateHelper into your test namespace, so in Global_EndRequest, you're probably closing the wrong session.

That would make perfect sense. The problem would then be in CalendarPage_GetAppointment. Oh yes that's definitely a problem:

1. You load the appointment.
2. Then you get the User and Clerk instances that NHibernate loaded
3. Then you replace them with users loaded from System.Web.Security.Membership.GetUser().

These instances are different and NH knows it because the Session keeps track of everything it ever loaded. Even if they implement equals, NH knows they are different instances and that's why it is throwing the error.

If you call Session.Flush() anytime after running CalendarPage_GetAppointment, you will see the error.

You should get rid of Stage 3. Your problem is in stage 2.

This does beg the question why you are shoe-horning your Membership Users in after loading them with NH. Probably because it's a pain to integrate with the SQL Membership provider. Still, stuff like that is error-prone. Better to copy the attributes over or have a property like MortCalUser.MemberUser for clean separation, than to replace entire instances. You're sure to run into errors like this.

Personally, I think the way to go is an NHibernate Membership provider. That's what we use. We threw away all the SQL providers for Roles, Profiles, and Membership and wrote our own that used NHibernate, which made it easy to integrate our NHibernate business model with webforms security. (Actually we didn't bother with Profile provider - never needed it. Just wrote our own CRUD for user profiles with NHibernate).

Now, after that rant, won't my face be red if it turns out I was wrong?


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 10, 2008 9:10 am 
Newbie

Joined: Wed Dec 26, 2007 10:32 pm
Posts: 10
wfassett wrote:
This does beg the question why you are shoe-horning your Membership Users in after loading them with NH. Probably because it's a pain to integrate with the SQL Membership provider. Still, stuff like that is error-prone. Better to copy the attributes over or have a property like MortCalUser.MemberUser for clean separation, than to replace entire instances. You're sure to run into errors like this.


Would a MortCalUser.ImportMembershipUser(MembershipUser user) method be alright? It would copy the properties over from the MembershipUser into the MortCalUser instance.

wfassett wrote:
Personally, I think the way to go is an NHibernate Membership provider. That's what we use. We threw away all the SQL providers for Roles, Profiles, and Membership and wrote our own that used NHibernate, which made it easy to integrate our NHibernate business model with webforms security. (Actually we didn't bother with Profile provider - never needed it. Just wrote our own CRUD for user profiles with NHibernate).

Now, after that rant, won't my face be red if it turns out I was wrong?


Yes, I've just done some quick googling and it looks like other people have implemented it too. For example, this one looks promising.

I see if adding the .ImportMembershipUser method fixes the problem, if not I'll try hacking in an NhibernateMembershipProvider, but since this would require a redux of the DB design I'm less keen on that strategy.

I'll report back my findings, if it works (and I'm true to my words) I guess I owe you $100. :)


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 10, 2008 12:53 pm 
Newbie

Joined: Wed Dec 26, 2007 10:32 pm
Posts: 10
Yep. By changing my SetAppointmentUsers method to this, it now works:

Code:
private static void SetAppointmentUsers(Appointment appointment) {
         
         MortCalUser user  = appointment.User;
         MortCalUser clerk = appointment.Clerk;
         
         if(user == null || clerk == null) {
            throw new CalendarException( Resources.Calendar_AppointmentUsersNull );   
         } else if( user.ProviderUserKey == null || clerk.ProviderUserKey == null ) {
            throw new CalendarException( Resources.Calendar_AppointmentUserKeysNull );   
         }
         
         user.ImportMembershipUser( (MortCalUser)System.Web.Security.Membership.GetUser(   user.ProviderUserKey ) );
         clerk.ImportMembershipUser( (MortCalUser)System.Web.Security.Membership.GetUser(  clerk.ProviderUserKey ) );
         
      }


Where .ImportMembershipUser is defines as:

Code:
public void ImportMembershipUser(MortCalUser user) {
         
         Comment          = user.Comment;
         Email            = user.Email;
         IsApproved       = user.IsApproved;
         LastActivityDate = user.LastActivityDate;
         LastLoginDate    = user.LastLoginDate;
         
         SetField("_CreationDate"           , user.CreationDate);
         SetField("_IsLockedOut"            , user.IsLockedOut);
         SetField("_LastLockoutDate"        , user.LastLockoutDate);
         SetField("_LastPasswordChangedDate", user.LastPasswordChangedDate);
         SetField("_PasswordQuestion"       , user.PasswordQuestion);
         SetField("_ProviderName"           , user.ProviderName);
         SetField("_UserName"               , user.UserName);
         
      }
      
      private void SetField(String fieldName, Object value) {
         
         Type t = typeof(MembershipUser);
         System.Reflection.FieldInfo fi;
         
         fi = t.GetField(fieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
         fi.SetValue( this, value );
      }


Of course using Reflection isn't ideal, but this should do for a basic quick fix until I can work in an NHibernate Membership, Roles, and Profile provider.

Many thanks wfassett, do you want the $100? :)


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 10, 2008 1:22 pm 
Regular
Regular

Joined: Tue Dec 02, 2003 6:25 pm
Posts: 61
Location: Dallas, TX
Glad to hear it.

Also, if you're going to stick with the existing Membership provider, its going to end up being annoying to have to go in and inject your properties every time NH loads a MortCalUser. You might take a look at IInterceptor or ILifecycle to do it automatically, but again, I'd recommend an NH-based set of providers.

Quote:
Of course using Reflection isn't ideal, but this should do for a basic quick fix until I can work in an NHibernate Membership, Roles, and Profile provider.


Given the amount of reflection NH already does, you're fine. But I do think you should go for an NH membership provider.

Quote:
Yes, I've just done some quick googling and it looks like other people have implemented it too. For example, this one looks promising.



Yes I looked at that one briefly a few months ago. It looks pretty compatible with the existing schema that the SQL Providers use, so you might be able to swap it out pretty easily. I wanted a simpler schema so I wrote my own. For me, I did it so that I could have total control over the schema, the way the provider accessed the DB, and also so that I could use my normal User class everywhere without having to mess with System.Web.Security.MembershipUser.

I try to avoid touching the provider-related classes at all. My approach was to extend MembershipUser to a class called EntityMembershipUser which simply had an Entity property that returned the class I'd mapped with NH. My NHMembershipProvider would return an EntityMembershipUser and then I'd just pull my User object off of it and leave the EntityMembershipUser behind. So, basically, I did the inverse of what you are doing. In my NH Provider I have code that creates a MembershipUser *from* my Project.Model.User, which is just a POCO, so that my model knows nothing about the System.Web.Security namespace.

Code:
        protected MembershipUser CreateMembershipUser(User user)
        {
            if (user == null) return null;
            return new EntityMembershipUser(
                user,
                this.Name,
                user.UserName,
                user.Id,
                user.Email,
....

                user);


So EntityMembershipUser gets all its properties from User, plus it keeps a reference to the original User object. The properties for IsLockedOut, CreationDate, etc, all come from my User object, rather than the other way around. That way I don't have to mess with reflection to set them.

The only time I really have to mess with MembershipUser is when I want to get the corresponding User object from the current HttpContext. And to do that I just have a helper property in my HttpApplication:
Code:
    public User CurrentUser
    {
        get
        {
            EntityMembershipUser mu = (EntityMembershipUser)MembershipHelper.GetUser(Context);
            if (mu != null)
                return (User)mu.Entity;
            return null;
        }
    }


Quote:
Many thanks wfassett, do you want the $100? :)


If it's coming out of your pocket, no, you've suffered enough. But if it's coming from your employer's pocket, then sure! They can send me a paypal at [fassettw at yahoo].


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 10, 2008 2:48 pm 
Newbie

Joined: Wed Dec 26, 2007 10:32 pm
Posts: 10
wfassett wrote:
Glad to hear it.


Thanks for the extra tips. But yeah, the current kludge mixing SqlMembershipProvider and NHibernate will be replaced with a fully NH stack... eventually.

wfassett wrote:
Quote:
Many thanks wfassett, do you want the $100? :)


If it's coming out of your pocket, no, you've suffered enough. But if it's coming from your employer's pocket, then sure! They can send me a paypal at [fassettw at yahoo].


Well, it's from my own pocket. How about a $50 compromise? :)


Top
 Profile  
 
 Post subject: Problem solved
PostPosted: Mon Aug 04, 2008 2:24 pm 
Newbie

Joined: Fri Aug 01, 2008 2:20 pm
Posts: 6
Yes, I want to mix asp.net membership objects with NHibernate.

Your solution does solve problem.

Thank you very much!


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 13 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.