Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Odin.sln
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.Testing.NUnitUtility",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.Patterns.Commands.Abstractions", "Patterns\Commands.Abstractions\Odin.Patterns.Commands.Abstractions.csproj", "{7749636C-2D34-4E0B-81C9-9E89C5630858}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Odin.Patterns.Queries.Abstractions", "Patterns\Queries.Abstractions\Odin.Patterns.Queries.Abstractions.csproj", "{6B3B86A4-CB53-4E25-8681-31245BA33961}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -280,6 +282,10 @@ Global
{7749636C-2D34-4E0B-81C9-9E89C5630858}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7749636C-2D34-4E0B-81C9-9E89C5630858}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7749636C-2D34-4E0B-81C9-9E89C5630858}.Release|Any CPU.Build.0 = Release|Any CPU
{6B3B86A4-CB53-4E25-8681-31245BA33961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B3B86A4-CB53-4E25-8681-31245BA33961}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B3B86A4-CB53-4E25-8681-31245BA33961}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B3B86A4-CB53-4E25-8681-31245BA33961}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CE323D9C-635B-EFD3-5B3F-7CE371D8A86A} = {BF440C74-E223-3CBF-8FA7-83F7D164F7C3}
Expand Down Expand Up @@ -320,5 +326,6 @@ Global
{1491A6A6-ED85-4AD0-A6EE-57F4CAF3192B} = {605A7674-8EA4-458D-9FEB-A86C927AC0F0}
{46BCDE44-FA4E-4AE0-A6C0-9AFAE1BE09DF} = {51D0FF83-33F9-490F-A0CB-A32DC1CC48AF}
{7749636C-2D34-4E0B-81C9-9E89C5630858} = {605A7674-8EA4-458D-9FEB-A86C927AC0F0}
{6B3B86A4-CB53-4E25-8681-31245BA33961} = {605A7674-8EA4-458D-9FEB-A86C927AC0F0}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ namespace Odin.Patterns.Queries;

/// <summary>
/// Defines a query request that returns
/// one or other for of data or a result.
/// one or other form of data or a result.
/// </summary>
/// <typeparam name="TResult"></typeparam>
public interface IQuery<out TResult> { }

21 changes: 21 additions & 0 deletions Patterns/Queries.Abstractions/IQueryDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Odin.Patterns.Queries;

/// <summary>
/// Dispatches queries to their registered query handlers.
/// </summary>
public interface IQueryDispatcher
{
/// <summary>
/// Dispatches a query that returns a result
/// to its registered query handler.
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TQueryResult"></typeparam>
/// <param name="query"></param>
/// <param name="ct"></param>
/// <returns></returns>
Task<TQueryResult> DispatchAsync<TQuery, TQueryResult>(
TQuery query,
CancellationToken ct = default)
where TQuery : IQuery<TQueryResult>;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
namespace Odin.Patterns.Queries;

/// <summary>
/// Defines the handling implementation for a command request that returns a Result.
/// Defines the handling implementation for a query request that returns a result.
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TQueryResult"></typeparam>
public interface IQueryHandler<in TQuery, TQueryResult>
public interface IQueryHandler<in TQuery, TQueryResult>
where TQuery : IQuery<TQueryResult>
{
/// <summary>
Expand All @@ -15,4 +15,4 @@ public interface IQueryHandler<in TQuery, TQueryResult>
/// <param name="ct"></param>
/// <returns></returns>
Task<TQueryResult> HandleAsync(TQuery query, CancellationToken ct = default);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RootNamespace>Odin.Patterns.Queries</RootNamespace>
<Description>All abstractions for the query dispatch pattern.

</Description>
<WarningsAsErrors>1591;1573;</WarningsAsErrors> <!-- Not to be removed. Documentation is required. -->
</PropertyGroup>

<ItemGroup>
<None Include="../../Assets/icon.png" Pack="true" PackagePath=""/>
<None Include="..\README.md" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>
32 changes: 32 additions & 0 deletions Patterns/Queries/FakeQueryHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Odin.Patterns.Queries;

/// <summary>
/// Returns the result passed in via the constructor.
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TResult"></typeparam>
public class FakeQueryHandler<TQuery, TResult> : IQueryHandler<TQuery, TResult>
where TQuery : IQuery<TResult>
{
private readonly TResult _resultToReturn;

/// <summary>
/// Initialise to return 'result' on HandleAsync.
/// </summary>
/// <param name="result"></param>
public FakeQueryHandler(TResult result)
{
_resultToReturn = result;
}

/// <summary>
/// Does nothing.
/// </summary>
/// <param name="query"></param>
/// <param name="ct"></param>
/// <exception cref="NotImplementedException"></exception>
public async Task<TResult> HandleAsync(TQuery query, CancellationToken ct = default)
{
return await Task.FromResult(_resultToReturn);
}
}
13 changes: 12 additions & 1 deletion Patterns/Queries/Odin.Patterns.Queries.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,23 @@
<Nullable>enable</Nullable>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Description>
<Description>Implementation of a query dispatch pattern.

</Description>
<WarningsAsErrors>1591;1573;</WarningsAsErrors> <!-- Not to be removed. Documentation is required. -->
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\DesignContracts\Core\Odin.DesignContracts.csproj" />
<ProjectReference Include="..\..\Logging\Core\Odin.Logging.csproj" />
<ProjectReference Include="..\Queries.Abstractions\Odin.Patterns.Queries.Abstractions.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="../../Assets/icon.png" Pack="true" PackagePath=""/>
<None Include="..\README.md" Pack="true" PackagePath=""/>
Expand Down
105 changes: 105 additions & 0 deletions Patterns/Queries/ServiceProviderQueryDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using Microsoft.Extensions.DependencyInjection;
using Odin.DesignContracts;
using Odin.Logging;

namespace Odin.Patterns.Queries;

/// <summary>
/// Dispatches queries by resolving their matching query handler from an <see cref="IServiceProvider"/>.
/// </summary>
public sealed class ServiceProviderQueryDispatcher : IQueryDispatcher
{
private readonly IServiceProvider _serviceProvider;
private readonly ILoggerWrapper<ServiceProviderQueryDispatcher> _logger;

/// <summary>
/// Creates a new dispatcher.
/// </summary>
/// <param name="serviceProvider">The service provider used to resolve query handlers.</param>
/// <param name="logger">The logger used to record dispatch activity.</param>
public ServiceProviderQueryDispatcher(
IServiceProvider serviceProvider,
ILoggerWrapper<ServiceProviderQueryDispatcher> logger)
{
Precondition.RequiresNotNull(serviceProvider);
Precondition.RequiresNotNull(logger);

_serviceProvider = serviceProvider;
_logger = logger;
}

/// <inheritdoc />
public async Task<TQueryResult> DispatchAsync<TQuery, TQueryResult>(
TQuery query,
CancellationToken ct = default)
where TQuery : IQuery<TQueryResult>
{
Precondition.RequiresNotNull(query);

Type queryType = typeof(TQuery);
Type handlerInterfaceType = typeof(IQueryHandler<TQuery, TQueryResult>);

_logger.LogDebug(
"Dispatching query {QueryType} using handler interface {HandlerInterface}.",
FormatTypeName(queryType),
FormatTypeName(handlerInterfaceType));

IQueryHandler<TQuery, TQueryResult> handler =
ResolveHandler<IQueryHandler<TQuery, TQueryResult>>(queryType, handlerInterfaceType);

try
{
TQueryResult result = await handler.HandleAsync(query, ct).ConfigureAwait(false);

_logger.LogDebug(
"Query {QueryType} completed successfully using handler {HandlerType}.",
FormatTypeName(queryType),
FormatTypeName(handler.GetType()));

return result;
}
catch (OperationCanceledException ex) when (ct.IsCancellationRequested)
{
_logger.LogDebug(
ex,
"Query {QueryType} was cancelled while executing handler {HandlerType}.",
FormatTypeName(queryType),
FormatTypeName(handler.GetType()));
throw;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Query {QueryType} failed while executing handler {HandlerType}.",
FormatTypeName(queryType),
FormatTypeName(handler.GetType()));
throw;
}
}

private THandler ResolveHandler<THandler>(Type queryType, Type handlerInterfaceType)
where THandler : class
{
THandler[] handlers = _serviceProvider.GetServices<THandler>().ToArray();

if (handlers.Length == 1)
{
return handlers[0];
}

string message = handlers.Length == 0
? $"No query handler was registered for query type '{FormatTypeName(queryType)}'. " +
$"Expected handler interface: '{FormatTypeName(handlerInterfaceType)}'."
: $"Multiple query handlers were registered for query type '{FormatTypeName(queryType)}'. " +
$"Expected handler interface: '{FormatTypeName(handlerInterfaceType)}'. Found {handlers.Length} registrations.";

_logger.LogError(message);
throw new InvalidOperationException(message);
}

private static string FormatTypeName(Type type)
{
return type.FullName ?? type.Name;
}
}
Loading