簡體   English   中英

如何從非泛型方法分派到泛型處理程序?

[英]How to dispatch to generic handler from a non-generic method?

我有一個方法需要返工,特別是我需要刪除簽名中的通用參數。 該方法接收一個參數,該參數始終實現特定接口。

這是方法:

public void SendCommand<T>(T command) where T : ICommand
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);

        var service = scope.ServiceProvider.GetService(handlerType);
        (service as ICommandHandler<T>).Handle(command);
    }
}

症結在於(service as ICommandHandler<T>).Handle(command)行,它接收實現ICommand的對象的類型參數。 根據參數的實際類型,檢索到的服務是不同的。

有什么方法可以刪除泛型參數,並使用參數的實際類型作為ICommandHandler<T>行的泛型參數?

編輯:

這個返工可以解決問題,但它暴露了一個非常奇怪的,可能是錯誤的行為。

public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);

        dynamic cmd = command;
        dynamic service = scope.ServiceProvider.GetService(handlerType);

        var method = handlerType.GetMethods().Single(s => s.Name == "Handle");
        method.Invoke(service, new[] { command });

        service.Handle(cmd);
    }
}

從服務對象中提取Handle方法並手動調用它就可以了。 但是使用service.Handle(cmd)方法調用會引發異常(對象沒有Handle的定義)。

這太奇怪了,因為提取方法確實有效。

任何人都可以闡明這種怪異現象嗎?

這里有幾個選項:

首先,如果保留泛型類型參數是一個選項,您可以將方法的復雜性降低到以下程度:

public void SendCommand<T>(T command) where T : ICommand
{   
    using (var scope = services.CreateScope())
    {
        var handler = scope.ServiceProvider
            .GetRequiredService<ICommandHandler<T>>();
        handler.Handle(command);
    }
}

當然,這不是你的問題。 刪除泛型類型參數允許以更動態的方式分派命令,這在編譯時未知命令類型時很有用。 在這種情況下,您可以使用動態類型,如下所示:

public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType =
            typeof(ICommandHandler<>).MakeGenericType(commandType);

        dynamic handler = scope.ServiceProvider
            .GetRequiredService(handlerType);
        handler.Handle((dynamic)command);
    }
}

這里注意兩件事:

  1. 已解析的處理程序存儲在dynamic變量中。 因此,它的Handle方法是一個動態調用,其中Handle在運行時解析。
  2. 由於ICommandHandler<{commandType}>不包含Handle(ICommand)方法,因此command參數需要轉換為dynamic的。 這指示 C# 綁定它應該查找任何名為Handle method 的方法,其中一個參數與提供的運行時類型的command相匹配。

此選項效果很好,但這種“動態”方法有兩個缺點:

  1. 缺少編譯時支持將使對ICommandHandler<T>接口的任何重構都不會引起注意。 這可能不是一個大問題,因為它可以很容易地進行單元測試。
  2. 應用於任何ICommandHandler<T>實現的任何裝飾器都需要確保將其定義為公共類。 當類是內部類時, Handle方法的動態調用將(奇怪地)失敗,因為 C# 綁定器不會發現ICommandHandler<T>接口的Handle方法是可公開訪問的。

因此,除了使用動態之外,您還可以使用良好的舊泛型,類似於您的方法:

public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType =
            typeof(ICommandHandler<>).MakeGenericType(commandType);

        object handler = scope.ServiceProvider.GetRequiredService(handlerType);

        var handleMethod = handlerType.GetMethods()
            .Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));

        handleMethod.Invoke(handler, new[] { command });
    }
}

這可以防止以前方法出現的問題,因為這將繼續重構命令處理程序接口,並且即使處理程序是內部的,它也可以調用Handle方法。

另一方面,它確實引入了一個新問題。 如果處理程序拋出異常,則對MethodBase.Invoke的調用將導致將該異常包裝在InvocationException中。 當消費層捕獲某些異常時,這可能會導致調用堆棧出現問題。 在那種情況下,應該首先解包異常,這意味着SendCommand正在向其消費者泄露實現細節。

有幾種方法可以解決這個問題,例如:

public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType =
            typeof(ICommandHandler<>).MakeGenericType(commandType);

        object handler = scope.ServiceProvider.GetRequiredService(handlerType);

        var handleMethod = handlerType.GetMethods()
            .Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));

        try
        {        
            handleMethod.Invoke(handler, new[] { command });
        }
        catch (InvocationException ex)
        {
            throw ex.InnerException;
        }
    }
}

然而,這種方法的缺點是您丟失了原始異常的堆棧跟蹤,因為該異常被重新拋出(這通常不是一個好主意)。 因此,您可以執行以下操作:

public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType =
            typeof(ICommandHandler<>).MakeGenericType(commandType);

        object handler = scope.ServiceProvider.GetRequiredService(handlerType);

        var handleMethod = handlerType.GetMethods()
            .Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));

        try
        {        
            handleMethod.Invoke(handler, new[] { command });
        }
        catch (InvocationException ex)
        {
            ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
        }
    }
}

這利用了 .NET 4.5 的ExceptionDispatchInfo ,它在 .NET Core 1.0 及更高版本和 .NET Standard 1.0 下也可用。

作為最后一個選項,您還可以解析實現非泛型接口的包裝器類型,而不是解析ICommandHandler<T> 這使代碼類型安全,但確實會強制您注冊額外的通用包裝器類型。 過程如下:

public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var wrapperType =
            typeof(CommandHandlerWrapper<>).MakeGenericType(commandType);

        var wrapper = (ICommandHandlerWrapper)scope.ServiceProvider
            .GetRequiredService(wrapperType);

        wrapper.Handle(command);
    }
}

public interface ICommandHandlerWrapper
{
    void Handle(ICommand command);
}

public class CommandHandlerWrapper<T> : ICommandHandlerWrapper
    where T : ICommand
{
    private readonly ICommandHandler<T> handler;
    public CommandHandlerWrapper(ICommandHandler<T> handler) =>
        this.handler = handler;

    public Handle(ICommand command) => this.handler.Handle((T)command);
}

// Extra registration
services.AddTransient(typeof(CommandHandlerWrapper<>));

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM