Requests - BisocM/CQRSharp GitHub Wiki
Requests is the baseline name for all entities handled by the Dispatcher, and are the primary unit of the library. There are two types of requests:
- Queries
- Commands
The primary difference between the two major types of requests is that Commands
return a generic CommandResult
instance, which is in place to simply indicate the completion status of the command, and does not hold any semantic context. On the other hand, Queries
return any user-defined return type. Essentially, Commands
are for the purpose of changing the state of a given system, while Queries
are expected to perform CRUD operations.
Internally, both QueryBase
and CommandBase
are simple marker/wrapper classes, that wrap around the RequestBase
:
using CQRSharp.Shared.Core.Data.Interfaces.Context;
using CQRSharp.Shared.Core.Data.Models.Requests;
namespace CQRSharp.Shared.Core.Data.Interfaces.Markers.Request;
/// <summary>
/// Represents a fundamental base class for handling requests.
/// </summary>
public abstract class RequestBase<TContext> : IRequest where TContext : IRequestContext
{
/// <summary>
/// A context object that stores request-level metadata like RequestId and UserId.
/// </summary>
public TContext? Context { get; set; }
IRequestContext? IRequest.Context
{
get => Context;
set
{
if (value != null) Context = (TContext)value;
else throw new ArgumentNullException(nameof(value), "Context cannot be null.");
}
}
/// <summary>
/// Metadata related to the request. Contains runtime-specific data.
/// </summary>
public RequestMetadata? Metadata { get; set; }
}
This was done for the purposes of segregation of queries from commands, and allow for relevant population of context in cases where the consuming user does or does not want to use any specific context. The default context that is injected to absolutel every single command is the RequestContextBase
- which is also discussed in other sections of this wiki, that go through context creation.
Commands are created with a command class, and a class that implements the handling logic. Here is an example of a fully complete stack:
public class SnailCommand : CommandBase<SampleRequestContext>;
In this particular case, we specify the CommandBase<SampleRequestContext>
, this is just to showcase the ability to inject custom contexts depending on the user-defined command. You can use CommandBase
by itself, in which case the default RequestContextBase
will be used.
public class SnailCommandHandler : ICommandHandler<SnailCommand, SampleRequestContext>
{
public async Task<CommandResult> Handle(SnailCommand command, CancellationToken cancellationToken)
{
Console.WriteLine(@"
o o
\_____/
/=O=O=\ _______
/ ^ \ /\\\\\\\\
\ \___/ / /\ ___ \
\_ V _/ /\ /\\\\ \
\ \__/\ /\ @_/ /
\____\____\______/
");
//Modify the state so that we return back to the primary menu.
command.Context.NextState = MenuState.PrimaryMenu;
return CommandResult.FromSuccess();
}
}
In this particular context, the SnailCommand
functions as a simple marker class, due to the nature of the designed application. However, if you would like, the command classes can handle certain information input:
public class AddUserCommand : CommandBase<SampleRequestContext>
{
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string Role { get; set; }
//...etc
}
This way, the command actually passes the data within itself, and this data can later be utilized by the handler:
public async Task<CommandResult> Handle(AddUserCommand command, CancellationToken cancellationToken)
{
string userName = command.Name;
//...etc
}
However, this pattern is not particularly adivsed if you are creating multi-tenant applications, and it is far more favorable to utilize the provided request context population system - please refer to documentation regarding the IRequestContextFactory
. Using contexts over introducing data within commands is far more favorable, as it allows for centralized command of all data flows within your application, as well as introduction of more complex data retrieval like lazy loading mechanisms and others.
As mentioned before, queries are not much different from commands, with the exception that they have a user-specified return data type. Here is a full stack of a query:
NOTE: Currently, you need to specify the return type on both the
QueryBase
andIQueryHandler
declarations. This might change in future, makingIQueryHandler<T1, T2, T3>
turn intoIQueryHandler<T1, T2>
. If this change is not reflected here, please be wary of the possibility.
public class GetUserQuery : QueryBase<User, SampleRequestContext>;
public class HandleGetUserQuery : IQueryHandler<GetUserQuery, User, SampleRequestContext>
{
public Task<CustomInMemoryUserStore.User> Handle(GetUserQuery query, CancellationToken cancellationToken)
{
//Handle here...
}
}
Unfortunaltely, the declarations are currently quite a bit verbose, but the entire gist of the query handling is very similar to the command handling flow.