I have many lines of code like:
private SomeOutputModel Method(SomeInputModel model)
{
_logger.LogDebug(JsonSerializer.Serialize(model));
SomeOutputModel result = DoSomething();
_logger.LogDebug(JsonSerializer.Serialize(result));
return result;
}
practically I need to log the input and output of some methods. I know I should do this in respect to AoP and I know there is some non-free library like PostSharp outside, but I need a hint to create a simple decorator pattern for my methods or delegates to give me this ability to invoke or execute these methods. Hence I would be able to inject any behavior among the functions, but I don't want to use reflection as much as possible because I assume it will make the code meaningless and comparatively slow to original one.
CodePudding user response:
One approach for accomplishing this would be to use something like MediatR, which, to quote the author, is "a low-ambition library trying to solve a simple problem — decoupling the in-process sending of messages from handling messages."
That decoupling introduces opportunities to create handlers for specific requests types that can be wrapped with a pipeline behavior at a more generic level to cover cross-cutting concerns like logging.
You might create a handler like follows:
public class SomeOutputModelHandler : IRequestHandler<SomeInputModel, SomeOutputModel>
{
private ISomeInterfaceDependency _somethingDoer;
public ModelHandler(ISomeInterfaceDependency somethingDoer)
{
_somethingDoer = somethingDoer;
}
public async Task<SomeOutputModel> Handle(SomeInputModel request, CancellationToken cancellationToken)
{
var response = await _somethingDoer.DoSomething(request);
return response;
}
}
This would be invoked by passing the model to Mediatr like: var outputModel = await mediator.Send(myInputModel);
At this point, lifting directly from the MediatR wiki, you can register a generic pipeline behavior that covers all of your handler cases that logs everything before and after execution:
https://github.com/jbogard/MediatR/wiki/Behaviors#registering-pipeline-behaviors
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger; public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) { _logger = logger; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { _logger.LogInformation($"Handling {typeof(TRequest).Name}"); var response = await next(); _logger.LogInformation($"Handled {typeof(TResponse).Name}"); return response; } }
You can see examples of this request/response behavior including pipelines in the MediatR samples at https://github.com/jbogard/MediatR/tree/master/samples/MediatR.Examples
CodePudding user response:
Without using reflection, and with the caveat that the "decoration" has to be done on method call instead of on method definition, a simple and basic solution could be:
For each generic Func or Action signature you need, create a corresponding method that would perform the Logging.
Using Serilog (or equivalent), you wouldn't have to do serialization by yourself to log the input and output objects, by leveraging the Serilog @ operator.
Serilog homepage explain briefly the @ operator use, see: https://serilog.net/
Here is how it could be done:
public static class LogUtils
{
public static TResult LogDecorate<TSource, TResult>(Func<TSource, TResult> method, TSource input)
{
Log.Information("{@input1}", input1);
var result = method(input);
Log.Information("{@result}", result);
return result;
}
public static TResult LogDecorate<TSource1, TSource2, TResult>(Func<TSource1, TSource2, TResult> Method, TSource1 input1, TSource2 input2)
{
Log.Information("{@input1} - {@input2}", input1, input2);
var result = Method(input);
Log.Information("{@result}", result);
return result;
}
// Etc...
}
Use would be:
var result = LogUtils.LogDecorate(MyMethod, input);