jasontaylordev / NorthwindTraders

Northwind Traders is a sample application built using ASP.NET Core and Entity Framework Core.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Dealing with duplication

andrew1555 opened this issue · comments

Hi

What is the best way to deal with duplication within a query, for example say we have a currency object which each use cases needs. It feels wrong to call a query from a query, and feels wrong to repeat the code in each one too, if our logic on currency changes we would then have a lot of places to change.

Also from a performance point of view we want to cache repeated queries, so if a query contains a database call which could be cached if the query was to be ran again with different parameters, would we cache this within the query or refactor it out into some sort of repository.

Any ideas would be welcome

Thanks

Thats actually I problem I encountered too. Did not find another solution yet as it would introduce a new layer in the architecture which then makes the whole thing useless.

Hi.
There is no easy way to share logic between CQRS handlers.
What can I suggest:

  1. You can call one query from another query or command handler. And you use MediatR in this case sou all pipelines will be used again. Do you need it? Sometimes it can be a good idea but in most cases, you do not need it.
  2. Inject a QueryHandler to the other handler and run it. No duplicated code and no MediatR usage.
  3. You can create a base class for handlers and put shared logic here. If you use the base class for a couple of handlers - it works fine. But when you use the base handler for all your handlers - better to move this logic to a new abstraction layer.
  4. And the last and my favorite - add a new abstraction layer. Introduce new "Application services" and put duplicated logic from different handlers here. I do not like repositories because it is related to data access. And you can use application services from your handlers. You also can use any infrastructure (data access, cache, current user, ...) from the application services. But you can not use handlers from them.
    It works great because in most cases you do not need a lot of application services. You need them only for duplicated logic. So most of your handlers will be independent. And some of them will use shared logic via application services.

What do you think about these approaches?

Hi

Thanks for your answers, I'm not a fan on the first 3 tbh but the 4th is similar to what I've been doing. I've created a application service for each separate feature which looks like

    public class SizeConversionsService : ISizeConversionsService
    {
        private readonly IMediator _mediator;

        public SizeConversionsService(IMediator mediator)
        {
            _mediator = mediator;
        }

        public async Task<List<SizeConversionResult>> GetSizeConversion(SizingType type)
        {
            return await _mediator.Send(new GetSizeConversionsQuery { Type = type });
        }
    }

But in my case I was still using queries via mediator to benefit from the pipelines, I have a cache pipeline which saves the result in redis, so the query is in charge of cache duration etc. I then inject the ISizeConversionsService into any usecase which needs to know about sizes. With it being a service too like you say I'm able to inject in other things like current user, currency, settings etc.

My handlers would then look like this

 public class Handler : IRequestHandler<GetBuyGridQuery, BuyGrid>
  {
            private readonly ISizeConversionsService _sizeConversionsService;
  }

To solve my repeated sql call issue I added in caching into the handler. So for example I have a query which gets a product based on productId but a sub query would get the category, since we have far fewer categories than products repeatedly getting the same category from the database would add up to around 5k identical db calls an hour so I placed it around a cache

 var category = await _cacheManager.GetOrCreateAsync($"GetProductByUrlQuery:GetCategory:CategoryId_{productResult.Product.CategoryId}", 
                        () => GetCategory(productResult.Product.CategoryId), 3600);   

This way repeated calls to the usecase for different products wouldn't result in the same sql query to get the same category, if that makes sense. I could have solved it using a application service instead and then caching would be shared if any other usecase needed it also, but wasn't sure which solution was better.

What do you think?

I also came across this link https://lostechies.com/jimmybogard/2016/12/12/dealing-with-duplication-in-mediatr-handlers/ which offers some alternatives also

Thanks

Hi

So you wrap mediator calls by the application services. It is item No1 in my list of suggestions.
Why do you need this abstraction? I believe you can inject a mediator into the handler.
Less code => less bugs (and abstraction layers).

And I totally agree with Jimmy about his article. How to deal with duplicated logic - it depends...
There are too many different kinds of logic: data access, domain, or application logic.
But if I understand correctly we are talking about the application logic.

PS: you can make your handlers internal because they are not a part of the Application Layer contract (Contract = Commands/Queries + DTO).

PPS: When you use GetOrCreateAsync it does not guarantee that GetCategory will be called exactly once.

Great thank you for your reply, I was under the impression injecting mediator into the handlers and calling other handlers would go against the recommendation, do you think it's no different then to using a service class which then calls mediator. The thinking behind it was it was then a single location which calls the query handler everywhere else uses the service so if I needed to change how it's called it would be a single place.

Do you have any working examples of reusing queries in a clean way. Jimmy talks about methods in the dbContext but then how would do this with dapper like data calls.

Thanks for the tip on internal keyword will give that a go.

Thank you for your interest in this project. This repository has been archived and is no longer actively maintained or supported. We appreciate your understanding. Feel free to explore the codebase and adapt it to your own needs if it serves as a useful reference. If you have any further questions or concerns, please refer to the README for more information.