dotnet version: 6.0.300
I am currently writing a custom monitorization feature, that observes requests to all controllers, as well as outbound requests to other apis.
Currently, I have integrated two separate solutions - that practically do the same thing:
Inbound requests are monitored using a Middleware, and Outbound requests are monitored using a DelegatingHandler. It would be great if I could utilise the same code for both, seeing they are both, in essence, HttpRequest
s and HttpResponse
s.
So my question is: Is it possible to use a DelegatingHandler
, to intercepts every controller request. As well as use that same DelegatingHandler
to intercept every outbound http request?
And if not, is there an alternative way to achieve the desired result? Or am I stuck with having two separate classes (A middleware and a DelegatingHandler
)
From what I inferred from reading Microsoft's own documentation: they recommend shifting to Middlewares.
"If your app is using custom HTTP modules or HTTP handlers, you'll need a plan to migrate them to ASP.NET Core. The most likely replacement in ASP.NET Core is middleware." - https://learn.microsoft.com/en-us/dotnet/architecture/porting-existing-aspnet-apps/middleware-modules-handlers#aspnet-modules-and-handlers
Thanks for any insight you can provide :)
Middleware
public class MyMiddleware
{
private const string FailedToReadBodyErrorMessage = "Error: failed to read request body";
private readonly RequestDelegate _next;
public MonitorOperationsMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IMonitorDispatchService activity, IHttpContextAccessor accessor)
{
var requestPath = context.Request.Path.ToString();
await using OperationMonitorRequest request = activity.StartRequest(requestPath);
request
.SetRoot(requestPath)
.AddPayload("Method", context.Request.Method)
.AddPayload("Query", context.Request.Query);
string requestBodyContent = await GetRequestBody(context);
request.SetRequest(requestBodyContent);
await using OperationMonitorResponse response = request.StartResponse();
try
{
string bodyResponse = await GetResponse(context);
response.SetResponse((HttpStatusCode) context.Response.StatusCode, bodyResponse);
}
catch (Exception e)
{
response.SetResponse(e);
throw;
}
}
private static async Task<string> GetRequestBody(HttpContext context)
{
if (!context.Request.Body.CanRead)
{
return string.Empty;
}
try
{
// Enable buffering so the request can be re-read
context.Request.EnableBuffering();
// Leave the body open so the next middleware can read it.
// Leave stream open so next middleware can read it
using var reader = new StreamReader(
context.Request.Body,
Encoding.UTF8,
false,
512,
true
);
string body = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;
return body;
}
catch
{
return FailedToReadBodyErrorMessage;
}
}
private async Task<string> GetResponse(HttpContext context)
{
/* The response.body is read only, so we need to temporarily convert it into a memory stream,
so we can read the response value and log it */
Stream originalBodyStream = context.Response.Body;
await using var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
await _next(context);
// Read response
try
{
memoryStream.Seek(0, SeekOrigin.Begin);
return await new StreamReader(context.Response.Body).ReadToEndAsync();
}
finally
{
// Reset the stream to not interfere with any other possible middlewares
memoryStream.Seek(0, SeekOrigin.Begin);
context.Response.Body = originalBodyStream;
// Copy the contents of the new memory stream (containing the response), to the original stream:
// in order to return the user the original response
await memoryStream.CopyToAsync(originalBodyStream);
}
}
}
Delegating Handler
public class MyDelegatingHandler : DelegatingHandler
{
private readonly IMonitorDispatchService _monitorService;
public OperationMonitorDelegatingHandler(IMonitorDispatchService monitorService)
{
_monitorService = monitorService;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
)
{
string requestName = request.RequestUri?.AbsolutePath ?? "Unknown Request";
await using OperationMonitorRequest monitorRequest = _monitorService.StartRequest(requestName);
monitorRequest
.AddPayload("Method", request.Method.Method)
.AddPayload("Source", request.RequestUri?.AbsoluteUri);
if (request.Content != null)
{
string body = await ReadContentAsStringAsync(request.Content, cancellationToken);
monitorRequest.SetRequest(body);
}
await using OperationMonitorResponse monitorResponse = monitorRequest.StartResponse();
try
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
string body = await ReadContentAsStringAsync(response.Content, cancellationToken);
monitorResponse.SetResponse(response.StatusCode, body);
return response;
}
catch (Exception e)
{
monitorResponse.SetResponse(e);
throw;
}
}
private static async Task<string> ReadContentAsStringAsync(
HttpContent? content,
CancellationToken cancellationToken
)
{
if (content == null)
{
return string.Empty;
}
return await content.ReadAsStringAsync(cancellationToken);
}
}