Sieve icon indicating copy to clipboard operation
Sieve copied to clipboard

Trying to use Sieve with MediatR but Dependency Injection not working

Open yehia2amer opened this issue 5 years ago • 10 comments

Problem Trying to use Sieve with MediatR & CQRS, so that sieveModel is passed from Controller to Query class then handled by QueryHandler

Expected Behaviour ISieveProcessor should be injected properly in QueryHandler

Current Behaviour Failed to find ISieveProcessor in QueryHandler although it is injected in Startup.cs as below

// Add Sieve
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));

Exception Message

Error constructing handler for request of type
MediatR.IRequestHandler 2[Northwind.Application.Products.Queries.GetAllProducts.GetAllProductsQuery,Northwind.Application.Products.Queries.GetAllProducts.ProductsListViewModel].
Register your handlers with the container. See the samples in GitHub for examples.

StackTrace

   at MediatR.Internal.RequestHandlerBase.GetHandler[THandler](ServiceFactory factory)
   at MediatR.Internal.RequestHandlerWrapperImpl`2.<>c__DisplayClass0_0.<Handle>g__Handler|0()
   at MediatR.Pipeline.RequestPostProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
   at MediatR.Pipeline.RequestPreProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
   at Northwind.Application.Infrastructure.RequestPerformanceBehaviour`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next) in C:\Users\amery\Desktop\MediatR CQRS & Sieve\NorthwindTraders-master\Northwind.Application\Infrastructure\RequestPerformanceBehaviour.cs:line 25
   at MediatR.Pipeline.RequestPreProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
   at Northwind.WebUI.Controllers.ProductsController.GetAll(SieveModel sieveModel) in C:\Users\amery\Desktop\MediatR CQRS & Sieve\NorthwindTraders-master\Northwind.WebUI\Controllers\ProductsController.cs:line 21
   at lambda_method(Closure , Object )
   at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()
   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync()

Example I Created This Gist that replicate the issue here: https://gist.github.com/yehia2amer/869984e1837b4683767035964aac680d This is based on NorthwindTraders Sample.

Full Project Example https://github.com/yehia2amer/NorthwindTraders_MediatR_Sieve

yehia2amer avatar Feb 28 '19 14:02 yehia2amer

The issue is DI is unable to resolve service for type ISieveCustomSortMethods and ISieveCustomFilterMethods while attempting to activate ApplicationSieveProcessor. The latter should be (as per your gist):

public class ApplicationSieveProcessor : SieveProcessor
{
	public ApplicationSieveProcessor(Microsoft.Extensions.Options.IOptions<SieveOptions> options) : base(options)
	{
	}

	protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
	{
		mapper.Property<Product>(e => e.ProductId).CanSort().CanFilter();
		mapper.Property<Product>(e => e.ProductName).CanSort().CanFilter();

		return mapper;
	}
}

Unless you require custom sort/filter methods.

daviddesmet avatar May 23 '19 22:05 daviddesmet

I am trying to implement the Sieve package to my project but my skill does not let me to this... I have tried like you, but you have a little bit other architecture.

If you can help me will be great.

This is link to my problem: https://stackoverflow.com/questions/72757353/cqrs-mediatr-query-filtering-sorting-and-paging-with-3party-sieve-package

Lukash88 avatar Jun 28 '22 20:06 Lukash88

@Lukash88

You just need to register your Sieve processor as you normally do:

services.AddScoped<ISieveProcessor, MyCustomSieveProcessor>();

Then inject in your ctor handler, for example, this is my handler:

public class GetSessionsQueryHandler : IRequestHandler<GetSessionsQuery, PagedResponse<SessionResponse>>
{
    private readonly IDbContext _db;
    private readonly IMapper _mapper;
    private readonly ISieveProcessor _sieve;
    private readonly ILogger<GetSessionsQueryHandler> _logger;

    public GetSessionsQueryHandler(IDbContext db, IMapper mapper, ISieveProcessor sieve, ILogger<GetSessionsQueryHandler> logger)
    {
        _db = db;
        _mapper = mapper;
        _sieve = sieve;
        _logger = logger;
    }

    public async Task<PagedResponse<SessionResponse>> Handle(GetSessionsQuery request, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Getting a list of Sessions");
        // use Sieve here as you normally do, I'm using an extension method I created for my own purpose
        return await _db.Set<Session>().AsNoTracking().ToPagedAsync<Session, SessionResponse>(_mapper, _sieve, request.Model);
    }
}

And the corresponding controller which calls the above handler:

public async Task<IActionResult> GetSessions([FromQuery] SieveModel model) => Ok(await Mediator.Send(new GetSessionsQuery(model)));

daviddesmet avatar Jun 28 '22 21:06 daviddesmet

@daviddesmet

Dankjewel :)

As long I try to dive in this implementation as more problems I am facing... I do want to give up but it is getting more difficult.

Is PagedResponse<T> using XAct.Messages NuGet?

If yes I got compability problem: Package 'CommonServiceLocator 1.3.0' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8' instead of the project target framework 'net5.0'. This package may not be fully compatible with your project

also other problems:

  • S0311 The type 'ApplicationServices.API.Domain.Product.GetProductsRequest' cannot be used as type parameter 'TRequest' in the generic type or method 'IRequestHandler<TRequest, TResponse>'. There is no implicit reference conversion from 'ApplicationServices.API.Domain.Product.GetProductsRequest' to 'MediatR.IRequest<XAct.Messages.PagedResponse<FlowerShop.ApplicationServices.API.Domain.Product.GetProductsResponse>>'. .ApplicationServices C:...ApplicationServices\API\Handlers\Product\GetProductsHandler.cs 16 Active

  • Cannot implicitly convert type '.ApplicationServices.API.Domain.Product.GetProductsResponse' to 'XAct.Messages.PagedResponse<.ApplicationServices.API.Domain.Product.GetProductsResponse>' .ApplicationServices C:.ApplicationServices\API\Handlers\Product\GetProductsHandler.cs 38 Active

  • The type arguments for method 'ISieveProcessor<SieveModel, FilterTerm, SortTerm>.Apply<TEntity>(SieveModel, IQueryable<TEntity>, object[], bool, bool, bool)' cannot be inferred from the usage. Try specifying the type arguments explicitly. FlowerShop.ApplicationServices C:.ApplicationServices\API\Handlers\Product\GetProductsHandler.cs 45 Active

  • Cannot implicitly convert type '.ApplicationServices.API.Domain.Product.GetProductsResponse' to 'XAct.Messages.PagedResponse<.ApplicationServices.API.Domain.Product.GetProductsResponse>' .ApplicationServices C:.ApplicationServices\API\Handlers\Product\GetProductsHandler.cs 54 Active

    public class GetProductsHandler : IRequestHandler<GetProductsRequest, PagedResponse<GetProductsResponse>> // line 16
    {
        private readonly IMapper mapper;
        private readonly IQueryExecutor queryExecutor;
        private readonly ISieveProcessor sieveProcessor;

        public GetProductsHandler(IMapper mapper, IQueryExecutor queryExecutor, ISieveProcessor sieveProcessor)
        {
            this.mapper = mapper;
            this.queryExecutor = queryExecutor;
            this.sieveProcessor = sieveProcessor;
        }

        public async Task<PagedResponse<GetProductsResponse>> Handle(GetProductsRequest request, CancellationToken cancellationToken)
        {
            var query = new GetProductsQuery()
            {
                Name = request.Name
            };
            var products = await this.queryExecutor.Execute(query);
            if (products == null)
            {
                return new GetProductsResponse()   // line 38 
                {
                    Error = new ErrorModel(ErrorType.NotFound)
                };
            }


            var sortedFilteredPaginatedProducts = this.sieveProcessor.Apply(request.SieveModel, products);  // line 45

            var mappedProducts = this.mapper.Map<List<Domain.Models.ProductDTO>>(sortedFilteredPaginatedProducts);

            var response = new GetProductsResponse()
            {
                Data = mappedProducts
            };

            return response;   // line 54
        }
    }
}
  public class ErrorModel
    {
        public ErrorModel(string error)
        {
            this.Error = error;
        }

        public string Error { get; set; }
    }
public abstract class ApiControllerBase : ControllerBase
    {

        private readonly IMediator mediator;

        public ApiControllerBase(IMediator mediator)
        {
            this.mediator = mediator;
        }

        protected async Task<IActionResult> HandleRequest<TRequest, TResponse>(TRequest request)
            where TRequest : IRequest<TResponse>
            where TResponse : ErrorResponseBase
        {
            if (!this.ModelState.IsValid)
            {
                return this.BadRequest(
                    this.ModelState
                    .Where(x => x.Value.Errors.Any())
                    .Select(x => new { property = x.Key, errors = x.Value.Errors}));
            }

            var userName = this.User.FindFirstValue(ClaimTypes.Name);

            var response = await this.mediator.Send(request);
            if (response.Error != null)
            {
                return this.ErrorResponse(response.Error);
            }     
            
                return this.Ok(response);
        }

        private IActionResult ErrorResponse(ErrorModel errorModel)
        {
            var httpCode = GetHttpStatusCode(errorModel.Error);
            return StatusCode((int)httpCode, errorModel);
        }

        private static HttpStatusCode GetHttpStatusCode(string errorType)
        {
            return errorType switch
            {
                ErrorType.NotFound => HttpStatusCode.NotFound,
                ErrorType.InternalServerError => HttpStatusCode.InternalServerError,
                ErrorType.Unauthorized => HttpStatusCode.Unauthorized,
                ErrorType.RequestTooLarge => HttpStatusCode.RequestEntityTooLarge,
                ErrorType.UnsupportedMediaType => HttpStatusCode.UnsupportedMediaType,
                ErrorType.UnsupportedMethod => HttpStatusCode.MethodNotAllowed,
                ErrorType.TooManyRequests => (HttpStatusCode)429,
                ErrorType.Conflict => HttpStatusCode.Conflict,
                _ => HttpStatusCode.BadRequest,
            };
        }
    }
  public class ApplicationSieveProcessor : SieveProcessor
    {
        public ApplicationSieveProcessor(
            IOptions<SieveOptions> options,
            ISieveCustomSortMethods customSortMethods,
            ISieveCustomFilterMethods customFilterMethods)
            : base(options, customSortMethods, customFilterMethods)
            {
            }

        protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
        {
            return mapper.ApplyConfigurationsFromAssembly(typeof(ApplicationSieveProcessor).Assembly);
        }
    }
 public class SieveConfigurationForProduct : ISieveConfiguration
    {
        public void Configure(SievePropertyMapper mapper)
        {
            mapper.Property<Product>(p => p.Name)
                .CanSort()
                .CanFilter();

            mapper.Property<Product>(p => p.Price)
                .CanSort()
                .CanFilter();

            mapper.Property<Product>(p => p.Category)
                .CanSort()
                .CanFilter();

            //return mapper;
        }
    }

If you have some capacity to help will be great.

Alvast bedankt!

Mvg, Lukas

Lukash88 avatar Jun 29 '22 18:06 Lukash88

@Lukash88

PagedResponse is a custom-made class I use for paginated responses which is returned by the handler to the controller and then to the front-end:

/// <summary>
/// A Paged Response DTO (Data Transfer Object).
/// </summary>
/// <typeparam name="TResponse">The type of the response.</typeparam>
public class PagedResponse<TResponse> where TResponse : class
{
    /// <summary>
    /// Gets or sets the current page.
    /// </summary>
    /// <value>The current page.</value>
    public int CurrentPage { get; set; }

    /// <summary>
    /// Gets or sets the size of the page.
    /// </summary>
    /// <value>The size of the page.</value>
    public int PageSize { get; set; }

    /// <summary>
    /// Gets or sets the page count.
    /// </summary>
    /// <value>The page count.</value>
    public int PageCount { get; set; }

    /// <summary>
    /// Gets or sets the row count.
    /// </summary>
    /// <value>The row count.</value>
    public long RowCount { get; set; }

    /// <summary>
    /// Gets or sets the results.
    /// </summary>
    /// <value>The results.</value>
    public IList<TResponse> Results { get; set; } = new List<TResponse>();
}

The above class is returned by this call in the handler I shared previously:

return await _db.Set<Session>().AsNoTracking().ToPagedAsync<Session, SessionResponse>(_mapper, _sieve, request.Model);

I defined as extension method:

public static async Task<PagedResponse<TResponse>> ToPagedAsync<TEntity, TResponse>(this IQueryable<TEntity> query, IMapper mapper, ISieveProcessor sieve, SieveModel model = null, CancellationToken cancellationToken = default) where TResponse : class
{
    var page = model?.Page ?? 1;
    var pageSize = model?.PageSize ?? 50;

    if (model != null)
        query = sieve.Apply(model, query, applyPagination: false);

    var rowCount = await query.CountAsync(cancellationToken);
    var pageCount = (int)Math.Ceiling((double)rowCount / pageSize);

    var skip = (page - 1) * pageSize;
    var pagedQuery = query.Skip(skip).Take(pageSize);

    var response = new PagedResponse<TResponse>
    {
        CurrentPage = page,
        PageSize = pageSize,
        PageCount = pageCount,
        RowCount = rowCount
    };
    response.Results = await pagedQuery.ProjectTo<TResponse>(mapper.ConfigurationProvider).ToListAsync(cancellationToken);

    return response;
}

daviddesmet avatar Jun 29 '22 20:06 daviddesmet

@daviddesmet

So you have created 2 classes:

  • PagedResponse<TResponse> where TResponse : class
  • PagedResponseExtension - with extension method?

Lukash88 avatar Jul 01 '22 18:07 Lukash88

@Lukash88

It was all really for maintainability and code reuse. In the end, it depends on how you want to architect your solution.

daviddesmet avatar Jul 01 '22 19:07 daviddesmet

@daviddesmet

Thank you for your effort and guidance but I cannot go further with Sieve because I am facing a few errors which I cannot still resolve due to my skill gap/lack.

Lukash88 avatar Jul 01 '22 20:07 Lukash88

@Lukash88

Is this repository where are you working on? https://github.com/Lukash88/FlowerShop If so, you can create a branch over there and I can have a look.

daviddesmet avatar Jul 01 '22 20:07 daviddesmet

@daviddesmet

https://github.com/Lukash88/FlowerShop/tree/Sieve

Any help is priceless.

Lukash88 avatar Jul 01 '22 21:07 Lukash88