0

I've just been reading the trace for one of my ASP.NET pages and I've noticed that the page user is being loaded from the database each time the user is required. Since each ISession is supposed to cache objects, I'm really mystified about this.

Logically, the problem must surely be one of the following two things:

  1. The ISession's cache isn't working properly
  2. Each time the user is requested, it's being loaded using a different ISession

I assume that the problem is number 2). I'm using Castle Windsor to manage object lifecycles so I've posted some of the code I'm using in case someone can help spot the problem. The classes being managed by Castle Windsor are:

  1. MooseUserRepository - a repository class for managing MooseUser instances (i.e. the page user in this case)
  2. KctcUnitOfWork - a wrapper for the ISession

MooseUserRepository has a constructor dependency on KctcUnitOfWork like this:

public MooseUserRepository(IUnitOfWork unitOfWork)
    {

    }

The config file looks like this:

<component id="KctcUnitOfWork" service="Kctc.BusinessLayer.Kctc.IUnitOfWork,Kctc.BusinessLayer" type="Kctc.NHibernate.Kctc.UnitOfWork,Kctc.NHibernate" lifestyle="PerWebRequest"/>
<component id="MooseUserRepository" service="Kctc.BusinessLayer.Kctc.Repositories.IMooseUserRepository,Kctc.BusinessLayer" type="Kctc.NHibernate.Kctc.Repositories.MooseUserRepository,Kctc.NHibernate" lifestyle="PerWebRequest"/>

Note the PerWebRequest lifestyles.

The Castle Windsor container is simply a static property of a sort of utility class called Moose.Application so it's always there:

private static IWindsorContainer _windsorContainer;

    public static IWindsorContainer WindsorContainer
    {
      get
      {
        if (_windsorContainer == null)
        {
          _windsorContainer = new WindsorContainer(new XmlInterpreter(HttpContext.Current.Server.MapPath("~/CastleWindsorConfiguration.xml")));
        }
        return _windsorContainer;
      }
    }

The page itself has a IMooseUserRepository instance like this:

private IMooseUserRepository _mooseUserRepository;
private IMooseUserRepository MooseUserRepository
  {
    get
    {
      if (_mooseUserRepository == null)
      {
        _mooseUserRepository = Moose.Application.WindsorContainer.Resolve<IMooseUserRepository>();
      }
      return _mooseUserRepository;
    }
  }

The user of the page is accessed by a property which looks like this:

private MooseUser PageUser
  {
    get { return MooseUserRepository.Load(ApplicationSettings.UsernameFromWeb); }}

It appears that these subsequent calls to PageUser are causing the duplicate SQL commands:

txtSubject.Enabled = PageUser.CanHandleLegalWorks;
    ddlDue.Enabled = PageUser.CanHandleLegalWorks;

Now obviously I can work around this problem by storing the loaded MooseUser object in a private variable, but my understanding is that the ISession is supposed to do this for me.

Can anyone hazard a guess as to what's going wrong?

David
  • 15,750
  • 22
  • 90
  • 150
  • Are you by any change inheriting your class from a different entity? Post your mapping and entity class. – jishi Jan 07 '11 at 15:25
  • depending on the concurrency in your app you should take a call about caching or not. – Baz1nga Jan 07 '11 at 18:08
  • Fire up NhProf on it and see if you're getting multiple Sessions created per request - it has a 30-day free trial. – DanB Jan 10 '11 at 11:10
  • David, can you post up what a Kctc.NHibernate.Kctc.UnitOfWork looks like? – DanB Jan 10 '11 at 15:39

4 Answers4

2

You state the following:

Logically, the problem must surely be one of the following two things:

  1. The ISession's cache isn't working properly
  2. Each time the user is requested, it's being loaded using a
    different ISession

I think you may be confusing Nhibernate's First (Session) Level Cache with the Second Level Cache.

Sessions are cheap to manufacture and throw away. In a Web app, you would typically use one session per request. Once you get or load an entity for the first time it is placed into the first level cache which is scoped to the lifetime of the session. When the Session is closed and disposed of at the end of the request you will no longer have access to objects in the session-level cache. Indeed, each User is being loaded by different sessions- this is perfectly normal.

The Second-Level cache is scoped to the lifetime of the Session Factory. If 2nd-level cache is enabled, once you load an entity by its primary key, it is stored in the 2nd-level cache and can be accessed by ALL sessions without hitting the database again until it is removed from the cache. You will need to explicitly enable caching on a per-entity basis. This is the behaviour you are looking for.

Further reading:

edit

You'll need to pick a Cache Provider from the NHContrib project. You probably want SysCache2 which uses the Asp.Net cache but you could go with MemCached or Velocity or a couple of others if you wanted to. I also recommmend you give Nhibernate Profiler a try. I've found it invaluable in poking under the hood and finding out what Nhibernate's getting upto.

tinonetic
  • 7,751
  • 11
  • 54
  • 79
DanB
  • 1,060
  • 7
  • 13
  • I'm definitely thinking of the first level cache - cache storage per ISession (web request). I hadn't ever considered setting up the second level cache but I suppose it could be useful for keeping hold of user data between requests. (I'm a little concerned about the data going stale though.) Thanks for the thought! – David Jan 07 '11 at 16:26
  • @David: Ah ok. You are having problems where different sessions are being used in the same web request? – DanB Jan 10 '11 at 11:01
1

Like you have noticed in your question you are using a PerWebRequest lifestyle for the repository, so the ISession (used by repository) will be recreated on every request. I find this behavior the right one, the ISession should be created on every request and every operation on NH should be transacted.

If you wanna use one ISession as singleton you should declare the repositories lifestyle as singleton.

I think you should have some sort of SessionProvider o SessionFactory in your app, maybe you could work on that for the singleton session.

HTH

ema
  • 5,668
  • 1
  • 25
  • 31
  • It's essential to use PerWebRequest with ISession, otherwise different users' NHibernate sessions would get mixed up! Every time the session is flushed, you would have no idea how many users' data was being flushed or what they were in the middle of doing. – David Jan 07 '11 at 15:20
  • You really don't want to make a session into a singleton. – DanB Jan 07 '11 at 15:43
1

I've worked out what the problem is and it is quite a subtle one.

I'm retrieving the user with the following code:

private MooseUser PageUser
  {
    get { return MooseUserRepository.Load(ApplicationSettings.UsernameFromWeb); }
}

ApplicationSettings.UsernameFromWeb retrieves the username of the current user as far as ASP.NET is concerned. The username of the user is a natural key for the Users table, but it's not the primary key! As far as I know, the first level cache only works for objects retrieved by primary key.

Edit: I solved this problem by creating a property which stuffs the loaded user in HttpContext.Current.Items and checks there first before loading as per this article.

David
  • 15,750
  • 22
  • 90
  • 150
  • You can store the users PK instead of the username as the "username" in the FormsAuth ticket. – DanB Jan 07 '11 at 15:42
  • Thanks for the excellent tip but no good in this case. I use NTLM authentication generally for the app (thus the username is the user's Windows login name), but use a database to store additional info about each user. The table in the database uses a surrogate key. I know I can create additional properties in AD, but don't for several other reasons. – David Jan 07 '11 at 16:18
1

You can use natural-id (NaturalId in Fluent NH) in your mapping to bypass second level cache expiration for just this situation.

Jamie Ide
  • 48,427
  • 16
  • 81
  • 117
  • I'm not using the second level cache, but otherwise this would be a fantastic suggestion. Why does natural-id only make objects cacheable in the second level cache??? – David Jan 07 '11 at 16:28