首页 > 解决方案 > 如何在不调用 .Result 的情况下使用作为任务注入的类中的函数?

问题描述

我试图在这里实现 lolsharp 给出的答案:gRPC-Web Channel Authentication with Blazor Webassembly via Dependency Injection

他们注入了一个 GrpcChannel 类型的任务,其中 GrpcChannel 是一个类:

@inject Task<GrpcChannel> Channel

这是因为通道是使用异步注册的。

我尝试在此类中使用一个函数,如下所示:

GetAllResponse getAllResponse = await Channel.Result.GetAllAsync(new Google.Protobuf.WellKnownTypes.Empty());

但这会失败并出现错误“无法在此运行时等待监视器”,因为单线程 Web 程序集不支持该机制(如Blazor 启动错误中所述:System.Threading.SynchronizationLockException: 无法在此运行时等待监视器)。

如果通道是异步注册的,如何在 Razor 页面中使用 Channel 类中的函数?

编辑:根据要求,这是 Program.cs 中声明的依赖项

builder.Services.AddSingleton(async services =>
        {
            Console.WriteLine("In addsingleton");
            var config = services.GetRequiredService<IConfiguration>();
            #if DEBUG
            var baseUri = "http://localhost:8999/";
#else
            var baseUri = "[mysite]";
#endif
            Console.WriteLine("About to set new httpclient");
            var httpClient = new HttpClient(new 
GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
            
            var scopedFactory = 
services.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopedFactory.CreateScope())
            {
                
                var authenticationService = 
scope.ServiceProvider.GetRequiredService<IAccessTokenProvider>();

                var tokenResult = await 
authenticationService.RequestAccessToken();
                
                if (tokenResult.TryGetToken(out var token))
                {
                    var credentials = CallCredentials.FromInterceptor((context, metadata) =>
                    {
                        if (!string.IsNullOrEmpty(token.Value))
                        {
                            metadata.Add("Authorization", $"Bearer 
{token.Value}");
                        }
                        return Task.CompletedTask;
                    });

                    var channel = GrpcChannel.ForAddress(baseUri, new 
GrpcChannelOptions { HttpClient = httpClient, Credentials = 
ChannelCredentials.Create(new SslCredentials(), credentials) });
                    var client = new 
GrpcServices.GrpcServicesClient(channel);
                    return client;
                }
            }

标签: c#.netmultithreadingdependency-injectionblazor

解决方案


为了避免不得不调用.ResultChannel你将不得不等待它。例如:

var channel = await Channel;
GetAllResponse getAllResponse =
    await channel.GetAllAsync(new Google.Protobuf.WellKnownTypes.Empty());

旁注:正如我在评论中提到的,您通常应该希望在对象解析期间避免做任何涉及 I/O 的事情,因为它会使对象解析变得脆弱且无法测试。相反,您应该能够自信地组合您的对象图,正如Mark Seemann所说的。您可以通过将任务隐藏在抽象后面来推迟任务的创建来做到这一点。例如:

public interface IGrpcChannelProvider
{
    Task<GrpcChannel> Channel { get; }
}

这允许您将所有注册代码移动到以下实现中IGrpcChannelProvider

public sealed class GrpcChannelProvider : IGrpcChannelProvider, IDisposable
{
    private readonly IConfiguration config;
    private readonly IAccessTokenProvider authenticationService;

    private readonly Lazy<Task<GrpcChannel>> channel;

    public GrpcChannelProvider(
        IConfiguration config, IAccessTokenProvider authenticationService)
    {
        this.config = config;
        this.authenticationService = authenticationService;

        this.channel = new Lazy<Task<GrpcChannel>>(this.CreateChannel);
    }

    public Task<GrpcChannel> Channel => this.channel.Value;

    public void Dispose()
    {
        if (this.channel.IsValueCreated) this.channel.Value.Dispose();
    }

    // This is your original code
    private async Task<GrpcChannel> CreateChannel()
    {
#if DEBUG
        var baseUri = "http://localhost:8999/";
#else
var baseUri = "[mysite]";
#endif
        var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));

        var tokenResult = await this.authenticationService.RequestAccessToken();

        if (tokenResult.TryGetToken(out var token))
        {
            var credentials = CallCredentials.FromInterceptor((context, metadata) =>
            {
                if (!string.IsNullOrEmpty(token.Value))
                {
                    metadata.Add("Authorization", $"Bearer {token.Value}");
                }
                return Task.CompletedTask;
            });

            var channel = GrpcChannel.ForAddress(baseUri,
                new GrpcChannelOptions
                {
                    HttpClient = httpClient,
                    Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
                });

            var client = new GrpcServices.GrpcServicesClient(channel);
            return client;
        }
    }
}

该组件可以注册如下:

services.AddSingleton<IGrpcChannelProvider, GrpcChannelProvider>();

或者 - 如果在应用程序域期间缓存通道导致安全问题 - 将组件注册为范围:

services.AddScoped<IGrpcChannelProvider, GrpcChannelProvider>();

在视图中,注入这个IGrpcChannelProvider而不是通道:

@inject IGrpcChannelProvider Provider

并按如下方式使用它:

var channel = await Provider.Channel;
GetAllResponse getAllResponse =
    await channel.GetAllAsync(new Google.Protobuf.WellKnownTypes.Empty());

更进一步,您甚至可能希望阻止对 Razor 页面内的服务进行任何调用,而是依赖于预先填充的模型:

@model AllResponseModel

@model AllResponseModel

GetAllResponse getAllResponse = Model.AllResponses;

现在您可以改为注入IGrpcChannelProviderRazor AllResponseModel


推荐阅读