lucabriguglia / OpenCQRS

.NET Standard framework to create simple and clean design. Advanced features for DDD, CQRS and Event Sourcing.
Apache License 2.0
3 stars 115 forks source link

IPolicy interface that allows business-rules enforcement #90

Closed kwentinn closed 4 years ago

kwentinn commented 4 years ago

Hi guys, I'm currently working on a side-project to check out the possibilities of DDD, CQRS and Event Sourcing. It's a simple scheduling program. I've introduced an IPolicy interface that aims at checking some more stuff that the aggregate root cannot (eg. query a read model via some other read model repository). It simply defines a single method CanExecuteAsync that the deriving classes must implement.

Here's a quick look at the code :

public interface IPolicy<IDomainCommand, IAggregateRoot>
    where IDomainCommand : Kledex.Domain.IDomainCommand
    where IAggregateRoot : Kledex.Domain.IAggregateRoot
{
    Task<PolicyResult> CanExecuteAsync(IDomainCommand command, IAggregateRoot aggregateRoot);
}

Say we want to register a user, but we want to make sure we don't have 2 users with the same email address. We define a RegisterUserPolicy class implementing IPolicy<RegisterUser, User> to enforce the business rule :

public async Task<PolicyResult> CanExecuteAsync(RegisterUser command, User aggregateRoot) {
    var userExists = _userRepository.DoesUserExistWithEmail(aggregateRoot.Email);
    if (userExists) {
        return await Task.FromResult(new PolicyResult {
            CanExecute = false,
            Reason = "A user exists with the same e-mail address."
        });
    }
    return await Task.FromResult(new PolicyResult { CanExecute = true });
}

Here we handle the RegisterUser command, making use of the policy :

public async Task<CommandResponse> HandleAsync(RegisterUser command)
{
    var user = new User(command.AggregateRootId, command.Firstname, command.Lastname, command.Email, command.TimeZoneCode);

    var policyResult = await _policy.CanExecuteAsync(command, user);
    if (!policyResult.CanExecute)
    {
        throw new ApplicationException($"Cannot register the user. Policy error: {policyResult.Reason}");
    }

    await _repository.SaveAsync(user);

    return new CommandResponse
    {
        Events = user.Events,
        Result = user.Id
    };
}

I was just wondering what you guys think about this IPolicy interface. How would you handle such a business rule ? Would you add it directly inside the aggregate ?

It's working fine, but I'm worried about the lack of synchronisation mechanism between the Read & Write models... If the read model is out of sync, the business rule will fail and may lead to some data inconsistencies.

TL;DR I've created a simple interface and I'm looking for feedback :)

lucabriguglia commented 4 years ago

Hi @kwentinn we already have something similar. There is a validation service that can called automatically to validate the commands before they are sent to the command handler.

lucabriguglia commented 4 years ago

Documentation: https://lucabriguglia.github.io/Kledex/Validation

lucabriguglia commented 4 years ago

Regarding the business rules, I add in the aggregate only the rules for the invariants and everything else into a separate validator.

quentin-villevieille commented 4 years ago

I guess I didn't really understand the point of your validation service ! I'm gonna shift this implementation into a validation service and that should do the trick. Thanks for your feedback @lucabriguglia :)

lucabriguglia commented 4 years ago

No problem, give me a shout if you don't want to use Fluent Validation and you want to create a custom validation provider and need help ;-)

lucabriguglia commented 4 years ago

Closing as it seems resolved.

kwentinn commented 4 years ago

I finished working on it and it works fine ! Here's how I did it :

First, I redesigned the IPolicy interface

public interface IPolicy<ICommand>
    where ICommand : Kledex.Commands.ICommand
{
    PolicyResult CanExecute(ICommand command);
    Task<PolicyResult> CanExecuteAsync(ICommand command);
}

PolicyResult is just a simple class

public class PolicyResult
{
    public bool CanExecute { get; set; }
    public string Reason { get; set; }
}

Then I defined a new PolicyValidationProvider class :

public class PolicyValidationProvider : IValidationProvider
{
    private readonly IHandlerResolver _handlerResolver;

    public PolicyValidationProvider(IHandlerResolver handlerResolver)
    {
        _handlerResolver = handlerResolver;
    }

    public async Task<ValidationResponse> ValidateAsync(ICommand command)
    {
        var validator = _handlerResolver.ResolveHandler(command, typeof(IPolicy<>));

        var validatorType = validator.GetType();
        var cmdType = command.GetType();

        var validateMethod = validator.GetType().GetMethod("CanExecuteAsync", new[] { command.GetType() });
        var policyResult = await (Task<PolicyResult>)validateMethod.Invoke(validator, new object[] { command });

        return BuildValidationResponse(policyResult);
    }

    public ValidationResponse Validate(ICommand command)
    {
        var validator = _handlerResolver.ResolveHandler(command, typeof(IPolicy<>));
        var validateMethod = validator.GetType().GetMethod("Validate", new[] { command.GetType() });
        var policyResult = (PolicyResult)validateMethod.Invoke(validator, new object[] { command });

        return BuildValidationResponse(policyResult);
    }

    private static ValidationResponse BuildValidationResponse(PolicyResult policyResult)
    {
        var errors = new List<ValidationError>();
        if (!policyResult.CanExecute)
        {
            errors.Add(new ValidationError { ErrorMessage = policyResult.Reason });
        }
        return new ValidationResponse { Errors = errors };
    }
}

I create a static class for Kledex extension like so

public static class ServiceCollectionExtensions
{
    public static IKledexServiceBuilder AddPolicyValidationProvider(this IKledexServiceBuilder builder)
    {
        builder.Services.AddScoped<IValidationProvider, PolicyValidationProvider>();

        return builder;
    }
}

Finally I just need to call the validation provider in the Startup.cs file via service.AddKledex().AddPolicyValidationProvider(); and that's it !

kwentinn commented 4 years ago

Now, new question ! What if I try to use 2 validation providers at the same time on the same commands ? My guess is that the resolver doesn't know what to do and crashes because I believe you cannot have multiple interfaces having different implementations with aspnetcore DI, am I right ?

lucabriguglia commented 4 years ago

@kwentinn excellent! Regarding your new question, it is potentially possible to resolve at runtime multiple implementations of the same interface (like event handlers) but what's the reason for having multiple providers?

quentin-villevieille commented 4 years ago

Well, let's say I'd like to check the command's parameters first (with the FluentValidationProvider), and then enforce a business rule with a policy like a user e-mail address must be unique (with the PolicyValidationProvider).

I might as well use FluentValidation inside my Policy class. But what I'd like to do is really separate the field or value validation and the business rules which can't be inside the aggregate.

lucabriguglia commented 4 years ago

I get your point, but you can do it with FluentValidation as I did here: https://github.com/lucabriguglia/Weapsy/blob/vnext/src/Weapsy.Domain.Validators/Sites/CreateSiteValidator.cs

But, this is an interesting concept. Let's say I don't want my aggregate to be polluted with a lot of business logic, I could have one validator for the command and another one for the aggregate itself to check the invariants. In this case I might have two validators but one provider.

kwentinn commented 4 years ago

I guess we can close this issue now as it was more a discussion than a real issue. And your solution is quite elegant, so it's fine by me.