【问题标题】:Autofac - constructor parameter based on custom attribute, with assembly scanningAutofac - 基于自定义属性的构造函数参数,带有程序集扫描
【发布时间】:2019-07-25 08:43:56
【问题描述】:

我遇到了以下问题,如何配置 Autofac 容器,但找不到解决方案。

假设我有一堆存储库,例如 AccountRepository、ContactRepository、LeadRepository 等。

每个存储库都有一个 IService 类型的构造函数参数,它提供基本 CRUD 方法的实现。在我的例子中,它是一个到第 3 方应用程序的通用 Web 服务连接,但这并不重要。

例如,我有这样的事情:

public class AccountRepository
{
    private readonly IService service;

    public AccountRepository(IService service)
    {
        this.service = service ?? throw new ArgumentNullException(nameof(service));
    }

    public int GetContactCount(Guid accountId)
    {
        using(DataContext ctx = new DataContext(service))
        {
            return ctx.Contacts.Where(c => c.AccountId == accountId).Count();
        }
    }
}

我的域代码是通过命令和事件实现的。因此,假设我有一个使用上述存储库的以下命令处理程序:

public class UpdateNrOfContactsCommandHandler : IHandleCommand<UpdateNrOfContactsCommand, Account>
{
    private readonly AccountRepository accountRepo;

    public UpdateNrOfContactsCommandHandler(AccountRepository accountRepo)
    {
        this.accountRepo = accountRepo ?? throw new ArgumentNullException(nameof(accountRepo));
    }

    public void Execute(Account account)
    {
        account.NrOfContacts = repo.GetContactCount(account.Id);
    }
}

我不会将存储库隐藏在任何接口后面,因为它是业务逻辑的一部分,并且只有一个实现。 IService 是动态部分(也是我在单元测试中伪造的部分)。 如果有帮助,我可以在上面添加一个界面,但这只是我想避免的额外输入。

存储库和命令处理程序都是通过程序集扫描动态注册的。我显然不想在每次添加新的存储库或命令处理程序时更新我的​​组合根。它应该只配置一次(除非我们引入一些新的抽象)。

所以基本上我注册了来自某个程序集的所有存储库和来自另一个程序集的所有命令处理程序。当命令进来时,我解析处理程序并在其上调用 .Execute() 方法。很标准的东西。

问题是取决于上下文(只有命令处理程序知道,存储库应该是完全不可知的)我需要存储库在系统管理上下文中或在当前用户上下文。

我有两个 IService 实例:

1. IService serviceAsAdmin
2. IService serviceAsCurrentUser

管理上下文中的服务应该是默认服务。

我认为优雅的解决方案是这样的:

  1. 定义一个自定义属性,如

    public class InUserContextAttribute : Attribute 
    {
    }    
    
  2. 以后像这样使用这个属性:

    public class UpdateNrOfContactsCommandHandler : IHandleCommand<UpdateNrOfContactsCommand, Account>
    {
        private readonly AccountRepository accountRepo;
        private readonly AccountRepository accountRepoAsUser;
    
        public UpdateNrOfContactsCommandHandler(AccountRepository accountRepo, [InUserContext] AccountRepository accountRepoAsUser)
        {
            this.accountRepo = accountRepo ?? throw new ArgumentNullException(nameof(accountRepo));
            this.accountRepoAsUser = accountRepoAsUser ?? throw new ArgumentNullException(nameof(accountRepoAsUser));
        }
    
        public void Execute(Account account)
        {
            account.NrOfContacts = repo.GetContactCount(account.Id);
            account.NrOfContactsSeenByCurrentUser = accountRepoAsUser.GetContactCount(account.Id);
        }
    }
    

我不知道该怎么做 :) 研究了很多样本​​,但似乎没有一个适合这种情况。额外的复杂性是这需要通过程序集扫描进行动态处理。

我知道如何通过属性注入(通过使用 Autofac 的 .OnActivated() 方法)很容易地实现类似的东西,但是这些存储库不是可选的,所以它们应该在构造函数中传递。

我还想避免在组合根/命令总线之外的任何 Autofac 引用。绝对不想在我的业务逻辑中添加任何 Autofac 特定的东西。

基本上,我需要做的是以某种方式注册 2 个 IService 实例,这样如果参数未修饰,则解析为其中一个实例,如果使用 InUserContextAttribute 属性修饰,则解析为另一个实例。

我正在努力实现的目标有可能吗?如何? ;)

【问题讨论】:

    标签: constructor attributes autofac code-injection


    【解决方案1】:

    你有很多选择来实现你的目标,我更喜欢使用Named Services

    为了使用命名服务,您需要注册两个 IService 实现:

    // using strings to keep things simple
    
    // first register your services
    builder.RegisterType<AdminService>().Named<IService>("admin");
    builder.RegisterType<UserService>().Named<IService>("user");
    
    // Then you register your repositories, twice:
    
    foreach(var repoType in assembly.GetTypes.Where(t => IsRepository(t))
    {
        builder.RegisterType(repoType)
            .WithParameter(new ResolvedParameter(
               (pi, ctx) => pi.ParameterType == typeof(IService),
               (pi, ctx) => ctx.ResolveNamed("admin"))
            .Named("admin");
    
        builder.RegisterType(repoType)
            .WithParameter(new ResolvedParameter(
               (pi, ctx) => pi.ParameterType == typeof(IService),
               (pi, ctx) => ctx.ResolveNamed("user"))
            .Named("user");
    }
    
    

    在您的存储库构造函数中,您可以:

    public UpdateNrOfContactsCommandHandler([KeyFilter("admin")] accountRepo, [KeyFilter("user")] AccountRepository accountRepoAsUser)
        {
            this.accountRepo = accountRepo ?? throw new ArgumentNullException(nameof(accountRepo));
            this.accountRepoAsUser = accountRepoAsUser ?? throw new ArgumentNullException(nameof(accountRepoAsUser));
        }
    

    您还可以使用其他技术,例如实现解析逻辑的额外类,如下所示:

    public class RepoFactory<T>
    {
        private ILifetimeScope _scope;
    
        public RepoFactory(ILifetimeScope scope)
        {
            _scope = scope;
        }
    
        public class RepoContext : IDisposable
        {
           public T Instance { get; }
    
           public void Dispose()
           {
              // handle disposal of Instance
           }
        }
    
        public RepoContext<T> AsAdmin()
        {
            var service = scope.ResolveNamed<IService>("admin");
            // keeping it simple, you can leverage more Autofac to improve performance if needed
            var repo = Activator.CreateInstance(typeof(T), service);
            return new RepoContext<T>(repo);
        }
    }
    

    【讨论】:

    • 感谢您:1. 这会将 Autofac 特定的东西添加到业务逻辑中,我想避免这种情况 2. 这是关于在命令处理程序中使用该工厂来获取存储库的实例吗?当然我可以通过多种方式做到这一点,但我想要实现的是干净的构造函数注入。
    • 唯一与 Autofac 相关的“特定内容”是属性。无论如何你都需要它(你想重新实现它)。所以我的建议是:不要重新发明轮子,继续努力。至于二:它是一种避免使用属性的方法,并且它完成了“干净的构造函数注入”:如果(且仅当)您不想使用 Autofac 属性时,您可以将解析逻辑放入专用的工厂类中并且避免在应用程序的其余部分中使用 Autofac 特定代码。这是一个你喜欢把Autofac管理放在哪里的问题。
    【解决方案2】:

    回答我自己的问题,因为我想出了一种方法来做到这一点。基本上解决方案(实际上不是很复杂)涉及利用 Autofac 的 .Resolve() 方法的 ResolvedParameter 参数。

    它允许您将特定参数注入正在解析的对象中。

    如果有人感兴趣,下面是我使用的确切代码。 注意 - 这是在考虑 CRM 系统的情况下制作的。 “服务连接”的类型是 IOrganizationService,我上面提到的 “存储库” 被称为 “查询”,并且它应该能够正常工作从名为 CrmQuery 的抽象泛型类继承(这是因为有一些共享代码,但也用于解决方案)。

    public abstract class CrmQuery<TEntity> where TEntity : Entity
    {
    

    示例查询类如下所示:

    public class AccountQueries : CrmQuery<Account>
    {
        public AccountQueries(IOrganizationService orgService) : base(orgService) { }
    
        public Something[] GetSomething(Guid accountId)
        {
            //...
        }
    }
    

    现在这个查询在命令处理程序中使用,(简化的)代码如下:

    public class SetNrOfContactsCommandHandler : CommandHandler<SetNrOfContactsCommand>
    {
        public SetNrOfContactsCommandHandler(IOrganizationServiceWrapper orgServiceWrapper, IEventBus eventBus, 
            AccountQueries accountQueries, [InUserContext] AccountQueries accountQueriesAsUser) 
            : base(orgServiceWrapper)
        {
        }   
    }
    

    这里重要的是有两个 AccountQueries 类型的参数,但其中一个是用 [InUserContext] 属性修饰的。

    public class InUserContextAttribute : Attribute
    {
    
    }
    

    现在是解决方案

    public class Bus : ICommandBus
    {
        private readonly IContainer container = null;
    
        public Bus(IOrganizationServiceWrapper orgServiceWrapper)
        {
            var builder = new ContainerBuilder();
    
            Assembly domain = typeof(Locator).Assembly;
    
            builder.RegisterInstance(orgServiceWrapper);            
            builder.RegisterAssemblyTypes(domain).AsClosedTypesOf(typeof(IHandleCommand<>));          
            builder.RegisterAssemblyTypes(domain).AsClosedTypesOf(typeof(CrmQuery<>));
    
            container = builder.Build();
        }
    
        public void Handle(ICommand command)
        {
            using(ILifetimeScope scope = container.BeginLifetimeScope())
            { 
                var handlerType = typeof(IHandleCommand<>).MakeGenericType(command.GetType());
    
                dynamic handler = scope.Resolve(handlerType, new ResolvedParameter(
                    (pi, ctx) => {
                        // Determine if we're looking for a parameter that is of a type that extends CrmQuery<>
                        bool isCrmQuery = pi.ParameterType.IsClass
                                          && pi.ParameterType.BaseType.IsGenericType
                                          && pi.ParameterType.BaseType.GetGenericTypeDefinition() == typeof(CrmQuery<>);
    
                        return isCrmQuery;
                    },
                    (pi, ctx) => {
                        // Check if it has the [InUserContext] attribute
                        bool useUserContextService = pi.CustomAttributes.Any(attr => attr.AttributeType == typeof(InUserContextAttribute));
    
                        // This contains both the system context and user context CRM service connections
                        IOrganizationServiceWrapper orgServiceWrapper = scope.Resolve<IOrganizationServiceWrapper>();
    
                        // Inject the correct CRM service reference
                        object resolvedQueryHandler = scope.Resolve(pi.ParameterType, new ResolvedParameter(
                            (_pi, _ctx) => _pi.ParameterType == typeof(IOrganizationService),
                            (_pi, _ctx) => useUserContextService ? orgServiceWrapper.OrgService : orgServiceWrapper.OrgServiceAsSystem
                        ));
    
                        return resolvedQueryHandler;
                    }
                ));
    
                handler.Execute((dynamic)command);
            }
        }
    }
    

    有趣的部分在 .Handle() 方法中。

    1. 我发现所有类型为 CrmQuery 的参数。
    2. 我看看它们是否用 [InUserContext] 属性装饰。
    3. 我尝试解析我正在寻找的“查询”的一个实例。
    4. 对于该分辨率,我再次使用 ResolvedParameter,这次注入了正确的 IOgranizationService 实例

    作品:)

    【讨论】:

    • 干得好!我只是避免以这种方式直接使用容器:我更喜欢在所有配置中使用模块,因为它们使其余代码更有条理。有了这个较小的警告,代码正是我所说的“工厂”类:您将所有与 Autofac 相关的东西都保存在这个方法和类型中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-17
    • 1970-01-01
    • 2020-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多