Change injected object at runtime
Asked Answered
B

2

9

I want to have multiples implementation of the IUserRepository each implementation will work with a database type either MongoDB or any SQL database. To do this I have ITenant interface that have a connection string and other tenant configuration. The tenant is been injected into IUserRepository either MongoDB or any SQLDB implementation. What I need to know is how properly change the injected repository to choose the database base on the tenant.

Interfaces

public interface IUserRepository 
{
    string Login(string username, string password);
    string Logoff(Guid id);
}

public class User
{
    public Guid Id { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }

}

public interface ITenant
{
    string CompanyName { get; }
    string ConnectionString { get; }
    string DataBaseName { get; }
    string EncriptionKey { get; }

}

Is important to know that the tenant id is been pass to an API via header request

StartUp.cs

// set inject httpcontet to the tenant implemantion
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();

// inject tenant
services.AddTransient<ITenant, Tenant>();

// inject mongo repository but I want this to be programmatically
services.AddTransient<IUserRepository, UserMongoRepository>();

Sample Mongo Implementation

public class UserMongoRepository : IUserRepository
{

    protected ITenant Tenant 

    public UserMongoRepository(ITenant tenant) :
        base(tenant)
    {
        this.Tenant = tenant;
    }

    public string Login(string username, string password)
    {

        var query = new QueryBuilder<User>().Where(x => x.Username == username);
        var client = new MongoClient(this.Tenant.ConnectionString);var server = client.GetServer();
        var database =  client.GetServer().GetDatabase(this.Tenant.DataBaseName);
        var user = database.GetCollection<User>.FindAs<User>(query).AsQueryable().FirstOrDefault();

        if (user == null)
            throw new Exception("invalid username or password");

        if (user.Password != password)
            throw new Exception("invalid username or password");

         return "Sample Token";

    }

    public string Logoff(Guid id)
    {

        throw new NotImplementedException();
    }

}

Tenant

public class Tenant : ITenant
{

    protected IHttpContextAccessor Accesor;
    protected IConfiguration Configuration;

    public Tenant(IHttpContextAccessor accesor, IDBConfiguration config)
    {
        this.Accesor = accesor;
        this.Configuration = new Configuration().AddEnvironmentVariables();
        if (!config.IsConfigure)
            config.ConfigureDataBase();
    }


    private string _CompanyName;
    public string CompanyName
    {
        get
        {
            if (string.IsNullOrWhiteSpace(_CompanyName))
            {
                _CompanyName = this.Accesor.Value.Request.Headers["Company"];
                if (string.IsNullOrWhiteSpace(_CompanyName))
                    throw new Exception("Invalid Company");
            }
            return _CompanyName;
        }
    }

    private string _ConnectionString;
    public string ConnectionString
    {
        get
        {
            if (string.IsNullOrWhiteSpace(_ConnectionString))
            {
                _ConnectionString = this.Configuration.Get(this.CompanyName + "_" + "ConnectionString");
                if (string.IsNullOrWhiteSpace(_ConnectionString))
                    throw new Exception("Invalid ConnectionString Setup");
            }
            return _ConnectionString;
        }
    }

    private string _EncriptionKey;
    public string EncriptionKey
    {
        get
        {
            if (string.IsNullOrWhiteSpace(_EncriptionKey))
            {
                _EncriptionKey = this.Configuration.Get(this.CompanyName + "_" + "EncriptionKey");
                if (string.IsNullOrWhiteSpace(_EncriptionKey))
                    throw new Exception("Invalid Company Setup");
            }
            return _EncriptionKey;
        }
    }

    private string _DataBaseName;
    public string DataBaseName
    {
        get
        {
            if (string.IsNullOrWhiteSpace(_DataBaseName))
            {
                _DataBaseName = this.Configuration.Get(this.CompanyName + "_" + "DataBaseName");
                if (string.IsNullOrWhiteSpace(_DataBaseName))
                    throw new Exception("Invalid Company Setup");
            }
            return _DataBaseName;
        }
    }
}

Controller

public class UsersController : Controller
{
    protected IUserRepository DataService;

    public UsersController(IUserRepository dataService)
    {
        this.DataService = dataService;
    }

    // the controller implematation

}
Blacksmith answered 18/3, 2015 at 2:34 Comment(0)
J
7

You can try injecting a factory rather than the actual repository. The factory will be responsible for building the correct repository based on the current user identity.

It might require a little more boiler plate code but it can achieve what you want. A little bit of inheritance might even make the controller code simpler.

Jaban answered 18/3, 2015 at 6:32 Comment(2)
But if you do that what object you will inject in the controller?Blacksmith
An IUserRepositoryFactory which has a method that can resolve the current tenant's repositoryJaban
M
9

You should define a proxy implementation for IUserRepository and hide the actual implementations behind this proxy and at runtime decide which repository to forward the call to. For instance:

public class UserRepositoryDispatcher : IUserRepository
{
    private readonly Func<bool> selector;
    private readonly IUserRepository trueRepository;
    private readonly IUserRepository falseRepository;

    public UserRepositoryDispatcher(Func<bool> selector,
        IUserRepository trueRepository, IUserRepository falseRepository) {
        this.selector = selector;
        this.trueRepository = trueRepository;
        this.falseRepository = falseRepository;
    }

    public string Login(string username, string password) {
        return this.CurrentRepository.Login(username, password);
    }

    public string Logoff(Guid id) {
        return this.CurrentRepository.Logoff(id);
    }

    private IRepository CurrentRepository {
        get { return selector() ? this.trueRepository : this.falseRepository;
    }
}

Using this proxy class you can easily create a runtime predicate that decides which repository to use. For instance:

services.AddTransient<IUserRepository>(c =>
    new UserRepositoryDispatcher(
        () => c.GetRequiredService<ITenant>().DataBaseName.Contains("Mongo"),
        trueRepository: c.GetRequiredService<UserMongoRepository>()
        falseRepository: c.GetRequiredService<UserSqlRepository>()));
Moonstruck answered 6/6, 2015 at 8:30 Comment(6)
Thanks for the reply @steve make sense, I can see the advantage of using the dispatcher, The only thing that I notice is that the constructor will get really big because I'm planing to support multiples DB type, can I pass the tenant to the dispatcher and make the selection in the dispatcherBlacksmith
hi @Steven, why dont you directly create an instance of UserMongoRepository class like "trueRepository: new UserMongoRepository()". Is it because there might be something injected to this class before?Buckra
@BarbarosAlp: I think you answered your own question. The UserMongoRepository might need to be built-up by the container as well, because it has dependencies of its own.Moonstruck
@Steven: Thanks for the answer. If you don't mind i want to ask you one more question. Shouldn't we use "c.GetRequiredService<IUserMongoRepository>()" instead of "c.GetRequiredService<UserMongoRepository>()". I am confused because i always look for <Interface, Concrete>.Buckra
@BarbarosAlp: In the context of the original question, it would be useless to have an IUserMongoRepository, since the UserMongoRepository is simply an implementation of IUserRepository.Moonstruck
services.AddSingleton(s => new TextLogger(Configuration["Log:ToTextPath"])); services.AddSingleton(s => new SqsLogger(Configuration["Log:ToSqsUrl"], s.GetRequiredService<TextLogger>())); services.AddSingleton<ILogger>(s => s.GetRequiredService<SqsLogger>());Buckra
J
7

You can try injecting a factory rather than the actual repository. The factory will be responsible for building the correct repository based on the current user identity.

It might require a little more boiler plate code but it can achieve what you want. A little bit of inheritance might even make the controller code simpler.

Jaban answered 18/3, 2015 at 6:32 Comment(2)
But if you do that what object you will inject in the controller?Blacksmith
An IUserRepositoryFactory which has a method that can resolve the current tenant's repositoryJaban

© 2022 - 2024 — McMap. All rights reserved.