dotnetjunkie / solidservices

Reference architecture application for .NET that demonstrates how to build highly maintainable web services.
MIT License
194 stars 24 forks source link

Comparison with MediatR library #34

Open bluezebra opened 3 years ago

bluezebra commented 3 years ago

Hello Steven

Please could you check my reasoning.

I'm trying to compare the popular MediatR library with your separate Command and Query Mediator pattern. My colleagues are keen on adopting MediatR and I want to understand the subtle differences.

CQS separately handles Commands and Queries, with an optional Mediator for Queries to reduce dependencies as they are usually more numerous. Commands are usually more isolated, so less issue with over injection, thus do not require the additional Mediator which would obfuscate dependencies. Having both Command and Query dependencies abstracted to a Mediator might be good for decoupling, but could be considered an antipattern in hiding dependencies.

MediatR has less distinction between Command and Queries. Confusingly both are called Requests, and does not enforce void/unit on Commands. However some Commands might be permitted to return a Result type if desired for flexibly handling success or failure in a more functional way. Relying on a library instead of rolling your own is a dependency in itself however it might be easier for other developers to understand.

Then there is .Net Core’s FromServices attribute for injecting directly to Actions and relieving the Constructor overloading problem and perhaps pointing towards the simpler explicit Query injection. Thus having the freedom to roll your own is more flexible.

Thanks for an inspiring DI book, articles and comment responses.

Regards

Stephen

dotnetjunkie commented 3 years ago

CQS separately handles Commands and Queries, with an optional Mediator for Queries to reduce dependencies as they are usually more numerous. Commands are usually more isolated, so less issue with over injection, thus do not require the additional Mediator which would obfuscate dependencies.

I think you are more specifically talking about CQRS, as CQS is a more general concept that talks about separating command methods from query methods. But what you're stating here is exactly my view on this topic, as I expressed in the past. But I'm not sure there is an overall consensus that this is the way to apply CQRS.

Relying on a library instead of rolling your own is a dependency in itself however it might be easier for other developers to understand.

I am myself reluctant in using external libraries for parts that I consider core parts of my architecture. That's why I myself don't advice the use of MediatR. I want to be in complete control over the core interfaces. I expressed this on the MediatR forum.

That said, using MediatR is not a bad thing, but just make sure that it's design perfectly aligns with what's best for your application or organization. If not, it's better to define the interfaces yourself. For instance, if you are seeing that command often need different cross-cutting concerns compared to queries, separating them in terms of interfaces might make more sense, as I expressed on the MediatR forum.

In this case, be careful of the argument that this means you are redesigning MediatR yourself. This will hardly be the case. That's because 1. If you take a look in MediatR, you see there isn't a whole lot of code to begin with. It's mostly interfaces, and 2. a lot of the code (and its complexity) stems from the MediatR design decision to allow to be applicable to every DI Container out there (such as the very limited built-in .NET Core container). This is why recent versions moved away from decorators, and now use pipelines. This design seems solely chosen because some DI Containers don't have (good) support for decorators.

So this is something else that should influence your decision. If your DI Container does have good support for decorators, a design that uses decorators is typically simpler and easier to comprehend and, therefore, makes more sense. But again, this is a decision that should be up to the architect, not enforced by an external library.

Of course, it can make a lot of sense to reuse a well-known solution compared to a custom-made solution. For instance, you would typically use log4net or Serilog instead of creating your own library. Many developers are already familiar with these libraries. But I'm not sure that this argument holds for MediatR. MediatR is basically a set of interfaces, and whether or not they are placed in an external library or defined by the application itself, it doesn't change the fact that you need to understand why the purpose and benefit is of these interfaces. It's more an issue of understanding the underlying design principles. My experience is that developers who have worked with MediatR-like abstractions, have no problem in working in applications where command and query interfaces are separated—or the other way around.

Then there is .Net Core’s FromServices attribute for injecting directly to Actions and relieving the Constructor overloading problem and perhaps pointing towards the simpler explicit Query injection. Thus having the freedom to roll your own is more flexible.

This is a completely different topic :-). ASP.NET Core's FromServices is just Method Injection as described in my book. In the book I describe the downsides of Method Injection. I also added information about FromServices in the Simple Injector documentation that explains why they are a bad idea. Instead, make your controllers small. Or perhaps even better, don't write controllers at all.

bluezebra commented 3 years ago

Hi Steven

Thanks for your excellent response, there is a rich seam of great comments to absorb and I feel I have a good understanding now. It'll make for an interesting debate at work.

It helps to have a label and I wasn't sure how to refer to your approach, that why I wrongly used the term CQS.

Our common approach has been to use .Net Core's built in IoC. Now I realise that MS.DI has no built-in support for Decorators and implementing a PoC looks like a challenge unless I use a different IoC. So to take the custom approach we might need to introduce a better IoC container. From this perspective it harder for me to make the argument to roll our own over using MediatR.

I should also do some more reading in how Pipeline Behaviours differ from Decorators.

Regards

Stephen

bluezebra commented 3 years ago

Are you saying that MediatR has the same 'decorated' behaviour for Commands and Queries? But instead we often want different aspects for Commands, audits, transactions etc to Queries.

MediatR has an advantage in .Net Core with the limits of the built in MS.DI IoC container. Extra effort is required for 'lazy' devs to learn about IoC and DI ;) So I assume MediatR is doing some magic wiring taking over the role of the IoC, is this like a ServiceLocator?

I'm now trying to wire up a proof of concept AoP example using SimpleInjector in an Azure Serverless Function App. Is there no way of hooking into an Application Start class or do I have to call the DIContainer from every function?

I've been following your advice here: https://github.com/simpleinjector/SimpleInjector/issues/536 https://stackoverflow.com/questions/61752980/di-in-azure-function-using-simple-injector

It would be great to have a full code example of AoP with SimpleInjector in a Function App similar to the book code download.

dotnetjunkie commented 3 years ago

TIP: The discussion in #30 might also be of value to you.

dotnetjunkie commented 3 years ago

From this perspective it harder for me to make the argument to roll our own over using MediatR.

As I said, you should pick the tools, patterns, and practices that align best with your organization. If this means picking MS.DI and MediatR, by all means, please do. I certainly won't judge.

TIP: Document these architectural decisions for future developers and architects.

Are you saying that MediatR has the same 'decorated' behaviour for Commands and Queries? But instead we often want different aspects for Commands, audits, transactions etc to Queries.

No, I'm not saying that, because I don't know the MediatR code base that well. You will have to investigate how to differentiate your cross-cutting concerns over commands and queries when using MediatR.

MediatR has an advantage in .Net Core with the limits of the built in MS.DI IoC container.

But it does so at the cost of (IMO) design. In earlier releases, MediatR used the decorator pattern (IMO clearly indicating that Jimmy preferred the decorator design pattern over the chain of responsibility pattern), but only stopped doing that after he wanted to support 'less mature' DI Containers.

So I assume MediatR is doing some magic wiring taking over the role of the IoC

There is little "magic wiring" going on in MediatR. It's repo contains documentation that shows you how to register your components in your favorite DI Container.

is this like a ServiceLocator?

It's certainly true that the Mediator class (and its dependencies) take a dependency on a Service Locator-like interface (Func<Type, object>). Whether or not this is actually an implementation of the Service Locator anti-pattern depends on whether you consider the Mediator implementation to be part of your Composition Root or not.

I would say that, since MediatR is the framework that delegates your code, I would consider it part of the Composition Root, even though, from a assembly-dependency perspective it's officially not (as the Mediator implementation lives in the assembly that your complete application depends upon.)

dotnetjunkie commented 3 years ago

Is there no way of hooking into an Application Start class or do I have to call the DI Container from every function?

As mentioned by @CasperWSchmidt in 839, you can inject dependencies into your Azure function class.

This allows you to inject the IMediator (or similar interface) into your function:

public record YourFunction(IMediator mediator) // Excuse my C# 9.0
{
    [FunctionName("YourFunctionName")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "v1/yourFunctionName")]
        HttpRequest req)
    {
        await mediator.Execute(new MyCommand { ... });
    }
}

In the Startup of your application, just add the Simple Injector-configured Mediator class into the IServiceCollection:

[assembly: FunctionsStartup(typeof(Startup))]
namespace YourFunction.DI
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var configuration = builder.GetContext().Configuration;

            var simpleInjectorContainer = Setup(configuration);

            builder.Services.AddSingleton<IMediator>(
                new Mediator(simpleInjectorContainer.GetInstance));
        }

        // Could be non-static
        public static Container Setup(IConfiguration configuration)
        {
            //This is where the container is set up and verified
        }
    }
}

WARNING: The Mediator implementation does not start a new Simple Injector Scope around the resolution and execution of any handlers, which means that Simple Injector will throw an exception when you try to resolve a Scoped instance. To solve this, you will have to create a custom Mediator implementation (try creating a decorator) or create your own mediator abstraction, whose implementation forwards to MediatR's IMediator.

bluezebra commented 3 years ago

For my PoC I've injected the Startup defined SI Container into my function's constructor and resolved each dependency inside the function. This works quite well but feels awkward compared to Constructor Injection and doesn't appear testable. e.g.

var createTicketService = _container.GetInstance<ICommandService<CreateTicket>>();
var _queryTickets = _container.GetInstance<IQueryTickets>();

I can't see how to implement your last example with the IMediator wrapper around the SimpleInjectorContainer Instance. And am worried I'm making it too difficult by implementing an external IoC in an Azure Function App.

Function Apps being relatively new, still don't have good support for using external IoC Containers other than MS.DI and I couldn't find much written on this, nor an approach that works as nice as in a regular WebAPI app.

So my PoC doesn't grow too big a task, I'm going to demonstrate the custom approach with SI in a WebAPI application and show the limitations of DI in Function Apps.

Our architect, has fallen in love with Function Apps! I'm thinking MediatR might work really well for a less complex domain when using an Azure Function App. However for a more complex domain I'm favouring the custom approach you have shown, built on a WebAPI AppService. The custom approach allows nuanced decisions, to handle Commands AoP separately, choice of simple single interfaces or a Mediator for Queries and combined with a separate process for composing Domain Events.

Thanks once again

CasperWSchmidt commented 3 years ago

@bluezebra I'm not sure what you mean by "awkward" and not being testable. If you do as suggested in #839 and let your function be a humble object all it does is resolve the handler and call it which is two lines of code with no logic that needs testing. All your business logic should be in the handler and perhaps some decorators 😊 which are all testable. We have this exact design for multiple azure functions already in production.

@dotnetjunkie I had a quick look at the ASP.NET extension with auto-bridging between Microsoft DI and Simple Injector, but couldn't figure out how to do a similar thing for azure functions, which would be really cool as that means the functions can be the handlers themselves (at least for some of the trigger types like http)

bluezebra commented 3 years ago

I just meant the functions themselves were not testable or I don't know how to, for example testing for returning http codes like bad request, success etc. Not much info I could find, most show only Ms.di. thanks for looking into this. It would be a great issue to solve.

CasperWSchmidt commented 3 years ago

IMHO it shouldn't be necessary to have tests if the function simply looks like this

//get or create command from trigger object
var handler = container.GetInstance<ICommandHandler<SomeCommand>>();
var isSuccessful = handler.handle(command);
//you could also have the handler return a result object with error description in case something is wrong 
if (isSuccessful)
    return new OkResult();

return new BadRequestResult();

There's not really any business logic in there that needs testing. If you insist, you can depend on IContainer and simply mock/stub the GetInstance method used, but the hard part is perhaps to mock the HttpRequest object (your function can also have the runtime map that to your own command class which is easier to test)

dotnetjunkie commented 3 years ago

I just meant the functions themselves were not testable or I don't know how to, for example testing for returning http codes like bad request, success etc. Not much info I could find, most show only Ms.di. thanks for looking into this. It would be a great issue to solve.

@bluezebra, whenever possible, prevent implementing returning of custom HTTP codes inside your Azure functions. Instead, try an approach as I demonstrate in this repository. That is: try to implement the returning of HTTP error codes in a maintainable fashion, where you have a small piece of infrastructure (e.g. a decorator, extension method, or the like) that maps exceptions or command results to HTTP error codes.

I had a quick look at the ASP.NET extension with auto-bridging between Microsoft DI and Simple Injector, but couldn't figure out how to do a similar thing for azure functions, which would be really cool as that means the functions can be the handlers themselves (at least for some of the trigger types like http)

@CasperWSchmidt if you means by "ASP.NET extension with auto-bridging" the Simple Injector extension packages for ASP.NET Core, they rely on the existence of seams/interception points, usch as IControllerActivator, IViewModelActivator, and ITagHelperActivator. Those extension points replace ASP.NET Core's default implementations with ones that delegate the request to Simple Injector. Without the existence of similar interfaces for Azure Functions... integrating can be quite daunting.