0

I'm trying to implement a web application using ASP.NET MVC and the Microsoft Unity DI framework. The application needs to support multiple user sessions at the same time, each of them with their own connection to a separate database (but all users using the same DbContext; the database schemas are identical, it's just the data that is different).

Upon a user's log-in, I register the necessary type mappings to the application's Unity container, using a session-based lifetime manager that I found in another question here.

My container is initialized like this:

// Global.asax.cs

public static UnityContainer CurrentUnityContainer { get; set; }

protected void Application_Start()
{
    // ...other code...
    CurrentUnityContainer = UnityConfig.Initialize();
    // misc services - nothing data access related, apart from the fact that they all depend on IRepository<ClientContext>
    UnityConfig.RegisterComponents(CurrentUnityContainer);
}

// UnityConfig.cs

public static UnityContainer Initialize()
{
    UnityContainer container = new UnityContainer();
    DependencyResolver.SetResolver(new UnityDependencyResolver(container));
    GlobalConfiguration.Configuration.DependencyResolver = new Unity.WebApi.UnityDependencyResolver(container);

    return container;
}

This is the code that's called upon logging in:

// UserController.cs

UnityConfig.RegisterUserDataAccess(MvcApplication.CurrentUnityContainer, UserData.Get(model.AzureUID).CurrentDatabase);

// UnityConfig.cs

public static void RegisterUserDataAccess(IUnityContainer container, string databaseName)
{
    container.AddExtension(new DataAccessDependencies(databaseName));
}

// DataAccessDependencies.cs

public class DataAccessDependencies : UnityContainerExtension
{
    private readonly string _databaseName;

    public DataAccessDependencies(string databaseName)
    {
        _databaseName = databaseName;
    }

    protected override void Initialize()
    {
        IConfigurationBuilder configurationBuilder = Container.Resolve<IConfigurationBuilder>();

        Container.RegisterType<ClientContext>(new SessionLifetimeManager(), new InjectionConstructor(configurationBuilder.GetConnectionString(_databaseName)));
        Container.RegisterType<IRepository<ClientContext>, RepositoryService<ClientContext>>(new SessionLifetimeManager());
    }
}

// SessionLifetimeManager.cs

public class SessionLifetimeManager : LifetimeManager
{
    private readonly string _key = Guid.NewGuid().ToString();

    public override void RemoveValue(ILifetimeContainer container = null)
    {
        HttpContext.Current.Session.Remove(_key);
    }

    public override void SetValue(object newValue, ILifetimeContainer container = null)
    {
        HttpContext.Current.Session[_key] = newValue;
    }

    public override object GetValue(ILifetimeContainer container = null)
    {
        return HttpContext.Current.Session[_key];
    }

    protected override LifetimeManager OnCreateLifetimeManager()
    {
        return new SessionLifetimeManager();
    }
}

This works fine as long as only one user is logged in at a time. The data is fetched properly, the dashboards work as expected, and everything's just peachy keen.

Then, as soon as a second user logs in, disaster strikes.

The last user to have prompted a call to RegisterUserDataAccess seems to always have "priority"; their data is displayed on the dashboard, and nothing else. Whether this is initiated by a log-in, or through a database access selection in my web application that calls the same method to re-route the user's connection to another database they have permission to access, the last one to draw always imposes their data on all other users of the web application. If I understand correctly, this is a problem the SessionLifetimeManager was supposed to solve - unfortunately, I really can't seem to get it to work.

I sincerely doubt that a simple and common use-case like this - multiple users logged into an MVC application who each are supposed to access their own, separate data - is beyond the abilities of Unity, so obviously, I must be doing something very wrong here. Having spent most of my day searching through depths of the internet I wasn't even sure truly existed, I must, unfortunately, now realize that I am at a total and utter loss here.

Has anyone dealt with this issue before? Has anyone dealt with this use-case before, and if yes, can anyone tell me how to change my approach to make this a little less headache-inducing? I am utterly desperate at this point and am considering rewriting my entire data access methodology just to make it work - not the healthiest mindset for clean and maintainable code.

Many thanks.

Community
  • 1
  • 1

2 Answers2

1

the issue seems to originate from your registration call, when registering the same type multiple times with unity, the last registration call wins, in this case, that will be data access object for whoever user logs-in last. Unity will take that as the default registration, and will create instances that have the connection to that user's database.

The SessionLifetimeManager is there to make sure you get only one instance of the objects you resolve under one session.

One option to solve this is to use named registration syntax to register the data-access types under a key that maps to the logged-in user (could be the database name), and on the resolve side, retrieve this user key, and use it resolve the corresponding data access implementation for the user

Mohammed Kamil
  • 133
  • 2
  • 8
0

Thank you, Mohammed. Your answer has put me on the right track - I ended up finally solving this using a RepositoryFactory which is instantiated in an InjectionFactory during registration and returns a repository that always wraps around a ClientContext pointing to the currently logged on user's currently selected database.

// DataAccessDependencies.cs

protected override void Initialize()
{
    IConfigurationBuilder configurationBuilder = Container.Resolve<IConfigurationBuilder>();

    Container.RegisterType<IRepository<ClientContext>>(new InjectionFactory(c => {
        ClientRepositoryFactory repositoryFactory = new ClientRepositoryFactory(configurationBuilder);
        return repositoryFactory.GetRepository();
    }));
}

// ClientRepositoryFactory.cs

public class ClientRepositoryFactory : IRepositoryFactory<RepositoryService<ClientContext>>
{
    private readonly IConfigurationBuilder _configurationBuilder;

    public ClientRepositoryFactory(IConfigurationBuilder configurationBuilder)
    {
        _configurationBuilder = configurationBuilder;
    }

    public RepositoryService<ClientContext> GetRepository()
    {
        var connectionString = _configurationBuilder.GetConnectionString(UserData.Current.CurrentPermission);
        ClientContext ctx = new ClientContext(connectionString);
        RepositoryService<ClientContext> repository = new RepositoryService<ClientContext>(ctx);

        return repository;
    }
}

// UserData.cs (multiton-singleton-hybrid)

public static UserData Current
{
    get
    {
        var currentAADUID = (string)(HttpContext.Current.Session["currentAADUID"]);
        return Get(currentAADUID);
    }
}

public static UserData Get(string AADUID)
{
    UserData instance;

    lock(_instances)
    {
        if(!_instances.TryGetValue(AADUID, out instance))
        {
            throw new UserDataNotInitializedException();
        }
    }
    return instance;
}

public static UserData Current
{
    get
    {
        var currentAADUID = (string)(HttpContext.Current.Session["currentAADUID"]);
        return Get(currentAADUID);
    }
}

public static UserData Get(string AADUID)
{
    UserData instance;

    lock(_instances)
    {
        if(!_instances.TryGetValue(AADUID, out instance))
        {
            throw new UserDataNotInitializedException();
        }
    }
    return instance;
}