bartoszlenar / Validot

Validot is a performance-first, compact library for advanced model validation. Using a simple declarative fluent interface, it efficiently handles classes, structs, nested members, collections, nullables, plus any relation or combination of them. It also supports translations, custom logic extensions with tests, and DI containers.
MIT License
310 stars 19 forks source link

ASP.Net Core integration/automatic IOC Container resolver #10

Closed TwentyFourMinutes closed 3 years ago

TwentyFourMinutes commented 4 years ago

Feature description

As stated in the README:

Integration with ASP.NET or other frameworks: Making such a thing wouldn't be a difficult task at all, but Validot tries to remain a single-purpose library, and all integrations need to be done individually.

I totally understand that and I agree with that, however as I am currently trying to replace FluentValidation with Validot I find it especially tedious to register all of those validators by hand and usually I use methods which automatically observe all the types/validators.

IMHO it is very important to keep things as simple as possible, in order to make a package as accessible as possible. This however, creates some kind of obstacle, at least to some degree. Especially due to the support for ASP.Net Core by FluentValidation.

To keep all worlds happy I'd suggest a second nuget package which helps solving that. On the one hand the core library is still totally independent and kept nice and tidy and on the other hand it is more user friendly.

Feature in action

Depending on the supported IOC Containers you would have something like the following.

services.AddValidators<Type>(); // The type would be in an assembly with all the other validators.
// Maybe with an overload to add multiple assemblies at once.
services.AddValidators(typeof(ValidatorA).Assembly, typoef(ValidatorB).Assembly);

Feature details

However this would raise the question, which kind of pattern would be used for this. I personally would have a few ideas, but would love to hear other ideas.

  1. A static class containing all the validators

    public static class Validators
    {
    public static Specification<Type> ValidatorA = ...;
    // Omitted for brevity
    }

    Pros It keeps things nice and tidy and you don't need to create a mess with dozen of classes. Cons As I am coming from a DDD background I strongly dislike this approach since a lot of these validators wouldn't belong in one class. Additionally this would be hard to observe and still would require some kind of naming convention or similar.

  2. Classes with properties enforced through interfaces

    public class ValidatorA : IValidator
    {
    public Specification<Type> Validator => ...;
    // Omitted for brevity
    }

    Pros It would be more DDD conform and it would be way easier to observe with reflection. Cons This would obviously force the user to create a class for each validator which can lead to a lot of boilerplate.

Discussion

Now would you guys even be up to support/endorse something like that? Other implementation ideas?

bartoszlenar commented 4 years ago

Hi @TwentyFourMinutes, thank you for your post.

I've been thinking about it a lot and still not fully convinced if we need a full, separate and independent nuget package just to wrap this use case. My only concern is that the functionality you described (automatic IoC registration) could be wrapped literally within a few lines of code. Let me sleep on it and I'll get back to this within a week, maybe with the solution draft.

PS. Regarding the objects that could be detected within the assembly, I think I already have something that meets your criteria: specification holders that can be combined with translation holders. What do you think?

TwentyFourMinutes commented 4 years ago

Glad you are considering it. The specification holder is basically exactly what would be needed for this. If you want to lift some work over feel free to do so, I'd be more than happy to help out wherever I can.

bartoszlenar commented 3 years ago

Hey @TwentyFourMinutes , first of all - apologies for the terribly late reply. Over this exactly two months I've been covering other issues, including breaking changes that I plan to publish in version 2.0 just next month. Also, I had a plenty of time to think about this problem and ultimately ended up with the following solution:

https://github.com/bartoszlenar/Validot/blob/1c98809a73f352b07b2e4a0741f8d9ba99573b89/src/Validot/Factory/ValidatorFactory.cs#L104

Long story short: it's a function that scans the assemblies for ISpecificationHolder<T> implementations and creates the helper objects that contain all information about these implementations (their type, type of the specification they keep, etc.). On top of this, it can create the validator out of the holder with merely a single, parameterless call. More information here, in the "Fetching holders" section.

Having that, the integration with DI container could be wrapped within few lines of code. Like this:

public static IServiceCollection AddValidators(this IServiceCollection @this, params Assembly[] assemblies)
{
    var holders = Validator.Factory.FetchHolders(assemblies)
        .GroupBy(h => h.SpecifiedType)
        .Select(s => new
        {
            ValidatorType = s.First().ValidatorType,
            ValidatorInstance = s.First().CreateValidator()
        });

    foreach (var holder in holders)
    {
        @this.AddSingleton(holder.ValidatorType, holder.ValidatorInstance);
    }

    return @this;
}
public void ConfigureServices(IServiceCollection services)
{
    // ... registering other dependencies ...

    services.AddValidators();
}

This is described with more details in the "Dependency injection" docs section.

What do you think?

TwentyFourMinutes commented 3 years ago

No worries about the late reply, people too often expect OS library maintainers to behave as someone who is getting paid for what they are doing, even if they are not. So shoutout to you for maintaining Validot ^^.

That being said, I appreciate the helper API and I am quite certain this is a totally viable options between both ideas. So from my side I am totally fine with that and the change is greatly appreciated.

My initial uncertainty came from the fact, that a lot of people might not be to familiar with Reflection and all its little quirks. Even though this is not the most beginner library. However with this and the documentation in place, I am pretty sure this won't be an issue.

bartoszlenar commented 3 years ago

@TwentyFourMinutes I believe for now the issue could be closed. I've just published version 2.0.0 (https://github.com/bartoszlenar/Validot/releases/tag/v2.0.0) and it already contains helpers for DI containers.

Tutorial how to use them is both in the project's readme and more details (including step-by-step guidance about implementing services.AddValidators() method) is in the docs.

TwentyFourMinutes commented 3 years ago

Looks good, thanks for the effort!