Exception thrown in middleware being logged as error instead of warning - c#

I am having quite a few exceptions being logged for clients disconnecting, but they are being logged as errors. I want to "downgrade" them to warnings, because when I monitor my logs, these logs really aren't something I want to see. How do I downgrade them to warning?
Connection ID "17726168134941057923", Request ID "8002a384-0000-f600-b63f-84710c7967bb": An unhandled exception was thrown by the application. Microsoft.AspNetCore.Connections.ConnectionResetException: The client has disconnected ---> System.Runtime.InteropServices.COMException (0x80070040): The specified network name is no longer available. (0x80070040)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.IIS.Core.IO.AsyncIOOperation.GetResult(Int16 token)
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.ReadBody()
at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
at System.IO.Pipelines.Pipe.GetReadAsyncResult()
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.ReadAsync(Memory`1 memory, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.IIS.Core.HttpRequestStream.ReadAsyncInternal(Memory`1 buffer, CancellationToken cancellationToken)
at Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
at System.IO.StreamReader.ReadBufferAsync(CancellationToken cancellationToken)
at System.IO.StreamReader.ReadToEndAsyncInternal()
at MyAPI.Middleware.LoggingMiddleware.InvokeAsync(HttpContext context)
at MyAPI.Middleware.CorsMessageMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Builder.Extensions.UsePathBaseMiddleware.InvokeCore(HttpContext context, String matchedPath, String remainingPath)
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()
My Startup.Configure looks like this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions<AppSettings> options)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(config =>
{
config.SwaggerEndpoint("v1/swagger.json", "My API");
config.SupportedSubmitMethods(Array.Empty<SubmitMethod>());
});
app.UseMiddleware<CorsMessageMiddleware>();
app.UseMiddleware<LoggingMiddleware>();
app.UseMiddleware<ExceptionMiddleware>();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseHealthChecks("/health");
}
I have the exceptions caught in ExceptionMiddleware, but it seems the client disconnects somewhere above in the pipeline.
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (TaskCanceledException ex)
{
_logger.LogWarning(ex, ex.Message);
await HandleExceptionAsync(context);
}
catch (OperationCanceledException ex)
{
_logger.LogWarning(ex, ex.Message);
await HandleExceptionAsync(context);
}
catch (COMException ex)
{
_logger.LogWarning(ex, ex.Message);
await HandleExceptionAsync(context);
}
catch (ConnectionResetException ex)
{
_logger.LogWarning(ex, ex.Message);
await HandleExceptionAsync(context);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
await HandleExceptionAsync(context);
}
}
private static Task HandleExceptionAsync(HttpContext context)
{
var result = new ActionResult<object>
{
Messages = new List<string> { "INTERNAL_SERVER_ERROR" }
};
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
return context.Response.WriteAsync(SerializeObject(result));
}
private static string SerializeObject(object data)
{
return System.Text.Json.JsonSerializer.Serialize(data);
}
}
LoggingMiddleware:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LoggingMiddleware> _logger;
private readonly List<string> _contentSubTypes = new List<string>
{
"text",
"json",
"xml",
"*/*",
};
public LoggingMiddleware(ILogger<LoggingMiddleware> logger, RequestDelegate next)
{
_logger = logger;
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// WebSocket requests should be avoided
if (context.WebSockets.IsWebSocketRequest)
{
return;
}
StringBuilder log = new StringBuilder();
// Request
log.AppendLine($"A {context.Request.Method} request arrived at {context.Request.Path}{context.Request.QueryString}");
log.AppendLine($"Request time (local): {DateTime.Now:r}");
log.AppendLine($"Request headers: {{{string.Join("; ", context.Request.Headers.Select(x => $"{x.Key}:{x.Value}"))}}}");
var supportedMethods = new[] { "POST", "PATCH", "PUT" };
if (supportedMethods.Any(x => x.Equals(context.Request.Method, StringComparison.OrdinalIgnoreCase)))
{
context.Request.EnableBuffering();
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, true, 10240, true))
{
string body = await reader.ReadToEndAsync();
string bodyLog = !string.IsNullOrWhiteSpace(body) ? body : "EMPTY";
log.AppendLine($"Body: {bodyLog}");
context.Request.Body.Seek(0, SeekOrigin.Begin);
}
}
// Response
if (!_contentSubTypes.Any(subType => context.Request.Headers["Accept"].Any(acceptHeaderValue => acceptHeaderValue.Contains(subType))))
{
await _next(context);
var response = context.Response;
log.AppendLine($"Response time (local): {DateTime.Now:r}");
log.AppendLine($"Response status: {response.StatusCode}");
log.AppendLine($"Response headers: {string.Join("; ", response.Headers.Select(x => $"{x.Key}:{x.Value}"))}");
}
else
{
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
context.Response.Body = responseBody;
// Invoke next middleware
await _next(context);
var response = context.Response;
log.AppendLine($"Response time (local): {DateTime.Now:r}");
log.AppendLine($"Response status: {response.StatusCode}");
log.AppendLine($"Response headers: {string.Join("; ", response.Headers.Select(x => $"{x.Key}:{x.Value}"))}");
response.Body.Seek(0, SeekOrigin.Begin);
// response.ContentType is NULL empty when querying stuff such as favicon.ico
if (!string.IsNullOrEmpty(response.ContentType)
&& _contentSubTypes.Any(subType => response.ContentType.Contains(subType)))
{
using (var streamReader = new StreamReader(response.Body, Encoding.UTF8, true, 10240, true))
{
string responseText = await streamReader.ReadToEndAsync();
string bodyLog = !string.IsNullOrWhiteSpace(responseText) ? responseText : "EMPTY";
response.Body.Seek(0, SeekOrigin.Begin);
log.AppendLine($"Body: {bodyLog}");
}
}
await responseBody.CopyToAsync(originalBodyStream);
}
}
await Task.Run(() =>
{
_logger.LogDebug(log.ToString());
return Task.CompletedTask;
});
}
}

Related

Aspnetcore Midlleware not reversed in .net6 when using Graphql

In versions before .net6, I used to add an ErrorHandlerMiddleware to the pipeline, so I can centralize the need of returning error types in the application with different status codes.
an example is something exactly like this:
public class ErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception error)
{
string responseMessage = error.Message;
string exceptionMessage;
object ResponseObject;
var response = context.Response;
response.ContentType = "application/json";
switch (error)
{
case ApplicationBadRequestException e:
if (e.metaData is { })
ResponseObject = new { responseMessage, e.metaData };
else ResponseObject = new { responseMessage };
response.StatusCode = StatusCodes.Status400BadRequest;
break;
case ApplicationNotFoundException e:
if (e.metaData is { })
ResponseObject = new { responseMessage, e.metaData };
else ResponseObject = new { responseMessage };
response.StatusCode = StatusCodes.Status404NotFound;
break;
case ApplicationUnAuthorizedException e:
responseMessage = "You are not authorized to perform this operation.";
ResponseObject = new { responseMessage };
response.StatusCode = StatusCodes.Status401Unauthorized;
break;
default:
responseMessage = "An error has occurred...Please try again later.";
exceptionMessage = getExMessage(error);
ResponseObject = new { responseMessage, exceptionMessage };
response.StatusCode = (int)HttpStatusCode.InternalServerError;
break;
}
var result = JsonSerializer.Serialize(ResponseObject);
await response.WriteAsync(result);
}
}
}
Then at any controller in my application, when I need to return a certain error code in the response - for example 400 BadRequest - I do it like so
throw new ApplicationBadRequestException("this is the error message");
This causes the middleware pipeline to be reversed and this exception is fetched inside the ErrorhandlerMiddleware.
Now I am trying to use GraphQL in.net6, when I make this I can see that the Middleware pipeline is NOT reversed and the ErrorHandlerMiddleware class is not invoked. why does this happen?
EDIT: I have tried the same scenario with rest apis and the code works perfectly, seems that the problem happens due to the existence of GraphQL configuration.
this is my Program.cs file
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var connectionString = builder.Configuration["ConnectionStrings:DBConnectionString"];
builder.Services.AddDbContextFactory<DBEntities>(options => options.UseSqlServer(connectionString, x => x.UseNetTopologySuite()));
builder.Services.AddScoped<DBEntities>(options => options.GetRequiredService<IDbContextFactory<DBEntities>>().CreateDbContext());
builder.Services.AddGraphQLServer().AddQueryType<Query>().AddMutationType<Mutation>()
.AddProjections().AddFiltering().AddSorting();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseMiddleware<ErrorHandlerMiddleware>();
app.MapGraphQL("/graphql");
app.Run();

.NET Core WebApi return Unathorized code to user from middleware

I want to send user bad request code back when something goes wrong in middleware.
My Startup.cs looks like this:
// configure method
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("CorsPolicy");
app.UseMiddleware<RequestMiddleware>();
app.UseMiddleware<SecondRequestMiddleware>();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
My middleware looks like this:
public class RequestMiddleware
{
private readonly RequestDelegate _next;
public RequestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IAuthInfoService authInfoService, IPowiadomieniaCacheService cacheService)
{
string jwt = context.Request.Headers["custom_header"];
if (string.IsNullOrEmpty(jwt))
{
// no jwt in headers so i want to return Unauthorized to user:
await ReturnErrorResponse(HttpContext context);
}
}
private Task ReturnErrorResponse(HttpContext context)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}
}
But still i getting into my SecondRequestMiddleware. I want to return 401 Stauts Code to user, when there is no jwt in headers (thats what my RequestMiddleware checks) and stopped processing this request.
How to validate request in middleware and if conditions passed, return error code / response to user?
You could modify your middleware to short circuit a request, like this.
Where await context.Response.StartAsync(); will start a response and will not proceed further.
public class RequestMiddleware
{
private readonly RequestDelegate _next;
public RequestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IAuthInfoService authInfoService, IPowiadomieniaCacheService cacheService)
{
string jwt = context.Request.Headers["custom_header"];
if (string.IsNullOrEmpty(jwt))
{
// no jwt in headers so i want to return Unauthorized to user:
await ReturnErrorResponse(HttpContext context);
}
else
{
await _next(context);
}
}
private async Task ReturnErrorResponse(HttpContext context)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.StartAsync();
}
}
when there is no jwt in headers (thats what my RequestMiddleware
checks) and stopped processing this request.
You can terminate the request by writing the following:
await context.Response.WriteAsync("error message");
If the headers have the jwt , then you need to trigger the following middleware through the following statement, otherwise it will not be executed automatically:
await _next.Invoke(context);
More details, refer to ASP.NET Core Middleware.
Change your RequestMiddleware as follow:
public class RequestMiddleware
{
private readonly RequestDelegate _next;
public RequestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
string jwt = context.Request.Headers["custom_header"];
if (string.IsNullOrEmpty(jwt))
{
await ReturnErrorResponse(context);
}
else
{
await _next.Invoke(context);// call next middleware
}
}
private async Task ReturnErrorResponse(HttpContext context)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("error message!");
}
}

Return json from .net core api when using NotFound()

I'm trying to make my web api core return application/json, but it strangely always returns this html page breaking the error convention established by the team.
Here's the code i'm trying to execute but with no success at all so far:
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.Add(new IsoDateTimeConverter { DateTimeFormat = "dd/MM/yyyy" });
});
services.AddMvcCore().AddRazorViewEngine().AddRazorRuntimeCompilation().ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
var errorList = (from item in actionContext.ModelState
where item.Value.Errors.Any()
select item.Value.Errors[0].ErrorMessage).ToList();
return new BadRequestObjectResult(new
{
ErrorType = "bad_request",
HasError = true,
StatusCode = (int)HttpStatusCode.BadRequest,
Message = "Formato do request inválido",
Result = new
{
errors = errorList
}
});
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseCors(
options => options.AllowAnyOrigin().SetIsOriginAllowed(x => _ = true).AllowAnyMethod().AllowAnyHeader()
);
app.UseHttpsRedirection();
app.UseRouting();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
AuthController.cs
[HttpPost("refreshtoken")]
public IActionResult PostRefreshToken(Guid token)
{
if (!_authTokenService.IsValid(token))
{
return NotFound(new JsonResponse
{
HasError = true,
ErrorType = "not_found",
StatusCode = (int)HttpStatusCode.NotFound,
Title = "Token não encontrado",
Message = "refresh is not valid because it was not found or does not comply",
});
}
var savedToken = _authTokenService.Get(token);
...
return Ok(new JsonResponse
{
StatusCode = (int)HttpStatusCode.OK,
Title = "Token atualizado",
Message = "jwt access token refreshed with success, please update your keys for subsequent requests",
Result = new
{
Expiration = accessToken.Expiration.ToString("dd/MM/yyyy HH:mm:ss"),
AccessToken = accessToken.Token,
RefreshToken = refreshToken.Token,
}
});
}
when this code is executed i was expecting a json result when NotFound() block is reached, but instead it returns this text/html page
ErrorHandlingMiddleware.cs
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
var code = HttpStatusCode.InternalServerError;
var result = JsonConvert.SerializeObject(new
{
HasError = true,
StatusCode = (int)code,
Message = ex.Message
}, new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy()
}
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}
In my case it ended up being the visual studio extension Conveyor by Keyoti being the culprit of the errors aforementioned.
When i disabled the extension, the code was revealed to be ok and returning the right code, a json object body sent by the server.

Can't get response body to log it in application insights

In order to log the response body into app-insights from HttpContext I should develop a Middleware, that intercepts each request and response and extracts the HttpContext, in my case Response.Body and then sends it into app-insights using telemetryClient.TrackTrace().
The problem is that when I debug and extract the body of the respone is always empty.
This is my Middleware class
public class ResponseBodyInitializer
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly RequestDelegate _next;
public ResponseBodyInitializer(IHttpContextAccessor httpContextAccessor, RequestDelegate next)
{
_httpContextAccessor = httpContextAccessor;
_next = next;
}
public async Task Invoke(HttpContext context)
{
string resBody = await GetResponseBodyForTelemetry(context);
SendDataToTelemetryLog(resBody, context);
}
private static void SendDataToTelemetryLog(string respBody, HttpContext context)
{
if (context.Request.Method == "POST" || context.Request.Method =="PUT")
{
var telemetryClient = new TelemetryClient();
var traceTelemetry = new TraceTelemetry
{
Message = respBody,
SeverityLevel = SeverityLevel.Information
};
//Send a trace message for display in Diagnostic Search.
telemetryClient.TrackTrace(traceTelemetry);
}
}
private async Task<string> GetResponseBodyForTelemetry(HttpContext context)
{
var originalBody = context.Response.Body;
try
{
using (var memStream = new MemoryStream())
{
context.Response.Body = memStream;
//await the responsebody
await _next(context);
if (context.Response.StatusCode == 204)
{
return null;
}
memStream.Position = 0;
var responseBody = new StreamReader(memStream).ReadToEnd();
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
return responseBody;
}
}
finally
{
context.Response.Body = originalBody;
}
}
}
StartUp.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
app.UseDefaultFiles()
.UseStaticFiles()
.UseMiddleware<ResponseBodyInitializer>()
.UseBotFramework();
}
OR is there any other way to log the response in app-insights?
Here are the steps which you have to do it:-
1) You have to implement ITelemetryInitializer.
2) Inject the IHttpContextAccessor to the class and read the response stream within the Initialize method.
3) Ensure the passed ITelemetry object is from type RequestTelemetry and that the HttpRequest was either a Post or Put.
4) Then you can read the response using the IHttpContext.HttpContext.Response.Body property and log it using Application Insight.
Finally, register your class within the ConfigureService method in your Startup.cs
I see, you are not implementing ITelemetryInitializer ,also read the stream in Initialize method
Try these steps and see if it helps.

How to auto log every request in .NET Core WebAPI?

I'd like to have every request logged automatically. In previous .Net Framwork WebAPI project, I used to register a delegateHandler to do so.
WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
config.MessageHandlers.Add(new AutoLogDelegateHandler());
}
AutoLogDelegateHandler.cs
public class AutoLogDelegateHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var requestBody = request.Content.ReadAsStringAsync().Result;
return await base.SendAsync(request, cancellationToken)
.ContinueWith(task =>
{
HttpResponseMessage response = task.Result;
//Log use log4net
_LogHandle(request, requestBody, response);
return response;
});
}
}
an example of the log content:
------------------------------------------------------
2017-08-02 19:34:58,840
uri: /emp/register
body: {
"timeStamp": 1481013427,
"id": "0322654451",
"type": "t3",
"remark": "system auto reg"
}
response: {"msg":"c556f652fc52f94af081a130dc627433","success":"true"}
------------------------------------------------------
But in .NET Core WebAPI project, there is no WebApiConfig , or the register function at Global.asax GlobalConfiguration.Configure(WebApiConfig.Register);
So is there any way to achieve that in .NET Core WebAPI?
ActionFilter will work until you need to log only requests processed by MVC middleware (as controller actions).
If you need logging for all incoming requests, then you need to use a middleware approach.
Good visual explanation:
Note that middleware order is important, and if your logging should be done at the start of pipeline execution, your middleware should be one of the first one.
Simple example from docs:
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
// Do loging
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
For someone that wants a quick and (very) dirty solution for debugging purposes (that works on .Net Core 3), here's an expansion of this answer that's all you need...
app.Use(async (context, next) =>
{
var initialBody = context.Request.Body;
using (var bodyReader = new StreamReader(context.Request.Body))
{
string body = await bodyReader.ReadToEndAsync();
Console.WriteLine(body);
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
await next.Invoke();
context.Request.Body = initialBody;
}
});
You can create your own filter attribute...
public class InterceptionAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var x = "This is my custom line of code I need executed before any of the controller actions, for example log stuff";
base.OnActionExecuting(actionContext);
}
}
... and you would register it with GlobalFilters, but since you said you're using .NET Core, this is how you can try proceeding...
From learn.microsoft.com:
You can register a filter globally (for all controllers and actions)
by adding it to the MvcOptions.Filters collection in the
ConfigureServices method in the Startup class:
Let us know if it worked.
P.S.
Here's a whole tutorial on intercepting requests with WebAPI, in case someone needs more details.
Demo:
AutologArribute.cs (new file)
/// <summary>
/// <see cref="https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#Dependency injection"/>
/// </summary>
public class AutoLogAttribute : TypeFilterAttribute
{
public AutoLogAttribute() : base(typeof(AutoLogActionFilterImpl))
{
}
private class AutoLogActionFilterImpl : IActionFilter
{
private readonly ILogger _logger;
public AutoLogActionFilterImpl(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<AutoLogAttribute>();
}
public void OnActionExecuting(ActionExecutingContext context)
{
// perform some business logic work
}
public void OnActionExecuted(ActionExecutedContext context)
{
//TODO: log body content and response as well
_logger.LogDebug($"path: {context.HttpContext.Request.Path}");
}
}
}
StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
//....
// https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#filter-scopes-and-order-of-execution
services.AddMvc(opts=> {
opts.Filters.Add(new AutoLogAttribute());
});
//....
}
This is a complete Log component for .NET Core 2.2 Web API.
It will log Requests and Responses, both Headers and Bodies.
Just make sure you have a "Logs" folder.
AutoLogMiddleWare.cs (New file)
public class AutoLogMiddleWare
{
private readonly RequestDelegate _next;
public AutoLogMiddleWare(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
string route = context.Request.Path.Value;
string httpStatus = "0";
// Log Request
var originalRequestBody = context.Request.Body;
originalRequestBody.Seek(0, SeekOrigin.Begin);
string requestBody = new StreamReader(originalRequestBody).ReadToEnd();
originalRequestBody.Seek(0, SeekOrigin.Begin);
// Log Response
string responseBody = string.Empty;
using (var swapStream = new MemoryStream())
{
var originalResponseBody = context.Response.Body;
context.Response.Body = swapStream;
await _next(context);
swapStream.Seek(0, SeekOrigin.Begin);
responseBody = new StreamReader(swapStream).ReadToEnd();
swapStream.Seek(0, SeekOrigin.Begin);
await swapStream.CopyToAsync(originalResponseBody);
context.Response.Body = originalResponseBody;
httpStatus = context.Response.StatusCode.ToString();
}
// Clean route
string cleanRoute = route;
foreach (var c in Path.GetInvalidFileNameChars())
{
cleanRoute = cleanRoute.Replace(c, '-');
}
StringBuilder sbRequestHeaders = new StringBuilder();
foreach (var item in context.Request.Headers)
{
sbRequestHeaders.AppendLine(item.Key + ": " + item.Value.ToString());
}
StringBuilder sbResponseHeaders = new StringBuilder();
foreach (var item in context.Response.Headers)
{
sbResponseHeaders.AppendLine(item.Key + ": " + item.Value.ToString());
}
string filename = DateTime.Now.ToString("yyyyMMdd.HHmmss.fff") + "_" + httpStatus + "_" + cleanRoute + ".log";
StringBuilder sbLog = new StringBuilder();
sbLog.AppendLine("Status: " + httpStatus + " - Route: " + route);
sbLog.AppendLine("=============");
sbLog.AppendLine("Request Headers:");
sbLog.AppendLine(sbRequestHeaders.ToString());
sbLog.AppendLine("=============");
sbLog.AppendLine("Request Body:");
sbLog.AppendLine(requestBody);
sbLog.AppendLine("=============");
sbLog.AppendLine("Response Headers:");
sbLog.AppendLine(sbResponseHeaders.ToString());
sbLog.AppendLine("=============");
sbLog.AppendLine("Response Body:");
sbLog.AppendLine(responseBody);
sbLog.AppendLine("=============");
var path = Directory.GetCurrentDirectory();
string filepath = ($"{path}\\Logs\\{filename}");
File.WriteAllText(filepath, sbLog.ToString());
}
catch (Exception ex)
{
// It cannot cause errors no matter what
}
}
}
public class EnableRequestRewindMiddleware
{
private readonly RequestDelegate _next;
public EnableRequestRewindMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
context.Request.EnableRewind();
await _next(context);
}
}
public static class EnableRequestRewindExtension
{
public static IApplicationBuilder UseEnableRequestRewind(this IApplicationBuilder builder)
{
return builder.UseMiddleware<EnableRequestRewindMiddleware>();
}
}
Startup.cs (existing file)
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IMapper mapper, ILoggerFactory loggerFactory)
{
bool isLogEnabled = true; // Replace it by some setting, Idk
if(isLogEnabled)
app.UseEnableRequestRewind(); // Add this first
(...)
if(isLogEnabled)
app.UseMiddleware<AutoLogMiddleWare>(); // Add this just above UseMvc()
app.UseMvc();
}
Starting with ASP.NET Core 6 you can use default middleware for such behaviour (source):
app.UseHttpLogging();

Categories