DelegatingHandler not getting invoked if exception caught by ExceptionHandler - c#

I am trying to understand how Web API Http pipeline works!
In my Web API project, I am using the following technique to log/handle exceptions:
ExceptionHandler -> Handle exceptions at the global level
ExceptionFilterAttribute -> Handle custom exception thrown by user
DelegatingHandler -> log request and response data
Sample code for each implementation:
ExceptionFilter:
public class CustomExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var request = context.ActionContext.Request;
if (context.Exception is ItemNotFoundException)
{
context.Response = request.CreateResponse(HttpStatusCode.NotFound, context.Exception.Message);
}
else if (context.Exception is InvalidRequestException)
{
context.Response = request.CreateResponse(HttpStatusCode.BadRequest, context.Exception.Message);
}
}
}
Exception Handler:
public class GlobalExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
var result = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(Constant.ErrorMessage.InternalServerError)
};
context.Result = new ErrorMessageResult(context.Request, result);
}
}
public class ErrorMessageResult : IHttpActionResult
{
private readonly HttpRequestMessage _request;
private readonly HttpResponseMessage _httpResponseMessage;
public ErrorMessageResult(HttpRequestMessage request, HttpResponseMessage httpResponseMessage)
{
_request = request;
_httpResponseMessage = httpResponseMessage;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_httpResponseMessage);
}
}
DelegatingHandler:
public class LogRequestAndResponseHandler : DelegatingHandler
{
private readonly ILoggingService _loggingService;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string requestBody = await request.Content.ReadAsStringAsync();
_loggingService.FirstLevelServiceLog(requestBody);
var result = await base.SendAsync(request, cancellationToken);
if (result.Content != null)
{
var responseBody = await result.Content.ReadAsStringAsync();
_loggingService.FirstLevelServiceLog(responseBody);
}
return result;
}
}
Observation:
When there is an custom exception CustomExceptionFilter is getting invoked and later the response is logged in LogRequestAndResponseHandler.
However, if the exception is not handled, it goes in GlobalExceptionHandler then the response DOES NOT come to LogRequestAndResponseHandler for logging.
Could anyone let me know, what code change have to be done in CustomExceptionFilter/GlobalExceptionHandler in order to receive the response in DelegatingHandler?
Solution: (Updated 10/09/2018)
Okay, so i found the solution here
By modifying ExceptionHandler code, i am able to catch the response in DelegatingHandler
Key was to inherit from IExceptionHandler rather than ExceptionHandler
Code:
public class GlobalExceptionHandler : IExceptionHandler
{
public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
var httpResponse = context.Request.CreateResponse(HttpStatusCode.InternalServerError, Constant.ErrorMessage.InternalServerError);
context.Result = new ResponseMessageResult(httpResponse);
return Task.FromResult(0);
}
}
Question:
I am still not able to understand how it's working? What is the difference between IExceptionHandler & ExceptionHandler?
Could anyone shed some light on this?

ExceptionHandler implements IExceptionHandler like this:
Task IExceptionHandler.HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
if (context == null)
throw new ArgumentNullException(nameof (context));
ExceptionContext exceptionContext = context.ExceptionContext;
if (!this.ShouldHandle(context))
return TaskHelpers.Completed();
return this.HandleAsync(context, cancellationToken);
}
Where I suspect you're seeing the difference is in that ShouldHandle check, which is implemented like this:
public virtual bool ShouldHandle(ExceptionHandlerContext context)
{
if (context == null)
throw new ArgumentNullException(nameof (context));
return context.ExceptionContext.CatchBlock.IsTopLevel;
}
I'm not intimately familiar with the pipeline, but from what I've seen it appears that exceptions can be handled at various points, and the ExceptionHandler base class assumes you probably only want to handle exceptions at the top level of the execution stack. I've seen cases where other handlers like CORS get in the way of this, and the catch block never ends up being at the top level.
If this is what you're seeing, you can still extend ExceptionHandler, and override the ShouldHandle method to just always return true. Or you could be more surgical and specifically detect whether CORS is likely to get in the way of the top-level check as suggested in this comment.

Related

How to disable Serilog to log one exception?

(Asp.Net core 3.1/.Net 6)
I need to disable the ERR logging of exception TaskCanceledException in an Asp.Net application (built by others). I've used the following middleware to suppress the TaskCanceledException
app.UseMiddleware<TaskCanceledMiddleware>();
public class TaskCanceledMiddleware
{
private readonly RequestDelegate _next;
public TaskCanceledMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (TaskCanceledException)
{
// Set StatusCode 499 Client Closed Request
logger.WARN("...")
context.Response.StatusCode = 499;
}
}
}
I can see the WARN log in the logs. However, I can still find the following EROR messages by Serilog.AspNetCore.RequestLoggingMiddleware? (Note the error level is EROR)
2022-07-06 07:30:40.6636|116344477|EROR|Serilog.AspNetCore.RequestLoggingMiddleware|HTTP "GET" "/api/v1/myurl" responded 500 in 23213.3233 ms
System.Threading.Tasks.TaskCanceledException: A task was canceled.
at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at ....
Why there is still errors of TaskCanceledException after using app.UseMiddleware<TaskCanceledMiddleware>()? (BTW, what's the level of EROR? Shouldn't it be ERR?)
you can try excluding logging for certain exception like this.
new LoggerConfiguration()
....
.Filter.ByExcluding(logEvent => logEvent.Exception != null && logEvent.Exception.GetType() == typeof(TaskCanceledException))
...CreateLogger();
do you know where the exception is thrown?
Maybe it throws not inside asp.net core action execution pipeline (where Middleware works).
You can register a filter to intercept application wide exceptions inside .AddControllers() registration like this:
_ = services
.AddControllers(options =>
{
//Global filters
_ = options.Filters.Add<ApiGlobalExceptionFilterAttribute>();
///...omissis...
})
Here a simple exception filter attribute implementation:
public sealed class ApiGlobalExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly ILogger<ApiGlobalExceptionFilterAttribute> _logger;
public ApiGlobalExceptionFilterAttribute(ILogger<ApiGlobalExceptionFilterAttribute> logger)
{
_logger = logger;
}
public override void OnException(ExceptionContext context)
{
var request = context.HttpContext?.Request;
if (context.Exception is TaskCanceledException tcExc)
{
// Set StatusCode 499 Client Closed Request
_logger.WARN("...");
context.Result = new ErrorActionResult(499);
}
else
{
//TODO: manage errors
}
context.ExceptionHandled = true;
base.OnException(context);
}
}
internal class ErrorActionResult : IActionResult
{
[JsonIgnore]
public int StatusCode { get; private set; }
public ErrorActionResult(int statusCode)
{
StatusCode = statusCode;
}
public async Task ExecuteResultAsync(ActionContext context)
{
var error = new
{
Code = StatusCode,
Message = "Internal server error"
};
var objectResult = new ObjectResult(error)
{
StatusCode = StatusCode
};
await objectResult.ExecuteResultAsync(context);
}
}
The context.ExceptionHandled = true; stops exception to be propagated inside pipeline.
Hope it helps!

Using DelegatingHandler with custom data on HttpClient

Given the well known dilemma's and issues of using HttpClient - namely socket exhaustion and not respecting DNS updates, its considered best practice to use IHttpClientFactory and let the container decide when and how to utilise http pool connections efficiency. Which is all good, but now I cannot instantiate a custom DelegatingHandler with custom data on each request.
Sample below on how I did it before using the factory method:
public class HttpClientInterceptor : DelegatingHandler
{
private readonly int _id;
public HttpClientInterceptor(int id)
{
_id = id;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// associate the id with this request
Database.InsertEnquiry(_id, request);
return await base.SendAsync(request, cancellationToken);
}
}
For every time I instantiate a HttpClient a Id can be passed along:
public void DoEnquiry()
{
// Insert a new enquiry hypothetically
int id = Database.InsertNewEnquiry();
using (var http = new HttpClient(new HttpClientInterceptor(id)))
{
// and do some operations on the http client
// which will be logged in the database associated with id
http.GetStringAsync("http://url.com");
}
}
But now I cannot instantiate the HttpClient nor the Handlers.
public void DoEnquiry(IHttpClientFactory factory)
{
int id = Database.InsertNewEnquiry();
var http = factory.CreateClient();
// and now??
http.GetStringAsync("http://url.com");
}
How would I be able to achieve similar using the factory?
I cannot instantiate a custom DelegatingHandler with custom data on each request.
This is correct. But you can use a custom DelegatingHandler that is reusable (and stateless), and pass the data as part of the request. This is what HttpRequestMessage.Properties is for. When doing these kinds of "custom context" operations, I prefer to define my context "property" as an extension method:
public static class HttpRequestExtensions
{
public static HttpRequestMessage WithId(this HttpRequestMessage request, int id)
{
request.Properties[IdKey] = id;
return request;
}
public static int? TryGetId(this HttpRequestMessage request)
{
if (request.Properties.TryGetValue(IdKey, out var value))
return value as int?;
return null;
}
private static readonly string IdKey = Guid.NewGuid().ToString("N");
}
Then you can use it as such in a (reusable) DelegatingHandler:
public class HttpClientInterceptor : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var id = request.TryGetId();
if (id == null)
throw new InvalidOperationException("This request must have an id set.");
// associate the id with this request
Database.InsertEnquiry(id.Value, request);
return await base.SendAsync(request, cancellationToken);
}
}
The disadvantage to this approach is that each callsite then has to specify the id. This means you can't use the built-in convenience methods like GetStringAsync; if you do, the exception above will be thrown. Instead, your code will have to use the lower-level SendAsync method:
var request = new HttpRequestMessage(HttpMethod.Get, "http://url.com");
request.SetId(id);
var response = await client.SendAsync(request);
The calling boilerplate is rather ugly. You can wrap this up into your own GetStringAsync convenience method; something like this should work:
public static class HttpClientExtensions
{
public static async Task<string> GetStringAsync(int id, string url)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.SetId(id);
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
}
and now your call sites end up looking cleaner again:
public async Task DoEnquiry(IHttpClientFactory factory)
{
int id = Database.InsertNewEnquiry();
var http = factory.CreateClient();
var result = await http.GetStringAsync(id, "http://url.com");
}

Redirect outside of the Controllers context in ASP.NET Core

I do not know if this is actually possible, but I think it' s worth a try to find out.
There are maybe other and better patterns (if you know one let me know, I will look them up) to do this, but I'm just curious to know if this is possible.
When you have to call an API you could do it directly from within the controller using the HttpClient like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: get some access token to use in api call
var accessToken = await HttpContext.GetTokenAsync("access_token");
//Example: do an API call direcly using a static HttpClient wrapt in a service
var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api/some/endpoint");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _client.Client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var rederectUrl = "/account/login?returnUrl="+Request.Path;
return Redirect(rederectUrl);
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
return null;
}
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
When you would do this you'll have to reuse some code over and over again. You could just make a Repository but for some scenarios that would be overkill and you just want to make some quick and dirty API calls.
Now what I want to know is, when we move the logic of setting an Authorization header or handling the 401 and 403 responses outside the controller, how do you redirect or control the controller's action.
Lets say I create a Middleware for the HttpClient like this:
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var context = _contextAccessor.HttpContext;
var rederectUrl = "/account/login?returnUrl="+context.Request.Path;
context.Response.Redirect(rederectUrl); //not working
}
//When 403 user probably does not have authorization to use endpoint
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
}
return response;
}
}
We can just do the request like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: do an API call direcly using a static HttpClient initiated with Middleware wrapt in a service
var response = await _client.Client.GetAsync("https://example.com/api/some/endpoint");
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
The problem here is that context.Response.Redirect(rederectUrl); does not work. It does not break off the flow to redirect. Is it possible to implement this, and how would you solve this?
Ok since nobody answers my question I've thought about it thoroughly and I came up with the following:
Setup
We have a resource gateway (RG). The RG can return a 401 or 403 meaning that the session is expired (401) or the user does not have sufficient rights (403). We use an access token (AT) to authenticate and authorize our requests to the RG.
authentication
When we get a 401 and we have a refresh token (RT) we want to trigger something that will retrieve a new AT. When there is no RT or the RT is expired we want to reauthenticate the user.
authorization
When we get a 403 we want to show the user that he has no access or something similar like that.
Solution
To handle the above, without making it a hassle for the programmer that uses the API or API wrapper class we can use a Middleware that will specifically handle the Exception thrown by using the API or the API wrapper. The middleware can handle any of the above situations.
Create custom Exceptions
public class ApiAuthenticationException : Exception
{
public ApiAuthenticationException()
{
}
public ApiAuthenticationException(string message) : base(message)
{
}
}
public class ApiAuthorizationException : Exception
{
public ApiAuthorizationException()
{
}
public ApiAuthorizationException(string message) : base(message)
{
}
}
Throw exceptions
Create a wrapper or use the HttpClient middleware to manage the exception throwing.
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new ApiAuthenticationException();
}
//When 403 user probably does not have authorization to use endpoint -> show error page
if (response.StatusCode == HttpStatusCode.Forbidden)
{
throw new ApiAuthorizationException();
}
return response;
}
}
Now you have to setup the HttpClient inside your Startup.cs. There are multiple ways to do this. I advise to use AddTransient to innitiate a wrapper class that uses a HttpClient as a static.
You could do it like this:
public class ResourceGatewayClient : IApiClient
{
private static HttpClient _client;
public HttpClient Client => _client;
public ResourceGatewayClient(IHttpContextAccessor contextAccessor)
{
if (_client == null)
{
_client = new HttpClient(new ResourceGatewayMessageHandler(contextAccessor));
//configurate default base address
_client.BaseAddress = "https://gateway.domain.com/api";
}
}
}
And in your Startup.cs inside the ConfigureServices(IServiceCollection services) you can do:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<ResourceGatewayClient>();
Now you can use the dependency injection in any controller you would like.
Handle the Exceptions
Create something like this middleware (with thanks to this answer):
public class ApiErrorMiddleWare
{
private readonly RequestDelegate next;
public ApiErrorMiddleWare(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
if (exception is ApiAuthenticationException)
{
context.Response.Redirect("/account/login");
}
if (exception is ApiAuthorizationException)
{
//handle not authorized
}
}
Register your middleware
Go to Startup.cs and go to the Configure(IApplicationBuilder app, IHostingEnvironment env) method and add app.UseMiddleware<ApiErrorMiddleWare>();.
This should do it. Currently, I'm creating an example when it is publicly available (after peer review) I'll add a github reference.
I would like to hear some feedback on this solution or an alternative approach.

Custom Message for Expired Refresh Token WebApi

Instead of the usual response of Status : 400 and body message of "Error" : "invalid_client" when the token has expired, are there any methods of changing the status code and body to display something else?
Currently, I've managed to do something with headers as following :
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
AuthenticationTicket ticket;
if (_refreshTokens.TryRemove(context.Token, out ticket))
{
if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc.Value.LocalDateTime < DateTime.Now)
{
context.Response.Headers.Add("Expired", new string[] { "Yes" });
}
context.SetTicket(ticket);
}
}
Any help anyone?
Thanks.
You can implement a custom ASP.NET WebApi DelegatingHandler (if you want the validation to happen for all the requests) or ActionFilter (if you want the validation to happen for specific requests/per endpoint) to check whether the token is still valid and interrupt the request to return a more meaningful response. See the links for details.
I've implemented a simple one for your reference:
public class CustomTokenCheckMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (HasMyTokenExpired())
{
return new HttpResponseMessage
{
StatusCode = System.Net.HttpStatusCode.Unauthorized,
ReasonPhrase = "",
Content = new StringContent("Test") // See HttpContent for more https://msdn.microsoft.com/en-us/library/system.net.http.httpcontent(v=vs.118).aspx
};
}
return await base.SendAsync(request, cancellationToken);
}
public bool HasMyTokenExpired()
{
//Your custom logic here
return true;
}
}
Then you need to register it in the WebApiConfig file like this:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
/*
All other config goes here
*/
//This line registers the handler
config.MessageHandlers.Add(new CustomTokenCheckMessageHandler());
}
}

base.SendAsync -How symmetrical execution was done?

In asp.net - using Message Handlers - I can customize the request/response by adding message handlers.
So, a request comes in , going through multiple message handlers and then the response is back through the same handlers( in opposite direction).
So, for example : if I attach 2 message handlers : (yes I know, async/await is preferred, but that's from a book)
public class CustomMessageHandler1 : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Debug.WriteLine("CustomMessageHandler1 request invoked");
return base.SendAsync(request, cancellationToken)
.ContinueWith(task =>
{
Debug.WriteLine("CustomMessageHandler1 response invoked");
var response = task.Result;
return response;
});
}
}
public class CustomMessageHandler2 : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Debug.WriteLine("CustomMessageHandler2 request invoked");
return base.SendAsync(request, cancellationToken)
.ContinueWith(task =>
{
Debug.WriteLine("CustomMessageHandler2 response invoked");
var response = task.Result;
return response;
});
}
}
Let's not forget to register those in global.asax :
var config = GlobalConfiguration.Configuration;
config.MessageHandlers.Add(new CustomMessageHandler1());
config.MessageHandlers.Add(new CustomMessageHandler2());
And the result is :
As you can see , like I said and like this article says : The execution is symmetrical.
Great.
But then I thought to myself - how did they do that symmetrical execution?
So I succeed creating my own demo of symmetrical execution using continuation:
void Main()
{
Method1() ;
}
public async Task Method1 ()
{
Console.WriteLine ("Method_1"); //alias to request
await Method2();
Console.WriteLine ("Finished Method_1"); //alias to response
}
public async Task Method2 ()
{
Console.WriteLine ("Method_2"); //alias to request
await Task.FromResult("...");//dummy
Console.WriteLine ("Finished Method_2"); //alias to response
}
And the result was indeed symetrical :
Method_1
Method_2
Finished Method_2
Finished Method_1
But in my code Method1 called Method2 and that's why it worked !.
But in the first code above - they do NOT call each other ! it's like something is invoking only the first part ( before the ContinueWith) from each method , and then run the second part( after the ContinueWith) from each method.
Something like :
So i've look at the reference source for base.Sendasync : But couldn't find how base.Sendasync is doing this symmetrical execution
Question
How does base.Sendasync is doing that symmetrical execution without having one method calling the other?
Here is the console-appified Web API pipeline for you.
abstract class BaseHandler // HttpHandler
{
public abstract Task MyMethodAsync();
}
abstract class Handler : BaseHandler // MessageHandler
{
public Handler InnerHandler { get; set; }
public override Task MyMethodAsync()
{
if (this.InnerHandler != null)
return this.InnerHandler.MyMethodAsync();
else
return Task.FromResult<object>(null);
}
}
class Handler1 : Handler
{
public override async Task MyMethodAsync()
{
Console.WriteLine("Method_1"); //alias to request
await base.MyMethodAsync();
Console.WriteLine("Finished Method_1"); //alias to response
}
}
class Handler2 : Handler
{
public override async Task MyMethodAsync()
{
Console.WriteLine("Method_2"); //alias to request
await base.MyMethodAsync();
Console.WriteLine("Finished Method_2"); //alias to response
}
}
class LastHandler : Handler
{
public override async Task MyMethodAsync()
{
// Does nothing
await base.MyMethodAsync();
}
}
class Program
{
static void Main(string[] args)
{
List<Handler> handlers = new List<Handler>();
// You do this when you add the handler to config
handlers.Add(new Handler1());
handlers.Add(new Handler2());
// This part is done by HttpClientFactory
Handler pipeline = new LastHandler();
handlers.Reverse();
foreach (var handler in handlers)
{
handler.InnerHandler = pipeline;
pipeline = handler;
}
pipeline.MyMethodAsync().Wait();
}
}
Each delegating handler is aware of its "next" handler, and DelegatingHandler.SendAsync does call SendAsync on the next ("inner") handler. You can think of it like a linked list, as such:
public abstract class MyDelegatingHandler
{
private readonly MyDelegatingHandler _next;
public MyDelegatingHandler(MyDelegatingHandler next = null)
{
_next = next;
}
public virtual Task SendAsync()
{
if (_next == null)
return Task.FromResult(0);
return _next.SendAsync();
}
}

Categories