-1

I have a generic class I have created:

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;
    }
}

As far as the function of the class is concerned, it works. But, I am trying to inject a different validator based on the type of T. You can see I am trying to inject NotNullValidator<T> which is a class in itself:

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;
    }
}

But what I would like to inject is this:

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

As you can imagine, I have a validator class for every entity in the project, so being able to get the correct validator is important. Does anyone know how I can do this using .net core?

r3plica
  • 13,017
  • 23
  • 128
  • 290
  • I see three ways of doing that : Attributes, configuration, or convention. For the first one, define an attibute that you'll declare on the class that describe the validator that needs to be injected. For the second, you could have a configuration class that would map each class to it's validator (can become quite verbose). Or third , you'd need to establish a convention based for example on the name of the class and use reflection to find the corresponding validator. – Irwene Sep 14 '20 at 09:27
  • I have no idea how to do any of the options you have mentioned, but for your third, all my validators are named like `Validator` – r3plica Sep 14 '20 at 09:31
  • Apart from the second, option, first and third are reflection-based. I'll try to get some examples written down in an answer later today. I'll maybe start with the third option, since, your validators seem well named for the convention approach. – Irwene Sep 14 '20 at 09:33

2 Answers2

0

As promised a first example based on convention:

// 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);

Other ways to instantiate your validator if needed: https://stackoverflow.com/a/981344/2245256

Irwene
  • 2,807
  • 23
  • 48
  • Would this be in it's own classs? Do I inject that into the GenericCreateHandler instead of the validator? – r3plica Sep 14 '20 at 10:10
  • This code would be placed in you GenericCreateHandler, with the last line being assigned to your _validator field. You wouldn't need the `NotNullValidator` parameter, since you'd be able to find the validator from the type parameter – Irwene Sep 14 '20 at 10:12
  • I guess it could also be its own class, that would allow following the SRP better – Irwene Sep 14 '20 at 10:13
0

Just to clarify, incase anyone else has the issue. I used Sidewinders answer and create this extension method:

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);
    }
}

I was then able to update my generic class like this:

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;
    }
}

note the constructor:

_validator = ValidatorExtensions.GetValidator<T>();

That allowed my tests to pass without issue:

[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>
{
}

The first test passes, which is what the issue was, because NotNullValidator doesn't include rules for names, etc, but VenueValidator does; which means the extension method is working.

r3plica
  • 13,017
  • 23
  • 128
  • 290
  • But why constrict yourself to this locator anti-pattern? – Nkosi Sep 14 '20 at 12:08
  • can you elaborate? – r3plica Sep 14 '20 at 12:11
  • Your class depends on the validator. You are currently resolving it in the constructor (locator anti-pattern) instead of explicitly injecting it into the needed class. This makes the target class misleading to consumers about what it actually needs to perform its function (explicit dependency principle). You may need to review the current design and come up with a more SOLID approach. – Nkosi Sep 14 '20 at 12:16
  • The problem is I don't know of another approach. That's why I asked the question in the first place :D – r3plica Sep 14 '20 at 12:20
  • @r3plica - Check if this leads to a solution which follows DI. [https://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core](https://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core). In that case, the current code you have written to create object can go as part of resolver. – user1672994 Sep 14 '20 at 12:27