cat on a spaceship

This commit is contained in:
Mathias Beaulieu-Duncan 2025-11-01 22:38:46 -04:00
parent 747fa227a1
commit f6dccf46d7
Signed by: mathias
GPG Key ID: 1C16CF05BAF9162D
89 changed files with 2663 additions and 581 deletions

View File

@ -0,0 +1,26 @@
{
"permissions": {
"allow": [
"Bash(dotnet clean:*)",
"Bash(dotnet run)",
"Bash(dotnet add:*)",
"Bash(timeout 5 dotnet run:*)",
"Bash(dotnet remove:*)",
"Bash(netstat:*)",
"Bash(findstr:*)",
"Bash(cat:*)",
"Bash(taskkill:*)",
"WebSearch",
"Bash(dotnet tool install:*)",
"Bash(protogen:*)",
"Bash(timeout 15 dotnet run:*)",
"Bash(where:*)",
"Bash(timeout 30 dotnet run:*)",
"Bash(timeout 60 dotnet run:*)",
"Bash(timeout 120 dotnet run:*)",
"Bash(git add:*)"
],
"deny": [],
"ask": []
}
}

314
CLAUDE.md Normal file
View File

@ -0,0 +1,314 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is Svrnty.CQRS, a modern implementation of Command Query Responsibility Segregation (CQRS) for .NET 10. It was forked from PoweredSoft.CQRS and provides:
- Automatic REST endpoint generation from command/query handlers
- Dynamic query capabilities (filtering, sorting, grouping, aggregation)
- ASP.NET Core MVC integration
- FluentValidation support
- AOT (Ahead-of-Time) compilation compatibility for core packages
## Solution Structure
The solution contains 9 projects organized by responsibility:
**Abstractions (interfaces and contracts only):**
- `Svrnty.CQRS.Abstractions` - Core interfaces (ICommandHandler, IQueryHandler, discovery contracts)
- `Svrnty.CQRS.AspNetCore.Abstractions` - ASP.NET Core attributes
- `Svrnty.CQRS.DynamicQuery.Abstractions` - Dynamic query interfaces (multi-targets netstandard2.1 and net10.0)
**Implementation:**
- `Svrnty.CQRS` - Core discovery and registration logic
- `Svrnty.CQRS.AspNetCore` - MVC controller generation (legacy/backward compatibility)
- `Svrnty.CQRS.MinimalApi` - Minimal API endpoint mapping (recommended for new projects)
- `Svrnty.CQRS.DynamicQuery` - PoweredSoft.DynamicQuery integration
- `Svrnty.CQRS.DynamicQuery.AspNetCore` - Dynamic query controllers
- `Svrnty.CQRS.FluentValidation` - Validation integration helpers
**Key Design Principle:** Abstractions projects contain ONLY interfaces/attributes with minimal dependencies. Implementation projects depend on abstractions. This allows consumers to reference abstractions without pulling in heavy implementation dependencies.
## Build Commands
```bash
# Restore dependencies
dotnet restore
# Build entire solution
dotnet build
# Build in Release mode
dotnet build -c Release
# Create NuGet packages (with version)
dotnet pack -c Release -o ./artifacts -p:Version=1.0.0
# Build specific project
dotnet build Svrnty.CQRS/Svrnty.CQRS.csproj
```
## Testing
This repository does not currently contain test projects. When adding tests:
- Place them in a `tests/` directory or alongside source projects
- Name them with `.Tests` suffix (e.g., `Svrnty.CQRS.Tests`)
## Architecture
### Core CQRS Pattern
The framework uses handler interfaces that follow this pattern:
```csharp
// Command with no result
ICommandHandler<TCommand>
Task HandleAsync(TCommand command, CancellationToken cancellationToken = default)
// Command with result
ICommandHandler<TCommand, TResult>
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default)
// Query (always returns result)
IQueryHandler<TQuery, TResult>
Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default)
```
### Metadata-Driven Discovery
The framework uses a **metadata pattern** for runtime discovery:
1. When you register a handler using `services.AddCommand<TCommand, THandler>()`, it:
- Registers the handler in DI as `ICommandHandler<TCommand, THandler>`
- Creates metadata (`ICommandMeta`) describing the command type, handler type, and result type
- Stores metadata as singleton in DI
2. Discovery services (`ICommandDiscovery`, `IQueryDiscovery`) implemented in `Svrnty.CQRS`:
- Query all registered metadata from DI container
- Provide lookup methods: `GetCommand(string name)`, `GetCommands()`, etc.
3. ASP.NET Core feature providers use discovery to:
- Enumerate all registered commands/queries
- Dynamically generate generic controller instances at startup
- Apply naming conventions (convert to lowerCamelCase)
**Key Files:**
- `Svrnty.CQRS.Abstractions/Discovery/` - Metadata interfaces
- `Svrnty.CQRS/Discovery/` - Discovery implementations
- `Svrnty.CQRS.AspNetCore/Mvc/*FeatureProvider.cs` - Dynamic controller generation
- `Svrnty.CQRS.AspNetCore/Mvc/*Convention.cs` - Naming conventions
### ASP.NET Core Integration Options
There are two options for integrating commands and queries with ASP.NET Core:
#### Option 1: Minimal API (Recommended)
The **Svrnty.CQRS.MinimalApi** package provides modern Minimal API endpoints:
**Registration:**
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register CQRS services
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
// Add your commands and queries
builder.Services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>();
builder.Services.AddQuery<PersonQuery, IQueryable<Person>, PersonQueryHandler>();
var app = builder.Build();
// Map endpoints (this creates routes automatically)
app.MapSvrntyCommands(); // Maps all commands to POST /api/command/{name}
app.MapSvrntyQueries(); // Maps all queries to POST/GET /api/query/{name}
app.Run();
```
**How It Works:**
1. Extension methods iterate through `ICommandDiscovery` and `IQueryDiscovery`
2. For each command/query, creates Minimal API endpoints using `MapPost()`/`MapGet()`
3. Applies same naming conventions as MVC (lowerCamelCase)
4. Respects `[CommandControllerIgnore]` and `[QueryControllerIgnore]` attributes
5. Integrates with `ICommandAuthorizationService` and `IQueryAuthorizationService`
6. Supports OpenAPI/Swagger documentation
**Features:**
- Queries support both POST (with JSON body) and GET (with query string parameters)
- Commands only support POST with JSON body
- Authorization via authorization services (returns 401/403 status codes)
- Customizable route prefixes: `MapSvrntyCommands("my-prefix")`
- Automatic OpenAPI tags: "Commands" and "Queries"
**Key Files:**
- `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs` - Main implementation
#### Option 2: MVC Controllers (Legacy)
The AspNetCore packages use **dynamic controller generation** (maintained for backward compatibility):
1. **Feature Providers** (`CommandControllerFeatureProvider`, `QueryControllerFeatureProvider`):
- Called during MVC startup
- Discover handlers via `ICommandDiscovery`/`IQueryDiscovery`
- Create generic controller types: `CommandController<TCommand>`, `QueryController<TQuery, TResult>`
- Skip handlers marked with `[CommandControllerIgnore]` or `[QueryControllerIgnore]`
2. **Conventions** (`CommandControllerConvention`, `QueryControllerConvention`):
- Apply naming to generated controllers
- Default: Type name without suffix, converted to lowerCamelCase
- Custom: Use `[CommandName]` or `[QueryName]` attribute
3. **Controllers** expose automatic endpoints:
- Commands: `POST /api/command/{name}`
- Queries: `POST /api/query/{name}` or `GET /api/query/{name}`
**Registration Pattern:**
```csharp
services
.AddControllers()
.AddSvrntyQueries() // Registers query feature provider
.AddSvrntyCommands(); // Registers command feature provider
```
### Dynamic Query System
Dynamic queries provide OData-like filtering capabilities:
**Core Components:**
- `IDynamicQuery<TSource, TDestination>` - Interface with GetFilters(), GetSorts(), GetGroups(), GetAggregates()
- `IQueryableProvider<TSource>` - Provides base IQueryable to query against
- `IAlterQueryableService<TSource, TDestination>` - Middleware to modify queries (e.g., security filters)
- `DynamicQueryHandler<TSource, TDestination>` - Executes queries using PoweredSoft.DynamicQuery
**Request Flow:**
1. HTTP request with filters/sorts/aggregates
2. DynamicQueryController receives request
3. DynamicQueryHandler gets base queryable from IQueryableProvider
4. Applies alterations from all registered IAlterQueryableService instances
5. Builds PoweredSoft query criteria
6. Executes and returns IQueryExecutionResult
**Registration Example:**
```csharp
services.AddDynamicQuery<Person, PersonDto>()
.AddDynamicQueryWithProvider<Person, PersonQueryableProvider>()
.AddAlterQueryable<Person, PersonDto, SecurityFilter>();
```
**Key Files:**
- `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs`
- `Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs` - Request models
## Package Configuration
All projects target .NET 10.0 and use C# 14, sharing common configuration:
- **Target Framework**: `net10.0` (except DynamicQuery.Abstractions which multi-targets `netstandard2.1;net10.0`)
- **Language Version**: C# 14
- **IsAotCompatible**: Currently set but not enforced (many dependencies are not AOT-compatible yet)
- **Symbols**: Portable debug symbols with source, published as `.snupkg`
- **NuGet metadata**: Icon, README, license (MIT), and repository URL included in packages
- **Authors**: David Lebee, Mathias Beaulieu-Duncan
- **Repository**: https://git.openharbor.io/Open-Harbor/dotnet-cqrs
### Package Dependencies
- **Microsoft.Extensions.DependencyInjection.Abstractions**: 10.0.0-rc.2.25502.107 (will update to stable when .NET 10 is released)
- **FluentValidation**: 11.11.0
- **Microsoft.CodeAnalysis.CSharp**: 4.13.0
- **PoweredSoft.DynamicQuery**: 3.0.1
- **Pluralize.NET**: 1.0.2
## Publishing
NuGet packages are published automatically via GitHub Actions when a release is created:
**Workflow:** `.github/workflows/publish-nugets.yml`
1. Triggered on release publication
2. Extracts version from release tag
3. Runs `dotnet pack -c Release -p:Version={tag}`
4. Pushes to NuGet.org using `NUGET_API_KEY` secret
**Manual publish:**
```bash
# Create packages with specific version
dotnet pack -c Release -o ./artifacts -p:Version=1.2.3
# Push to NuGet
dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key YOUR_KEY
```
## Development Workflow
**Adding a New Command/Query Handler:**
1. Create command/query POCO in consumer project
2. Implement handler: `ICommandHandler<TCommand, TResult>`
3. Register in DI: `services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>()`
4. (Optional) Add validator: `services.AddTransient<IValidator<CreatePersonCommand>, Validator>()`
5. Controller endpoint is automatically generated
**Adding a New Feature to Framework:**
1. Add interface to appropriate Abstractions project
2. Implement in corresponding implementation project
3. Update ServiceCollectionExtensions with registration method
4. Ensure all projects maintain AOT compatibility (unless AspNetCore-specific)
5. Update package version and release notes
**Naming Conventions:**
- Commands/Queries: Use `[CommandName]` or `[QueryName]` attribute for custom names
- Default naming: Strips "Command"/"Query" suffix, converts to lowerCamelCase
- Example: `CreatePersonCommand` -> `createPerson` endpoint
## C# 14 Language Features
The project now uses C# 14, which introduces several new features. Be aware of these breaking changes:
**Potential Breaking Changes:**
- **`field` keyword**: New contextual keyword in property accessors for implicit backing fields
- **`extension` keyword**: Reserved for extension containers; use `@extension` for identifiers
- **`partial` return type**: Cannot use `partial` as return type without escaping
- **Span<T> overload resolution**: New implicit conversions may select different overloads
- **`scoped` as lambda modifier**: Always treated as modifier in lambda parameters
**New Features Available:**
- Extension members (static extension members and extension properties)
- Implicit span conversions
- Unbound generic types with `nameof`
- Lambda parameter modifiers without type specification
- Partial instance constructors and events
- Null-conditional assignment (`?.=` and `?[]=`)
The codebase currently compiles without warnings on C# 14.
## Important Implementation Notes
1. **AOT Compatibility**: Currently not enforced. The `IsAotCompatible` property is set on some projects but many dependencies (including FluentValidation, PoweredSoft.DynamicQuery) are not AOT-compatible. Future work may address this.
2. **Async Everywhere**: All handlers are async. Always support CancellationToken.
3. **Generic Type Safety**: Framework relies heavily on generics for compile-time safety. When adding features, maintain strong typing.
4. **Metadata Pattern**: When extending discovery, always create corresponding metadata classes (implement ICommandMeta/IQueryMeta).
5. **Feature Provider Timing**: Controller generation happens during MVC startup. Discovery services must be registered before calling AddSvrntyCommands/Queries.
6. **FluentValidation**: This framework only REGISTERS validators. Actual validation execution requires separate middleware/filters in consumer applications.
7. **DynamicQuery Interceptors**: Support up to 5 interceptors per query type. Interceptors modify PoweredSoft DynamicQuery behavior.
## Common Code Locations
- Handler interfaces: `Svrnty.CQRS.Abstractions/ICommandHandler.cs`, `IQueryHandler.cs`
- Discovery implementations: `Svrnty.CQRS/Discovery/`
- Service registration: `*/ServiceCollectionExtensions.cs` in each project
- Controller templates: `Svrnty.CQRS.AspNetCore/Mvc/*Controller.cs`
- Dynamic query logic: `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs`
- Naming/routing: `Svrnty.CQRS.AspNetCore/Mvc/*Convention.cs`

View File

@ -1,38 +0,0 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using OpenHarbor.CQRS.Abstractions;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
[Produces("application/json")]
[ApiController, Route("api/command/[controller]")]
public class CommandController<TCommand> : Controller
where TCommand : class
{
[HttpPost, CommandControllerAuthorization]
public async Task<IActionResult> Handle([FromServices] ICommandHandler<TCommand> handler,
[FromBody] TCommand command)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
await handler.HandleAsync(command, this.Request.HttpContext.RequestAborted);
return Ok();
}
}
[Produces("application/json")]
[ApiController, Route("api/command/[controller]")]
public class CommandController<TCommand, TTCommandResult> : Controller
where TCommand : class
{
[HttpPost, CommandControllerAuthorization]
public async Task<ActionResult<TTCommandResult>> Handle([FromServices] ICommandHandler<TCommand, TTCommandResult> handler,
[FromBody] TCommand command)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
return Ok(await handler.HandleAsync(command, this.Request.HttpContext.RequestAborted));
}
}

View File

@ -1,43 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
using OpenHarbor.CQRS.Abstractions.Security;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public class CommandControllerAsyncAuthorizationFilter : IAsyncAuthorizationFilter
{
private readonly ICommandAuthorizationService _authorizationService;
public CommandControllerAsyncAuthorizationFilter(IServiceProvider serviceProvider)
{
_authorizationService = serviceProvider.GetService<ICommandAuthorizationService>();
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (_authorizationService == null)
return;
var action = context.ActionDescriptor as Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor;
if (action == null)
throw new Exception("Only Supports controller action descriptor");
var attribute = action.MethodInfo.GetCustomAttribute<CommandControllerAuthorizationAttribute>();
Type commandType;
if (attribute?.CommandType != null)
commandType = attribute.CommandType;
else
commandType = action.ControllerTypeInfo.GenericTypeArguments.First();
var ar = await _authorizationService.IsAllowedAsync(commandType);
if (ar == AuthorizationResult.Forbidden)
context.Result = new StatusCodeResult(403);
else if(ar == AuthorizationResult.Unauthorized)
context.Result = new StatusCodeResult(401);
}
}

View File

@ -1,20 +0,0 @@
using System;
using Microsoft.AspNetCore.Mvc;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
[AttributeUsage(AttributeTargets.Method)]
public class CommandControllerAuthorizationAttribute : TypeFilterAttribute
{
public CommandControllerAuthorizationAttribute() : base(typeof(CommandControllerAsyncAuthorizationFilter))
{
}
public CommandControllerAuthorizationAttribute(Type commandType) : base(typeof(CommandControllerAsyncAuthorizationFilter))
{
CommandType = commandType;
}
public Type CommandType { get; } = null;
}

View File

@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions.Discovery;
using System;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public class CommandControllerConvention : IControllerModelConvention
{
private readonly IServiceProvider _serviceProvider;
public CommandControllerConvention(IServiceProvider serviceProvider)
{
this._serviceProvider = serviceProvider;
}
public void Apply(ControllerModel controller)
{
if (!controller.ControllerType.IsGenericType)
return;
if (!controller.ControllerType.Name.Contains("CommandController"))
return;
if (controller.ControllerType.Assembly != typeof(CommandControllerConvention).Assembly)
return;
var genericType = controller.ControllerType.GenericTypeArguments[0];
var commandDiscovery = this._serviceProvider.GetRequiredService<ICommandDiscovery>();
var command = commandDiscovery.FindCommand(genericType);
controller.ControllerName = command.LowerCamelCaseName;
}
}

View File

@ -1,43 +0,0 @@
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions.Discovery;
using OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public class CommandControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
private readonly ServiceProvider _serviceProvider;
public CommandControllerFeatureProvider(ServiceProvider serviceProvider)
{
this._serviceProvider = serviceProvider;
}
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
var commandDiscovery = this._serviceProvider.GetRequiredService<ICommandDiscovery>();
foreach (var f in commandDiscovery.GetCommands())
{
var ignoreAttribute = f.CommandType.GetCustomAttribute<CommandControllerIgnoreAttribute>();
if (ignoreAttribute != null)
continue;
if (f.CommandResultType == null)
{
var controllerType = typeof(CommandController<>).MakeGenericType(f.CommandType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
else
{
var controllerType = typeof(CommandController<,>).MakeGenericType(f.CommandType, f.CommandResultType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
}
}
}

View File

@ -1,6 +0,0 @@
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public class CommandControllerOptions
{
}

View File

@ -1,27 +0,0 @@
using System;
using Microsoft.Extensions.DependencyInjection;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public static class MvcBuilderExtensions
{
public static IMvcBuilder AddOpenHarborQueries(this IMvcBuilder builder, Action<QueryControllerOptions> configuration = null)
{
var options = new QueryControllerOptions();
configuration?.Invoke(options);
var services = builder.Services;
var serviceProvider = services.BuildServiceProvider();
builder.AddMvcOptions(o => o.Conventions.Add(new QueryControllerConvention(serviceProvider)));
builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new QueryControllerFeatureProvider(serviceProvider)));
return builder;
}
public static IMvcBuilder AddOpenHarborCommands(this IMvcBuilder builder)
{
var services = builder.Services;
var serviceProvider = services.BuildServiceProvider();
builder.AddMvcOptions(o => o.Conventions.Add(new CommandControllerConvention(serviceProvider)));
builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new CommandControllerFeatureProvider(serviceProvider)));
return builder;
}
}

View File

@ -1,33 +0,0 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using OpenHarbor.CQRS.Abstractions;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
[Produces("application/json")]
[ApiController, Route("api/query/[controller]")]
public class QueryController<TQuery, TQueryResult> : Controller
where TQuery : class
{
[HttpPost, QueryControllerAuthorization]
public async Task<ActionResult<TQueryResult>> Handle([FromServices] IQueryHandler<TQuery, TQueryResult> handler,
[FromBody] TQuery query)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
return Ok(await handler.HandleAsync(query, this.Request.HttpContext.RequestAborted));
}
[HttpGet, QueryControllerAuthorization]
public async Task<ActionResult<TQueryResult>> HandleGet([FromServices] IQueryHandler<TQuery, TQueryResult> handler,
[FromQuery] TQuery query)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
return Ok(await handler.HandleAsync(query, this.Request.HttpContext.RequestAborted));
}
}

View File

@ -1,43 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
using OpenHarbor.CQRS.Abstractions.Security;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public class QueryControllerAsyncAuthorizationFilter : IAsyncAuthorizationFilter
{
private readonly IQueryAuthorizationService _authorizationService;
public QueryControllerAsyncAuthorizationFilter(IServiceProvider serviceProvider)
{
_authorizationService = serviceProvider.GetService<IQueryAuthorizationService>();
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (_authorizationService == null)
return;
var action = context.ActionDescriptor as Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor;
if (action == null)
throw new Exception("Only Supports controller action descriptor");
var attribute = action.MethodInfo.GetCustomAttribute<QueryControllerAuthorizationAttribute>();
Type queryType;
if (attribute?.QueryType != null)
queryType = attribute.QueryType;
else
queryType = action.ControllerTypeInfo.GenericTypeArguments.First();
var ar = await _authorizationService.IsAllowedAsync(queryType);
if (ar == AuthorizationResult.Forbidden)
context.Result = new StatusCodeResult(403);
else if (ar == AuthorizationResult.Unauthorized)
context.Result = new StatusCodeResult(401);
}
}

View File

@ -1,20 +0,0 @@
using System;
using Microsoft.AspNetCore.Mvc;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
[AttributeUsage(AttributeTargets.Method)]
public class QueryControllerAuthorizationAttribute : TypeFilterAttribute
{
public QueryControllerAuthorizationAttribute() : base(typeof(QueryControllerAsyncAuthorizationFilter))
{
}
public QueryControllerAuthorizationAttribute(Type queryType) : base(typeof(QueryControllerAsyncAuthorizationFilter))
{
QueryType = queryType;
}
public Type QueryType { get; }
}

View File

@ -1,27 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions.Discovery;
using System;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public class QueryControllerConvention : IControllerModelConvention
{
private readonly IServiceProvider _serviceProvider;
public QueryControllerConvention(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Apply(ControllerModel controller)
{
if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("QueryController") && controller.ControllerType.Assembly == typeof(QueryControllerConvention).Assembly)
{
var genericType = controller.ControllerType.GenericTypeArguments[0];
var queryDiscovery = _serviceProvider.GetRequiredService<IQueryDiscovery>();
var query = queryDiscovery.FindQuery(genericType);
controller.ControllerName = query.LowerCamelCaseName;
}
}
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions.Discovery;
using OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public class QueryControllerFeatureProvider(ServiceProvider serviceProvider)
: IApplicationFeatureProvider<ControllerFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
var queryDiscovery = serviceProvider.GetRequiredService<IQueryDiscovery>();
foreach (var queryMeta in queryDiscovery.GetQueries())
{
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
if (ignoreAttribute != null)
continue;
if (queryMeta.Category != "BasicQuery")
continue;
var controllerType = typeof(QueryController<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
}
}

View File

@ -1,6 +0,0 @@
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
public class QueryControllerOptions
{
}

View File

@ -1,5 +0,0 @@
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
public class DynamicQueryControllerOptions
{
}

View File

@ -1,73 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30907.101
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenHarbor.CQRS.Abstractions", "OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj", "{ED78E19D-31D4-4783-AE9E-2844A8541277}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenHarbor.CQRS", "OpenHarbor.CQRS\OpenHarbor.CQRS.csproj", "{7069B98F-8736-4114-8AF5-1ACE094E6238}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenHarbor.CQRS.AspNetCore", "OpenHarbor.CQRS.AspNetCore\OpenHarbor.CQRS.AspNetCore.csproj", "{A1D577E5-61BD-4E25-B2C8-1005C1D7665B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenHarbor.CQRS.AspNetCore.Abstractions", "OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj", "{4C466827-31D3-4081-A751-C2FC7C381D7E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{617BA357-1A1F-40C5-B19A-A65A960E6142}"
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenHarbor.CQRS.DynamicQuery", "OpenHarbor.CQRS.DynamicQuery\OpenHarbor.CQRS.DynamicQuery.csproj", "{A38CE930-191F-417C-B5BE-8CC62DB47513}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenHarbor.CQRS.DynamicQuery.AspNetCore", "OpenHarbor.CQRS.DynamicQuery.AspNetCore\OpenHarbor.CQRS.DynamicQuery.AspNetCore.csproj", "{0829B99A-0A20-4CAC-A91E-FB67E18444DE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenHarbor.CQRS.FluentValidation", "OpenHarbor.CQRS.FluentValidation\OpenHarbor.CQRS.FluentValidation.csproj", "{70BD37C4-7497-474D-9A40-A701203971D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenHarbor.CQRS.DynamicQuery.Abstractions", "OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj", "{8B9F8ACE-10EA-4215-9776-DE29EC93B020}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|Any CPU.Build.0 = Release|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.Build.0 = Release|Any CPU
{A1D577E5-61BD-4E25-B2C8-1005C1D7665B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1D577E5-61BD-4E25-B2C8-1005C1D7665B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1D577E5-61BD-4E25-B2C8-1005C1D7665B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1D577E5-61BD-4E25-B2C8-1005C1D7665B}.Release|Any CPU.Build.0 = Release|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.Build.0 = Release|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|Any CPU.Build.0 = Release|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.Build.0 = Release|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|Any CPU.Build.0 = Release|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D6D431EA-C04F-462B-8033-60F510FEB49E}
EndGlobalSection
EndGlobal

139
README.md
View File

@ -10,42 +10,141 @@ Our implementation of query and command responsibility segregation (CQRS).
| Package Name | NuGet | NuGet Install | | Package Name | NuGet | NuGet Install |
|-----------------------------------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------:| |-----------------------------------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------:|
| OpenHarbor.CQRS | [![NuGet](https://img.shields.io/nuget/v/OpenHarbor.CQRS.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/OpenHarbor.CQRS/) | ```dotnet add package OpenHarbor.CQRS ``` | | Svrnty.CQRS | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS/) | ```dotnet add package Svrnty.CQRS ``` |
| OpenHarbor.CQRS.AspNetCore | [![NuGet](https://img.shields.io/nuget/v/OpenHarbor.CQRS.AspNetCore.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/OpenHarbor.CQRS.AspNetCore/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore ``` | | Svrnty.CQRS.MinimalApi | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.MinimalApi.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.MinimalApi/) | ```dotnet add package Svrnty.CQRS.MinimalApi ``` |
| OpenHarbor.CQRS.FluentValidation | [![NuGet](https://img.shields.io/nuget/v/OpenHarbor.CQRS.FluentValidation.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/OpenHarbor.CQRS.FluentValidation/) | ```dotnet add package OpenHarbor.CQRS.FluentValidation ``` | | Svrnty.CQRS.AspNetCore | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.AspNetCore.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.AspNetCore/) | ```dotnet add package Svrnty.CQRS.AspNetCore ``` |
| OpenHarbor.CQRS.DynamicQuery | [![NuGet](https://img.shields.io/nuget/v/OpenHarbor.CQRS.DynamicQuery.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery/) | ```dotnet add package OpenHarbor.CQRS.DynamicQuery ``` | | Svrnty.CQRS.FluentValidation | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.FluentValidation.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.FluentValidation/) | ```dotnet add package Svrnty.CQRS.FluentValidation ``` |
| OpenHarbor.CQRS.DynamicQuery.AspNetCore | [![NuGet](https://img.shields.io/nuget/v/OpenHarbor.CQRS.DynamicQuery.AspNetCore.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery.AspNetCore/) | ```dotnet add package OpenHarbor.CQRS.DynamicQuery.AspNetCore ``` | | Svrnty.CQRS.DynamicQuery | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.DynamicQuery.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery/) | ```dotnet add package Svrnty.CQRS.DynamicQuery ``` |
| Svrnty.CQRS.DynamicQuery.AspNetCore | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.DynamicQuery.AspNetCore.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery.AspNetCore/) | ```dotnet add package Svrnty.CQRS.DynamicQuery.AspNetCore ``` |
| Svrnty.CQRS.Grpc | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.Grpc.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.Grpc/) | ```dotnet add package Svrnty.CQRS.Grpc ``` |
> Abstractions Packages. > Abstractions Packages.
| Package Name | NuGet | NuGet Install | | Package Name | NuGet | NuGet Install |
| ---------------------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------------------------------: | | ---------------------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------------------------------: |
| OpenHarbor.CQRS.Abstractions | [![NuGet](https://img.shields.io/nuget/v/OpenHarbor.CQRS.Abstractions.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/OpenHarbor.CQRS.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.Abstractions ``` | | Svrnty.CQRS.Abstractions | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.Abstractions.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.Abstractions/) | ```dotnet add package Svrnty.CQRS.Abstractions ``` |
| OpenHarbor.CQRS.AspNetCore.Abstractions | [![NuGet](https://img.shields.io/nuget/v/OpenHarbor.CQRS.AspNetCore.Abstractions.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/OpenHarbor.CQRS.AspNetCore.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore.Abstractions ``` | | Svrnty.CQRS.AspNetCore.Abstractions | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.AspNetCore.Abstractions.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.AspNetCore.Abstractions/) | ```dotnet add package Svrnty.CQRS.AspNetCore.Abstractions ``` |
| OpenHarbor.CQRS.DynamicQuery.Abstractions | [![NuGet](https://img.shields.io/nuget/v/OpenHarbor.CQRS.DynamicQuery.Abstractions.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore.Abstractions ``` | | Svrnty.CQRS.DynamicQuery.Abstractions | [![NuGet](https://img.shields.io/nuget/v/Svrnty.CQRS.DynamicQuery.Abstractions.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery.Abstractions/) | ```dotnet add package Svrnty.CQRS.AspNetCore.Abstractions ``` |
## Sample of startup code for aspnetcore MVC ## Sample of startup code for Minimal API (Recommended)
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register CQRS core services
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
// Add your commands and queries
AddQueries(builder.Services);
AddCommands(builder.Services);
// Add Swagger (optional)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// Map CQRS endpoints - automatically creates routes for all commands and queries
app.MapSvrntyCommands(); // Creates POST /api/command/{commandName} endpoints
app.MapSvrntyQueries(); // Creates POST/GET /api/query/{queryName} endpoints
app.Run();
```
## Sample of startup code for ASP.NET Core MVC (Legacy)
```csharp ```csharp
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
// make sure to add your queries and commands before configuring MvCBuilder with .AddOpenHarborCommands and .AddOpenHarborQueries // make sure to add your queries and commands before configuring MvcBuilder with .AddSvrntyCommands and .AddSvrntyQueries
AddQueries(services); AddQueries(services);
AddCommands(services); AddCommands(services);
// adds the non related to aspnet core features. // adds the non related to aspnet core features.
services.AddOpenHarborCQRS(); services.AddSvrntyCQRS();
services services
.AddControllers() .AddControllers()
.AddOpenHarborQueries() // adds queries to aspnetcore mvc.(you can make it configurable to load balance only commands on a instance) .AddSvrntyQueries() // adds queries to aspnetcore mvc.(you can make it configurable to load balance only commands on a instance)
.AddOpenHarborCommands() // adds commands to aspnetcore mvc. (you can make it configurable to load balance only commands on a instance) .AddSvrntyCommands() // adds commands to aspnetcore mvc. (you can make it configurable to load balance only commands on a instance)
.AddFluentValidation(); .AddFluentValidation();
services.AddSwaggerGen(); services.AddSwaggerGen();
} }
``` ```
## Sample of startup code for gRPC
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register CQRS core services
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
// Add your commands and queries
AddQueries(builder.Services);
AddCommands(builder.Services);
// Add gRPC support
builder.Services.AddSvrntyCqrsGrpc();
var app = builder.Build();
// Map gRPC endpoints
app.MapSvrntyGrpcCommands();
app.MapSvrntyGrpcQueries();
app.Run();
```
### Important: protobuf-net Requirements for gRPC
To use gRPC, your commands and queries must be annotated with protobuf-net attributes:
```csharp
using ProtoBuf;
[ProtoContract]
public class CreatePersonCommand
{
[ProtoMember(1)]
public string FirstName { get; set; } = string.Empty;
[ProtoMember(2)]
public string LastName { get; set; } = string.Empty;
[ProtoMember(3)]
public int Age { get; set; }
}
[ProtoContract]
public class Person
{
[ProtoMember(1)]
public int Id { get; set; }
[ProtoMember(2)]
public string FullName { get; set; } = string.Empty;
}
```
**Notes:**
- Add `[ProtoContract]` to each command/query/result class
- Add `[ProtoMember(n)]` to each property with sequential numbers starting from 1
- These attributes don't interfere with JSON serialization or FluentValidation
- You can use both HTTP REST (MinimalApi/MVC) and gRPC simultaneously
> Example how to add your queries and commands. > Example how to add your queries and commands.
```csharp ```csharp
@ -68,7 +167,7 @@ private void AddQueries(IServiceCollection services)
We use fluent validation in all of our projects, but we don't want it to be enforced. We use fluent validation in all of our projects, but we don't want it to be enforced.
If you install ```OpenHarbor.CQRS.FluentValidation``` you can use this way of registrating your commands. If you install ```Svrnty.CQRS.FluentValidation``` you can use this way of registrating your commands.
```csharp ```csharp
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
@ -80,19 +179,21 @@ public void ConfigureServices(IServiceCollection services)
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
// with OpenHarbor.CQRS.FluentValidation package. // with Svrnty.CQRS.FluentValidation package.
services.AddCommand<EchoCommand, string, EchoCommandHandler, EchoCommandValidator>(); services.AddCommand<EchoCommand, string, EchoCommandHandler, EchoCommandValidator>();
} }
``` ```
# 2024 Roadmap # 2024-2025 Roadmap
| Task | Description | Status | | Task | Description | Status |
|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------| |----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------|
| Support .NET 8 | Ensure compatibility with .NET 8. | ✅ | | Support .NET 8 | Ensure compatibility with .NET 8. | ✅ |
| Support .NET 10 | Upgrade to .NET 10 with C# 14 language support. | ✅ |
| Update FluentValidation | Upgrade FluentValidation to version 11.x for .NET 10 compatibility. | ✅ |
| Add gRPC Support with protobuf-net | Implement gRPC endpoints with binary protobuf serialization for high-performance scenarios. | ✅ |
| Create a new demo project as an example | Develop a new demo project to serve as an example for users. | ⬜️ | | Create a new demo project as an example | Develop a new demo project to serve as an example for users. | ⬜️ |
| New Independent Module for MVC | Develop a standalone module, independent of MVC, to enhance framework flexibility. | ⬜️ | | New Independent Module for MVC | Develop a standalone module, independent of MVC, to enhance framework flexibility. | ⬜️ |
| Implement .NET Native Compilation (AOT) | Enable Ahead-of-Time (AOT) compilation support for .NET 8. | ⬜️ | | Implement .NET Native Compilation (AOT) | Enable full Ahead-of-Time (AOT) compilation support (blocked by third-party dependencies). | ⬜️ |
| Update FluentValidation | Upgrade FluentValidation to the latest version, addressing potential breaking changes. | ⬜️ |
| Create a website for the Framework | Develop a website to host comprehensive documentation for the framework. | ⬜️ | | Create a website for the Framework | Develop a website to host comprehensive documentation for the framework. | ⬜️ |
| Re-add support for GraphQL | Re-integrate support for GraphQL, exploring lightweight solutions. | ⬜️ | | Re-add support for GraphQL | Re-integrate support for GraphQL, exploring lightweight solutions. | ⬜️ |

View File

@ -1,6 +1,6 @@
using System; using System;
namespace OpenHarbor.CQRS.Abstractions.Attributes; namespace Svrnty.CQRS.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CommandNameAttribute : Attribute public class CommandNameAttribute : Attribute

View File

@ -1,6 +1,6 @@
using System; using System;
namespace OpenHarbor.CQRS.Abstractions.Attributes; namespace Svrnty.CQRS.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class QueryNameAttribute : Attribute public class QueryNameAttribute : Attribute

View File

@ -1,8 +1,8 @@
using System; using System;
using System.Reflection; using System.Reflection;
using OpenHarbor.CQRS.Abstractions.Attributes; using Svrnty.CQRS.Abstractions.Attributes;
namespace OpenHarbor.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.Abstractions.Discovery;
public sealed class CommandMeta : ICommandMeta public sealed class CommandMeta : ICommandMeta
{ {

View File

@ -1,6 +1,6 @@
using System; using System;
namespace OpenHarbor.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.Abstractions.Discovery;
public interface ICommandMeta public interface ICommandMeta
{ {

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace OpenHarbor.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.Abstractions.Discovery;
public interface IQueryDiscovery public interface IQueryDiscovery
{ {

View File

@ -1,6 +1,6 @@
using System; using System;
namespace OpenHarbor.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.Abstractions.Discovery;
public interface IQueryMeta public interface IQueryMeta
{ {

View File

@ -1,8 +1,8 @@
using System; using System;
using System.Reflection; using System.Reflection;
using OpenHarbor.CQRS.Abstractions.Attributes; using Svrnty.CQRS.Abstractions.Attributes;
namespace OpenHarbor.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.Abstractions.Discovery;
public class QueryMeta : IQueryMeta public class QueryMeta : IQueryMeta
{ {

View File

@ -1,7 +1,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace OpenHarbor.CQRS.Abstractions; namespace Svrnty.CQRS.Abstractions;
public interface ICommandHandler<in TCommand> public interface ICommandHandler<in TCommand>
where TCommand : class where TCommand : class

View File

@ -1,7 +1,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace OpenHarbor.CQRS.Abstractions; namespace Svrnty.CQRS.Abstractions;
public interface IQueryHandler<in TQuery, TQueryResult> public interface IQueryHandler<in TQuery, TQueryResult>
where TQuery : class where TQuery : class

View File

@ -1,4 +1,4 @@
namespace OpenHarbor.CQRS.Abstractions.Security; namespace Svrnty.CQRS.Abstractions.Security;
public enum AuthorizationResult public enum AuthorizationResult
{ {

View File

@ -2,7 +2,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace OpenHarbor.CQRS.Abstractions.Security; namespace Svrnty.CQRS.Abstractions.Security;
public interface ICommandAuthorizationService public interface ICommandAuthorizationService
{ {

View File

@ -2,7 +2,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace OpenHarbor.CQRS.Abstractions.Security; namespace Svrnty.CQRS.Abstractions.Security;
public interface IQueryAuthorizationService public interface IQueryAuthorizationService
{ {

View File

@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
namespace OpenHarbor.CQRS.Abstractions; namespace Svrnty.CQRS.Abstractions;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors> <Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<LangVersion>default</LangVersion> <LangVersion>14</LangVersion>
<Company>Open Harbor</Company> <Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl> <RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -25,6 +25,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0-rc.1.23419.4" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,6 +1,6 @@
using System; using System;
namespace OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes; namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CommandControllerIgnoreAttribute : Attribute public class CommandControllerIgnoreAttribute : Attribute

View File

@ -0,0 +1,8 @@
using System;
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class QueryControllerAuthorizationAttribute : Attribute
{
}

View File

@ -1,6 +1,6 @@
using System; using System;
namespace OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes; namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class QueryControllerIgnoreAttribute : Attribute public class QueryControllerIgnoreAttribute : Attribute

View File

@ -1,13 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible>
<IsAotCompatible>false</IsAotCompatible> <IsAotCompatible>false</IsAotCompatible>
<LangVersion>default</LangVersion> <LangVersion>14</LangVersion>
<Company>Open Harbor</Company> <Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl> <RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;
public class DynamicQueryInterceptorProvider<TSource, TDestination> : IDynamicQueryInterceptorProvider<TSource, TDestination> public class DynamicQueryInterceptorProvider<TSource, TDestination> : IDynamicQueryInterceptorProvider<TSource, TDestination>
{ {

View File

@ -2,7 +2,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;
public interface IAlterQueryableService<TSource, TDestination> public interface IAlterQueryableService<TSource, TDestination>
{ {

View File

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;
public interface IDynamicQuery<TSource, TDestination> : IDynamicQuery public interface IDynamicQuery<TSource, TDestination> : IDynamicQuery
where TSource : class where TSource : class

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;
public interface IDynamicQueryInterceptorProvider<TSource, TDestination> public interface IDynamicQueryInterceptorProvider<TSource, TDestination>
{ {

View File

@ -1,4 +1,4 @@
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;
public interface IDynamicQueryParams<out TParams> public interface IDynamicQueryParams<out TParams>
where TParams : class where TParams : class

View File

@ -2,7 +2,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;
public interface IQueryableProvider<TSource> public interface IQueryableProvider<TSource>
{ {

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks> <TargetFrameworks>netstandard2.1;net10.0</TargetFrameworks>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible> <IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">true</IsAotCompatible>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>default</LangVersion> <LangVersion>14</LangVersion>
<Company>Open Harbor</Company> <Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl> <RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>

View File

@ -1,10 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenHarbor.CQRS.DynamicQuery.Abstractions; using Svrnty.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore; namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
public class DynamicQuery<TSource, TDestination> : DynamicQuery, IDynamicQuery<TSource, TDestination> public class DynamicQuery<TSource, TDestination> : DynamicQuery, IDynamicQuery<TSource, TDestination>
where TSource : class where TSource : class

View File

@ -2,7 +2,7 @@
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
using System; using System;
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore; namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
public class DynamicQueryAggregate public class DynamicQueryAggregate
{ {

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore; namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
public class DynamicQueryFilter public class DynamicQueryFilter
{ {

View File

@ -1,11 +1,11 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using OpenHarbor.CQRS.Abstractions; using Svrnty.CQRS.Abstractions;
using OpenHarbor.CQRS.AspNetCore.Mvc; using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
using OpenHarbor.CQRS.DynamicQuery.Abstractions; using Svrnty.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc; namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
[ApiController, Route("api/query/[controller]")] [ApiController, Route("api/query/[controller]")]
public class DynamicQueryController<TUnderlyingQuery, TSource, TDestination> : Controller public class DynamicQueryController<TUnderlyingQuery, TSource, TDestination> : Controller

View File

@ -1,9 +1,9 @@
using System; using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc; namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
public class DynamicQueryControllerConvention : IControllerModelConvention public class DynamicQueryControllerConvention : IControllerModelConvention
{ {

View File

@ -4,11 +4,11 @@ using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
using OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes; using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
using OpenHarbor.CQRS.DynamicQuery.Discover; using Svrnty.CQRS.DynamicQuery.Discover;
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc; namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider) public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider)
: IApplicationFeatureProvider<ControllerFeature> : IApplicationFeatureProvider<ControllerFeature>

View File

@ -0,0 +1,5 @@
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
public class DynamicQueryControllerOptions
{
}

View File

@ -1,8 +1,8 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc; using Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore; namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
public static class MvcBuilderExtensions public static class MvcBuilderExtensions
{ {

View File

@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible> <IsAotCompatible>false</IsAotCompatible>
<Company>Open Harbor</Company> <LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl> <RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -27,10 +28,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore\OpenHarbor.CQRS.AspNetCore.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery\OpenHarbor.CQRS.DynamicQuery.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,8 +1,8 @@
using System; using System;
using Pluralize.NET; using Pluralize.NET;
using OpenHarbor.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
namespace OpenHarbor.CQRS.DynamicQuery.Discover; namespace Svrnty.CQRS.DynamicQuery.Discover;
public class DynamicQueryMeta : QueryMeta public class DynamicQueryMeta : QueryMeta
{ {

View File

@ -1,4 +1,4 @@
using OpenHarbor.CQRS.DynamicQuery.Abstractions; using Svrnty.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -6,11 +6,11 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace OpenHarbor.CQRS.DynamicQuery; namespace Svrnty.CQRS.DynamicQuery;
public class DynamicQueryHandler<TSource, TDestination> public class DynamicQueryHandler<TSource, TDestination>
: DynamicQueryHandlerBase<TSource, TDestination>, : DynamicQueryHandlerBase<TSource, TDestination>,
OpenHarbor.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>> Svrnty.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>>
where TSource : class where TSource : class
where TDestination : class where TDestination : class
{ {
@ -30,7 +30,7 @@ public class DynamicQueryHandler<TSource, TDestination>
public class DynamicQueryHandler<TSource, TDestination, TParams> public class DynamicQueryHandler<TSource, TDestination, TParams>
: DynamicQueryHandlerBase<TSource, TDestination>, : DynamicQueryHandlerBase<TSource, TDestination>,
OpenHarbor.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>> Svrnty.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>
where TSource : class where TSource : class
where TDestination : class where TDestination : class
where TParams : class where TParams : class

View File

@ -3,11 +3,11 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenHarbor.CQRS.DynamicQuery.Abstractions; using Svrnty.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
namespace OpenHarbor.CQRS.DynamicQuery; namespace Svrnty.CQRS.DynamicQuery;
public abstract class DynamicQueryHandlerBase<TSource, TDestination> public abstract class DynamicQueryHandlerBase<TSource, TDestination>
where TSource : class where TSource : class

View File

@ -1,13 +1,13 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenHarbor.CQRS.Abstractions; using Svrnty.CQRS.Abstractions;
using OpenHarbor.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
using OpenHarbor.CQRS.DynamicQuery.Abstractions; using Svrnty.CQRS.DynamicQuery.Abstractions;
using OpenHarbor.CQRS.DynamicQuery.Discover; using Svrnty.CQRS.DynamicQuery.Discover;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
namespace OpenHarbor.CQRS.DynamicQuery; namespace Svrnty.CQRS.DynamicQuery;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {

View File

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
<LangVersion>default</LangVersion> <LangVersion>14</LangVersion>
<Company>Open Harbor</Company> <Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl> <RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -29,7 +29,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" /> <ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,9 +1,9 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenHarbor.CQRS.Abstractions; using Svrnty.CQRS.Abstractions;
namespace OpenHarbor.CQRS.FluentValidation; namespace Svrnty.CQRS.FluentValidation;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {

View File

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
<LangVersion>default</LangVersion> <LangVersion>14</LangVersion>
<Company>Open Harbor</Company> <Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl> <RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -24,10 +24,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="10.4.0" /> <PackageReference Include="FluentValidation" Version="11.11.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" /> <ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,8 @@
using System;
namespace Svrnty.CQRS.Grpc.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class GrpcIgnoreAttribute : Attribute
{
}

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<Nullable>enable</Nullable>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
<IncludeSymbols>true</IncludeSymbols>
<IncludeSource>true</IncludeSource>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,852 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Svrnty.CQRS.Grpc.Generators.Helpers;
using Svrnty.CQRS.Grpc.Generators.Models;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Svrnty.CQRS.Grpc.Generators
{
[Generator]
public class GrpcGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Find all types that might be commands or queries
var typeDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is TypeDeclarationSyntax,
transform: static (ctx, _) => GetTypeSymbol(ctx))
.Where(static symbol => symbol is not null);
// Combine with compilation
var compilationAndTypes = context.CompilationProvider.Combine(typeDeclarations.Collect());
// Register source output
context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right!, spc));
}
private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context)
{
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration);
return symbol as INamedTypeSymbol;
}
private static void Execute(Compilation compilation, IEnumerable<INamedTypeSymbol?> types, SourceProductionContext context)
{
var grpcIgnoreAttribute = compilation.GetTypeByMetadataName("Svrnty.CQRS.Grpc.Abstractions.Attributes.GrpcIgnoreAttribute");
var commandHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`1");
var commandHandlerWithResultInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`2");
var queryHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.IQueryHandler`2");
if (commandHandlerInterface == null || queryHandlerInterface == null)
{
return; // Handler interfaces not found
}
var commandMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol?>(SymbolEqualityComparer.Default); // Command -> Result type (null if no result)
var queryMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol>(SymbolEqualityComparer.Default); // Query -> Result type
// Find all command and query types by looking at handler implementations
foreach (var typeSymbol in types)
{
if (typeSymbol == null || typeSymbol.IsAbstract || typeSymbol.IsStatic)
continue;
// Check if this type implements ICommandHandler<T> or ICommandHandler<T, TResult>
foreach (var iface in typeSymbol.AllInterfaces)
{
if (iface.IsGenericType)
{
// Check for ICommandHandler<TCommand>
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerInterface) && iface.TypeArguments.Length == 1)
{
var commandType = iface.TypeArguments[0] as INamedTypeSymbol;
if (commandType != null && !commandMap.ContainsKey(commandType))
commandMap[commandType] = null; // No result type
}
// Check for ICommandHandler<TCommand, TResult>
else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerWithResultInterface) && iface.TypeArguments.Length == 2)
{
var commandType = iface.TypeArguments[0] as INamedTypeSymbol;
var resultType = iface.TypeArguments[1] as INamedTypeSymbol;
if (commandType != null && resultType != null)
commandMap[commandType] = resultType;
}
// Check for IQueryHandler<TQuery, TResult>
else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryHandlerInterface) && iface.TypeArguments.Length == 2)
{
var queryType = iface.TypeArguments[0] as INamedTypeSymbol;
var resultType = iface.TypeArguments[1] as INamedTypeSymbol;
if (queryType != null && resultType != null)
queryMap[queryType] = resultType;
}
}
}
}
var commands = new List<CommandInfo>();
var queries = new List<QueryInfo>();
// Process discovered command types
foreach (var kvp in commandMap)
{
var commandType = kvp.Key;
var resultType = kvp.Value;
// Skip if marked with [GrpcIgnore]
if (grpcIgnoreAttribute != null && HasAttribute(commandType, grpcIgnoreAttribute))
continue;
var commandInfo = ExtractCommandInfo(commandType, resultType);
if (commandInfo != null)
commands.Add(commandInfo);
}
// Process discovered query types
foreach (var kvp in queryMap)
{
var queryType = kvp.Key;
var resultType = kvp.Value;
// Skip if marked with [GrpcIgnore]
if (grpcIgnoreAttribute != null && HasAttribute(queryType, grpcIgnoreAttribute))
continue;
var queryInfo = ExtractQueryInfo(queryType, resultType);
if (queryInfo != null)
queries.Add(queryInfo);
}
// Generate services if we found any commands or queries
if (commands.Any() || queries.Any())
{
GenerateProtoAndServices(context, commands, queries, compilation);
}
}
private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
{
return typeSymbol.GetAttributes().Any(attr =>
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol));
}
private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? interfaceSymbol)
{
if (interfaceSymbol == null)
return false;
return typeSymbol.AllInterfaces.Any(i =>
SymbolEqualityComparer.Default.Equals(i, interfaceSymbol));
}
private static bool ImplementsGenericInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? genericInterfaceSymbol)
{
if (genericInterfaceSymbol == null)
return false;
return typeSymbol.AllInterfaces.Any(i =>
i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, genericInterfaceSymbol));
}
private static CommandInfo? ExtractCommandInfo(INamedTypeSymbol commandType, INamedTypeSymbol? resultType)
{
var commandInfo = new CommandInfo
{
Name = commandType.Name,
FullyQualifiedName = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
Namespace = commandType.ContainingNamespace.ToDisplayString(),
Properties = new List<PropertyInfo>()
};
// Set result type if provided
if (resultType != null)
{
commandInfo.ResultType = resultType.Name;
commandInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandType.Name}, {resultType.Name}>";
}
else
{
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandType.Name}>";
}
// Extract properties
var properties = commandType.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.ToList();
int fieldNumber = 1;
foreach (var property in properties)
{
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
commandInfo.Properties.Add(new PropertyInfo
{
Name = property.Name,
Type = propertyType,
ProtoType = protoType,
FieldNumber = fieldNumber++
});
}
return commandInfo;
}
private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType)
{
var queryInfo = new QueryInfo
{
Name = queryType.Name,
FullyQualifiedName = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
Namespace = queryType.ContainingNamespace.ToDisplayString(),
Properties = new List<PropertyInfo>()
};
// Set result type
queryInfo.ResultType = resultType.Name;
queryInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryType.Name}, {resultType.Name}>";
// Extract properties
var properties = queryType.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.ToList();
int fieldNumber = 1;
foreach (var property in properties)
{
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
queryInfo.Properties.Add(new PropertyInfo
{
Name = property.Name,
Type = propertyType,
ProtoType = protoType,
FieldNumber = fieldNumber++
});
}
return queryInfo;
}
private static void GenerateProtoAndServices(SourceProductionContext context, List<CommandInfo> commands, List<QueryInfo> queries, Compilation compilation)
{
// Get root namespace from compilation
var rootNamespace = compilation.AssemblyName ?? "Application";
// Generate service implementations for commands
if (commands.Any())
{
var commandService = GenerateCommandServiceImpl(commands, rootNamespace);
context.AddSource("CommandServiceImpl.g.cs", commandService);
}
// Generate service implementations for queries
if (queries.Any())
{
var queryService = GenerateQueryServiceImpl(queries, rootNamespace);
context.AddSource("QueryServiceImpl.g.cs", queryService);
}
// Generate registration extensions
var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), rootNamespace);
context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions);
}
private static string GenerateCommandMessages(List<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.Runtime.Serialization;");
sb.AppendLine("using ProtoBuf;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages");
sb.AppendLine("{");
foreach (var command in commands)
{
// Generate command DTO
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class {command.Name}Dto");
sb.AppendLine(" {");
foreach (var prop in command.Properties)
{
sb.AppendLine($" [ProtoMember({prop.FieldNumber})]");
sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]");
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();
// Generate result DTO if command has a result
if (command.HasResult)
{
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class {command.Name}ResultDto");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine($" public {command.ResultFullyQualifiedName} Result {{ get; set; }}");
sb.AppendLine(" }");
sb.AppendLine();
}
}
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateQueryMessages(List<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.Runtime.Serialization;");
sb.AppendLine("using ProtoBuf;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages");
sb.AppendLine("{");
foreach (var query in queries)
{
// Generate query DTO
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class {query.Name}Dto");
sb.AppendLine(" {");
foreach (var prop in query.Properties)
{
sb.AppendLine($" [ProtoMember({prop.FieldNumber})]");
sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]");
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();
// Generate result DTO
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class {query.Name}ResultDto");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine($" public {query.ResultFullyQualifiedName} Result {{ get; set; }}");
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateCommandService(List<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.ServiceModel;");
sb.AppendLine("using System.Threading;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc.Messages;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine("using ProtoBuf.Grpc;");
sb.AppendLine();
// Generate service interface
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" [ServiceContract]");
sb.AppendLine(" public interface ICommandService");
sb.AppendLine(" {");
foreach (var command in commands)
{
if (command.HasResult)
{
sb.AppendLine($" [OperationContract]");
sb.AppendLine($" Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);");
}
else
{
sb.AppendLine($" [OperationContract]");
sb.AppendLine($" Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);");
}
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();
// Generate service implementation
sb.AppendLine(" public sealed class CommandService : ICommandService");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
sb.AppendLine();
sb.AppendLine(" public CommandService(IServiceProvider serviceProvider)");
sb.AppendLine(" {");
sb.AppendLine(" _serviceProvider = serviceProvider;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var command in commands)
{
if (command.HasResult)
{
sb.AppendLine($" public async Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)");
sb.AppendLine(" {");
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in command.Properties)
{
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
}
sb.AppendLine(" };");
sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);");
sb.AppendLine($" return new {command.Name}ResultDto {{ Result = result }};");
sb.AppendLine(" }");
}
else
{
sb.AppendLine($" public async Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)");
sb.AppendLine(" {");
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in command.Properties)
{
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
}
sb.AppendLine(" };");
sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);");
sb.AppendLine(" }");
}
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateQueryService(List<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.ServiceModel;");
sb.AppendLine("using System.Threading;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc.Messages;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine("using ProtoBuf.Grpc;");
sb.AppendLine();
// Generate service interface
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" [ServiceContract]");
sb.AppendLine(" public interface IQueryService");
sb.AppendLine(" {");
foreach (var query in queries)
{
sb.AppendLine($" [OperationContract]");
sb.AppendLine($" Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default);");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();
// Generate service implementation
sb.AppendLine(" public sealed class QueryService : IQueryService");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
sb.AppendLine();
sb.AppendLine(" public QueryService(IServiceProvider serviceProvider)");
sb.AppendLine(" {");
sb.AppendLine(" _serviceProvider = serviceProvider;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var query in queries)
{
sb.AppendLine($" public async Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default)");
sb.AppendLine(" {");
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();");
sb.AppendLine($" var query = new {query.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in query.Properties)
{
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
}
sb.AppendLine(" };");
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
sb.AppendLine($" return new {query.Name}ResultDto {{ Result = result }};");
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using Microsoft.AspNetCore.Builder;");
sb.AppendLine("using Microsoft.AspNetCore.Routing;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc.Services;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Extensions");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static class GrpcServiceRegistrationExtensions");
sb.AppendLine(" {");
if (hasCommands)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated Command gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasQueries)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated Query gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasCommands && hasQueries)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers both Command and Query gRPC services");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcCommandsAndQueries(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddGrpcReflection();");
if (hasCommands)
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
if (hasQueries)
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps both Command and Query gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
if (hasCommands)
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
if (hasQueries)
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string ToCamelCase(string str)
{
if (string.IsNullOrEmpty(str) || char.IsLower(str[0]))
return str;
return char.ToLowerInvariant(str[0]) + str.Substring(1);
}
// New methods for standard gRPC generation
private static string GenerateCommandsProto(List<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("syntax = \"proto3\";");
sb.AppendLine();
sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";");
sb.AppendLine();
sb.AppendLine("package cqrs;");
sb.AppendLine();
sb.AppendLine("// Command service for CQRS operations");
sb.AppendLine("service CommandService {");
foreach (var command in commands)
{
var methodName = command.Name.Replace("Command", "");
sb.AppendLine($" // {command.Name}");
sb.AppendLine($" rpc {methodName} ({command.Name}Request) returns ({command.Name}Response);");
}
sb.AppendLine("}");
sb.AppendLine();
// Generate message types
foreach (var command in commands)
{
// Request message
sb.AppendLine($"message {command.Name}Request {{");
foreach (var prop in command.Properties)
{
sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};");
}
sb.AppendLine("}");
sb.AppendLine();
// Response message
sb.AppendLine($"message {command.Name}Response {{");
if (command.HasResult)
{
sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(command.ResultFullyQualifiedName!, out _, out _)} result = 1;");
}
sb.AppendLine("}");
sb.AppendLine();
}
return sb.ToString();
}
private static string GenerateQueriesProto(List<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("syntax = \"proto3\";");
sb.AppendLine();
sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";");
sb.AppendLine();
sb.AppendLine("package cqrs;");
sb.AppendLine();
sb.AppendLine("// Query service for CQRS operations");
sb.AppendLine("service QueryService {");
foreach (var query in queries)
{
var methodName = query.Name.Replace("Query", "");
sb.AppendLine($" // {query.Name}");
sb.AppendLine($" rpc {methodName} ({query.Name}Request) returns ({query.Name}Response);");
}
sb.AppendLine("}");
sb.AppendLine();
// Generate message types
foreach (var query in queries)
{
// Request message
sb.AppendLine($"message {query.Name}Request {{");
foreach (var prop in query.Properties)
{
sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};");
}
sb.AppendLine("}");
sb.AppendLine();
// Response message
sb.AppendLine($"message {query.Name}Response {{");
sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(query.ResultFullyQualifiedName, out _, out _)} result = 1;");
sb.AppendLine("}");
sb.AppendLine();
}
return sb.ToString();
}
private static string GenerateCommandServiceImpl(List<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using Grpc.Core;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using System.Linq;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine("using FluentValidation;");
sb.AppendLine($"using {rootNamespace}.Grpc;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public sealed class CommandServiceImpl : CommandService.CommandServiceBase");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
sb.AppendLine();
sb.AppendLine(" public CommandServiceImpl(IServiceProvider serviceProvider)");
sb.AppendLine(" {");
sb.AppendLine(" _serviceProvider = serviceProvider;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var command in commands)
{
var methodName = command.Name.Replace("Command", "");
var requestType = $"{command.Name}Request";
var responseType = $"{command.Name}Response";
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
sb.AppendLine($" {requestType} request,");
sb.AppendLine(" ServerCallContext context)");
sb.AppendLine(" {");
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in command.Properties)
{
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
}
sb.AppendLine(" };");
sb.AppendLine();
sb.AppendLine(" // Validate command if validator is registered");
sb.AppendLine($" var validator = _serviceProvider.GetService<IValidator<{command.FullyQualifiedName}>>();");
sb.AppendLine(" if (validator != null)");
sb.AppendLine(" {");
sb.AppendLine(" var validationResult = await validator.ValidateAsync(command, context.CancellationToken);");
sb.AppendLine(" if (!validationResult.IsValid)");
sb.AppendLine(" {");
sb.AppendLine(" var errors = string.Join(\", \", validationResult.Errors.Select(e => e.ErrorMessage));");
sb.AppendLine(" throw new RpcException(new Status(StatusCode.InvalidArgument, $\"Validation failed: {errors}\"));");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
if (command.HasResult)
{
sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);");
sb.AppendLine($" return new {responseType} {{ Result = result }};");
}
else
{
sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);");
sb.AppendLine($" return new {responseType}();");
}
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateQueryServiceImpl(List<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using Grpc.Core;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public sealed class QueryServiceImpl : QueryService.QueryServiceBase");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
sb.AppendLine();
sb.AppendLine(" public QueryServiceImpl(IServiceProvider serviceProvider)");
sb.AppendLine(" {");
sb.AppendLine(" _serviceProvider = serviceProvider;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var query in queries)
{
var methodName = query.Name.Replace("Query", "");
var requestType = $"{query.Name}Request";
var responseType = $"{query.Name}Response";
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
sb.AppendLine($" {requestType} request,");
sb.AppendLine(" ServerCallContext context)");
sb.AppendLine(" {");
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();");
sb.AppendLine($" var query = new {query.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in query.Properties)
{
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
}
sb.AppendLine(" };");
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
sb.AppendLine($" return new {responseType} {{ Result = result }};");
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
}
}

View File

@ -0,0 +1,96 @@
using System.Collections.Generic;
namespace Svrnty.CQRS.Grpc.Generators.Helpers
{
internal static class ProtoTypeMapper
{
private static readonly Dictionary<string, string> TypeMap = new Dictionary<string, string>
{
// Primitives
{ "System.String", "string" },
{ "System.Boolean", "bool" },
{ "System.Int32", "int32" },
{ "System.Int64", "int64" },
{ "System.UInt32", "uint32" },
{ "System.UInt64", "uint64" },
{ "System.Single", "float" },
{ "System.Double", "double" },
{ "System.Byte", "uint32" },
{ "System.SByte", "int32" },
{ "System.Int16", "int32" },
{ "System.UInt16", "uint32" },
{ "System.Decimal", "string" }, // Decimal as string to preserve precision
{ "System.DateTime", "int64" }, // Unix timestamp
{ "System.DateTimeOffset", "int64" }, // Unix timestamp
{ "System.Guid", "string" },
{ "System.TimeSpan", "int64" }, // Ticks
// Nullable variants
{ "System.Boolean?", "bool" },
{ "System.Int32?", "int32" },
{ "System.Int64?", "int64" },
{ "System.UInt32?", "uint32" },
{ "System.UInt64?", "uint64" },
{ "System.Single?", "float" },
{ "System.Double?", "double" },
{ "System.Byte?", "uint32" },
{ "System.SByte?", "int32" },
{ "System.Int16?", "int32" },
{ "System.UInt16?", "uint32" },
{ "System.Decimal?", "string" },
{ "System.DateTime?", "int64" },
{ "System.DateTimeOffset?", "int64" },
{ "System.Guid?", "string" },
{ "System.TimeSpan?", "int64" },
};
public static string MapToProtoType(string csharpType, out bool isRepeated, out bool isOptional)
{
isRepeated = false;
isOptional = false;
// Handle arrays
if (csharpType.EndsWith("[]"))
{
isRepeated = true;
var elementType = csharpType.Substring(0, csharpType.Length - 2);
return MapToProtoType(elementType, out _, out _);
}
// Handle generic collections
if (csharpType.StartsWith("System.Collections.Generic.List<") ||
csharpType.StartsWith("System.Collections.Generic.IList<") ||
csharpType.StartsWith("System.Collections.Generic.IEnumerable<") ||
csharpType.StartsWith("System.Collections.Generic.ICollection<"))
{
isRepeated = true;
var startIndex = csharpType.IndexOf('<') + 1;
var endIndex = csharpType.LastIndexOf('>');
var elementType = csharpType.Substring(startIndex, endIndex - startIndex);
return MapToProtoType(elementType, out _, out _);
}
// Handle nullable value types
if (csharpType.EndsWith("?"))
{
isOptional = true;
}
// Check if it's a known primitive type
if (TypeMap.TryGetValue(csharpType, out var protoType))
{
return protoType;
}
// For unknown types, assume it's a custom message type
// Extract just the type name without namespace
var lastDot = csharpType.LastIndexOf('.');
if (lastDot >= 0)
{
return csharpType.Substring(lastDot + 1).Replace("?", "");
}
return csharpType.Replace("?", "");
}
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace Svrnty.CQRS.Grpc.Generators.Models
{
public class CommandInfo
{
public string Name { get; set; }
public string FullyQualifiedName { get; set; }
public string Namespace { get; set; }
public List<PropertyInfo> Properties { get; set; }
public string? ResultType { get; set; }
public string? ResultFullyQualifiedName { get; set; }
public bool HasResult => ResultType != null;
public string HandlerInterfaceName { get; set; }
public CommandInfo()
{
Name = string.Empty;
FullyQualifiedName = string.Empty;
Namespace = string.Empty;
Properties = new List<PropertyInfo>();
HandlerInterfaceName = string.Empty;
}
}
public class PropertyInfo
{
public string Name { get; set; }
public string Type { get; set; }
public string ProtoType { get; set; }
public int FieldNumber { get; set; }
public PropertyInfo()
{
Name = string.Empty;
Type = string.Empty;
ProtoType = string.Empty;
}
}
}

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace Svrnty.CQRS.Grpc.Generators.Models
{
public class QueryInfo
{
public string Name { get; set; }
public string FullyQualifiedName { get; set; }
public string Namespace { get; set; }
public List<PropertyInfo> Properties { get; set; }
public string ResultType { get; set; }
public string ResultFullyQualifiedName { get; set; }
public string HandlerInterfaceName { get; set; }
public QueryInfo()
{
Name = string.Empty;
FullyQualifiedName = string.Empty;
Namespace = string.Empty;
Properties = new List<PropertyInfo>();
ResultType = string.Empty;
ResultFullyQualifiedName = string.Empty;
HandlerInterfaceName = string.Empty;
}
}
}

View File

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsPackable>true</IsPackable>
<DevelopmentDependency>true</DevelopmentDependency>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<PropertyGroup>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Source Generator for Svrnty.CQRS.Grpc - generates .proto files and gRPC service implementations from commands and queries</Description>
</PropertyGroup>
<ItemGroup>
<None Include="..\icon.png" Pack="true" PackagePath="" />
<None Include="..\README.md" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,40 @@
using FluentValidation;
using Svrnty.CQRS.Abstractions;
namespace Svrnty.CQRS.Grpc.Sample;
public record AddUserCommand
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public int Age { get; set; }
}
public class AddUserCommandValidator : AbstractValidator<AddUserCommand>
{
public AddUserCommandValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.WithMessage("Email is required")
.EmailAddress()
.WithMessage("Email must be a valid email address");
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name is required");
RuleFor(x => x.Age)
.GreaterThan(0)
.WithMessage("Age must be greater than 0");
}
}
public class AddUserCommandHandler : ICommandHandler<AddUserCommand, int>
{
public Task<int> HandleAsync(AddUserCommand command, CancellationToken cancellationToken = default)
{
// Simulate adding a user and returning ID
return Task.FromResult(123);
}
}

View File

@ -0,0 +1,29 @@
using Svrnty.CQRS.Abstractions;
namespace Svrnty.CQRS.Grpc.Sample;
public record User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public record FetchUserQuery
{
public int UserId { get; set; }
}
public class FetchUserQueryHandler : IQueryHandler<FetchUserQuery, User>
{
public Task<User> HandleAsync(FetchUserQuery query, CancellationToken cancellationToken = default)
{
// Simulate fetching a user
return Task.FromResult(new User
{
Id = query.UserId,
Name = "John Doe",
Email = "john@example.com"
});
}
}

View File

@ -0,0 +1,21 @@
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.Grpc.Abstractions.Attributes;
namespace Svrnty.CQRS.Grpc.Sample;
// This command is marked with [GrpcIgnore] so it won't be exposed via gRPC
[GrpcIgnore]
public record InternalCommand
{
public string Data { get; set; } = string.Empty;
}
public class InternalCommandHandler : ICommandHandler<InternalCommand>
{
public Task HandleAsync(InternalCommand command, CancellationToken cancellationToken = default)
{
// This is an internal command that should not be exposed via gRPC
Console.WriteLine($"Processing internal command: {command.Data}");
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,29 @@
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.FluentValidation;
using Svrnty.CQRS.Grpc.Sample;
using Svrnty.CQRS.Grpc.Sample.Grpc.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Register command handlers with CQRS and FluentValidation
builder.Services.AddCommand<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
// Register query handlers with CQRS
builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
// Auto-generated: Register gRPC services for both commands and queries (includes reflection)
builder.Services.AddGrpcCommandsAndQueries();
var app = builder.Build();
// Auto-generated: Map gRPC endpoints for both commands and queries
app.MapGrpcCommandsAndQueries();
// Map gRPC reflection service
app.MapGrpcReflectionService();
Console.WriteLine("Auto-Generated gRPC Server with Reflection and Validation");
Console.WriteLine("http://localhost:5000");
app.Run();

View File

@ -0,0 +1,58 @@
syntax = "proto3";
option csharp_namespace = "Svrnty.CQRS.Grpc.Sample.Grpc";
package cqrs;
// Command service for CQRS operations
service CommandService {
// Adds a new user and returns the user ID
rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse);
// Removes a user
rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse);
}
// Query service for CQRS operations
service QueryService {
// Fetches a user by ID
rpc FetchUser (FetchUserQueryRequest) returns (FetchUserQueryResponse);
}
// Request message for adding a user
message AddUserCommandRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
// Response message containing the added user ID
message AddUserCommandResponse {
int32 result = 1;
}
// Request message for removing a user
message RemoveUserCommandRequest {
int32 user_id = 1;
}
// Response message for remove user (empty)
message RemoveUserCommandResponse {
}
// Request message for fetching a user
message FetchUserQueryRequest {
int32 user_id = 1;
}
// Response message containing the user
message FetchUserQueryResponse {
User result = 1;
}
// User entity
message User {
int32 id = 1;
string name = 2;
string email = 3;
}

View File

@ -0,0 +1,17 @@
using Svrnty.CQRS.Abstractions;
namespace Svrnty.CQRS.Grpc.Sample;
public record RemoveUserCommand
{
public int UserId { get; set; }
}
public class RemoveUserCommandHandler : ICommandHandler<RemoveUserCommand>
{
public Task HandleAsync(RemoveUserCommand command, CancellationToken cancellationToken = default)
{
// Simulate removing a user
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.70.0" />
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.71.0" />
<PackageReference Include="Grpc.Tools" Version="2.70.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Grpc\Svrnty.CQRS.Grpc.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Svrnty.CQRS.FluentValidation\Svrnty.CQRS.FluentValidation.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Server.Kestrel": "Information"
}
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000",
"Protocols": "Http2"
}
}
}
}

View File

@ -0,0 +1,24 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000",
"Protocols": "Http2"
},
"Https": {
"Url": "https://localhost:5001",
"Protocols": "Http2"
}
},
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}

View File

@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<Nullable>enable</Nullable>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
<IncludeSymbols>true</IncludeSymbols>
<IncludeSource>true</IncludeSource>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
<None Include="build\Svrnty.CQRS.Grpc.targets" Pack="true" PackagePath="build\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.70.0" />
<PackageReference Include="Grpc.Tools" Version="2.70.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentValidation" Version="11.11.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Abstractions\Svrnty.CQRS.Grpc.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<GrpcProtosDirectory>$(MSBuildProjectDirectory)\Protos</GrpcProtosDirectory>
</PropertyGroup>
<ItemGroup>
<!-- Auto-include generated proto files -->
<Protobuf Include="$(GrpcProtosDirectory)\*.proto" GrpcServices="Server" />
</ItemGroup>
<Target Name="EnsureProtosDirectory" BeforeTargets="BeforeBuild">
<MakeDir Directories="$(GrpcProtosDirectory)" Condition="!Exists('$(GrpcProtosDirectory)')" />
</Target>
</Project>

View File

@ -0,0 +1,254 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.Abstractions.Discovery;
using Svrnty.CQRS.Abstractions.Security;
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
namespace Svrnty.CQRS.MinimalApi;
public static class EndpointRouteBuilderExtensions
{
public static IEndpointRouteBuilder MapOpenHarborQueries(this IEndpointRouteBuilder endpoints, string routePrefix = "api/query")
{
var queryDiscovery = endpoints.ServiceProvider.GetRequiredService<IQueryDiscovery>();
var authorizationService = endpoints.ServiceProvider.GetService<IQueryAuthorizationService>();
foreach (var queryMeta in queryDiscovery.GetQueries())
{
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
if (ignoreAttribute != null)
continue;
var route = $"{routePrefix}/{queryMeta.LowerCamelCaseName}";
MapQueryPost(endpoints, route, queryMeta, authorizationService);
MapQueryGet(endpoints, route, queryMeta, authorizationService);
}
return endpoints;
}
private static void MapQueryPost(
IEndpointRouteBuilder endpoints,
string route,
IQueryMeta queryMeta,
IQueryAuthorizationService? authorizationService)
{
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
{
if (authorizationService != null)
{
var authorizationResult = await authorizationService.IsAllowedAsync(queryMeta.QueryType, cancellationToken);
if (authorizationResult == AuthorizationResult.Forbidden)
return Results.StatusCode(403);
if (authorizationResult == AuthorizationResult.Unauthorized)
return Results.Unauthorized();
}
var query = await context.Request.ReadFromJsonAsync(queryMeta.QueryType, cancellationToken);
if (query == null)
return Results.BadRequest("Invalid query payload");
var handler = serviceProvider.GetRequiredService(handlerType);
var handleMethod = handlerType.GetMethod("HandleAsync");
if (handleMethod == null)
return Results.Problem("Handler method not found");
var task = (Task)handleMethod.Invoke(handler, [query, cancellationToken])!;
await task;
var resultProperty = task.GetType().GetProperty("Result");
var result = resultProperty?.GetValue(task);
return Results.Ok(result);
})
.WithName($"Query_{queryMeta.LowerCamelCaseName}_Post")
.WithTags("Queries")
.Produces(200, queryMeta.QueryResultType)
.Produces(400)
.Produces(401)
.Produces(403);
}
private static void MapQueryGet(
IEndpointRouteBuilder endpoints,
string route,
IQueryMeta queryMeta,
IQueryAuthorizationService? authorizationService)
{
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
endpoints.MapGet(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
{
if (authorizationService != null)
{
var authorizationResult = await authorizationService.IsAllowedAsync(queryMeta.QueryType, cancellationToken);
if (authorizationResult == AuthorizationResult.Forbidden)
return Results.StatusCode(403);
if (authorizationResult == AuthorizationResult.Unauthorized)
return Results.Unauthorized();
}
var query = Activator.CreateInstance(queryMeta.QueryType);
if (query == null)
return Results.BadRequest("Could not create query instance");
foreach (var property in queryMeta.QueryType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (!property.CanWrite)
continue;
var queryStringValue = context.Request.Query[property.Name].FirstOrDefault();
if (queryStringValue != null)
{
try
{
var convertedValue = Convert.ChangeType(queryStringValue, property.PropertyType);
property.SetValue(query, convertedValue);
}
catch
{
}
}
}
var handler = serviceProvider.GetRequiredService(handlerType);
var handleMethod = handlerType.GetMethod("HandleAsync");
if (handleMethod == null)
return Results.Problem("Handler method not found");
var task = (Task)handleMethod.Invoke(handler, [query, cancellationToken])!;
await task;
var resultProperty = task.GetType().GetProperty("Result");
var result = resultProperty?.GetValue(task);
return Results.Ok(result);
})
.WithName($"Query_{queryMeta.LowerCamelCaseName}_Get")
.WithTags("Queries")
.Produces(200, queryMeta.QueryResultType)
.Produces(400)
.Produces(401)
.Produces(403);
}
public static IEndpointRouteBuilder MapOpenHarborCommands(this IEndpointRouteBuilder endpoints, string routePrefix = "api/command")
{
var commandDiscovery = endpoints.ServiceProvider.GetRequiredService<ICommandDiscovery>();
var authorizationService = endpoints.ServiceProvider.GetService<ICommandAuthorizationService>();
foreach (var commandMeta in commandDiscovery.GetCommands())
{
var ignoreAttribute = commandMeta.CommandType.GetCustomAttribute<CommandControllerIgnoreAttribute>();
if (ignoreAttribute != null)
continue;
var route = $"{routePrefix}/{commandMeta.LowerCamelCaseName}";
if (commandMeta.CommandResultType == null)
{
MapCommandWithoutResult(endpoints, route, commandMeta, authorizationService);
}
else
{
MapCommandWithResult(endpoints, route, commandMeta, authorizationService);
}
}
return endpoints;
}
private static void MapCommandWithoutResult(
IEndpointRouteBuilder endpoints,
string route,
ICommandMeta commandMeta,
ICommandAuthorizationService? authorizationService)
{
var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandMeta.CommandType);
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
{
if (authorizationService != null)
{
var authorizationResult = await authorizationService.IsAllowedAsync(commandMeta.CommandType, cancellationToken);
if (authorizationResult == AuthorizationResult.Forbidden)
return Results.StatusCode(403);
if (authorizationResult == AuthorizationResult.Unauthorized)
return Results.Unauthorized();
}
var command = await context.Request.ReadFromJsonAsync(commandMeta.CommandType, cancellationToken);
if (command == null)
return Results.BadRequest("Invalid command payload");
var handler = serviceProvider.GetRequiredService(handlerType);
var handleMethod = handlerType.GetMethod("HandleAsync");
if (handleMethod == null)
return Results.Problem("Handler method not found");
await (Task)handleMethod.Invoke(handler, [command, cancellationToken])!;
return Results.Ok();
})
.WithName($"Command_{commandMeta.LowerCamelCaseName}")
.WithTags("Commands")
.Produces(200)
.Produces(400)
.Produces(401)
.Produces(403);
}
private static void MapCommandWithResult(
IEndpointRouteBuilder endpoints,
string route,
ICommandMeta commandMeta,
ICommandAuthorizationService? authorizationService)
{
var handlerType = typeof(ICommandHandler<,>).MakeGenericType(commandMeta.CommandType, commandMeta.CommandResultType!);
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
{
if (authorizationService != null)
{
var authorizationResult = await authorizationService.IsAllowedAsync(commandMeta.CommandType, cancellationToken);
if (authorizationResult == AuthorizationResult.Forbidden)
return Results.StatusCode(403);
if (authorizationResult == AuthorizationResult.Unauthorized)
return Results.Unauthorized();
}
var command = await context.Request.ReadFromJsonAsync(commandMeta.CommandType, cancellationToken);
if (command == null)
return Results.BadRequest("Invalid command payload");
var handler = serviceProvider.GetRequiredService(handlerType);
var handleMethod = handlerType.GetMethod("HandleAsync");
if (handleMethod == null)
return Results.Problem("Handler method not found");
var task = (Task)handleMethod.Invoke(handler, [command, cancellationToken])!;
await task;
var resultProperty = task.GetType().GetProperty("Result");
var result = resultProperty?.GetValue(task);
return Results.Ok(result);
})
.WithName($"Command_{commandMeta.LowerCamelCaseName}")
.WithTags("Commands")
.Produces(200, commandMeta.CommandResultType)
.Produces(400)
.Produces(401)
.Produces(403);
}
}

View File

@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible> <IsAotCompatible>false</IsAotCompatible>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors> <Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<Company>Open Harbor</Company> <Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl> <RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -29,12 +30,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
</ItemGroup>
</Project> </Project>

197
Svrnty.CQRS.sln Normal file
View File

@ -0,0 +1,197 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30907.101
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.Abstractions", "Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj", "{ED78E19D-31D4-4783-AE9E-2844A8541277}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS", "Svrnty.CQRS\Svrnty.CQRS.csproj", "{7069B98F-8736-4114-8AF5-1ACE094E6238}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.AspNetCore.Abstractions", "Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj", "{4C466827-31D3-4081-A751-C2FC7C381D7E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{617BA357-1A1F-40C5-B19A-A65A960E6142}"
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.DynamicQuery", "Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj", "{A38CE930-191F-417C-B5BE-8CC62DB47513}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.DynamicQuery.AspNetCore", "Svrnty.CQRS.DynamicQuery.AspNetCore\Svrnty.CQRS.DynamicQuery.AspNetCore.csproj", "{0829B99A-0A20-4CAC-A91E-FB67E18444DE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.FluentValidation", "Svrnty.CQRS.FluentValidation\Svrnty.CQRS.FluentValidation.csproj", "{70BD37C4-7497-474D-9A40-A701203971D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.DynamicQuery.Abstractions", "Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj", "{8B9F8ACE-10EA-4215-9776-DE29EC93B020}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.MinimalApi", "Svrnty.CQRS.MinimalApi\Svrnty.CQRS.MinimalApi.csproj", "{E0756B59-F436-441A-8A98-37B6712D4B96}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc", "Svrnty.CQRS.Grpc\Svrnty.CQRS.Grpc.csproj", "{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Abstractions", "Svrnty.CQRS.Grpc.Abstractions\Svrnty.CQRS.Grpc.Abstractions.csproj", "{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Generators", "Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj", "{11537382-592C-4FE5-9103-272F358F0CAA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Sample", "Svrnty.CQRS.Grpc.Sample\Svrnty.CQRS.Grpc.Sample.csproj", "{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x64.ActiveCfg = Debug|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x64.Build.0 = Debug|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x86.ActiveCfg = Debug|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x86.Build.0 = Debug|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|Any CPU.Build.0 = Release|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x64.ActiveCfg = Release|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x64.Build.0 = Release|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x86.ActiveCfg = Release|Any CPU
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x86.Build.0 = Release|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x64.ActiveCfg = Debug|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x64.Build.0 = Debug|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x86.ActiveCfg = Debug|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x86.Build.0 = Debug|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.Build.0 = Release|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x64.ActiveCfg = Release|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x64.Build.0 = Release|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x86.ActiveCfg = Release|Any CPU
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x86.Build.0 = Release|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x64.ActiveCfg = Debug|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x64.Build.0 = Debug|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x86.ActiveCfg = Debug|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x86.Build.0 = Debug|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.Build.0 = Release|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x64.ActiveCfg = Release|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x64.Build.0 = Release|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x86.ActiveCfg = Release|Any CPU
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x86.Build.0 = Release|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x64.ActiveCfg = Debug|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x64.Build.0 = Debug|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x86.ActiveCfg = Debug|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x86.Build.0 = Debug|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|Any CPU.Build.0 = Release|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x64.ActiveCfg = Release|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x64.Build.0 = Release|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x86.ActiveCfg = Release|Any CPU
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x86.Build.0 = Release|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x64.ActiveCfg = Debug|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x64.Build.0 = Debug|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x86.ActiveCfg = Debug|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x86.Build.0 = Debug|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.Build.0 = Release|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x64.ActiveCfg = Release|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x64.Build.0 = Release|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x86.ActiveCfg = Release|Any CPU
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x86.Build.0 = Release|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x64.ActiveCfg = Debug|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x64.Build.0 = Debug|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x86.ActiveCfg = Debug|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x86.Build.0 = Debug|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|Any CPU.Build.0 = Release|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x64.ActiveCfg = Release|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x64.Build.0 = Release|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x86.ActiveCfg = Release|Any CPU
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x86.Build.0 = Release|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x64.ActiveCfg = Debug|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x64.Build.0 = Debug|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x86.ActiveCfg = Debug|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x86.Build.0 = Debug|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|Any CPU.Build.0 = Release|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x64.ActiveCfg = Release|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x64.Build.0 = Release|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x86.ActiveCfg = Release|Any CPU
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x86.Build.0 = Release|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x64.ActiveCfg = Debug|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x64.Build.0 = Debug|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x86.ActiveCfg = Debug|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x86.Build.0 = Debug|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|Any CPU.Build.0 = Release|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x64.ActiveCfg = Release|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x64.Build.0 = Release|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x86.ActiveCfg = Release|Any CPU
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x86.Build.0 = Release|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x64.ActiveCfg = Debug|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x64.Build.0 = Debug|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x86.ActiveCfg = Debug|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x86.Build.0 = Debug|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|Any CPU.Build.0 = Release|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x64.ActiveCfg = Release|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x64.Build.0 = Release|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x86.ActiveCfg = Release|Any CPU
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x86.Build.0 = Release|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x64.ActiveCfg = Debug|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x64.Build.0 = Debug|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x86.ActiveCfg = Debug|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x86.Build.0 = Debug|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|Any CPU.Build.0 = Release|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x64.ActiveCfg = Release|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x64.Build.0 = Release|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x86.ActiveCfg = Release|Any CPU
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x86.Build.0 = Release|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x64.ActiveCfg = Debug|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x64.Build.0 = Debug|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x86.ActiveCfg = Debug|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x86.Build.0 = Debug|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|Any CPU.Build.0 = Release|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x64.ActiveCfg = Release|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x64.Build.0 = Release|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x86.ActiveCfg = Release|Any CPU
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x86.Build.0 = Release|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x64.ActiveCfg = Debug|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x64.Build.0 = Debug|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x86.ActiveCfg = Debug|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x86.Build.0 = Debug|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|Any CPU.Build.0 = Release|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x64.ActiveCfg = Release|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x64.Build.0 = Release|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x86.ActiveCfg = Release|Any CPU
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D6D431EA-C04F-462B-8033-60F510FEB49E}
EndGlobalSection
EndGlobal

View File

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenHarbor.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
namespace OpenHarbor.CQRS.Discovery; namespace Svrnty.CQRS.Discovery;
public sealed class CommandDiscovery : ICommandDiscovery public sealed class CommandDiscovery : ICommandDiscovery
{ {

View File

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenHarbor.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
namespace OpenHarbor.CQRS.Discovery; namespace Svrnty.CQRS.Discovery;
public sealed class QueryDiscovery : IQueryDiscovery public sealed class QueryDiscovery : IQueryDiscovery
{ {

View File

@ -1,9 +1,9 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenHarbor.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
using OpenHarbor.CQRS.Discovery; using Svrnty.CQRS.Discovery;
namespace OpenHarbor.CQRS; namespace Svrnty.CQRS;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors> <Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<LangVersion>default</LangVersion> <LangVersion>14</LangVersion>
<Company>Open Harbor</Company> <Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl> <RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -25,6 +25,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

36
TestClient.csx Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env dotnet-script
#r "nuget: Grpc.Net.Client, 2.70.0"
#r "nuget: Google.Protobuf, 3.28.3"
#r "nuget: Grpc.Tools, 2.70.0"
using Grpc.Net.Client;
using Grpc.Core;
using System;
using System.Threading.Tasks;
// We'll use reflection/dynamic to call the gRPC service
// This is a simple HTTP/2 test
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
using var channel = GrpcChannel.ForAddress("http://localhost:5000", new GrpcChannelOptions
{
HttpHandler = handler
});
Console.WriteLine("Connected to gRPC server at http://localhost:5000");
Console.WriteLine("Channel state: " + channel.State);
// Test basic connectivity
try
{
await channel.ConnectAsync();
Console.WriteLine("Successfully connected!");
}
catch (Exception ex)
{
Console.WriteLine($"Connection failed: {ex.Message}");
}

100
TestGrpcClient/Program.cs Normal file
View File

@ -0,0 +1,100 @@
using Grpc.Core;
using Grpc.Net.Client;
using Svrnty.CQRS.Grpc.Sample.Grpc;
Console.WriteLine("=== gRPC Client Validation Test ===");
Console.WriteLine();
// Create a gRPC channel
using var channel = GrpcChannel.ForAddress("http://localhost:5000");
// Create the gRPC client
var client = new CommandService.CommandServiceClient(channel);
// Test 1: Valid request
Console.WriteLine("Test 1: Valid AddUser request...");
var validRequest = new AddUserCommandRequest
{
Name = "John Doe",
Email = "john.doe@example.com",
Age = 30
};
try
{
var response = await client.AddUserAsync(validRequest);
Console.WriteLine($"✓ Success! User added with ID: {response.Result}");
}
catch (RpcException ex)
{
Console.WriteLine($"✗ Unexpected error: {ex.Status.Detail}");
}
Console.WriteLine();
// Test 2: Invalid email (empty)
Console.WriteLine("Test 2: Invalid email (empty)...");
var invalidEmailRequest = new AddUserCommandRequest
{
Name = "Jane Doe",
Email = "",
Age = 25
};
try
{
var response = await client.AddUserAsync(invalidEmailRequest);
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
}
catch (RpcException ex)
{
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
Console.WriteLine($" Message: {ex.Status.Detail}");
}
Console.WriteLine();
// Test 3: Invalid email format
Console.WriteLine("Test 3: Invalid email format...");
var badEmailRequest = new AddUserCommandRequest
{
Name = "Bob Smith",
Email = "not-an-email",
Age = 40
};
try
{
var response = await client.AddUserAsync(badEmailRequest);
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
}
catch (RpcException ex)
{
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
Console.WriteLine($" Message: {ex.Status.Detail}");
}
Console.WriteLine();
// Test 4: Invalid age (0)
Console.WriteLine("Test 4: Invalid age (0)...");
var invalidAgeRequest = new AddUserCommandRequest
{
Name = "Alice Brown",
Email = "alice@example.com",
Age = 0
};
try
{
var response = await client.AddUserAsync(invalidAgeRequest);
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
}
catch (RpcException ex)
{
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
Console.WriteLine($" Message: {ex.Status.Detail}");
}
Console.WriteLine();
Console.WriteLine("All tests completed!");

View File

@ -0,0 +1,58 @@
syntax = "proto3";
option csharp_namespace = "Svrnty.CQRS.Grpc.Sample.Grpc";
package cqrs;
// Command service for CQRS operations
service CommandService {
// Adds a new user and returns the user ID
rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse);
// Removes a user
rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse);
}
// Query service for CQRS operations
service QueryService {
// Fetches a user by ID
rpc FetchUser (FetchUserQueryRequest) returns (FetchUserQueryResponse);
}
// Request message for adding a user
message AddUserCommandRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
// Response message containing the added user ID
message AddUserCommandResponse {
int32 result = 1;
}
// Request message for removing a user
message RemoveUserCommandRequest {
int32 user_id = 1;
}
// Response message for remove user (empty)
message RemoveUserCommandResponse {
}
// Request message for fetching a user
message FetchUserQueryRequest {
int32 user_id = 1;
}
// Response message containing the user
message FetchUserQueryResponse {
User result = 1;
}
// User entity
message User {
int32 id = 1;
string name = 2;
string email = 3;
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
<PackageReference Include="Grpc.Tools" Version="2.76.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>