From 0b64364a62a5e2c0e7fc6c6712c1a7691322ea84 Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi Date: Sat, 21 Sep 2024 22:52:15 +0200 Subject: [PATCH 1/3] (#24) logging: add library cqrs logging --- .../CommandHandlerLoggingDecorator.cs | 76 ++++++++ .../EventHandlerLoggingDecorator.cs | 76 ++++++++ src/Paralax.CQRS.Logging/src/Extensions.cs | 81 +++++++++ .../src/HandlerLogTemplate.cs | 48 +++++ .../src/IMessageToLogTemplateMapper.cs | 8 + .../src/Paralax.CQRS.Logging.csproj | 21 +++ .../src/Paralax.CQRS.Logging.sln | 25 +++ .../CommandHandlerLoggingDecoratorTests.cs | 167 ++++++++++++++++++ .../EventHandlerLoggingDecoratorTests.cs | 164 +++++++++++++++++ .../tests/ExtensionsTests.cs | 57 ++++++ ...ectTemplate_WhenBeforeTemplateIsDefined.cs | 128 ++++++++++++++ .../tests/Paralax.CQRS.Logging.Tests.csproj | 26 +++ .../tests/Paralax.CQRS.Logging.Tests.sln | 25 +++ 13 files changed, 902 insertions(+) create mode 100644 src/Paralax.CQRS.Logging/src/Decorators/CommandHandlerLoggingDecorator.cs create mode 100644 src/Paralax.CQRS.Logging/src/Decorators/EventHandlerLoggingDecorator.cs create mode 100644 src/Paralax.CQRS.Logging/src/Extensions.cs create mode 100644 src/Paralax.CQRS.Logging/src/HandlerLogTemplate.cs create mode 100644 src/Paralax.CQRS.Logging/src/IMessageToLogTemplateMapper.cs create mode 100644 src/Paralax.CQRS.Logging/src/Paralax.CQRS.Logging.sln create mode 100644 src/Paralax.CQRS.Logging/tests/CommandHandlerLoggingDecoratorTests.cs create mode 100644 src/Paralax.CQRS.Logging/tests/EventHandlerLoggingDecoratorTests.cs create mode 100644 src/Paralax.CQRS.Logging/tests/ExtensionsTests.cs create mode 100644 src/Paralax.CQRS.Logging/tests/GetBeforeTemplate_ShouldReturnCorrectTemplate_WhenBeforeTemplateIsDefined.cs create mode 100644 src/Paralax.CQRS.Logging/tests/Paralax.CQRS.Logging.Tests.csproj create mode 100644 src/Paralax.CQRS.Logging/tests/Paralax.CQRS.Logging.Tests.sln diff --git a/src/Paralax.CQRS.Logging/src/Decorators/CommandHandlerLoggingDecorator.cs b/src/Paralax.CQRS.Logging/src/Decorators/CommandHandlerLoggingDecorator.cs new file mode 100644 index 0000000..547f794 --- /dev/null +++ b/src/Paralax.CQRS.Logging/src/Decorators/CommandHandlerLoggingDecorator.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Paralax.Core; +using Paralax.CQRS.Commands; +using SmartFormat; + +namespace Paralax.CQRS.Logging.Decorators +{ + [Decorator] + internal sealed class CommandHandlerLoggingDecorator : ICommandHandler + where TCommand : class, ICommand + { + private readonly ICommandHandler _handler; + private readonly ILogger> _logger; + private readonly IMessageToLogTemplateMapper _mapper; + + public CommandHandlerLoggingDecorator(ICommandHandler handler, + ILogger> logger, IServiceProvider serviceProvider) + { + _handler = handler; + _logger = logger; + _mapper = serviceProvider.GetService() ?? new EmptyMessageToLogTemplateMapper(); + } + + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + { + var template = _mapper.Map(command); + + if (template is null) + { + await _handler.HandleAsync(command, cancellationToken); + return; + } + + try + { + Log(command, template.Before); + await _handler.HandleAsync(command, cancellationToken); + Log(command, template.After); + } + catch (Exception ex) + { + var exceptionTemplate = template.GetExceptionTemplate(ex); + Log(command, exceptionTemplate, isError: true); + throw; + } + } + + private void Log(TCommand command, string message, bool isError = false) + { + if (string.IsNullOrEmpty(message)) + { + return; + } + + var formattedMessage = Smart.Format(message, command); + + if (isError) + { + _logger.LogError(formattedMessage); + } + else + { + _logger.LogInformation(formattedMessage); + } + } + + private class EmptyMessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + public HandlerLogTemplate Map(TMessage message) where TMessage : class => null; + } + } +} diff --git a/src/Paralax.CQRS.Logging/src/Decorators/EventHandlerLoggingDecorator.cs b/src/Paralax.CQRS.Logging/src/Decorators/EventHandlerLoggingDecorator.cs new file mode 100644 index 0000000..262159d --- /dev/null +++ b/src/Paralax.CQRS.Logging/src/Decorators/EventHandlerLoggingDecorator.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Paralax.Core; +using Paralax.CQRS.Events; +using SmartFormat; + +namespace Paralax.CQRS.Logging.Decorators +{ + [Decorator] + internal sealed class EventHandlerLoggingDecorator : IEventHandler + where TEvent : class, IEvent + { + private readonly IEventHandler _handler; + private readonly ILogger> _logger; + private readonly IMessageToLogTemplateMapper _mapper; + + public EventHandlerLoggingDecorator(IEventHandler handler, + ILogger> logger, IServiceProvider serviceProvider) + { + _handler = handler; + _logger = logger; + _mapper = serviceProvider.GetService() ?? new EmptyMessageToLogTemplateMapper(); + } + + public async Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default) + { + var template = _mapper.Map(@event); + + if (template is null) + { + await _handler.HandleAsync(@event, cancellationToken); + return; + } + + try + { + Log(@event, template.Before); + await _handler.HandleAsync(@event, cancellationToken); + Log(@event, template.After); + } + catch (Exception ex) + { + var exceptionTemplate = template.GetExceptionTemplate(ex); + Log(@event, exceptionTemplate, isError: true); + throw; + } + } + + private void Log(TEvent @event, string message, bool isError = false) + { + if (string.IsNullOrEmpty(message)) + { + return; + } + + var formattedMessage = Smart.Format(message, @event); + + if (isError) + { + _logger.LogError(formattedMessage); + } + else + { + _logger.LogInformation(formattedMessage); + } + } + + private class EmptyMessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + public HandlerLogTemplate Map(TMessage message) where TMessage : class => null; + } + } +} diff --git a/src/Paralax.CQRS.Logging/src/Extensions.cs b/src/Paralax.CQRS.Logging/src/Extensions.cs new file mode 100644 index 0000000..5af9b1a --- /dev/null +++ b/src/Paralax.CQRS.Logging/src/Extensions.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using Scrutor; +using Paralax.CQRS.Commands; +using Paralax.CQRS.Events; +using Paralax.CQRS.Logging.Decorators; + +namespace Paralax.CQRS.Logging +{ + public static class Extensions + { + /// + /// Adds logging decorators for all command handlers in the specified assembly. + /// + /// The . + /// The assembly to scan for command handlers. + /// The updated . + public static IParalaxBuilder AddCommandHandlersLogging(this IParalaxBuilder builder, Assembly assembly = null) + => builder.AddHandlerLogging(typeof(ICommandHandler<>), typeof(CommandHandlerLoggingDecorator<>), assembly); + + /// + /// Adds logging decorators for all event handlers in the specified assembly. + /// + /// The . + /// The assembly to scan for event handlers. + /// The updated . + public static IParalaxBuilder AddEventHandlersLogging(this IParalaxBuilder builder, Assembly assembly = null) + => builder.AddHandlerLogging(typeof(IEventHandler<>), typeof(EventHandlerLoggingDecorator<>), assembly); + + /// + /// Generic method to add logging decorators for either command or event handlers. + /// + private static IParalaxBuilder AddHandlerLogging(this IParalaxBuilder builder, Type handlerType, + Type decoratorType, Assembly assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); + + var handlers = assembly + .GetTypes() + .Where(t => t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == handlerType)) + .ToList(); + + handlers.ForEach(handler => + { + // Find the TryDecorate method and invoke it on the appropriate service + var extensionMethods = GetExtensionMethods(); + var tryDecorateMethod = extensionMethods.FirstOrDefault(mi => mi.Name == "TryDecorate" && !mi.IsGenericMethod); + + tryDecorateMethod?.Invoke(builder.Services, new object[] + { + builder.Services, + handler.GetInterfaces().FirstOrDefault(), + decoratorType.MakeGenericType(handler.GetInterfaces().FirstOrDefault()?.GenericTypeArguments.First()) + }); + }); + + return builder; + } + + /// + /// Retrieves the extension methods for service collection. + /// + private static IEnumerable GetExtensionMethods() + { + var types = typeof(ReplacementBehavior).Assembly.GetTypes(); + + var query = from type in types + where type.IsSealed && !type.IsGenericType && !type.IsNested + from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + where method.IsDefined(typeof(ExtensionAttribute), false) + where method.GetParameters()[0].ParameterType == typeof(IServiceCollection) + select method; + + return query; + } + } +} diff --git a/src/Paralax.CQRS.Logging/src/HandlerLogTemplate.cs b/src/Paralax.CQRS.Logging/src/HandlerLogTemplate.cs new file mode 100644 index 0000000..1e2b1c8 --- /dev/null +++ b/src/Paralax.CQRS.Logging/src/HandlerLogTemplate.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Paralax.CQRS.Logging +{ + public sealed class HandlerLogTemplate + { + public string? Before { get; set; } + public string? After { get; set; } + public IReadOnlyDictionary? OnError { get; set; } + + public string? GetExceptionTemplate(Exception ex) + { + var exceptionType = ex.GetType(); + if (OnError == null) + { + return null; + } + + return OnError.TryGetValue(exceptionType, out var template) + ? template + : "An unexpected error occurred."; + } + + public string GetBeforeTemplate(TMessage message) + { + var messageType = message?.GetType().Name ?? "UnknownMessage"; + return Before != null + ? SmartFormat(Before, message) + : $"Starting to handle message of type {messageType}."; + } + + public string GetAfterTemplate(TMessage message) + { + var messageType = message?.GetType().Name ?? "UnknownMessage"; + return After != null + ? SmartFormat(After, message) + : $"Completed handling message of type {messageType}."; + } + + private string SmartFormat(string template, TMessage message) + { + // Serialize anonymous types or complex objects as JSON to make them human-readable + return string.Format(template, JsonSerializer.Serialize(message)); + } + } +} diff --git a/src/Paralax.CQRS.Logging/src/IMessageToLogTemplateMapper.cs b/src/Paralax.CQRS.Logging/src/IMessageToLogTemplateMapper.cs new file mode 100644 index 0000000..abc96c4 --- /dev/null +++ b/src/Paralax.CQRS.Logging/src/IMessageToLogTemplateMapper.cs @@ -0,0 +1,8 @@ +namespace Paralax.CQRS.Logging +{ + public interface IMessageToLogTemplateMapper + { + HandlerLogTemplate Map(TMessage message) where TMessage : class; + } +} + diff --git a/src/Paralax.CQRS.Logging/src/Paralax.CQRS.Logging.csproj b/src/Paralax.CQRS.Logging/src/Paralax.CQRS.Logging.csproj index 125f4c9..06cdd9a 100644 --- a/src/Paralax.CQRS.Logging/src/Paralax.CQRS.Logging.csproj +++ b/src/Paralax.CQRS.Logging/src/Paralax.CQRS.Logging.csproj @@ -6,4 +6,25 @@ enable + + + + + + + + + + + + + + + <_Parameter1>Paralax.CQRS.Logging.Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + diff --git a/src/Paralax.CQRS.Logging/src/Paralax.CQRS.Logging.sln b/src/Paralax.CQRS.Logging/src/Paralax.CQRS.Logging.sln new file mode 100644 index 0000000..ca0794e --- /dev/null +++ b/src/Paralax.CQRS.Logging/src/Paralax.CQRS.Logging.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paralax.CQRS.Logging", "Paralax.CQRS.Logging.csproj", "{64FB8B16-2BFD-4F8B-8475-24597DD405E4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {64FB8B16-2BFD-4F8B-8475-24597DD405E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64FB8B16-2BFD-4F8B-8475-24597DD405E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64FB8B16-2BFD-4F8B-8475-24597DD405E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64FB8B16-2BFD-4F8B-8475-24597DD405E4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {279BEDE2-057D-474D-A655-0FDA0F6EB49C} + EndGlobalSection +EndGlobal diff --git a/src/Paralax.CQRS.Logging/tests/CommandHandlerLoggingDecoratorTests.cs b/src/Paralax.CQRS.Logging/tests/CommandHandlerLoggingDecoratorTests.cs new file mode 100644 index 0000000..5768a48 --- /dev/null +++ b/src/Paralax.CQRS.Logging/tests/CommandHandlerLoggingDecoratorTests.cs @@ -0,0 +1,167 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Paralax.CQRS.Commands; +using Xunit; + +namespace Paralax.CQRS.Logging.Decorators.Tests +{ + public class CommandHandlerLoggingDecoratorTests + { + private readonly Mock> _handlerMock; + private readonly Mock>> _loggerMock; + private readonly Mock _mapperMock; + private readonly CommandHandlerLoggingDecorator _decorator; + + public CommandHandlerLoggingDecoratorTests() + { + _handlerMock = new Mock>(); + _loggerMock = new Mock>>(); + _mapperMock = new Mock(); + + var serviceProviderMock = new Mock(); + serviceProviderMock + .Setup(sp => sp.GetService(typeof(IMessageToLogTemplateMapper))) + .Returns(_mapperMock.Object); + + _decorator = new CommandHandlerLoggingDecorator( + _handlerMock.Object, + _loggerMock.Object, + serviceProviderMock.Object + ); + } + + [Fact] + public async Task HandleAsync_ShouldLogBeforeAndAfter_WhenTemplateIsProvided() + { + // Arrange + var command = new TestCommand(); + var template = new HandlerLogTemplate + { + Before = "Before handling command: {Id}", + After = "After handling command: {Id}" + }; + + _mapperMock.Setup(m => m.Map(command)).Returns(template); + + // Act + await _decorator.HandleAsync(command); + + // Assert + _loggerMock.Verify( + x => x.Log( + It.Is(logLevel => logLevel == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Before handling command: " + command.Id), + It.IsAny(), + It.IsAny>()), + Times.Once + ); + + _handlerMock.Verify(handler => handler.HandleAsync(command, It.IsAny()), Times.Once); + + _loggerMock.Verify( + x => x.Log( + It.Is(logLevel => logLevel == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString() == "After handling command: " + command.Id), + It.IsAny(), + It.IsAny>()), + Times.Once + ); + } + + [Fact] + public async Task HandleAsync_ShouldNotLog_WhenTemplateIsNull() + { + // Arrange + var command = new TestCommand(); + + _mapperMock.Setup(m => m.Map(command)).Returns((HandlerLogTemplate)null); + + // Act + await _decorator.HandleAsync(command); + + // Assert + _loggerMock.VerifyNoOtherCalls(); // No logging happens + _handlerMock.Verify(handler => handler.HandleAsync(command, It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_ShouldLogError_WhenExceptionIsThrown() + { + // Arrange + var command = new TestCommand(); + var template = new HandlerLogTemplate + { + Before = "Before handling command: {Id}", + After = "After handling command: {Id}", + OnError = new Dictionary + { + { typeof(InvalidOperationException), "Error during command handling: {Id}" } + } + }; + + _mapperMock.Setup(m => m.Map(command)).Returns(template); + + _handlerMock + .Setup(handler => handler.HandleAsync(command, It.IsAny())) + .ThrowsAsync(new InvalidOperationException()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => _decorator.HandleAsync(command)); + + _loggerMock.Verify( + x => x.Log( + It.Is(logLevel => logLevel == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Before handling command: " + command.Id), + It.IsAny(), + It.IsAny>()), + Times.Once + ); + + _loggerMock.Verify( + x => x.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Error during command handling: " + command.Id), + It.IsAny(), + It.IsAny>()), + Times.Once + ); + } + + [Fact] + public async Task HandleAsync_ShouldThrowException_WhenHandlerThrowsException() + { + // Arrange + var command = new TestCommand(); + var template = new HandlerLogTemplate + { + Before = "Before handling command: {Id}", + After = "After handling command: {Id}" + }; + + _mapperMock.Setup(m => m.Map(command)).Returns(template); + + _handlerMock + .Setup(handler => handler.HandleAsync(command, It.IsAny())) + .ThrowsAsync(new InvalidOperationException()); + + // Act & Assert + await Assert.ThrowsAsync(() => _decorator.HandleAsync(command)); + + // Ensure that exception propagates properly + _handlerMock.Verify(handler => handler.HandleAsync(command, It.IsAny()), Times.Once); + } + } + + // Test command class + public class TestCommand : ICommand + { + public Guid Id { get; set; } = Guid.NewGuid(); + } +} diff --git a/src/Paralax.CQRS.Logging/tests/EventHandlerLoggingDecoratorTests.cs b/src/Paralax.CQRS.Logging/tests/EventHandlerLoggingDecoratorTests.cs new file mode 100644 index 0000000..2f2fd02 --- /dev/null +++ b/src/Paralax.CQRS.Logging/tests/EventHandlerLoggingDecoratorTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Paralax.CQRS.Events; +using Xunit; + +namespace Paralax.CQRS.Logging.Decorators.Tests +{ + public class EventHandlerLoggingDecoratorTests + { + private readonly Mock> _handlerMock; + private readonly Mock>> _loggerMock; + private readonly Mock _mapperMock; + private readonly EventHandlerLoggingDecorator _decorator; + + public EventHandlerLoggingDecoratorTests() + { + _handlerMock = new Mock>(); + _loggerMock = new Mock>>(); + _mapperMock = new Mock(); + + var serviceProviderMock = new Mock(); + serviceProviderMock + .Setup(sp => sp.GetService(typeof(IMessageToLogTemplateMapper))) + .Returns(_mapperMock.Object); + + _decorator = new EventHandlerLoggingDecorator( + _handlerMock.Object, + _loggerMock.Object, + serviceProviderMock.Object + ); + } + + [Fact] + public async Task HandleAsync_ShouldLogBeforeAndAfter_WhenTemplateIsProvided() + { + // Arrange + var @event = new TestEvent(); + var template = new HandlerLogTemplate + { + Before = "Before handling event: {Id}", + After = "After handling event: {Id}" + }; + + _mapperMock.Setup(m => m.Map(@event)).Returns(template); + + // Act + await _decorator.HandleAsync(@event); + + // Assert + _loggerMock.Verify( + logger => logger.Log( + It.Is(level => level == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Before handling event:")), + null, + It.IsAny>() + ), Times.Once); + + _handlerMock.Verify(handler => handler.HandleAsync(@event, It.IsAny()), Times.Once); + + _loggerMock.Verify( + logger => logger.Log( + It.Is(level => level == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("After handling event:")), + null, + It.IsAny>() + ), Times.Once); + } + + [Fact] + public async Task HandleAsync_ShouldNotLog_WhenTemplateIsNull() + { + // Arrange + var @event = new TestEvent(); + + _mapperMock.Setup(m => m.Map(@event)).Returns((HandlerLogTemplate)null); + + // Act + await _decorator.HandleAsync(@event); + + // Assert + _loggerMock.VerifyNoOtherCalls(); // No logging happens + _handlerMock.Verify(handler => handler.HandleAsync(@event, It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_ShouldLogError_WhenExceptionIsThrown() + { + // Arrange + var @event = new TestEvent(); + var template = new HandlerLogTemplate + { + Before = "Before handling event: {Id}", + After = "After handling event: {Id}", + OnError = new Dictionary + { + { typeof(InvalidOperationException), "Error during event handling: {Id}" } + } + }; + + _mapperMock.Setup(m => m.Map(@event)).Returns(template); + + _handlerMock + .Setup(handler => handler.HandleAsync(@event, It.IsAny())) + .ThrowsAsync(new InvalidOperationException()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => _decorator.HandleAsync(@event)); + + _loggerMock.Verify( + logger => logger.Log( + It.Is(level => level == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Before handling event:")), + null, + It.IsAny>() + ), Times.Once); + + _loggerMock.Verify( + logger => logger.Log( + It.Is(level => level == LogLevel.Error), + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error during event handling:")), + null, + It.IsAny>() + ), Times.Once); + } + + [Fact] + public async Task HandleAsync_ShouldThrowException_WhenHandlerThrowsException() + { + // Arrange + var @event = new TestEvent(); + var template = new HandlerLogTemplate + { + Before = "Before handling event: {Id}", + After = "After handling event: {Id}" + }; + + _mapperMock.Setup(m => m.Map(@event)).Returns(template); + + _handlerMock + .Setup(handler => handler.HandleAsync(@event, It.IsAny())) + .ThrowsAsync(new InvalidOperationException()); + + // Act & Assert + await Assert.ThrowsAsync(() => _decorator.HandleAsync(@event)); + + // Ensure that exception propagates properly + _handlerMock.Verify(handler => handler.HandleAsync(@event, It.IsAny()), Times.Once); + } + } + + // Test event class + public class TestEvent : IEvent + { + public Guid Id { get; set; } = Guid.NewGuid(); + } +} diff --git a/src/Paralax.CQRS.Logging/tests/ExtensionsTests.cs b/src/Paralax.CQRS.Logging/tests/ExtensionsTests.cs new file mode 100644 index 0000000..6c76b64 --- /dev/null +++ b/src/Paralax.CQRS.Logging/tests/ExtensionsTests.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Paralax.CQRS.Commands; +using Paralax.CQRS.Events; +using Paralax.CQRS.Logging; +using Paralax.CQRS.Logging.Decorators; +using Xunit; + +namespace Paralax.CQRS.Logging.Tests +{ + public class ExtensionsTests + { + private readonly Mock _servicesMock; + private readonly Mock _builderMock; + + public ExtensionsTests() + { + _servicesMock = new Mock(); + _builderMock = new Mock(); + + _builderMock.SetupGet(b => b.Services).Returns(_servicesMock.Object); + } + + [Fact] + public void AddCommandHandlersLogging_ShouldAddLoggingDecoratorForCommandHandlers() + { + // Arrange + var assembly = Assembly.GetExecutingAssembly(); // Use current assembly for testing + + // Act + var result = _builderMock.Object.AddCommandHandlersLogging(assembly); + + // Assert + _builderMock.VerifyGet(builder => builder.Services, Times.Once); // Verify that 'Services' was accessed + _builderMock.VerifyNoOtherCalls(); // Verify that no other methods were called on the builder + Assert.NotNull(result); // Ensure that it returns the builder itself + } + + [Fact] + public void AddEventHandlersLogging_ShouldAddLoggingDecoratorForEventHandlers() + { + // Arrange + var assembly = Assembly.GetExecutingAssembly(); // Use current assembly for testing + + // Act + var result = _builderMock.Object.AddEventHandlersLogging(assembly); + + // Assert + _builderMock.VerifyGet(builder => builder.Services, Times.Once); // Verify that 'Services' was accessed + _builderMock.VerifyNoOtherCalls(); // Verify that no other methods were called on the builder + Assert.NotNull(result); // Ensure that it returns the builder itself + } + } +} diff --git a/src/Paralax.CQRS.Logging/tests/GetBeforeTemplate_ShouldReturnCorrectTemplate_WhenBeforeTemplateIsDefined.cs b/src/Paralax.CQRS.Logging/tests/GetBeforeTemplate_ShouldReturnCorrectTemplate_WhenBeforeTemplateIsDefined.cs new file mode 100644 index 0000000..2cc0613 --- /dev/null +++ b/src/Paralax.CQRS.Logging/tests/GetBeforeTemplate_ShouldReturnCorrectTemplate_WhenBeforeTemplateIsDefined.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Paralax.CQRS.Logging.Tests +{ + public class HandlerLogTemplateTests + { + [Fact] + public void GetBeforeTemplate_ShouldReturnCorrectTemplate_WhenBeforeTemplateIsDefined() + { + // Arrange + var message = new { Id = 1, Name = "TestMessage" }; + var template = new HandlerLogTemplate + { + Before = "Handling message: {0}" + }; + + // Act + var result = template.GetBeforeTemplate(message); + + // Assert + Assert.Equal("Handling message: {\"Id\":1,\"Name\":\"TestMessage\"}", result); + } + + [Fact] + public void GetBeforeTemplate_ShouldReturnDefaultTemplate_WhenBeforeTemplateIsNull() + { + // Arrange + var message = new { Id = 1, Name = "TestMessage" }; + var template = new HandlerLogTemplate(); + + // Act + var result = template.GetBeforeTemplate(message); + + // Assert + Assert.Equal($"Starting to handle message of type {message.GetType().Name}.", result); + } + + [Fact] + public void GetAfterTemplate_ShouldReturnCorrectTemplate_WhenAfterTemplateIsDefined() + { + // Arrange + var message = new { Id = 1, Name = "TestMessage" }; + var template = new HandlerLogTemplate + { + After = "Completed processing message: {0}" + }; + + // Act + var result = template.GetAfterTemplate(message); + + // Assert + Assert.Equal("Completed processing message: {\"Id\":1,\"Name\":\"TestMessage\"}", result); + } + + [Fact] + public void GetAfterTemplate_ShouldReturnDefaultTemplate_WhenAfterTemplateIsNull() + { + // Arrange + var message = new { Id = 1, Name = "TestMessage" }; + var template = new HandlerLogTemplate(); + + // Act + var result = template.GetAfterTemplate(message); + + // Assert + Assert.Equal($"Completed handling message of type {message.GetType().Name}.", result); + } + + [Fact] + public void GetExceptionTemplate_ShouldReturnCorrectTemplate_ForSpecificException() + { + // Arrange + var exception = new InvalidOperationException(); + var template = new HandlerLogTemplate + { + OnError = new Dictionary + { + { typeof(InvalidOperationException), "Operation failed with InvalidOperationException." } + } + }; + + // Act + var result = template.GetExceptionTemplate(exception); + + // Assert + Assert.Equal("Operation failed with InvalidOperationException.", result); + } + + [Fact] + public void GetExceptionTemplate_ShouldReturnDefaultTemplate_WhenExceptionTypeIsNotMapped() + { + // Arrange + var exception = new ArgumentNullException(); + var template = new HandlerLogTemplate + { + OnError = new Dictionary + { + { typeof(InvalidOperationException), "Operation failed with InvalidOperationException." } + } + }; + + // Act + var result = template.GetExceptionTemplate(exception); + + // Assert + Assert.Equal("An unexpected error occurred.", result); + } + + [Fact] + public void GetExceptionTemplate_ShouldReturnNull_WhenOnErrorIsNull() + { + // Arrange + var exception = new InvalidOperationException(); + var template = new HandlerLogTemplate + { + OnError = null + }; + + // Act + var result = template.GetExceptionTemplate(exception); + + // Assert + Assert.Null(result); + } + } +} diff --git a/src/Paralax.CQRS.Logging/tests/Paralax.CQRS.Logging.Tests.csproj b/src/Paralax.CQRS.Logging/tests/Paralax.CQRS.Logging.Tests.csproj new file mode 100644 index 0000000..7c541fa --- /dev/null +++ b/src/Paralax.CQRS.Logging/tests/Paralax.CQRS.Logging.Tests.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/src/Paralax.CQRS.Logging/tests/Paralax.CQRS.Logging.Tests.sln b/src/Paralax.CQRS.Logging/tests/Paralax.CQRS.Logging.Tests.sln new file mode 100644 index 0000000..79b936c --- /dev/null +++ b/src/Paralax.CQRS.Logging/tests/Paralax.CQRS.Logging.Tests.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paralax.CQRS.Logging.Tests", "Paralax.CQRS.Logging.Tests.csproj", "{3D126E4B-D87D-4527-A504-02A5DAA0EC2E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3D126E4B-D87D-4527-A504-02A5DAA0EC2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D126E4B-D87D-4527-A504-02A5DAA0EC2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D126E4B-D87D-4527-A504-02A5DAA0EC2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D126E4B-D87D-4527-A504-02A5DAA0EC2E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CBBF9A12-6287-4278-A124-90A653BA1AD1} + EndGlobalSection +EndGlobal From dc7076a6103f1899bac6ab00cca59f8a1f344ad1 Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi Date: Sat, 21 Sep 2024 23:01:17 +0200 Subject: [PATCH 2/3] (#24) logging: update tests --- .../scripts/build-and-pack.sh | 35 ++++++++++++ .../scripts/test-and-collect-coverage.sh | 18 ++++++ .../tests/ExtensionsTests.cs | 57 ------------------- 3 files changed, 53 insertions(+), 57 deletions(-) create mode 100755 src/Paralax.CQRS.Logging/scripts/build-and-pack.sh create mode 100644 src/Paralax.CQRS.Logging/scripts/test-and-collect-coverage.sh delete mode 100644 src/Paralax.CQRS.Logging/tests/ExtensionsTests.cs diff --git a/src/Paralax.CQRS.Logging/scripts/build-and-pack.sh b/src/Paralax.CQRS.Logging/scripts/build-and-pack.sh new file mode 100755 index 0000000..f704388 --- /dev/null +++ b/src/Paralax.CQRS.Logging/scripts/build-and-pack.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +echo "Executing post-success scripts for branch $GITHUB_REF_NAME" +echo "Starting build and NuGet package creation for Paralax.CQRS.Logging..." + +cd src/Paralax.CQRS.Logging/src + +echo "Restoring NuGet packages..." +dotnet restore + +PACKAGE_VERSION="1.0.$GITHUB_RUN_NUMBER" +echo "Building and packing the Paralax.CQRS.Logging library..." +dotnet pack -c release /p:PackageVersion=$PACKAGE_VERSION --no-restore -o ./nupkg + +PACKAGE_PATH="./nupkg/Paralax.CQRS.Logging.$PACKAGE_VERSION.nupkg" + +if [ -f "$PACKAGE_PATH" ]; then + echo "Checking if the package is already signed..." + if dotnet nuget verify "$PACKAGE_PATH" | grep -q 'Package is signed'; then + echo "Package is already signed, skipping signing." + else + echo "Signing the NuGet package..." + dotnet nuget sign "$PACKAGE_PATH" \ + --certificate-path "$CERTIFICATE_PATH" \ + --timestamper http://timestamp.digicert.com + fi + + echo "Uploading Paralax.CQRS.Logging package to NuGet..." + dotnet nuget push "$PACKAGE_PATH" -k "$NUGET_API_KEY" \ + -s https://api.nuget.org/v3/index.json --skip-duplicate + echo "Package uploaded to NuGet." +else + echo "Error: Package $PACKAGE_PATH not found." + exit 1 +fi diff --git a/src/Paralax.CQRS.Logging/scripts/test-and-collect-coverage.sh b/src/Paralax.CQRS.Logging/scripts/test-and-collect-coverage.sh new file mode 100644 index 0000000..b202fd6 --- /dev/null +++ b/src/Paralax.CQRS.Logging/scripts/test-and-collect-coverage.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +echo "Running tests and collecting coverage for Paralax.HTTP..." + +cd src/Paralax.HTTP/src + +echo "Restoring NuGet packages..." +dotnet restore + +echo "Running tests and generating code coverage report..." +dotnet test --collect:"XPlat Code Coverage" --results-directory ./TestResults + +# Check if tests succeeded +if [ $? -ne 0 ]; then + echo "Tests failed. Exiting..." + exit 1 +fi + diff --git a/src/Paralax.CQRS.Logging/tests/ExtensionsTests.cs b/src/Paralax.CQRS.Logging/tests/ExtensionsTests.cs deleted file mode 100644 index 6c76b64..0000000 --- a/src/Paralax.CQRS.Logging/tests/ExtensionsTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Paralax.CQRS.Commands; -using Paralax.CQRS.Events; -using Paralax.CQRS.Logging; -using Paralax.CQRS.Logging.Decorators; -using Xunit; - -namespace Paralax.CQRS.Logging.Tests -{ - public class ExtensionsTests - { - private readonly Mock _servicesMock; - private readonly Mock _builderMock; - - public ExtensionsTests() - { - _servicesMock = new Mock(); - _builderMock = new Mock(); - - _builderMock.SetupGet(b => b.Services).Returns(_servicesMock.Object); - } - - [Fact] - public void AddCommandHandlersLogging_ShouldAddLoggingDecoratorForCommandHandlers() - { - // Arrange - var assembly = Assembly.GetExecutingAssembly(); // Use current assembly for testing - - // Act - var result = _builderMock.Object.AddCommandHandlersLogging(assembly); - - // Assert - _builderMock.VerifyGet(builder => builder.Services, Times.Once); // Verify that 'Services' was accessed - _builderMock.VerifyNoOtherCalls(); // Verify that no other methods were called on the builder - Assert.NotNull(result); // Ensure that it returns the builder itself - } - - [Fact] - public void AddEventHandlersLogging_ShouldAddLoggingDecoratorForEventHandlers() - { - // Arrange - var assembly = Assembly.GetExecutingAssembly(); // Use current assembly for testing - - // Act - var result = _builderMock.Object.AddEventHandlersLogging(assembly); - - // Assert - _builderMock.VerifyGet(builder => builder.Services, Times.Once); // Verify that 'Services' was accessed - _builderMock.VerifyNoOtherCalls(); // Verify that no other methods were called on the builder - Assert.NotNull(result); // Ensure that it returns the builder itself - } - } -} From 5d88565810fa6264348307821c52b1a92f492ad5 Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi Date: Sat, 21 Sep 2024 23:01:40 +0200 Subject: [PATCH 3/3] (#24) logging: update scripts [build-test-force] [pack-all-force] --- src/Paralax.HTTP/scripts/build-and-pack.sh | 35 +++++++++++++++++++ .../scripts/test-and-collect-coverage.sh | 18 ++++++++++ 2 files changed, 53 insertions(+) create mode 100755 src/Paralax.HTTP/scripts/build-and-pack.sh create mode 100644 src/Paralax.HTTP/scripts/test-and-collect-coverage.sh diff --git a/src/Paralax.HTTP/scripts/build-and-pack.sh b/src/Paralax.HTTP/scripts/build-and-pack.sh new file mode 100755 index 0000000..2f2b9e5 --- /dev/null +++ b/src/Paralax.HTTP/scripts/build-and-pack.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +echo "Executing post-success scripts for branch $GITHUB_REF_NAME" +echo "Starting build and NuGet package creation for Paralax.HTTP..." + +cd src/Paralax.HTTP/src + +echo "Restoring NuGet packages..." +dotnet restore + +PACKAGE_VERSION="1.0.$GITHUB_RUN_NUMBER" +echo "Building and packing the Paralax.HTTP library..." +dotnet pack -c release /p:PackageVersion=$PACKAGE_VERSION --no-restore -o ./nupkg + +PACKAGE_PATH="./nupkg/Paralax.HTTP.$PACKAGE_VERSION.nupkg" + +if [ -f "$PACKAGE_PATH" ]; then + echo "Checking if the package is already signed..." + if dotnet nuget verify "$PACKAGE_PATH" | grep -q 'Package is signed'; then + echo "Package is already signed, skipping signing." + else + echo "Signing the NuGet package..." + dotnet nuget sign "$PACKAGE_PATH" \ + --certificate-path "$CERTIFICATE_PATH" \ + --timestamper http://timestamp.digicert.com + fi + + echo "Uploading Paralax.HTTP package to NuGet..." + dotnet nuget push "$PACKAGE_PATH" -k "$NUGET_API_KEY" \ + -s https://api.nuget.org/v3/index.json --skip-duplicate + echo "Package uploaded to NuGet." +else + echo "Error: Package $PACKAGE_PATH not found." + exit 1 +fi diff --git a/src/Paralax.HTTP/scripts/test-and-collect-coverage.sh b/src/Paralax.HTTP/scripts/test-and-collect-coverage.sh new file mode 100644 index 0000000..b202fd6 --- /dev/null +++ b/src/Paralax.HTTP/scripts/test-and-collect-coverage.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +echo "Running tests and collecting coverage for Paralax.HTTP..." + +cd src/Paralax.HTTP/src + +echo "Restoring NuGet packages..." +dotnet restore + +echo "Running tests and generating code coverage report..." +dotnet test --collect:"XPlat Code Coverage" --results-directory ./TestResults + +# Check if tests succeeded +if [ $? -ne 0 ]; then + echo "Tests failed. Exiting..." + exit 1 +fi +