I use .net core 7, minimal api and I would like to log POST request body.
I use following code:
var endpoints = app.MapGroup("api/v1")
.AddEndpointFilter<RequestResponseLogFilter>();
endpoints.MapPost("/endpoint", async ([FromServices] ServiceA serviceA, [FromServices] ServiceB serviceB, [FromBody] MyRequest request) =>
await MyEndpoint.DoSomething(serviceA, serviceB, request));
Attribute that I use to distinguish that object is request body:
public class RequestBodyAttribute : Attribute
{
}
[RequestBody]
public class MyRequest
{
}
Logging filter:
public class RequestResponseLogFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var httpContext = context.HttpContext;
var logger = httpContext.RequestServices.GetService<ILogger>()!;
object? requestBody = context.Arguments.FirstOrDefault(a => a?.GetType().GetCustomAttribute<RequestBodyAttribute>() != null);
logger.Information("{@Request}", LogHelper.PrepareRequest(httpContext, requestBody));
var stopwatch = Stopwatch.StartNew();
var result = await next(context);
stopwatch.Stop();
logger.Information("{@Response}", LogHelper.PrepareResponse(httpContext, result, stopwatch.Elapsed));
return result;
}
I do not like approach that I need to put [RequestBody] attribute on each class that I use as POST body - this could lead to errors, I can forget to put attribute.
Another approach would be to use attribute position (for example last attribute is body), but this also could lead to errors.
I thinks, the best way would be to check for attribute [FromBody], but I do not have this info in my filter.
What would be the best way to distinguish between services and body in my filter (assuming that I use generic filter to process any DTO)?
CodePudding user response:
Not a direct answer to the question, but first of all note that Minimal APIs support HttpLogging and W3C logging, check it out, maybe it will be enough:
builder.Services
.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;
});
// ...
var app = builder.Build();
app.UseHttpLogging();
// ...
And in settings:
{
"Logging": {
"LogLevel": {
// ...
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
}
Though there is no "easy" way to enable it only for concrete endpoints, but you can workaround with UseWhen
- instead of app.UseHttpLogging();
:
app.UseWhen(ctx => ctx.Request.Path.Value?.Contains("test") == true, // some predicate
appBuilder => appBuilder.UseHttpLogging());
If this still is not suitable for use case I would argue still relying on handler parameters having attributes is still not a best option (FromBodyAttribute
is actually not required and can be skipped). You probably should consider approach used in HttpLoggingMiddleware
itself - by reading the request stream and logging the results (see the source code).
CodePudding user response:
I've managed to find solution, it could be solved by Filter Factory:
var endpoints = app.MapGroup("api/v1")
.AddEndpointFilterFactory(RequestResponseLogFilterFactory.AddLoggingFilter);
endpoints.MapPost("/endpoint", async ([FromServices] ServiceA serviceA, [FromServices] ServiceB serviceB, [FromBody] MyRequest request) =>
await MyEndpoint.DoSomething(serviceA, serviceB, request));
Factory itself:
public static class RequestResponseLogFilterFactory
{
public static EndpointFilterDelegate AddLoggingFilter(EndpointFilterFactoryContext context, EndpointFilterDelegate next)
{
var parameters = context.MethodInfo.GetParameters();
var parameter = parameters.FirstOrDefault(p =>
p.CustomAttributes.Any(a => a.AttributeType == typeof(Microsoft.AspNetCore.Mvc.FromBodyAttribute)));
return async invocationContext =>
{
var httpContext = invocationContext.HttpContext;
var logger = httpContext.RequestServices.GetService<ILogger>()!;
object? requestBody = null;
if(parameter is not null)
{
requestBody = invocationContext.Arguments.FirstOrDefault(a => a?.GetType() == parameter.ParameterType);
}
logger.Information("{@Request}", LogHelper.PrepareRequest(httpContext, requestBody));
var stopwatch = Stopwatch.StartNew();
var result = await next(invocationContext);
stopwatch.Stop();
logger.Information("{@Response}", LogHelper.PrepareResponse(httpContext, result, stopwatch.Elapsed));
return result;
};
}