0

I have an ASP.NET MVC application that uses Fluent NHibernate and AutoMapper. I am primarily using AutoMapper to map my Models to ViewModels and vice versa.

When doing the latter, mapping from my viewmodel back to a model, I am wondering how I can map this to a specific instance from the DB, so when I commit the changes back to the DB (using my NHibernate repository layer, via my service layer), the changes are persisted.

Example:

var advert = Mapper.Map<AdvertViewModel, Advert>(model);
_advertService.UpdateAdvert(advert); // then calls repo which commits current NHibernate trans * Nothing in the DB changes *

If I attempt to commit my NHibernate session, so as to UPDATE this advert in the DB, despite the advert being assigned the correct Key/Id as part of the mapping, I guess because the NHibernate session knows nothing about this advert instance(?) it doesn't write away the changes.

Therefore, I am wondering how to handle this mapping scenario in conjunction with NHibernate?

marcusstarnes
  • 6,393
  • 14
  • 65
  • 112

2 Answers2

4

You could do the following:

// fetch the domain model to update
var domainModelToUpdate = _advertService.Get(viewModel.Id);

// Map the properties that are present in the view model to the domain model
// leaving other properties intact
Mapper.Map<AdvertViewModel, Advert>(viewModel, domainModelToUpdate);

// Update the domain model
_advertService.UpdateAdvert(domainModelToUpdate);

But if the view model already contains everything, you don't need to fetch the domain model before updating. All you have to do is to specify the unsaved-value on your identity column mapping to so that NHibernate knows whether an instance is transient or not and then use SaveOrUpdate:

Id(x => x.ID).WithUnsavedValue(0); 

or if you are using nullable integers for your identities pass null.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • The viewmodel already contains everything, so I've tried your latter solution. My Fluent NH Advert mapping now looks like Id(x => x.Id) .GeneratedBy.Identity().UnsavedValue(0); and I call 'SaveOrUpdate', but then get the error 'a different object with the same identifier value was already associated with the session: '. Any idea where I'm likely to be going wrong? – marcusstarnes Jun 27 '12 at 07:51
  • error 'a different object with the same identifier value was already associated with the session usually occurs when you trying to save an entity in a different Session it was borned. As a workaround you may try Session.Merge but as for me - you should improve your session lifecycle – Andriy Zakharko Jun 27 '12 at 10:03
  • Strangely, if I create a new Advert instance and manually assign each property (including the existing 'Id' key), then call 'SaveOrUpdate', it updates the record without any errors. As soon as I perform the mapping using AutoMapper though, I get the 'different object with the same identifier value was already associated with the session' error. – marcusstarnes Jun 27 '12 at 11:51
  • Sounds like a session management issue: I've got some code which manages sessions for my WCF service which has support for winforms (or WPF) - I'll post it if you think it might be useful. Can't remember where I got it though! – Charleh Jun 27 '12 at 12:22
  • I've narrowed things down. If I perform the AutoMapping, then overwrite one of the properties ('TransId' which is a Guid) to Guid.NewGuid(), everything goes through fine. It's when I leave the mapped TransId property (Guid ) where it throws this 'same identifier' error! It makes no sense to me, as this TransId property isn't a key, it's simply 'another' property, albeit a Guid, not related to anything else. I don't suppose AutoMapper or Fluent NHibernate have any known issues relating to Guids does it?! – marcusstarnes Jun 27 '12 at 14:45
  • That's a bit weird - I'm pretty sure I'm using Guids for integration to CRM (though it might just be read only) - I'll have a look in my project and see. If there are no guid writes, I'll make a simple object and see if I get the same issue – Charleh Jun 27 '12 at 15:48
1

If it is indeed a session issue - the following singleton might help you out

/// <summary>
/// Handles creation and management of sessions and transactions.  It is a singleton because 
/// building the initial session factory is very expensive. Inspiration for this class came 
/// from Chapter 8 of Hibernate in Action by Bauer and King.  Although it is a sealed singleton
/// you can use TypeMock (http://www.typemock.com) for more flexible testing.
/// </summary>
public sealed class NHibernateSessionManager
{
    #region Thread-safe, lazy Singleton

    System.IO.StreamWriter ConsoleWriter = null;

    /// <summary>
    /// This is a thread-safe, lazy singleton.  See http://www.yoda.arachsys.com/csharp/singleton.html
    /// for more details about its implementation.
    /// </summary>
    public static NHibernateSessionManager Instance
    {
        get
        {
            return Nested.NHibernateSessionManager;
        }
    }

    /// <summary>
    /// Initializes the NHibernate session factory upon instantiation.
    /// </summary>
    private NHibernateSessionManager()
    {
        InitSessionFactory();
    }

    /// <summary>
    /// Assists with ensuring thread-safe, lazy singleton
    /// </summary>
    private class Nested
    {
        static Nested() { }
        internal static readonly NHibernateSessionManager NHibernateSessionManager =
            new NHibernateSessionManager();
    }

    #endregion

    private void InitSessionFactory()
    {
        // Hold the config var
        FluentConfiguration config = Fluently.Configure();

        // Set the DB config
        MsSqlConfiguration dbConfig = MsSqlConfiguration.MsSql2005.ConnectionString(ConfigurationManager.ConnectionStrings["iSearchConnection"].ConnectionString);
        config.Database(dbConfig);

        // Load mappings from this assembly
        config.Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly()));

        // Create session factory
        sessionFactory = config.BuildSessionFactory();
    }

    /// <summary>
    /// Allows you to register an interceptor on a new session.  This may not be called if there is already
    /// an open session attached to the HttpContext.  If you have an interceptor to be used, modify
    /// the HttpModule to call this before calling BeginTransaction().
    /// </summary>
    public void RegisterInterceptor(IInterceptor interceptor)
    {
        ISession session = ContextSession;

        if (session != null && session.IsOpen)
        {
            throw new CacheException("You cannot register an interceptor once a session has already been opened");
        }

        GetSession(interceptor);
    }

    public ISession GetSession()
    {
        return GetSession(null);
    }

    /// <summary>
    /// Gets a session with or without an interceptor.  This method is not called directly; instead,
    /// it gets invoked from other public methods.
    /// </summary>
    private ISession GetSession(IInterceptor interceptor)
    {
        ISession session = ContextSession;

        if (session == null)
        {
            if (interceptor != null)
            {
                session = sessionFactory.OpenSession(interceptor);
            }
            else
            {
                session = sessionFactory.OpenSession();
            }

            ContextSession = session;
        }

        return session;
    }

    /// <summary>
    /// Flushes anything left in the session and closes the connection.
    /// </summary>
    public void CloseSession()
    {
        ISession session = ContextSession;

        if (session != null && session.IsOpen)
        {
            session.Flush();
            session.Close();
        }

        if (ConsoleWriter != null)
        {
            ConsoleWriter.Flush();
            ConsoleWriter.Close();
        }

        ContextSession = null;
    }

    public void BeginTransaction()
    {
        ITransaction transaction = ContextTransaction;

        if (transaction == null)
        {
            transaction = GetSession().BeginTransaction();
            ContextTransaction = transaction;
        }
    }

    public void CommitTransaction()
    {
        ITransaction transaction = ContextTransaction;

        try
        {
            if (HasOpenTransaction())
            {
                transaction.Commit();
                ContextTransaction = null;
            }
        }
        catch (HibernateException)
        {
            RollbackTransaction();
            throw;
        }
    }

    public bool HasOpenTransaction()
    {
        ITransaction transaction = ContextTransaction;

        return transaction != null && !transaction.WasCommitted && !transaction.WasRolledBack;
    }

    public void RollbackTransaction()
    {
        ITransaction transaction = ContextTransaction;

        try
        {
            if (HasOpenTransaction())
            {
                transaction.Rollback();
            }

            ContextTransaction = null;
        }
        finally
        {
            CloseSession();
        }
    }

    /// <summary>
    /// If within a web context, this uses <see cref="HttpContext" /> instead of the WinForms 
    /// specific <see cref="CallContext" />.  Discussion concerning this found at 
    /// http://forum.springframework.net/showthread.php?t=572.
    /// </summary>
    private ITransaction ContextTransaction
    {
        get
        {
            if (IsInWebContext())
            {
                return (ITransaction)HttpContext.Current.Items[TRANSACTION_KEY];
            }
            else
            {
                return (ITransaction)CallContext.GetData(TRANSACTION_KEY);
            }
        }
        set
        {
            if (IsInWebContext())
            {
                HttpContext.Current.Items[TRANSACTION_KEY] = value;
            }
            else
            {
                CallContext.SetData(TRANSACTION_KEY, value);
            }
        }
    }

    /// <summary>
    /// If within a web context, this uses <see cref="HttpContext" /> instead of the WinForms 
    /// specific <see cref="CallContext" />.  Discussion concerning this found at 
    /// http://forum.springframework.net/showthread.php?t=572.
    /// </summary>
    private ISession ContextSession
    {
        get
        {
            if (IsInWebContext())
            {
                return (ISession)HttpContext.Current.Items[SESSION_KEY];
            }
            else
            {
                return (ISession)CallContext.GetData(SESSION_KEY);
            }
        }
        set
        {
            if (IsInWebContext())
            {
                HttpContext.Current.Items[SESSION_KEY] = value;
            }
            else
            {
                CallContext.SetData(SESSION_KEY, value);
            }
        }
    }

    private bool IsInWebContext()
    {
        return HttpContext.Current != null;
    }

    private const string TRANSACTION_KEY = "CONTEXT_TRANSACTION";
    private const string SESSION_KEY = "CONTEXT_SESSION";
    private ISessionFactory sessionFactory;
}

Got this off some NHibernate gurus website - though I can't remember which one - it basically tracks and reconstructs a session for you in whichever app context you are in - works great for my project.

Then you just call the standard methods on the manager:

ISession ctx = NHibernateSessionManager.Instance.GetSession();

try
{
    ctx.BeginTransaction();
    ctx.Update(entity);
    ctx.CommitTransaction();
}

You might have a great session handling already implemented - but from the info, what you are experiencing sounds like a session problem so let me know if that helps

Charleh
  • 13,749
  • 3
  • 37
  • 57