Skip to content

Commit

Permalink
Merge pull request #86 from wemogy/85-support-commandquery-definition…
Browse files Browse the repository at this point in the history
…s-across-multiple-assemblies

feat: #85 added support for commands and queries across multiple asse…
  • Loading branch information
SebastianKuesters authored Jun 3, 2024
2 parents b50d0ee + 4a57a02 commit 3e1b3c6
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 125 deletions.
205 changes: 110 additions & 95 deletions src/Wemogy.Cqrs.sln

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Wemogy.CQRS.Commands.Abstractions;

namespace Wemogy.CQRS.UnitTests.AssemblyA.Commands;

public class TrackUserActivityCommand : ICommand
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Wemogy.CQRS.Queries.Abstractions;

namespace Wemogy.CQRS.UnitTests.AssemblyA.Queries;

public class GetUserActivityQuery : IQuery<int>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Wemogy.CQRS\Wemogy.CQRS.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Wemogy.CQRS.Commands.Abstractions;
using Wemogy.CQRS.UnitTests.AssemblyA.Commands;
using Wemogy.CQRS.UnitTests.TestApplication;
using Wemogy.CQRS.UnitTests.TestApplication.Commands.CreateUser;
using Wemogy.CQRS.UnitTests.TestApplication.Commands.TrackUserActivity;
using Wemogy.CQRS.UnitTests.TestApplication.Commands.TrackUserLogin;
using Xunit;

Expand Down Expand Up @@ -48,4 +50,21 @@ public async Task RunAsync_ShouldReturnSupportVoidCommands()
// Assert
exception.Should().BeNull();
}

[Fact]
public async Task RunAsync_ShouldSupportMultipleAssemblyDefinitions()
{
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddTestApplication();
var serviceProvider = serviceCollection.BuildServiceProvider();
var commandsMediator = serviceProvider.GetRequiredService<ICommands>();
var createUserCommand = new TrackUserActivityCommand();

// Act
await commandsMediator.RunAsync(createUserCommand);

// Assert
TrackUserActivityCommandHandler.CalledCount.Should().Be(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.DependencyInjection;
using Wemogy.Core.Errors.Exceptions;
using Wemogy.CQRS.Queries.Abstractions;
using Wemogy.CQRS.UnitTests.AssemblyA.Queries;
using Wemogy.CQRS.UnitTests.TestApplication;
using Wemogy.CQRS.UnitTests.TestApplication.Queries.GetUser;
using Xunit;
Expand Down Expand Up @@ -68,4 +69,21 @@ public async Task QueryAsync_InvalidQueryParam_ShouldThrows()
.And.BeOfType<FluentValidation.ValidationException>()
.Which.Message.Should().Contain(nameof(GetUserQuery.FirstName));
}

[Fact]
public async Task QueryAsync_ShouldSupportMultipleAssemblyDefinitions()
{
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddTestApplication();
var serviceProvider = serviceCollection.BuildServiceProvider();
var queries = serviceProvider.GetRequiredService<IQueries>();
var getUserQuery = new GetUserActivityQuery();

// Act
var userActivity = await queries.QueryAsync(getUserQuery);

// Assert
userActivity.Should().Be(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Threading.Tasks;
using Wemogy.CQRS.Commands.Abstractions;
using Wemogy.CQRS.UnitTests.AssemblyA.Commands;

namespace Wemogy.CQRS.UnitTests.TestApplication.Commands.TrackUserActivity;

public class TrackUserActivityCommandHandler : ICommandHandler<TrackUserActivityCommand>
{
public static int CalledCount { get; private set; }
public Task HandleAsync(TrackUserActivityCommand command)
{
CalledCount++;
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Wemogy.CQRS.Setup;
using Wemogy.CQRS.UnitTests.AssemblyA.Commands;
using Wemogy.CQRS.UnitTests.TestApplication.Common.Contexts;

namespace Wemogy.CQRS.UnitTests.TestApplication;
Expand All @@ -19,7 +20,8 @@ public static CQRSSetupEnvironment AddTestApplication(this IServiceCollection se
new List<Assembly>()
{
Assembly.GetCallingAssembly(),
Assembly.GetExecutingAssembly()
Assembly.GetExecutingAssembly(),
typeof(TrackUserActivityCommand).Assembly
},
new Dictionary<Type, Type>()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using Wemogy.CQRS.Queries.Abstractions;
using Wemogy.CQRS.UnitTests.AssemblyA.Queries;

namespace Wemogy.CQRS.UnitTests.TestApplication.Queries.GetUserActivity;

public class GetUserActivityQueryHandler : IQueryHandler<GetUserActivityQuery, int>
{
public Task<int> HandleAsync(GetUserActivityQuery query, CancellationToken cancellationToken)
{
return Task.FromResult(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@

<ItemGroup>
<ProjectReference Include="../Wemogy.CQRS/Wemogy.CQRS.csproj" />
<ProjectReference Include="..\Wemogy.CQRS.UnitTests.AssemblyA\Wemogy.CQRS.UnitTests.AssemblyA.csproj" />
</ItemGroup>
</Project>
56 changes: 31 additions & 25 deletions src/core/Wemogy.CQRS/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public static CQRSSetupEnvironment AddCQRS(
List<Assembly> assemblies,
Dictionary<Type, Type>? dependencies = null)
{
// remove duplicates from assemblies, which can happen, if Assembly.GetCallingAssembly() and Assembly.GetExecutingAssembly() are both added
assemblies = assemblies.Distinct().ToList();

dependencies ??= new Dictionary<Type, Type>();
serviceCollection.AddCommands(assemblies, dependencies);
serviceCollection.AddQueries(assemblies);
Expand All @@ -60,7 +63,6 @@ private static void AddCommands(this IServiceCollection serviceCollection, List<

foreach (var commandType in commandTypes)
{
var assembly = commandType.Assembly;
if (!commandType.InheritsOrImplements(typeof(ICommand<>), out Type? genericCommandType) || genericCommandType == null)
{
if (!commandType.InheritsOrImplements(typeof(ICommand), out genericCommandType) || genericCommandType == null)
Expand All @@ -72,27 +74,27 @@ private static void AddCommands(this IServiceCollection serviceCollection, List<
var resultType = genericCommandType.GenericTypeArguments.ElementAtOrDefault(0);

// pre-processing
serviceCollection.AddPreProcessing(assembly, commandType);
serviceCollection.AddPreProcessing(assemblies, commandType);

if (resultType == null)
{
// command handler
serviceCollection.AddScopedGenericTypeWithImplementationFromAssembly(
assembly,
assemblies,
typeof(ICommandHandler<>),
commandType);

// command runners
serviceCollection.AddCommandRunners(commandType);

// post-processing
serviceCollection.AddPostProcessing(assembly, commandType);
serviceCollection.AddPostProcessing(assemblies, commandType);
}
else
{
// command handler
serviceCollection.AddScopedGenericTypeWithImplementationFromAssembly(
assembly,
assemblies,
typeof(ICommandHandler<,>),
commandType,
resultType);
Expand All @@ -101,14 +103,15 @@ private static void AddCommands(this IServiceCollection serviceCollection, List<
serviceCollection.AddCommandRunners(commandType, resultType);

// post-processing
serviceCollection.AddPostProcessing(assembly, commandType, resultType);
serviceCollection.AddPostProcessing(assemblies, commandType, resultType);
}
}

// ScheduledCommandDependencyResolver
serviceCollection.AddSingleton(
new ScheduledCommandDependencies(dependencies));
serviceCollection.AddScoped<IScheduledCommandDependencyResolver>(provider =>
serviceCollection.AddScoped<IScheduledCommandDependencyResolver>(
provider =>
new ScheduledCommandDependencyResolver(provider, dependencies));

// Add ICommands mediator
Expand All @@ -126,7 +129,6 @@ private static void AddQueries(this IServiceCollection serviceCollection, List<A

foreach (var queryType in queryTypes)
{
var assembly = queryType.Assembly;
if (!queryType.InheritsOrImplements(typeof(IQuery<>), out Type? genericQueryType) || genericQueryType == null)
{
throw new Exception("Query type must inherit from IQuery<>");
Expand All @@ -135,14 +137,14 @@ private static void AddQueries(this IServiceCollection serviceCollection, List<A
var resultType = genericQueryType.GenericTypeArguments[0];

// validators
serviceCollection.AddImplementationCollection(assembly, queryType, typeof(IQueryValidator<>));
serviceCollection.AddImplementationCollection(assemblies, queryType, typeof(IQueryValidator<>));

// authorization
serviceCollection.AddImplementationCollection(assembly, queryType, typeof(IQueryAuthorization<>));
serviceCollection.AddImplementationCollection(assemblies, queryType, typeof(IQueryAuthorization<>));

// handlers
var queryHandlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, resultType);
var queryHandlers = assembly.GetClassTypesWhichImplementInterface(queryHandlerType);
var queryHandlers = assemblies.GetClassTypesWhichImplementInterface(queryHandlerType);
if (queryHandlers.Count != 1)
{
throw new Exception(
Expand Down Expand Up @@ -185,31 +187,31 @@ private static void AddImplementation(

private static void AddPreProcessing(
this IServiceCollection serviceCollection,
Assembly assembly,
List<Assembly> assemblies,
Type commandType)
{
// validators
serviceCollection.AddImplementationCollection(assembly, commandType, typeof(ICommandValidator<>));
serviceCollection.AddImplementationCollection(assemblies, commandType, typeof(ICommandValidator<>));

// authorization
serviceCollection.AddImplementationCollection(assembly, commandType, typeof(ICommandAuthorization<>));
serviceCollection.AddImplementationCollection(assemblies, commandType, typeof(ICommandAuthorization<>));

// pre-processors
serviceCollection.AddImplementationCollection(assembly, commandType, typeof(ICommandPreProcessor<>));
serviceCollection.AddImplementationCollection(assemblies, commandType, typeof(ICommandPreProcessor<>));

// PreProcessingRunner
serviceCollection.AddImplementation(typeof(PreProcessingRunner<>), commandType);
}

private static void AddPostProcessing(
this IServiceCollection serviceCollection,
Assembly assembly,
List<Assembly> assemblies,
Type commandType,
Type resultType)
{
// validators
serviceCollection.AddImplementationCollection(
assembly,
assemblies,
commandType,
resultType,
typeof(ICommandPostProcessor<,>));
Expand All @@ -220,12 +222,12 @@ private static void AddPostProcessing(

private static void AddPostProcessing(
this IServiceCollection serviceCollection,
Assembly assembly,
List<Assembly> assemblies,
Type commandType)
{
// validators
serviceCollection.AddImplementationCollection(
assembly,
assemblies,
commandType,
typeof(ICommandPostProcessor<>));

Expand Down Expand Up @@ -282,14 +284,16 @@ private static void AddCommandRunners(

private static void AddImplementationCollection(
this IServiceCollection serviceCollection,
Assembly assembly,
List<Assembly> assemblies,
Type commandType,
Type genericInterfaceType)
{
var interfaceType = genericInterfaceType.MakeGenericType(commandType);
var implementationTypes = assembly.GetClassTypesWhichImplementInterface(interfaceType);
var implementationTypes = assemblies.GetClassTypesWhichImplementInterface(interfaceType);
var implementationCollectionType = typeof(List<>).MakeGenericType(interfaceType);
serviceCollection.AddScoped(implementationCollectionType, serviceProvider =>
serviceCollection.AddScoped(
implementationCollectionType,
serviceProvider =>
{
var implementationInstances =
implementationTypes
Expand All @@ -301,15 +305,17 @@ private static void AddImplementationCollection(

private static void AddImplementationCollection(
this IServiceCollection serviceCollection,
Assembly assembly,
List<Assembly> assemblies,
Type commandType,
Type resultType,
Type genericInterfaceType)
{
var interfaceType = genericInterfaceType.MakeGenericType(commandType, resultType);
var implementationTypes = assembly.GetClassTypesWhichImplementInterface(interfaceType);
var implementationTypes = assemblies.GetClassTypesWhichImplementInterface(interfaceType);
var implementationCollectionType = typeof(List<>).MakeGenericType(interfaceType);
serviceCollection.AddScoped(implementationCollectionType, serviceProvider =>
serviceCollection.AddScoped(
implementationCollectionType,
serviceProvider =>
{
var implementationInstances =
implementationTypes
Expand Down
13 changes: 9 additions & 4 deletions src/core/Wemogy.CQRS/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Wemogy.Core.Errors;
using Wemogy.Core.Extensions;

namespace Wemogy.CQRS.Extensions;
Expand All @@ -18,16 +21,18 @@ public static void AddScopedGenericType(

public static void AddScopedGenericTypeWithImplementationFromAssembly(
this IServiceCollection serviceCollection,
Assembly assembly,
List<Assembly> assemblies,
Type genericType,
params Type[] genericTypeArguments)
{
var serviceType = genericType.MakeGenericType(genericTypeArguments);
var serviceImplementations = assembly.GetClassTypesWhichImplementInterface(serviceType);
var serviceImplementations = assemblies.GetClassTypesWhichImplementInterface(serviceType);

if (serviceImplementations.Count != 1)
{
throw new Exception(
$"There must be exactly one {serviceType.FullName} registered in {assembly.FullName}.");
throw Error.Unexpected(
"InvalidServiceImplementation",
$"There must be exactly one {serviceType.FullName} declared in the assemblies.");
}

var serviceImplementationType = serviceImplementations[0];
Expand Down

0 comments on commit 3e1b3c6

Please sign in to comment.