JGUZDV.CQRS
2.4.3
dotnet add package JGUZDV.CQRS --version 2.4.3
NuGet\Install-Package JGUZDV.CQRS -Version 2.4.3
<PackageReference Include="JGUZDV.CQRS" Version="2.4.3" />
<PackageVersion Include="JGUZDV.CQRS" Version="2.4.3" />
<PackageReference Include="JGUZDV.CQRS" />
paket add JGUZDV.CQRS --version 2.4.3
#r "nuget: JGUZDV.CQRS, 2.4.3"
#:package JGUZDV.CQRS@2.4.3
#addin nuget:?package=JGUZDV.CQRS&version=2.4.3
#tool nuget:?package=JGUZDV.CQRS&version=2.4.3
JGUZDV.CQRS
This implements a simple CQRS style command and query structure. Combine it with JGUZDV.CQRS.AspNetCore for simple usage in AspNetCore scenarios.
The package contains classes describing a HandlerResult, a basic QueryHandler and a CommandHandler as well as interfaces
for queries, commands, and the aforementioned handlers.
Both handlers will run a 'default pipeline' that allows you to provide initialization code (commands only), command or query object normalization,
authorization, validation, execution and result authorization (queries only).
You can return results, that will easily translate to http results from the execution method of both handlers and you may throw CommandExceptions in command handlers
to stop the handler running during any point in the pipeline. It's written async only and will check your CancellationTokens between all steps.
Command handlers will faciliate context objects to better handle nullability, that can be used, if initialization is necessary.
Authorization of commands will return false, if you do not override them, whilst authorization of queries will return true.
Validation will in both cases return valid (e.g: an empty list).
QuerySample
using JGUZDV.CQRS;
namespace MyNamespace;
public class WeatherQuery : IQuery<MyResult> {
public string Location { get; set; }
}
public class WeatherQueryHandler : QueryHandler<WeatherQuery, MyResult> {
private IWeatherApi _weatherApi;
private ILogger<WeatherQueryHandler> _logger;
// You need to override the logger
public override ILogger Logger => _logger;
public WeatherQueryHandler(IWeatherApi weatherApi, ILogger<WeatherQueryHandler> logger)
{
_weatherApi = weatherApi;
_logger = logger;
}
protected override NormalizeQuery(WeatherQuery query, ClaimsPrincipal? principal)
{
query.Location = query.Location.Trim();
return query;
}
protected override Task<bool> AuthorizeExecuteAsync(WeatherQuery query, ClaimsPrincipal? principal, CancellationToken ct)
{
// If you choose to not override this, it will automatically return `true`
return Task.FromResult(principal != null && principal.Name == "Johannes");
}
protected override Task<List<ValidationResult>> ValidateAsync(WeatherQuery query, ClaimsPrincipal? principal, CancellationToken ct)
{
// If you choose to not override this, it will automatically be valid.
var result = new List<ValidationResult>();
if(query.Location == string.Empty) {
result.Add(new ("Location may not be empty"));
}
return result;
}
// You need to override at least this method
protected override async Task<QueryResult<TValue>> ExecuteInternalAsync(WeatherQuery query, ClaimsPrincipal? principal, CancellationToken ct) {
var result = await _weatherApi.GetWeatherOfLocation(query.Location, ct);
return new QueryResult(result);
}
protected override Task<bool> AuthorizeValueAsync(WeatherQuery query, MyResult value, ClaimsPrincipal? principal, CancellationToken ct)
{
// If you choose to not override this, it will automatically return `true`
return Task.FromResult(principal != null && value.DoesNotContainSecrets);
}
}
CommandSample
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
using Microsoft.Extensions.Logging;
namespace JGUZDV.CQRS.Commands;
// Sample class - DBSet<Counter> Counters
public class Counter
{
public int Id { get; set; }
public int Value { get; set; }
public string Reasons { get; set; }
}
// Command object
public class IncrementCounter : ICommand
{
public IncrementCounter(int counterId, int incValue, string reason)
{
CounterId = counterId;
IncValue = incValue;
Reason = reason;
}
public int CounterId { get; }
public int IncValue { get; }
public string Reason { get; }
}
internal class IncrementCounterHandler : CommandHandler<IncrementCounter, IncrementCounterHandler.CounterContext>
{
private readonly CounterDb _dbContext;
private readonly ILogger<IncrementCounterHandler> _logger;
public override ILogger Logger => _logger;
public IncrementCounterHandler(CounterDb dbContext, ILogger<IncrementCounterHandler> logger)
{
_dbContext = dbContext;
_logger = logger;
}
protected override async Task<CounterContext> InitializeAsync(IncrementCounter command, ClaimsPrincipal? principal, CancellationToken ct)
{
var counter = await _dbContext.Counters.FirstOrDefaultAsync(command.CounterId, ct);
if (counter == null)
throw new CommandException(HandlerResult.NotFound());
return new CounterContext(counter);
}
protected override IncrementCounter NormalizeCommand(IncrementCounter command, CounterContext context, ClaimsPrincipal? principal)
=> new IncrementCounter(
command.CounterId,
command.IncValue,
command.Reason.Trim()
);
protected override Task<bool> AuthorizeAsync(IncrementCounter command, CounterContext context, ClaimsPrincipal? principal, CancellationToken ct)
// If you do not override authorize async, authorization of the command will fail. You can skip authZ by setting `SkipAuthorization` to `true`
=> Task.FromResult(principal?.HasClaim(c => c.Type == "CounterAccess" && c.Value == $"{command.CounterId}") ?? false);
protected override Task<List<ValidationResult>> ValidateAsync(IncrementCounter command, CounterContext context, ClaimsPrincipal? principal, CancellationToken ct)
{
// If you do not override validation, the command will be considered valid.
var result = new List<ValidationResult>();
if (command.IncValue <= 0)
result.Add(new("Value may not be negative"));
if(command.IncValue > 10)
result.Add(new("Value needs to be <= 10"));
return Task.FromResult(result);
}
protected override async Task<HandlerResult> ExecuteInternalAsync(IncrementCounter command, CounterContext context, ClaimsPrincipal? principal, CancellationToken ct)
{
if (context.Counter.Value >= 100)
return HandlerResult.Conflict("Counter already at maximum");
context.Counter.Value += command.IncValue;
if (!string.IsNullOrEmpty(command.Reason))
context.Counter.Reasons += $"\r\n{command.Reason}";
await _dbContext.SaveChangesAsync(ct);
return HandlerResult.Success();
//If you would want to communication successfull creation, there is:
return HandlerResult.Created<int>(context.Counter.Id);
}
// Command Context object (not neccessarily nested)
public class CounterContext
{
public Counter Counter { get; set; }
public CounterContext(Counter c)
{
Counter = c;
}
}
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.1)
- Scrutor (>= 6.1.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on JGUZDV.CQRS:
| Package | Downloads |
|---|---|
|
JGUZDV.CQRS.AspNetCore
Extension methods to handle CQRS with AspNetCore |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.4.3 | 259 | 3/26/2026 |
| 2.4.2 | 142 | 3/18/2026 |
| 2.4.1 | 143 | 3/9/2026 |
| 2.4.0 | 242 | 7/28/2025 |
| 2.3.3 | 298 | 4/29/2025 |
| 2.3.2 | 599 | 3/26/2025 |
| 2.3.1 | 308 | 3/18/2025 |
| 2.3.0 | 290 | 3/17/2025 |
| 2.2.0 | 408 | 10/25/2023 |
| 2.1.2 | 320 | 7/19/2023 |
| 2.1.1 | 297 | 6/13/2023 |
| 2.1.0 | 307 | 6/12/2023 |
| 2.0.0 | 370 | 4/13/2023 |
| 1.0.0 | 411 | 1/23/2023 |