diff --git a/Odin.sln b/Odin.sln
index 7a01577..97aaa17 100644
--- a/Odin.sln
+++ b/Odin.sln
@@ -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
@@ -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}
@@ -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
diff --git a/Patterns/Queries/IQuery.cs b/Patterns/Queries.Abstractions/IQuery.cs
similarity index 81%
rename from Patterns/Queries/IQuery.cs
rename to Patterns/Queries.Abstractions/IQuery.cs
index 956eea9..36b1281 100644
--- a/Patterns/Queries/IQuery.cs
+++ b/Patterns/Queries.Abstractions/IQuery.cs
@@ -2,8 +2,7 @@ namespace Odin.Patterns.Queries;
///
/// Defines a query request that returns
-/// one or other for of data or a result.
+/// one or other form of data or a result.
///
///
public interface IQuery { }
-
diff --git a/Patterns/Queries.Abstractions/IQueryDispatcher.cs b/Patterns/Queries.Abstractions/IQueryDispatcher.cs
new file mode 100644
index 0000000..277f0ea
--- /dev/null
+++ b/Patterns/Queries.Abstractions/IQueryDispatcher.cs
@@ -0,0 +1,21 @@
+namespace Odin.Patterns.Queries;
+
+///
+/// Dispatches queries to their registered query handlers.
+///
+public interface IQueryDispatcher
+{
+ ///
+ /// Dispatches a query that returns a result
+ /// to its registered query handler.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task DispatchAsync(
+ TQuery query,
+ CancellationToken ct = default)
+ where TQuery : IQuery;
+}
diff --git a/Patterns/Queries/IQueryHandler.cs b/Patterns/Queries.Abstractions/IQueryHandler.cs
similarity index 76%
rename from Patterns/Queries/IQueryHandler.cs
rename to Patterns/Queries.Abstractions/IQueryHandler.cs
index b82db74..732614b 100644
--- a/Patterns/Queries/IQueryHandler.cs
+++ b/Patterns/Queries.Abstractions/IQueryHandler.cs
@@ -1,11 +1,11 @@
namespace Odin.Patterns.Queries;
///
-/// Defines the handling implementation for a command request that returns a Result.
+/// Defines the handling implementation for a query request that returns a result.
///
///
///
-public interface IQueryHandler
+public interface IQueryHandler
where TQuery : IQuery
{
///
@@ -15,4 +15,4 @@ public interface IQueryHandler
///
///
Task HandleAsync(TQuery query, CancellationToken ct = default);
-}
\ No newline at end of file
+}
diff --git a/Patterns/Queries.Abstractions/Odin.Patterns.Queries.Abstractions.csproj b/Patterns/Queries.Abstractions/Odin.Patterns.Queries.Abstractions.csproj
new file mode 100644
index 0000000..f79aec0
--- /dev/null
+++ b/Patterns/Queries.Abstractions/Odin.Patterns.Queries.Abstractions.csproj
@@ -0,0 +1,19 @@
+
+
+ net8.0;net9.0;net10.0
+ true
+ enable
+ icon.png
+ README.md
+ Odin.Patterns.Queries
+ All abstractions for the query dispatch pattern.
+
+
+ 1591;1573;
+
+
+
+
+
+
+
diff --git a/Patterns/Queries/FakeQueryHandler.cs b/Patterns/Queries/FakeQueryHandler.cs
new file mode 100644
index 0000000..177f4fc
--- /dev/null
+++ b/Patterns/Queries/FakeQueryHandler.cs
@@ -0,0 +1,32 @@
+namespace Odin.Patterns.Queries;
+
+///
+/// Returns the result passed in via the constructor.
+///
+///
+///
+public class FakeQueryHandler : IQueryHandler
+ where TQuery : IQuery
+{
+ private readonly TResult _resultToReturn;
+
+ ///
+ /// Initialise to return 'result' on HandleAsync.
+ ///
+ ///
+ public FakeQueryHandler(TResult result)
+ {
+ _resultToReturn = result;
+ }
+
+ ///
+ /// Does nothing.
+ ///
+ ///
+ ///
+ ///
+ public async Task HandleAsync(TQuery query, CancellationToken ct = default)
+ {
+ return await Task.FromResult(_resultToReturn);
+ }
+}
\ No newline at end of file
diff --git a/Patterns/Queries/Odin.Patterns.Queries.csproj b/Patterns/Queries/Odin.Patterns.Queries.csproj
index 746cd50..2d71dcd 100644
--- a/Patterns/Queries/Odin.Patterns.Queries.csproj
+++ b/Patterns/Queries/Odin.Patterns.Queries.csproj
@@ -5,12 +5,23 @@
enable
icon.png
README.md
-
+ Implementation of a query dispatch pattern.
1591;1573;
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Patterns/Queries/ServiceProviderQueryDispatcher.cs b/Patterns/Queries/ServiceProviderQueryDispatcher.cs
new file mode 100644
index 0000000..ec0f5be
--- /dev/null
+++ b/Patterns/Queries/ServiceProviderQueryDispatcher.cs
@@ -0,0 +1,105 @@
+using Microsoft.Extensions.DependencyInjection;
+using Odin.DesignContracts;
+using Odin.Logging;
+
+namespace Odin.Patterns.Queries;
+
+///
+/// Dispatches queries by resolving their matching query handler from an .
+///
+public sealed class ServiceProviderQueryDispatcher : IQueryDispatcher
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILoggerWrapper _logger;
+
+ ///
+ /// Creates a new dispatcher.
+ ///
+ /// The service provider used to resolve query handlers.
+ /// The logger used to record dispatch activity.
+ public ServiceProviderQueryDispatcher(
+ IServiceProvider serviceProvider,
+ ILoggerWrapper logger)
+ {
+ Precondition.RequiresNotNull(serviceProvider);
+ Precondition.RequiresNotNull(logger);
+
+ _serviceProvider = serviceProvider;
+ _logger = logger;
+ }
+
+ ///
+ public async Task DispatchAsync(
+ TQuery query,
+ CancellationToken ct = default)
+ where TQuery : IQuery
+ {
+ Precondition.RequiresNotNull(query);
+
+ Type queryType = typeof(TQuery);
+ Type handlerInterfaceType = typeof(IQueryHandler);
+
+ _logger.LogDebug(
+ "Dispatching query {QueryType} using handler interface {HandlerInterface}.",
+ FormatTypeName(queryType),
+ FormatTypeName(handlerInterfaceType));
+
+ IQueryHandler handler =
+ ResolveHandler>(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(Type queryType, Type handlerInterfaceType)
+ where THandler : class
+ {
+ THandler[] handlers = _serviceProvider.GetServices().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;
+ }
+}