【问题标题】:Inject concrete class into Generic c# .net core将具体类注入通用 c# .net 核心
【发布时间】:2020-09-14 09:14:44
【问题描述】:

我创建了一个通用类:

public class GenericCreate<T> : IRequest<Attempt<T>> where T: class
{
    public T Model { get; }
    public GenericCreate(T model) => Model = model;
}

public class GenericCreateHandler<T> : IRequestHandler<GenericCreate<T>, Attempt<T>> where T : class
{
    private readonly NotNullValidator<T> _validator;
    private readonly DatabaseContext _databaseContext;

    public GenericCreateHandler(NotNullValidator<T> validator, DatabaseContext databaseContext)
    {
        _validator = validator;
        _databaseContext = databaseContext;
    }

    public async Task<Attempt<T>> Handle(GenericCreate<T> request, CancellationToken cancellationToken)
    {
        var generic = request.Model;
        var validationAttempt = _validator.Validate(generic).ToAttempt();
        if (validationAttempt.Failure) return validationAttempt.Error;
        
        _databaseContext.Add(generic);
        await _databaseContext.SaveChangesAsync(cancellationToken);

        return generic;
    }
}

就类的功能而言,它是有效的。但是,我正在尝试根据Ttype 注入不同的验证器。 你可以看到我正在尝试注入 NotNullValidator&lt;T&gt; 这本身就是一个类:

public class NotNullValidator<T> : AbstractValidator<T>
{
    protected override bool PreValidate(ValidationContext<T> context, ValidationResult result)
    {
        if (context.InstanceToValidate != null) return true;

        result.Errors.Add(new ValidationFailure("", "Please ensure a model was supplied."));
        return false;
    }
}

但我想注入的是这样的:

public class CategoryValidator: NotNullValidator<Category>
{
    public CategoryValidator()
    {
        RuleFor(m => m.Id).NotEmpty();
        RuleFor(m => m.Name).NotEmpty();
    }
}

您可以想象,我为项目中的每个实体都有一个验证器类,因此能够获得正确的验证器很重要。 有谁知道我如何使用 .net core 做到这一点?

【问题讨论】:

  • 我看到了三种方法:属性、配置或约定。对于第一个,定义一个属性,您将在描述需要注入的验证器的类上声明该属性。第二,您可以有一个配置类,将每个类映射到它的验证器(可能变得非常冗长)。或者第三,您需要建立一个约定,例如基于类的名称并使用反射来找到相应的验证器。
  • 我不知道如何执行您提到的任何选项,但是对于您的第三个,我所有的验证器都命名为 &lt;ObjectName&gt;Validator
  • 除了第二个,option,第一个和第三个都是基于反射的。今天晚些时候,我将尝试在答案中写下一些示例。我可能会从第三个选项开始,因为您的验证器似乎很适合约定方法。

标签: c# generics dependency-injection


【解决方案1】:

正如承诺的第一个基于约定的示例:

// List all classes containing validators in their name, you might want to do this at startup to avoid the overhead each time you need to create a class
// You might also want to change which assembly you are searching in.
var validators = Assembly.GetExecutingAssembly().DefinedTypes.Where(t => t.Name.Contains("Validator")).ToList();

// You'll be able to get that from your type parameter T (i.e typeof(T))
var type = typeof(MyClass);

//This is your convention => <ClassName>Validator   
var validatorToUse = validators.Single(v => v.Name == $"{type.Name}Validator");

//Instantiate your validator (you should cast to your abstract type to use it more easily)
var obj = Activator.CreateInstance(validatorToUse);

如果需要,可以通过其他方式实例化您的验证器:https://stackoverflow.com/a/981344/2245256

【讨论】:

  • 这会在它自己的类中吗?我是否将其注入到 GenericCreateHandler 而不是验证器中?
  • 此代码将被放置在您的 GenericCreateHandler 中,最后一行被分配给您的 _validator 字段。您不需要NotNullValidator&lt;T&gt; 参数,因为您可以从类型参数中找到验证器
  • 我猜它也可以是它自己的类,这样可以更好地遵循 SRP
【解决方案2】:

只是澄清一下,以防其他人有问题。 我使用 Sidewinders 回答并创建了这个扩展方法:

public static class ValidatorExtensions
{
    public static NotNullValidator<T> GetValidator<T>()
    {
        // List all classes containing validators in their name, you might want to do this at startup to avoid the overhead each time you need to create a class
        // You might also want to change which assembly you are searching in.
        var validators = Assembly.GetExecutingAssembly().DefinedTypes.Where(t => t.Name.Contains("Validator")).ToList();

        // You'll be able to get that from your type parameter T (i.e typeof(T))
        var type = typeof(T);

        //This is your convention => <ClassName>Validator   
        var validatorToUse = validators.Single(v => v.Name == $"{type.Name}Validator");

        //Instantiate your validator (you should cast to your abstract type to use it more easily)
        return (NotNullValidator<T>) Activator.CreateInstance(validatorToUse);
    }
}

然后我可以像这样更新我的通用类:

public class GenericCreate<T> : IRequest<Attempt<T>> where T: class
{
    public T Model { get; }
    public GenericCreate(T model) => Model = model;
}

public class GenericCreateHandler<T> : IRequestHandler<GenericCreate<T>, Attempt<T>> where T : class
{
    private readonly NotNullValidator<T> _validator;
    private readonly DatabaseContext _databaseContext;

    public GenericCreateHandler(DatabaseContext databaseContext)
    {
        _validator = ValidatorExtensions.GetValidator<T>();
        _databaseContext = databaseContext;
    }

    public async Task<Attempt<T>> Handle(GenericCreate<T> request, CancellationToken cancellationToken)
    {
        var generic = request.Model;
        var validationAttempt = _validator.Validate(generic).ToAttempt();
        if (validationAttempt.Failure) return validationAttempt.Error;
        
        _databaseContext.Add(generic);
        await _databaseContext.SaveChangesAsync(cancellationToken);

        return generic;
    }
}

注意构造函数:

_validator = ValidatorExtensions.GetValidator<T>();

这让我的测试顺利通过:

[TestFixture]
public class GenericCreateShould
{
    [Test]
    public async Task ThrowValidationErrorWhenGenericIsInvalid()
    {
        // Assemble
        var services = GenericCreateContext<Venue>.GivenServices();
        var handler = services.WhenCreateHandler();

        // Act
        var response = await handler.Handle(new GenericCreate<Venue>(new Venue()), CancellationToken.None);

        // Assert
        response.Success.Should().BeFalse();
        response.Result.Should().BeNull();
        response.Error.Should().BeOfType<ValidationError>();
        response.Error.Message.Should().Be("'Name' must not be empty.");
    }

    [Test]
    public async Task ReturnGeneric()
    {
        // Assemble
        var services = GenericCreateContext<Venue>.GivenServices();
        var handler = services.WhenCreateHandler();
        var model = new GenericCreate<Venue>(new Venue
        {
            Name = "Main branch"
        });

        // Act
        var response = await handler.Handle(model, CancellationToken.None);

        // Assert
        response.Success.Should().BeTrue();
        response.Error.Should().BeNull();
        response.Result.Should().BeOfType<Venue>();
    }
}

public class GenericCreateContext<T, TKey> : DatabaseContextContext where T: TClass<TKey>
{
    public static GenericCreateContext<T, TKey> GivenServices() => new GenericCreateContext<T, TKey>();

    public GenericCreateHandler<T> WhenCreateHandler() => new GenericCreateHandler<T>(DatabaseContext);
}

public class GenericCreateContext<T> : GenericCreateContext<T, int> where T: TClass<int>
{
}

第一个测试通过了,这就是问题所在,因为NotNullValidator 不包含名称等规则,但VenueValidator 包含;这意味着扩展方法正在工作。

【讨论】:

  • 但是为什么要限制自己使用这种定位器反模式呢?
  • 你能详细说明一下吗?
  • 你的类依赖于验证器。您当前正在构造函数(定位器反模式)中解决它,而不是显式地将其注入所需的类中。这使得目标类误导消费者关于它实际需要什么来执行其功能(显式依赖原则)。您可能需要审查当前的设计并提出更可靠的方法。
  • 问题是我不知道另一种方法。这就是为什么我首先问这个问题:D
  • @r3plica - 检查这是否会导致遵循 DI 的解决方案。 https://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core。在这种情况下,您为创建对象而编写的当前代码可以作为解析器的一部分。
猜你喜欢
  • 1970-01-01
  • 2018-12-14
  • 2019-02-16
  • 2015-08-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-08
  • 1970-01-01
相关资源
最近更新 更多