在 SignalR ASP.NET Core 3.0 中使用 IAsyncEnumerable 尝试捕获

Dav*_*eer 4 c# signalr asp.net-core iasyncenumerable

尝试从 ASP.NET Core 3 SignalR Hub 捕获顶级异常

这很棘手,因为我使用的是 yield return,并且您不能将其包装在 try-catch 块中。它给出了这个编译器错误:

CS1626 C# 无法在带有 catch 子句的 try 块的主体中​​产生值

在这里讨论

那么,如何捕获这个异常呢?它被困在内部某处并发送到 javascript 客户端。我似乎看不到 ASP.NET Core 中间件管道中的异常。

// SignalR Hub 
public class CrawlHub : Hub
{
    public async IAsyncEnumerable<UIMessage> Crawl(string url, [EnumeratorCancellation]CancellationToken cancellationToken)
    {
        Log.Information("Here");
        // Trying to catch this error further up pipeline as
        // can't try catch here due to yield return
        throw new HubException("This error will be sent to the client!");
        // handing off to Crawler which returns back messages (UIMessage objects) every now and again on progress
        await foreach (var uiMessage in Crawler.Crawl(url, cancellationToken))
        {
// Check the cancellation token regularly so that the server will stop
            // producing items if the client disconnects.
            cancellationToken.ThrowIfCancellationRequested()
            // update the stream UI with whatever is happening in static Crawl
            yield return new UIMessage(uiMessage.Message, uiMessage.Hyperlink, uiMessage.NewLine);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

试图捕获异常,以便可以将Log.Fatal(ex)其写入 serilog

在此处输入图片说明

异常正在传递给 js 客户端。

2019-11-24 08:35:48.636 +00:00 [INF] 
2019-11-24 08:35:48.682 +00:00 [INF] Starting up BLC.Website (Program.cs)
2019-11-24 08:35:48.917 +00:00 [INF] Development environment - using developer exception page
2019-11-24 08:35:48.995 +00:00 [INF] Application started. Press Ctrl+C to shut down.
2019-11-24 08:35:48.997 +00:00 [INF] Hosting environment: Development
2019-11-24 08:35:48.998 +00:00 [INF] Content root path: c:\dev\test\BrokenLink\BLC.Website
2019-11-24 08:35:49.138 +00:00 [INF] HTTP GET / responded 200 in 125.315 ms
2019-11-24 08:35:54.652 +00:00 [INF] HTTP GET /scan?urlToCrawl=davemateer.com responded 200 in 34.0029 ms
2019-11-24 08:35:54.820 +00:00 [INF] HTTP POST /crawlHub/negotiate responded 200 in 11.954 ms
2019-11-24 08:35:54.947 +00:00 [INF] Here
Run Code Online (Sandbox Code Playgroud)

ASP.NET Core 3 日志记录未捕获异常。

Program.cs

2019-11-24 08:35:48.636 +00:00 [INF] 
2019-11-24 08:35:48.682 +00:00 [INF] Starting up BLC.Website (Program.cs)
2019-11-24 08:35:48.917 +00:00 [INF] Development environment - using developer exception page
2019-11-24 08:35:48.995 +00:00 [INF] Application started. Press Ctrl+C to shut down.
2019-11-24 08:35:48.997 +00:00 [INF] Hosting environment: Development
2019-11-24 08:35:48.998 +00:00 [INF] Content root path: c:\dev\test\BrokenLink\BLC.Website
2019-11-24 08:35:49.138 +00:00 [INF] HTTP GET / responded 200 in 125.315 ms
2019-11-24 08:35:54.652 +00:00 [INF] HTTP GET /scan?urlToCrawl=davemateer.com responded 200 in 34.0029 ms
2019-11-24 08:35:54.820 +00:00 [INF] HTTP POST /crawlHub/negotiate responded 200 in 11.954 ms
2019-11-24 08:35:54.947 +00:00 [INF] Here
Run Code Online (Sandbox Code Playgroud)

Startup.cs

public class Program
{
    public static void Main(string[] args)
    {
        Log.Logger = new LoggerConfiguration()
            //.MinimumLevel.Information() // this is the default
            // Suppress framework log noise eg routing and handling
            // so we'll see warnings and errors from the framework
            .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
            .Enrich.FromLogContext()
            .WriteTo.Console()
            .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day)
            .CreateLogger();

        try
        {
            Log.Information("");
            Log.Information("Starting up BLC.Website (Program.cs)");
            CreateHostBuilder(args).Build().Run();
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Application start-up failed");
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSerilog()
            // configuring logging for SignalR
            .ConfigureLogging(logging =>
            {
                logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Warning);
                // turn on for connection debugging
                //logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Debug);
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
Run Code Online (Sandbox Code Playgroud)

Dav*_*eer 5

这是我认为有效的答案:

// SignalR Hub 
public class CrawlHub : Hub
{
    // A wrapper iterator so can catch exceptions here which can't be done in 
    // a block which does yield return
    public async IAsyncEnumerable<UIMessage> Crawl(string url, [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        var enumerable = CrawlAndGetMessages(url, cancellationToken);
        await using var enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
        for (var more = true; more;)
        {
            // Catch exceptions only on executing/resuming the iterator function
            try
            {
                more = await enumerator.MoveNextAsync();
            }
            catch (Exception ex)
            {
                Log.Fatal("IteratorFunction() threw exception: " + ex);
                throw;
            }
            // yield out this UIMessage this has to be outside a try catch block
            yield return enumerator.Current;
        }
    }

    public async IAsyncEnumerable<UIMessage> CrawlAndGetMessages(string url, [EnumeratorCancellation]CancellationToken cancellationToken)
    {
        // handing off to Crawler which returns back messages (UIMessage objects) every now and again on progress
        await foreach (var uiMessage in Crawler.Crawl(url, cancellationToken))
        {
            // Check the cancellation token regularly so that the server will stop
            // producing items if the client disconnects.
            cancellationToken.ThrowIfCancellationRequested();

            if (uiMessage.Message.Contains("404"))
                // it should be displayed on the error list - this is not a stream
                await Clients.Caller.SendAsync("ReceiveBrokenLinkMessage", "404 error on blah", cancellationToken);
            else
                // update the stream UI with whatever is happening in static Crawl
                yield return new UIMessage(uiMessage.Message, uiMessage.Hyperlink, uiMessage.NewLine);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

感谢上面评论中的@JeroenMostert 和@TheodorZoulias 以及Jackson Dunstan 文章。当我对此有更多了解时,我会发布改进。

  • 如果 `more = wait enumerator.MoveNextAsync()` 返回 false,您仍然会 `yield return enumerator.Current`,这使得您的枚举始终包含一个额外的 `null`。 (3认同)