jbogard / MediatR

Simple, unambitious mediator implementation in .NET
Apache License 2.0
11.19k stars 1.18k forks source link

Example of generic IRequestHandler #521

Closed cirrusone closed 2 years ago

cirrusone commented 4 years ago

Are there any examples to show how to make a generic IRequestHandler?

I can obviously add lots of interfaces such as

public class MainWindowViewModel : BindableBase
,IRequestHandler<MainViewModelPing1, string>
,IRequestHandler<MainViewModelPing2, string>
,IRequestHandler<MainViewModelPing3, SomeOtherClass>

but cannot find any examples of a generic handler to handle different requests and responses in the same handler. Is something like the following possible?

public class MainViewModelPing : IRequest<T>
    {
        public string Message { get; set; }
    public T Data { get; set; }
    }

which would handle something like the following

    public class MainViewModelPing : IRequest<string>
    {
        public string Message { get; set; }
    }

    public class MainViewModelPing : IRequest<string>
    {
        public string Message { get; set; }
    public SomeClass Data { get; set; }
    }

    public class MainViewModelPing : IRequest<SomeOtherClass>
    {
        public string Message { get; set; }
    public SomeClass Data { get; set; }
    }
cirrusone commented 4 years ago

Is this a reasonable way to use Mediatr without any foreseeable issues?

App DI:

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    // MediatR DI dryloc
    var container = containerRegistry.GetContainer();
    container.RegisterDelegate<ServiceFactory>(r => r.Resolve);
    container.RegisterMany(new[] { typeof(IMediator).GetAssembly(), typeof(MainViewModelPing).GetAssembly() }, Registrator.Interfaces);

}

GenericRequest and GenericResponse:

public class MainViewModelPing : IRequest<PingResponse>
{
  public PingRequest Request { get; set; }
}

public abstract class PingRequest
{
}
public class GenericRequest<T> : PingRequest
{
  public string Message { get; set; }
  public T Data { get; set; }
}

public abstract class PingResponse
{
}
public class GenericResponse<T> : PingResponse
{
  public string Message { get; set; }
  public T Data { get; set; }
}

Generic Handler attached to MainViewModel:

public class MainWindowViewModel : BindableBase, IRequestHandler<MainViewModelPing, PingResponse>
{

        public Task<PingResponse> Handle(MainViewModelPing request, CancellationToken cancellationToken)
        {   
            // Test types and message to filter
            if(request.Request is GenericRequest<string> stringRequest)
            {
                if(stringRequest.Message.Equals("SomeVal"))
                {
                    // Perform action here
                }
            }
            else if (request.Request is GenericRequest<int> intRequest)
            {
                if(stringRequest.Message.Equals("SomeOtherVal"))
                {
                    // Perform action here
                }
            }

            // Example response
            var genericResponse = new GenericResponse<int> { Message = "It Works", Data = 42 };
            return Task.FromResult(genericResponse as PingResponse);
        }
}

Example request:

        private async void MediatrTest()
        {
            var genericRequest = new GenericRequest<string> { Message = "SomeVal", Data = "" };
            var response = await _mediator.Send(new MainViewModelPing() { Request = genericRequest });
        }
no1melman commented 4 years ago

I would say, making it more generic isn't an ideal use case.

The idea is that you have 1 request that maps to 1 handler because this follows SRP. It becomes easier to understand what the code is trying to achieve.

I would say that if you just want T to be stored in a database, handled the same way each time... I would probably just make it object Data {get;set;} and do some kind of reflection in the handler to get the object to "fit" into the database.

There maybe a design issue with what you're doing which has made this pattern emerge.

The pipeline handlers show how to "pass through" these requests and sort of do something with them.

hanslai commented 3 years ago

@cirrusone do you have this figured out. I am trying to do the same thing with asp.net core DI container, have not find the solution yet. Thanks

cirrusone commented 3 years ago

I actually changed to Microsoft.Toolkit.Mvvm as the messenger suited my use-case better. It also allows me to register lots of message types without lots of interfaces. Since it targets .NET Standard 2.0, this means that it can be used anywhere from UWP apps, to Uno, Xamarin, Unity, ASP.NET, etc. Literally any framework supporting the .NET Standard 2.0 feature set. There are no platform specific dependencies at all. The whole package is entirely platform, runtime, and UI stack agnostic.

https://github.com/windows-toolkit/WindowsCommunityToolkit/issues/3428

https://github.com/windows-toolkit/WindowsCommunityToolkit/issues/3230

https://github.com/windows-toolkit/MVVM-Samples/blob/master/docs/mvvm/Messenger.md

hanslai commented 3 years ago

@cirrusone Thanks a lot, I will check it out also.

zachpainter77 commented 3 years ago

I have a similar issue.. I was able to resolve it by using AutoFac container. But wished I could have solved it using the default container.

Here is what I did...

Request...

 public class AddNoteCommand<TEntity> : IRequest
        where TEntity : class, INoteEntity
{
    public int ParentId { get; set; }
    public int CurrentUserProfileId { get; set; }
    public string NoteContent { get; set; }
}

Handler

public class AddNoteCommandHandler<TEntity> : IRequestHandler<AddNoteCommand<TEntity>>
        where TEntity : class, INoteEntity, new()
{
    private readonly INoteRepository<TEntity> _repo;
    private readonly ITime _time;

    public AddNoteCommandHandler(INoteRepository<TEntity> repo, ITime time)
    {
        _repo = repo;
        _time = time;
    }
    public async Task<Unit> Handle(AddNoteCommand<TEntity> request, CancellationToken cancellationToken)
    {
        await _repo.Insert(new TEntity
        {
            CreatedByUserId = request.CurrentUserProfileId,
            CreatedDate = _time.Current,
            NoteContent = request.NoteContent,
            ParentId = request.ParentId                
        });

        await _repo.SaveChanges();

        return Unit.Value;
    }
}

Registration with AutoFac

public static ContainerBuilder AddGenericHandlers(this ContainerBuilder builder)
{
    builder.RegisterSource(new ContravariantRegistrationSource());    
    builder.RegisterGeneric(typeof(AddNoteCommandHandler<>)).AsImplementedInterfaces();
    return builder;
}

If anybody knows of a way to do this in the default asp.net core DI container then please show me!

alexgoto commented 3 years ago

@zachpainter77

https://github.com/jbogard/MediatR.Extensions.Microsoft.DependencyInjection

image

zachpainter77 commented 3 years ago

It is my understanding (and I tested this) that explicitly registering the GenericHandlerBase isn't necessary... If you are going to create concrete handlers for each generic type for that handler then MediatR will find the correct handler without issue..

Here is an example of this: image image

Here is the registration using Default DI Container: image

This works just fine... Now imagine if you had multiple entities, and further more, imagine if you had multiple service dependencies for your handler. While even though you would not have to implement the handle method for each derived handler you still need to create a concrete class that closes the generic type and pass all dependencies to the base constructor... In my opinion that is very tedious to have to do that for every entity and every service dependency.. In my opinion it is much simpler to simply register the generic handler via Autofac like I showed above. But if not using a third party DI container then by all means create the concrete handlers... If I couldn't use a third party di container for whatever reason then I would do it this way..

Cheers.