Ninject Named binding with IGenericRepository

2019-09-06 21:31发布

问题:

I'm developing an ASP.NET MVC 4 Web Api, with C#, .NET Framework 4.0, Entity Framework Code First 6.0 and Ninject.

I have two different DbContext custom implementations to connect with two different databases.

This is my NinjectConfigurator class (partial):

private void AddBindings(IKernel container)
{
    container.Bind<IUnitOfWork>().
       To<TRZICDbContext>().InRequestScope().Named("TRZIC");
    container.Bind<IUnitOfWork>().
       To<INICDbContext>().InRequestScope().Named("INIC");

    container.Bind<IGenericRepository<CONFIGURATIONS>>().
       To<GenericRepository<CONFIGURATIONS>>();
    container.Bind<IGenericRepository<INCREMENTAL_TABLE>>().
    To<GenericRepository<INCREMENTAL_TABLE>>();

    // More implementation...
}

CONFIGURATIONS is a TRZIC table and INCREMENTAL_TABLE is an INIC table.

I'm using a IGenericRepository and here it's where I have the problems:

public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
    protected DbSet<TEntity> DbSet;

    private readonly DbContext dbContext;

    public GenericRepository(IUnitOfWork unitOfWork)
    {
        dbContext = (DbContext)unitOfWork;
        DbSet = dbContext.Set<TEntity>();
    }

    // Hidden implementation..
}

I don't know how to use the [Named("TRZIC")] here public GenericRepository(IUnitOfWork unitOfWork) or maybe I need to use it elsewhere.

Here the IUnitOfWork implementation depends on TEntity.

Any advice?

回答1:

Let's start with the basics.

As far as i know named bindings work only with constant values attributed in code, like the [Named("foo")] attribute, or otherwise by using "service location" like IResolutionRoot.Get<T>(string name). Either does not work for your scenario, so a named binding is out of the question. That leaves you with conditional bindings (.When(...) methods).


You've got 2 database with n entities each. 2 Database means two configurations means 2 different IUnitOfWork configuration. However, the "user" is not requesting a specific database, but a specific entity. Thus you'll need a map entity-->database (a dictionary). I don't think there's a way to get around that, but you may devise some kind of convention & implement it by convention, so you don't have to type and maintain a lot of code.

Solution 1: .WhenInjectedInto<>

with out of the box ninject features, and lots of manual labor:

Bind<IUnitOfWork>().To<UnitOfWorkOfDatabaseA>()
    .WhenInjectedInto<IRepository<SomeEntityOfDatabaseA>>();

Bind<IUnitOfWork>().To<UnitOfWorkOfDatabaseA>()
    .WhenInjectedInto<IRepository<SomeOtherEntityOfDatabaseA>>();

Bind<IUnitOfWork>().To<UnitOfWorkOfDatabaseB>()
    .WhenInjectedInto<IRepository<SomeEntityOfDatabaseB>>();

you get the drift,.. right?


Solution 2.1: Custom When(..) implementation

Not so much manual labor and maintenance anymore. Let me just dump the code on you, see below:

public interface IRepository { IUnitOfWork UnitOfWork { get; } }

public class Repository<TEntity> : IRepository<TEntity>
{
    public IUnitOfWork UnitOfWork { get; set; }

    public Repository(IUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }
}

public interface IUnitOfWork { }
class UnitOfWorkA : IUnitOfWork { }
class UnitOfWorkB : IUnitOfWork { }

public class Test
{
    [Fact]
    public void asdf()
    {
        var kernel = new StandardKernel();

        kernel.Bind(typeof (IRepository<>)).To(typeof (Repository<>));

        kernel.Bind<IUnitOfWork>().To<UnitOfWorkA>()
            .When(request => IsRepositoryFor(request, new[] { typeof(string), typeof(bool) })); // these are strange entity types, i know ;-)

        kernel.Bind<IUnitOfWork>().To<UnitOfWorkB>()
            .When(request => IsRepositoryFor(request, new[] { typeof(int), typeof(double) }));


        // assert
        kernel.Get<IRepository<string>>()
            .UnitOfWork.Should().BeOfType<UnitOfWorkA>();

        kernel.Get<IRepository<double>>()
            .UnitOfWork.Should().BeOfType<UnitOfWorkB>();

    }

    private bool IsRepositoryFor(IRequest request, IEnumerable<Type> entities)
    {
        if (request.ParentRequest != null)
        {
            Type injectInto = request.ParentRequest.Service;
            if (injectInto.IsGenericType && injectInto.GetGenericTypeDefinition() == typeof (IRepository<>))
            {
                Type entityType = injectInto.GetGenericArguments().Single();
                return entities.Contains(entityType);
            }

        }

        return false;
    }
}

Solution 2.2 Custom convention based When(...)

Let's introduce a small convention. Entity names of database TRZIC start with TRZIC, for example TRZIC_Foo. Entity names of database INIC start with INIC, like INIC_Bar. We can now adapt the previous solution to:

public class Test
{
    [Fact]
    public void asdf()
    {
        var kernel = new StandardKernel();

        kernel.Bind(typeof (IRepository<>)).To(typeof (Repository<>));

        kernel.Bind<IUnitOfWork>().To<UnitOfWorkA>()
            .When(request => IsRepositoryFor(request, "TRZIC")); // these are strange entity types, i know ;-)

        kernel.Bind<IUnitOfWork>().To<UnitOfWorkB>()
            .When(request => IsRepositoryFor(request, "INIC"));


        // assert
        kernel.Get<IRepository<TRZIC_Foo>>()
            .UnitOfWork.Should().BeOfType<UnitOfWorkA>();

        kernel.Get<IRepository<INIC_Bar>>()
            .UnitOfWork.Should().BeOfType<UnitOfWorkB>();
    }

    private bool IsRepositoryFor(IRequest request, string entityNameStartsWith)
    {
        if (request.ParentRequest != null)
        {
            Type injectInto = request.ParentRequest.Service;
            if (injectInto.IsGenericType && injectInto.GetGenericTypeDefinition() == typeof (IRepository<>))
            {
                Type entityType = injectInto.GetGenericArguments().Single();
                return entityType.Name.StartsWith(entityNameStartsWith, StringComparison.OrdinalIgnoreCase);
            }

        }

        return false;
    }
}

This way we don't need explicit mapping (EntityA, EntityB, EntityC) => DatabaseA, (EntityD, EntityE, EntityF) => DatabaseB).



回答2:

If you say that IUnitOfWork depends on TEntity why not make IUnitOfWork generic too?

public class TRZIC {}

public class INIC {}

public interface IUnitOfWork<TEntity> {}

public class TRZICDbContext : DbContext, IUnitOfWork<TRZIC> {}

public class INICDbContext : DbContext, IUnitOfWork<INIC> {}

public interface IGenericRepository<TEntity> {}

public class GenericRepository<TEntity> : IGenericRepository<TEntity>
    where TEntity : class
{
    public GenericRepository(IUnitOfWork<TEntity> unitOfWork)
    {
        var dbContext = (DbContext) unitOfWork;
    }
}

private static void AddBindings(IKernel container)
{
    container
        .Bind<IUnitOfWork<TRZIC>>()
        .To<TRZICDbContext>();
    container
        .Bind<IUnitOfWork<INIC>>()
        .To<INICDbContext>();

    container
        .Bind<IGenericRepository<TRZIC>>()
        .To<GenericRepository<TRZIC>>();
    container
        .Bind<IGenericRepository<INIC>>()
        .To<GenericRepository<INIC>>();
}


回答3:

Another solution that also leverages code readability:

public interface IUnitOfWork {}

// both named A and B
public class UnitOfWorkA : IUnitOfWork {}

public class UnitOfWorkB : IUnitOfWork {}

public abstract class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
    protected DbSet<TEntity> DbSet;

    private readonly DbContext dbContext;

    public GenericRepository(IUnitOfWork unitOfWork)
    {
        dbContext = (DbContext)unitOfWork;
        DbSet = dbContext.Set<TEntity>();
    }

     // other IGenericRepository methods

}

public class GenericRepositoryForA<TEntity> : GenericRepository<TEntity>
{
    public GenericRepositoryForA([Named("A")]IUnitOfWork unitOfWork) 
        : base(unitOfWork)
    {

    }
}

public class GenericRepositoryForB<TEntity> : GenericRepository<TEntity>
{
    public GenericRepositoryForB([Named("B")]IUnitOfWork unitOfWork) 
        : base(unitOfWork)
    {

    }
}

This allows you to ask for a specific database context as a dependency, or get both of them if required. And you only need to implement GenericRepository once.

It greatly improvises code visiblity because you actually know which database context you are using by looking at the variable type/name, instead of getting a IUnitOfWork injected without any visual detail on its actual type.

I'd suggest adding some extra interfaces if you want to unittest it (you should!).

Simply adding

public interface IGenericRepositoryForA<TEntity> : IGenericRepository<TEntity>

and just let GenericRepositoryForA<TEntity> implement it aswell.



回答4:

Another solution:

private void AddBindings(IKernel container)
{
    container.Bind<IUnitOfWork>().To<TRZICDbContext>().InRequestScope();

    container.Bind<IGenericRepository<CONFIGURATIONS>>().
        To<GenericRepository<CONFIGURATIONS>>();
    container.Bind<IGenericRepository<INCREMENTAL_TABLE>>().
        To<GenericRepository<INCREMENTAL_TABLE>>().WithConstructorArgument("unitOfWork", new INICDbContext());

    // More code..
}

I have used WithConstructorArgument to indicate that I want to use INICDbContext.

I don't know if this is correct or not.