Nhibernate Lazy Load exception after a view except

2020-04-13 03:18发布

问题:

I get a weird behavior with NHibernate with Fluent Configuration.

Whenever a generic exception unrelated to the NHibernate occurs i.e. in the view a DivideByZeroException every request after the exception throws.

An exception of type 'NHibernate.LazyInitializationException' occurred in NHibernate.dll but was not handled in user code. Additional information: Initializing[Entity]-Could not initialize proxy - no Session.

Due to nature of the bug the bug is critical due to the fact that 1 user can make the whole website dead if he generates an exception

Following it is my HttpModule for Nhibernate with Asp.Net MVC 5 that takes care of sessions.

NHibernateSessionPerRequest.cs

public class NHibernateSessionPerRequest : IHttpModule
{
    private static readonly ISessionFactory SessionFactory;

    // Constructs our HTTP module
    static NHibernateSessionPerRequest()
    {
        SessionFactory = CreateSessionFactory();
    }

    // Initializes the HTTP module
    public void Init(HttpApplication context)
    {
        context.BeginRequest += BeginRequest;
        context.EndRequest += EndRequest;
    }

    // Disposes the HTTP module
    public void Dispose() { }

    // Returns the current session
    public static ISession GetCurrentSession()
    {
        return SessionFactory.GetCurrentSession();
    }

    // Opens the session, begins the transaction, and binds the session
    private static void BeginRequest(object sender, EventArgs e)
    {
        ISession session = SessionFactory.OpenSession();

        session.BeginTransaction();

        CurrentSessionContext.Bind(session);
    }

    // Unbinds the session, commits the transaction, and closes the session
    private static void EndRequest(object sender, EventArgs e)
    {
        ISession session = CurrentSessionContext.Unbind(SessionFactory);

        if (session == null) return;

        try
        {
            session.Transaction.Commit();
        }
        catch (Exception)
        {
            session.Transaction.Rollback();
            throw;
        }
        finally
        {
            session.Close();
            session.Dispose();
        }
    }

    // Returns our session factory
    private static ISessionFactory CreateSessionFactory()
    {
        if (HttpContext.Current != null) //for the web apps
            _configFile = HttpContext.Current.Server.MapPath(
                            string.Format("~/App_Data/{0}", CacheFile)
                            );

        _configuration = LoadConfigurationFromFile();
        if (_configuration == null)
        {
            FluentlyConfigure();
            SaveConfigurationToFile(_configuration);
        }
        if (_configuration != null) return _configuration.BuildSessionFactory();
        return null;
    }



    // Returns our database configuration
    private static MsSqlConfiguration CreateDbConfigDebug2()
    {
        return MsSqlConfiguration
            .MsSql2008
            .ConnectionString(c => c.FromConnectionStringWithKey("MyConnection"));
    }

    // Updates the database schema if there are any changes to the model,
    // or drops and creates it if it doesn't exist
    private static void UpdateSchema(Configuration cfg)
    {
        new SchemaUpdate(cfg)
            .Execute(false, true);
    }
    private static void SaveConfigurationToFile(Configuration configuration)
    {
        using (var file = File.Open(_configFile, FileMode.Create))
        {
            var bf = new BinaryFormatter();
            bf.Serialize(file, configuration);
        }
    }

    private static Configuration LoadConfigurationFromFile()
    {
        if (IsConfigurationFileValid == false)
            return null;
        try
        {
            using (var file = File.Open(_configFile, FileMode.Open))
            {
                var bf = new BinaryFormatter();
                return bf.Deserialize(file) as Configuration;
            }
        }
        catch (Exception)
        {
            return null;
        }

    }
    private static void FluentlyConfigure()
    {
        if (_configuration == null)
        {
            _configuration = Fluently.Configure()
            .Database(CreateDbConfigDebug2)
            .CurrentSessionContext<WebSessionContext>()
            .Cache(c => c.ProviderClass<SysCacheProvider>().UseQueryCache())
            .Mappings(m => m.FluentMappings.AddFromAssemblyOf<EntityMap>()
                .Conventions.Add(DefaultCascade.All(), DefaultLazy.Always()))
            .ExposeConfiguration(UpdateSchema)
            .ExposeConfiguration(c => c.Properties.Add("cache.use_second_level_cache", "true"))
            .BuildConfiguration();
        }
    }
    private static bool IsConfigurationFileValid
    {
        get
        {
            var ass = Assembly.GetAssembly(typeof(EntityMap));
            var configInfo = new FileInfo(_configFile);
            var assInfo = new FileInfo(ass.Location);
            return configInfo.LastWriteTime >= assInfo.LastWriteTime;
        }
    }

    private static Configuration _configuration;
    private static string _configFile;
    private const string CacheFile = "hibernate.cfg.xml";

}

Edit

The Repository Implementation i use

public class Repository<T> : IIntKeyedRepository<T> where T : class
{
    private readonly ISession _session;

    public Repository()
    {
        _session = NHibernateSessionPerRequest.GetCurrentSession();
    }

    #region IRepository<T> Members

    public bool Add(T entity)
    {
        _session.Save(entity);
        return true;
    }

    public bool Add(System.Collections.Generic.IEnumerable<T> items)
    {
        foreach (T item in items)
        {
            _session.Save(item);
        }
        return true;
    }

    public bool Update(T entity)
    {
        _session.Update(entity);
        return true;
    }

    public bool Delete(T entity)
    {
        _session.Delete(entity);
        return true;
    }

    public bool Delete(System.Collections.Generic.IEnumerable<T> entities)
    {
        foreach (T entity in entities)
        {
            _session.Delete(entity);
        }
        return true;
    }

    #endregion

    #region IIntKeyedRepository<T> Members

    public T FindBy(int id)
    {
        return _session.Get<T>(id);
    }

    #endregion

    #region IReadOnlyRepository<T> Members

    public IQueryable<T> All()
    {
        return _session.Query<T>();
    }

    public T FindBy(System.Linq.Expressions.Expression<System.Func<T, bool>> expression)
    {
        return FilterBy(expression).Single();
    }

    public IQueryable<T> FilterBy(System.Linq.Expressions.Expression<System.Func<T, bool>> expression)
    {
        return All().Where(expression).AsQueryable();
    }

    #endregion

}

Edit 2

The base controller class I use

public class BaseController : Controller
{
    private readonly IRepository<UserEntity> _userRepository;

    public BaseController()
    {
        _userRepository = new Repository<UserEntity>();
        BaseModel = new LayoutModel {Modals = new List<string>()};
    }

    public UserEntity LoggedUser { get; set; }
    public LayoutModel BaseModel { get; set; }

    protected override void OnActionExecuting(ActionExecutingContext ctx)
    {
        base.OnActionExecuting(ctx);

        if (HttpContext.User.Identity.IsAuthenticated)
        {
            if (Session != null && Session["User"] != null)
            {
                LoggedUser = (User) Session["User"];
            }
            var curUsername = HttpContext.User.Identity.Name;
            if (LoggedUser == null || LoggedUser.Entity2.un!= curUsername)
            {
                LoggedUser = _userRepository.FindBy(u => u.Entity2.un== curUsername);
                Session["User"] = LoggedUser;
            }
            BaseModel.LoggedUser = LoggedUser;
            BaseModel.Authenticated = true;
        }
        else
        {
            LoggedUser = new UserEntity
            {
                Entity= new Entity{un= "Guest"},
            };
            BaseModel.LoggedUser = LoggedUser;
        }
    }      
}

回答1:

The extended question and all the snippets - are finally helping to find out where is the issue.

There is a really big issue: Session["User"] = LoggedUser;

This would hardly work. Why?

  • because we place into long running object (Web Session)
  • an instance loaded via very shortly lasting Web Request

Not all its properties will/could be loaded, When we place LoggedUser into session. It could be just a root entity with many proxies representing references and collections. These will NEVER be loaded later, because its Mather session is closed... gone

Solution?

I would use .Clone() of the User object. In its implementation we can explicitly load all needed references and collections and clone them as well. Such object could be placed into the Web Session

[Serializable]
public class User, ICloneable, ...
{
    ...
    public override object Clone()
    {
        var entity = base.Clone() as User;
        entity.Role = Role.Clone() as Role;
        ...
        return entity;
    }

So, what would be placed into session?

Session["User"] = LoggedUser.Clone();


回答2:

As Radim Köhler noted i was saving a lazy-loaded object in Session that caused the problem.

But i wanted to avoid the Serilization of all objects and i fixed it as follows.

I added the following method to eager-load an entity instead of lazy

public T FindByEager(int id)
    {
        T entity = FindBy(id);
        NHibernateUtil.Initialize(entity);
        return entity;
    }

And changed BaseController to

if (Session != null) Session["User"] = userRepository.FindByEager(LoggedUser.Id);