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

137
README.md
View File

@ -10,42 +10,141 @@ Our implementation of query and command responsibility segregation (CQRS).
| 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 ``` |
| 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 ``` |
| 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 ``` |
| 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 ``` |
| 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 | [![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 ``` |
| 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 ``` |
| 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 ``` |
| 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 ``` |
| 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.
| 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 ``` |
| 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 ``` |
| 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.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 ``` |
| 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 ``` |
| 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
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);
AddCommands(services);
// adds the non related to aspnet core features.
services.AddOpenHarborCQRS();
services.AddSvrntyCQRS();
services
.AddControllers()
.AddOpenHarborQueries() // 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)
.AddSvrntyQueries() // adds queries 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();
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.
```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.
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
public void ConfigureServices(IServiceCollection services)
@ -80,19 +179,21 @@ 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>();
}
```
# 2024 Roadmap
# 2024-2025 Roadmap
| Task | Description | Status |
|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------|
| 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. | ⬜️ |
| 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. | ⬜️ |
| Update FluentValidation | Upgrade FluentValidation to the latest version, addressing potential breaking changes. | ⬜️ |
| Implement .NET Native Compilation (AOT) | Enable full Ahead-of-Time (AOT) compilation support (blocked by third-party dependencies). | ⬜️ |
| 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. | ⬜️ |

View File

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

View File

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

View File

@ -1,8 +1,8 @@
using System;
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
{

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
using System;
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
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis;
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
{

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<LangVersion>default</LangVersion>
<Company>Open Harbor</Company>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<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>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -25,6 +25,6 @@
</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>
</Project>

View File

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

View File

@ -1,13 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible>
<LangVersion>default</LangVersion>
<Company>Open Harbor</Company>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<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>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
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>
where TParams : class

View File

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

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
<TargetFrameworks>netstandard2.1;net10.0</TargetFrameworks>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">true</IsAotCompatible>
<Nullable>enable</Nullable>
<LangVersion>default</LangVersion>
<Company>Open Harbor</Company>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<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>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
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
{

View File

@ -4,11 +4,11 @@ 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;
using OpenHarbor.CQRS.DynamicQuery.Discover;
using Svrnty.CQRS.Abstractions.Discovery;
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
using Svrnty.CQRS.DynamicQuery.Discover;
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider)
: 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 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
{

View File

@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible>
<Company>Open Harbor</Company>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<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>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -27,10 +28,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore\OpenHarbor.CQRS.AspNetCore.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery\OpenHarbor.CQRS.DynamicQuery.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
</ItemGroup>
</Project>

View File

@ -1,8 +1,8 @@
using System;
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
{

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible>
<LangVersion>default</LangVersion>
<Company>Open Harbor</Company>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<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>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -29,7 +29,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
</ItemGroup>
</Project>

View File

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

View File

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible>
<LangVersion>default</LangVersion>
<Company>Open Harbor</Company>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<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>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -24,10 +24,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="10.4.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" />
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
</ItemGroup>
</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>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<Company>Open Harbor</Company>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<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>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -29,12 +30,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
</ItemGroup>
</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.Collections.Generic;
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
{

View File

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
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
{

View File

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

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<LangVersion>default</LangVersion>
<Company>Open Harbor</Company>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<PackageIcon>icon.png</PackageIcon>
<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>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -25,6 +25,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
</ItemGroup>
</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>