Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NHibernate and contextual entities

I'm trying to use NHibernate for a new app with a legacy database. It's going pretty well but I'm stuck and can't find a good solution for a problem.

Let's say I have this model :

  • a Service table (Id, ServiceName..)
  • a Movie table (Id, Title, ...)
  • a Contents table which associates a service and a movie (IdContent, Name, IdMovie, IdService)

So I mapped this and it all went good. Now I can retrieve a movie, get all the contents associated, ... My app is a movies shop "generator". Each "service" is in fact a different shop, when a user enter my website, he's redirected to one of the shops and obviously, I must show him only movies available for his shop. The idea is : user comes, his service is recognized, I present him movies which have contents linked to his service. I need to be able to retrieve all contents for a movie for the backoffice too. I'm trying to find the most transparent way to accomplish this with NHibernate. I can't really make changes to the db model.

I thought about a few solutions :

  • Add the service condition into all my queries. Would work but it's a bit cumbersome. The model is very complex and has tons of tables/queries..

  • Use nhibernate filter. Seemed ideal and worked pretty good, I added the filter on serviceid in all my mappings and did the EnableFilter as soon as my user's service was recognized but.. nhibernate filtered collections don't work with 2nd lvl cache (redis in my case) and 2nd lvl cache usage is mandatory.

  • Add computed properties to my object like Movie.PublishedContents(Int32 serviceId). Probably would work but requires to write a lot of code and "pollutes" my domain.

  • Add new entities inheriting from my nhibernate entity like a PublishedMovie : Movie wich only presents the contextual data

None of these really satisfies me. Is there a good way to do this ?

Thanks !

like image 624
Dawmz Avatar asked Nov 10 '22 12:11

Dawmz


1 Answers

You're asking about multi-tenancy with all the tenants in the same database. I've handled that scenario effectively using Ninject dependency injection. In my application the tenant is called "manual" and I'll use that in the sample code.

The route needs to contain the tenant e.g.

{manual}/{controller}/{action}/{id}

A constraint can be set on the tenant to limit the allowed tenants.

I use Ninject to configure and supply the ISessionFactory as a singleton and ISession in session-per-request strategy. This is encapsulated using Ninject Provider classes.

I do the filtering using lightweight repository classes, e.g.

public class ManualRepository
{
    private readonly int _manualId;
    private readonly ISession _session;

    public ManualRepository(int manualId, ISession session)
    {
        _manualId = manualId;
        _session = session;
    }

    public IQueryable<Manual> GetManual()
    {
        return _session.Query<Manual>().Where(m => m.ManualId == _manualId);
    }
}

If you want pretty urls you'll need to translate the tenant route parameter into its corresponding database value. I have these set up in web.config and I load them into a dictionary at startup. An IRouteConstraint implementation reads the "manual" route value, looks it up, and sets the "manualId" route value.

Ninject can handle injecting the ISession into the repository and the repository into the controller. Any queries in the controller actions must be based on the repository method so that the filter is applied. The trick is injecting the manualId from the routing value. In NinjectWebCommon I have two methods to accomplish this:

private static int GetManualIdForRequest()
{
    var httpContext = HttpContext.Current;
    var routeValues = httpContext.Request.RequestContext.RouteData.Values;
    if (routeValues.ContainsKey("manualId"))
    {
        return int.Parse(routeValues["manualId"].ToString());
    }
    const string msg = "Route does not contain 'manualId' required to construct object.";
    throw new HttpException((int)HttpStatusCode.BadRequest, msg);
}

/// <summary>
/// Binding extension that injects the manualId from route data values to the ctor.
/// </summary>
private static void WithManualIdConstructor<T>(this IBindingWithSyntax<T> binding)
{
    binding.WithConstructorArgument("manualId", context => GetManualIdForRequest());
}

And the repository bindings are declared to inject the manualId. There may be a better way to accomplish this through conventions.

kernel.Bind<ManualRepository>().ToSelf().WithManualIdConstructor();

The end result is that queries follow the pattern

var manual = _manualRepository
    .GetManual()
    .Where(m => m.EffectiveDate <= DateTime.Today)
    .Select(m => new ManualView
    {
        ManualId = m.ManualId,
        ManualName = m.Name
    }).List();

and I don't need to worry about filtering per tenant in my queries.

As for the 2nd level cache, I don't use it in this app but my understanding is that you can set the cache region to segregate tenants. This should get you started: http://ayende.com/blog/1708/nhibernate-caching-the-secong-level-cache-space-is-shared

like image 97
Jamie Ide Avatar answered Jan 02 '23 19:01

Jamie Ide