简体   繁体   中英

Autofac. Resolve Generic IRepository by Entity Attribute

I have 2 repositories - SqlRepository and MongoRepository. These repositories implement IRepository<T> .

I have multiple possible configurations in my application.

Example 1: I have 2 databases. I want use SqlRepository<T> when T has attibute [SmallDataAttribute] , else I want use MongoRepository .

Example 2: I have 1 database - but my app doesn't know about working database. I can check connection for this database when I start config Autofac.

Some code:

#region Registration providers

builder.RegisterInstance(new MongoClient("connectString")).As<IMongoClient>();
builder.RegisterInstance(new Context("MySQL", "connectString")).As<DbContext>();

#endregion

builder.Register(x => x.Resolve<IMongoClient>().GetDatabase("test")).As<IMongoDatabase>().SingleInstance();

#region Registration Repositories

builder.RegisterGeneric(typeof(MongoRepository<>))
    .WithParameter((info, context) => info.Name == "database",
        (info, context) => context.Resolve<IMongoDatabase>());

builder.RegisterGeneric(typeof(SqlRepository<>))
    .WithParameter((info, context) => info.Name == "context",
        (info, context) => context.Resolve<Context>());

#endregion

builder.RegisterInstance(new UnitOfWork()).As<IUnitOfWork>();
builder.Register(x => Container).AsSelf();
builder.RegisterBuildCallback(x => Container = x.Resolve<IContainer>());

PS Context & Database has method bool IsCanConnect() ;

How can I do it?

Additional, I need do something:

public IRepository<T> Repository<T>() where T : BaseEntity
        {
            if (_repositories == null)
                _repositories = new Dictionary<string, object>();

            var type = typeof(T).Name;

            if (_repositories.ContainsKey(type)) 
                return (IRepository<T>) _repositories[type];

            IRepository<T> repository;
            ITransaction transaction;

            if (((DatabaseEntityAttribute) typeof(T).GetCustomAttributes(typeof(DatabaseEntityAttribute), false).First()
                ).ProviderName == "MySQL" && AutofacModule.Scope.Resolve<DbContext>().Database.CanConnect())
            {
                transaction = new Transaction.Transaction(AutofacModule.Scope.Resolve<DbContext>().Database);
                repository = AutofacModule.Scope.Resolve<SqlRepository<T>>();
            }
            else
            {
                transaction = new Transaction.Transaction(AutofacModule.Scope.Resolve<IMongoDatabase>().Client.StartSession());
                repository = AutofacModule.Scope.Resolve<MongoRepository<T>>();
            }

            transaction.Begin();
            _transactions.Add(transaction);
            _repositories.Add(type, repository);
            return (IRepository<T>)_repositories[type];
        }

This only for example 1. I can add check for connection in this condition.

Autofac doesn't have the ability to just sort of "intuit" what you want to do and generate a factory like that. You also can't necessarily "register a factory for an open generic." However, you have the logic you need in that factory method and you can make use of the log.net integration module example to build something that should work.

Here's a working snippet. I simplified what you're doing so it's easier to read, but you can add back all your business logic and connection checking and everything else as needed.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Autofac;
using Autofac.Core;
using Autofac.Core.Registration;

namespace AutofacDemo
{
    public class Program
    {
        public static void Main()
        {
            var builder = new ContainerBuilder();
            builder.RegisterGeneric(typeof(MongoRepository<>)).AsSelf();
            builder.RegisterGeneric(typeof(SqlRepository<>)).AsSelf();
            builder.RegisterType<SqlConsumer>();
            builder.RegisterType<MongoConsumer>();
            builder.RegisterType<RepositoryFactory>().SingleInstance();
            builder.RegisterModule<RepositoryFactoryModule>();
            var container = builder.Build();

            var repo1 = container.Resolve<SqlConsumer>().Repository;
            Console.WriteLine("SqlEntity gets repo {0}", repo1.GetType());
            var repo2 = container.Resolve<MongoConsumer>().Repository;
            Console.WriteLine("MongoEntity gets repo {0}", repo2.GetType());
        }
    }

    // This is just a simplified version of the factory from the question.
    // It can do more complex stuff, but for the purposes of the example
    // it doesn't really need to.
    public class RepositoryFactory
    {
        private readonly Dictionary<string, object> _repositories = new Dictionary<string, object>();

        public IRepository<T> Repository<T>(IComponentContext context)
        {
            var type = typeof(T).Name;
            if (this._repositories.ContainsKey(type))
            {
                return (IRepository<T>)this._repositories[type];
            }

            IRepository<T> repository;
            var attribute = (DatabaseEntityAttribute)typeof(T).GetCustomAttributes(typeof(DatabaseEntityAttribute), false).FirstOrDefault();
            if (attribute?.ProviderName == "MySQL")
            {
                repository = context.Resolve<SqlRepository<T>>();
            }
            else
            {
                repository = context.Resolve<MongoRepository<T>>();
            }

            this._repositories[type] = repository;
            return repository;
        }
    }

    // Strongly based on the log4net integration module example.
    // https://autofaccn.readthedocs.io/en/latest/examples/log4net.html
    // You can't register a "factory for an open generic" so we use a
    // module to tie the factory to the resolution. Note this means
    // you CAN'T do
    // container.Resolve<IRepository<T>>()
    // directly - it will only inject stuff into consuming classes!
    public class RepositoryFactoryModule : Autofac.Module
    {
        // This handles injecting properties if property injection is enabled.
        private static void InjectRepositoryProperties(object instance, IComponentContext context)
        {
            var instanceType = instance.GetType();
            var properties = instanceType
              .GetProperties(BindingFlags.Public | BindingFlags.Instance)
              .Where(p => IsRepositoryType(p.PropertyType) && p.CanWrite && p.GetIndexParameters().Length == 0);

            // Set the properties located.
            var factory = context.Resolve<RepositoryFactory>();
            foreach (var propToSet in properties)
            {
                var entityType = EntityTypeFromRepositoryType(propToSet.PropertyType);
                propToSet.SetValue(instance, CallFactoryMethod(entityType, factory, context), null);
            }
        }

        // This handles injecting constructor parameters.
        private static void OnComponentPreparing(object sender, PreparingEventArgs e)
        {
            e.Parameters = e.Parameters.Union(
                new[]
                {
                    new ResolvedParameter(
                        (p, i) => IsRepositoryType(p.ParameterType),
                        (p, i) => CallFactoryMethod(EntityTypeFromRepositoryType(p.ParameterType), i.Resolve<RepositoryFactory>(), i)
                    ),
                });
        }

        // Convenience method for determining if we can inject the repository.
        private static bool IsRepositoryType(Type type)
        {
            return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IRepository<>);
        }

        // The factory method is an open generic so we need to make a closed generic
        // before we can call it.
        private static object CallFactoryMethod(Type entityType, RepositoryFactory factory, IComponentContext context)
        {
            var method = typeof(RepositoryFactory).GetMethod("Repository").MakeGenericMethod(entityType);
            return method.Invoke(factory, new object[] { context });
        }

        // We have IRepository<T>, we need the T part for constructing the factory method signature.
        private static Type EntityTypeFromRepositoryType(Type repositoryType)
        {
            return repositoryType.GetGenericArguments().First();
        }

        // This is what actually wires up the above methods to handle resolution ops.
        protected override void AttachToComponentRegistration(IComponentRegistryBuilder componentRegistryBuilder, IComponentRegistration registration)
        {
            // Handle constructor parameters.
            registration.Preparing += OnComponentPreparing;

            // Handle properties.
            registration.Activated += (sender, e) => InjectRepositoryProperties(e.Instance, e.Context);
        }
    }

    // The rest is just placeholder stuff to make it compile and basically
    // act like the thing in the example. Additional stuff isn't really
    // important - adding IsCanConnect() or whatever is really just adding
    // to the existing skeleton here; it doesn't impact the overall pattern.

    public interface IRepository<T>
    {
    }

    public class MongoRepository<T> : IRepository<T>
    {
    }

    public class SqlRepository<T> : IRepository<T>
    {
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class DatabaseEntityAttribute : Attribute
    {
        public DatabaseEntityAttribute(string providerName)
        {
            this.ProviderName = providerName;
        }

        public string ProviderName { get; }
    }

    [DatabaseEntity("Mongo")]
    public class MongoEntity
    {
    }

    [DatabaseEntity("MySQL")]
    public class SqlEntity
    {
    }

    public class MongoConsumer
    {
        public MongoConsumer(IRepository<MongoEntity> repository)
        {
            this.Repository = repository;
        }

        public IRepository<MongoEntity> Repository { get; }
    }

    public class SqlConsumer
    {
        public SqlConsumer(IRepository<SqlEntity> repository)
        {
            this.Repository = repository;
        }

        public IRepository<SqlEntity> Repository { get; }
    }
}

The premise here is:

  • You have a factory that can look at the attributes (or whatever) and make the decision about which thing to resolve.
  • An Autofac module can look at things being resolved and determine if there's an IRepository<T> parameter (either in constructor parameters or, if you enabled it, properties) and call your factory method.

There is a caveat here which is that you won't be able to directly resolve a repository . That is, if you call container.Resolve<IRepository<SomeEntity>> it does not go through the module. However, I'm guessing that any time you need a repository it's a constructor parameter, so you should be good. (Manually resolving stuff is called "service location" and you generally should avoid that anyway.)

There's a lot to digest here since what you're asking is pretty advanced stuff, but if you run the snippet you'll see it works - you get the right repository based on the constructor parameter and the attributes on the entities.

I will add a couple of other notes only somewhat related:

  • Make sure the repository factory is a singleton or the caching dictionary thing won't work.
  • The dictionary of repositories isn't thread-safe, so if you have multiple requests in, say, a web app all trying to get repositories, the cache isn't going to work the way you want. You need to put some locking around that.
  • You could optimize some of the reflection performance by creating static variables that hold stuff like typeof(RepositoryFactory).GetMethod("Repository") . That way you don't have to look it up each time.
  • The example there may be a little sloppy. Like in the CallFactoryMethod method it takes in a factory instance and a component context so the factory can resolve the appropriate backing repo. However, just before each call to CallFactoryMethod you'll see we have to resolve that factory instance. The CallFactoryMethod signature may be able to be optimized by just resolving the factory right in that method. On the other hand, by passing in the factory it allows us to do the foreach loop in the property resolution function without resolving the factory over and over. Anyway, there could be refactoring and optimization.

In order to choose the correct IRepository<T> based on a condition of T you can use a IRegistrationSource that will let you create registration when Autofac needs them. The RegistrationFor method will let you have access to T which will let you access attributes on T and return the correct concrete implementation based on it.

Let's see this registration source

public class RepositoryRegistrationSource : IRegistrationSource
{
    public bool IsAdapterForIndividualComponents => false;

    public IEnumerable<IComponentRegistration> RegistrationsFor(
        Service service,
        Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
    {
        var typedService = service as IServiceWithType;

        if (typedService != null
            && typedService.ServiceType.IsGenericType
            && typedService.ServiceType.GetGenericTypeDefinition() == typeof(IRepository<>))
        {
            var t = typedService.ServiceType.GetGenericArguments()[0];

            var key = "mongo"; // get key using t.GetCustomAttributes();

            // will create a delegate registration source that used a named service
            var r = RegistrationBuilder.ForDelegate(typedService.ServiceType, (c, p) => c.ResolveNamed(key, typedService.ServiceType, p))
                                       .CreateRegistration();

            yield return r;
        }
        yield break;
    }
}

The magic is here

var r = RegistrationBuilder.ForDelegate(typedService.ServiceType, (c, p) => c.ResolveNamed(key, typedService.ServiceType, p))
                           .CreateRegistration();

this is similar to (when T is XMongoEntity )

builder.Register(c => c.ResolveNamed<ISqlRepository<XMongoEntity>>("mongo"))
       .As<ISqlRepository<XMongoEntity>>()

but it is a dynamic registration based on a condition of T

Then you can register all your concrete repositories as named service.

builder.RegisterGeneric(typeof(MongoRepository<>))
       .WithParameter((info, context) => info.Name == "database",
            (info, context) => context.Resolve<IMongoDatabase>())
       .Named("sql", typeof(IRepository<>));

builder.RegisterGeneric(typeof(SqlRepository<>))
       .WithParameter((info, context) => info.Name == "context",
            (info, context) => context.Resolve<Context>())
       .Named("mongo", typeof(IRepository<>));

and register the registration source

builder.RegisterSource<RepositoryRegistrationSource>();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM