diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4ecb45a --- /dev/null +++ b/.claude/settings.local.json @@ -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": [] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..28879a9 --- /dev/null +++ b/CLAUDE.md @@ -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 + Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + +// Command with result +ICommandHandler + Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + +// Query (always returns result) +IQueryHandler + Task 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()`, it: + - Registers the handler in DI as `ICommandHandler` + - 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(); +builder.Services.AddQuery, 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`, `QueryController` + - 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` - Interface with GetFilters(), GetSorts(), GetGroups(), GetAggregates() +- `IQueryableProvider` - Provides base IQueryable to query against +- `IAlterQueryableService` - Middleware to modify queries (e.g., security filters) +- `DynamicQueryHandler` - 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() + .AddDynamicQueryWithProvider() + .AddAlterQueryable(); +``` + +**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` +3. Register in DI: `services.AddCommand()` +4. (Optional) Add validator: `services.AddTransient, 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 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` diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandController.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/CommandController.cs deleted file mode 100644 index fc642eb..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandController.cs +++ /dev/null @@ -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 : Controller - where TCommand : class -{ - [HttpPost, CommandControllerAuthorization] - public async Task Handle([FromServices] ICommandHandler 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 : Controller - where TCommand : class -{ - [HttpPost, CommandControllerAuthorization] - public async Task> Handle([FromServices] ICommandHandler handler, - [FromBody] TCommand command) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - return Ok(await handler.HandleAsync(command, this.Request.HttpContext.RequestAborted)); - } -} \ No newline at end of file diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerAsyncAuthorizationFilter.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerAsyncAuthorizationFilter.cs deleted file mode 100644 index ff83008..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerAsyncAuthorizationFilter.cs +++ /dev/null @@ -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(); - } - - 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(); - 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); - } -} diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerAuthorizationAttribute.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerAuthorizationAttribute.cs deleted file mode 100644 index d4ee0fe..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerAuthorizationAttribute.cs +++ /dev/null @@ -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; -} diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs deleted file mode 100644 index f9cad36..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs +++ /dev/null @@ -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(); - var command = commandDiscovery.FindCommand(genericType); - controller.ControllerName = command.LowerCamelCaseName; - } -} diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerFeatureProvider.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerFeatureProvider.cs deleted file mode 100644 index 1d4ddf1..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerFeatureProvider.cs +++ /dev/null @@ -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 -{ - private readonly ServiceProvider _serviceProvider; - - public CommandControllerFeatureProvider(ServiceProvider serviceProvider) - { - this._serviceProvider = serviceProvider; - } - - public void PopulateFeature(IEnumerable parts, ControllerFeature feature) - { - var commandDiscovery = this._serviceProvider.GetRequiredService(); - foreach (var f in commandDiscovery.GetCommands()) - { - var ignoreAttribute = f.CommandType.GetCustomAttribute(); - 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); - } - } - } -} diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerOptions.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerOptions.cs deleted file mode 100644 index 4ac6614..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/CommandControllerOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenHarbor.CQRS.AspNetCore.Mvc; - -public class CommandControllerOptions -{ - -} diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/MvcBuilderExensions.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/MvcBuilderExensions.cs deleted file mode 100644 index 187a1f2..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/MvcBuilderExensions.cs +++ /dev/null @@ -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 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; - } -} \ No newline at end of file diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryController.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/QueryController.cs deleted file mode 100644 index f96c0e2..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryController.cs +++ /dev/null @@ -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 : Controller - where TQuery : class -{ - [HttpPost, QueryControllerAuthorization] - public async Task> Handle([FromServices] IQueryHandler 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> HandleGet([FromServices] IQueryHandler handler, - [FromQuery] TQuery query) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - - return Ok(await handler.HandleAsync(query, this.Request.HttpContext.RequestAborted)); - } -} diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerAsyncAuthorizationFilter.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerAsyncAuthorizationFilter.cs deleted file mode 100644 index 87661a3..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerAsyncAuthorizationFilter.cs +++ /dev/null @@ -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(); - } - - 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(); - 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); - } -} diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerAuthorizationAttribute.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerAuthorizationAttribute.cs deleted file mode 100644 index 6e32c11..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerAuthorizationAttribute.cs +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs deleted file mode 100644 index ff13bf6..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs +++ /dev/null @@ -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(); - var query = queryDiscovery.FindQuery(genericType); - controller.ControllerName = query.LowerCamelCaseName; - } - } -} \ No newline at end of file diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerFeatureProvider.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerFeatureProvider.cs deleted file mode 100644 index 60d7af0..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerFeatureProvider.cs +++ /dev/null @@ -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 -{ - public void PopulateFeature(IEnumerable parts, ControllerFeature feature) - { - var queryDiscovery = serviceProvider.GetRequiredService(); - foreach (var queryMeta in queryDiscovery.GetQueries()) - { - var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute(); - 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); - } - } -} \ No newline at end of file diff --git a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerOptions.cs b/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerOptions.cs deleted file mode 100644 index e2638fa..0000000 --- a/OpenHarbor.CQRS.AspNetCore/Mvc/QueryControllerOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenHarbor.CQRS.AspNetCore.Mvc; - -public class QueryControllerOptions -{ - -} \ No newline at end of file diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs b/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs deleted file mode 100644 index 1285b47..0000000 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc; - -public class DynamicQueryControllerOptions -{ -} \ No newline at end of file diff --git a/OpenHarbor.CQRS.sln b/OpenHarbor.CQRS.sln deleted file mode 100644 index 43ca665..0000000 --- a/OpenHarbor.CQRS.sln +++ /dev/null @@ -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 diff --git a/README.md b/README.md index 4a5ea02..f542246 100644 --- a/README.md +++ b/README.md @@ -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(); } ``` -# 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. | ⬜️ | \ No newline at end of file diff --git a/OpenHarbor.CQRS.Abstractions/Attributes/CommandNameAttribute.cs b/Svrnty.CQRS.Abstractions/Attributes/CommandNameAttribute.cs similarity index 82% rename from OpenHarbor.CQRS.Abstractions/Attributes/CommandNameAttribute.cs rename to Svrnty.CQRS.Abstractions/Attributes/CommandNameAttribute.cs index 0b855a8..d08e3f8 100644 --- a/OpenHarbor.CQRS.Abstractions/Attributes/CommandNameAttribute.cs +++ b/Svrnty.CQRS.Abstractions/Attributes/CommandNameAttribute.cs @@ -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 diff --git a/OpenHarbor.CQRS.Abstractions/Attributes/QueryNameAttribute.cs b/Svrnty.CQRS.Abstractions/Attributes/QueryNameAttribute.cs similarity index 82% rename from OpenHarbor.CQRS.Abstractions/Attributes/QueryNameAttribute.cs rename to Svrnty.CQRS.Abstractions/Attributes/QueryNameAttribute.cs index 831d9c9..628a233 100644 --- a/OpenHarbor.CQRS.Abstractions/Attributes/QueryNameAttribute.cs +++ b/Svrnty.CQRS.Abstractions/Attributes/QueryNameAttribute.cs @@ -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 diff --git a/OpenHarbor.CQRS.Abstractions/Discovery/CommandMeta.cs b/Svrnty.CQRS.Abstractions/Discovery/CommandMeta.cs similarity index 92% rename from OpenHarbor.CQRS.Abstractions/Discovery/CommandMeta.cs rename to Svrnty.CQRS.Abstractions/Discovery/CommandMeta.cs index dcc01d2..d58502b 100644 --- a/OpenHarbor.CQRS.Abstractions/Discovery/CommandMeta.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/CommandMeta.cs @@ -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 { diff --git a/OpenHarbor.CQRS.Abstractions/Discovery/ICommandMeta.cs b/Svrnty.CQRS.Abstractions/Discovery/ICommandMeta.cs similarity index 81% rename from OpenHarbor.CQRS.Abstractions/Discovery/ICommandMeta.cs rename to Svrnty.CQRS.Abstractions/Discovery/ICommandMeta.cs index 0f2fc31..bffcd15 100644 --- a/OpenHarbor.CQRS.Abstractions/Discovery/ICommandMeta.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/ICommandMeta.cs @@ -1,6 +1,6 @@ using System; -namespace OpenHarbor.CQRS.Abstractions.Discovery; +namespace Svrnty.CQRS.Abstractions.Discovery; public interface ICommandMeta { diff --git a/OpenHarbor.CQRS.Abstractions/Discovery/IQueryDiscovery.cs b/Svrnty.CQRS.Abstractions/Discovery/IQueryDiscovery.cs similarity index 91% rename from OpenHarbor.CQRS.Abstractions/Discovery/IQueryDiscovery.cs rename to Svrnty.CQRS.Abstractions/Discovery/IQueryDiscovery.cs index ecd2cae..115ae1e 100644 --- a/OpenHarbor.CQRS.Abstractions/Discovery/IQueryDiscovery.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/IQueryDiscovery.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace OpenHarbor.CQRS.Abstractions.Discovery; +namespace Svrnty.CQRS.Abstractions.Discovery; public interface IQueryDiscovery { diff --git a/OpenHarbor.CQRS.Abstractions/Discovery/IQueryMeta.cs b/Svrnty.CQRS.Abstractions/Discovery/IQueryMeta.cs similarity index 82% rename from OpenHarbor.CQRS.Abstractions/Discovery/IQueryMeta.cs rename to Svrnty.CQRS.Abstractions/Discovery/IQueryMeta.cs index 0d2d8e3..0d51e3b 100644 --- a/OpenHarbor.CQRS.Abstractions/Discovery/IQueryMeta.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/IQueryMeta.cs @@ -1,6 +1,6 @@ using System; -namespace OpenHarbor.CQRS.Abstractions.Discovery; +namespace Svrnty.CQRS.Abstractions.Discovery; public interface IQueryMeta { diff --git a/OpenHarbor.CQRS.Abstractions/Discovery/QueryMeta.cs b/Svrnty.CQRS.Abstractions/Discovery/QueryMeta.cs similarity index 91% rename from OpenHarbor.CQRS.Abstractions/Discovery/QueryMeta.cs rename to Svrnty.CQRS.Abstractions/Discovery/QueryMeta.cs index ebde1e2..e6a4a14 100644 --- a/OpenHarbor.CQRS.Abstractions/Discovery/QueryMeta.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/QueryMeta.cs @@ -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 { diff --git a/OpenHarbor.CQRS.Abstractions/ICommandHandler.cs b/Svrnty.CQRS.Abstractions/ICommandHandler.cs similarity index 91% rename from OpenHarbor.CQRS.Abstractions/ICommandHandler.cs rename to Svrnty.CQRS.Abstractions/ICommandHandler.cs index b928ca0..d40f4b1 100644 --- a/OpenHarbor.CQRS.Abstractions/ICommandHandler.cs +++ b/Svrnty.CQRS.Abstractions/ICommandHandler.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace OpenHarbor.CQRS.Abstractions; +namespace Svrnty.CQRS.Abstractions; public interface ICommandHandler where TCommand : class diff --git a/OpenHarbor.CQRS.Abstractions/IQueryHandler.cs b/Svrnty.CQRS.Abstractions/IQueryHandler.cs similarity index 85% rename from OpenHarbor.CQRS.Abstractions/IQueryHandler.cs rename to Svrnty.CQRS.Abstractions/IQueryHandler.cs index 653b7c3..396e2c5 100644 --- a/OpenHarbor.CQRS.Abstractions/IQueryHandler.cs +++ b/Svrnty.CQRS.Abstractions/IQueryHandler.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace OpenHarbor.CQRS.Abstractions; +namespace Svrnty.CQRS.Abstractions; public interface IQueryHandler where TQuery : class diff --git a/OpenHarbor.CQRS.Abstractions/Security/AuthorizationResult.cs b/Svrnty.CQRS.Abstractions/Security/AuthorizationResult.cs similarity index 60% rename from OpenHarbor.CQRS.Abstractions/Security/AuthorizationResult.cs rename to Svrnty.CQRS.Abstractions/Security/AuthorizationResult.cs index a5f0ac4..c1f32e4 100644 --- a/OpenHarbor.CQRS.Abstractions/Security/AuthorizationResult.cs +++ b/Svrnty.CQRS.Abstractions/Security/AuthorizationResult.cs @@ -1,4 +1,4 @@ -namespace OpenHarbor.CQRS.Abstractions.Security; +namespace Svrnty.CQRS.Abstractions.Security; public enum AuthorizationResult { diff --git a/OpenHarbor.CQRS.Abstractions/Security/ICommandAuthorizationService.cs b/Svrnty.CQRS.Abstractions/Security/ICommandAuthorizationService.cs similarity index 82% rename from OpenHarbor.CQRS.Abstractions/Security/ICommandAuthorizationService.cs rename to Svrnty.CQRS.Abstractions/Security/ICommandAuthorizationService.cs index 46d2cb9..8b75b3b 100644 --- a/OpenHarbor.CQRS.Abstractions/Security/ICommandAuthorizationService.cs +++ b/Svrnty.CQRS.Abstractions/Security/ICommandAuthorizationService.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace OpenHarbor.CQRS.Abstractions.Security; +namespace Svrnty.CQRS.Abstractions.Security; public interface ICommandAuthorizationService { diff --git a/OpenHarbor.CQRS.Abstractions/Security/IQueryAuthorizationService.cs b/Svrnty.CQRS.Abstractions/Security/IQueryAuthorizationService.cs similarity index 82% rename from OpenHarbor.CQRS.Abstractions/Security/IQueryAuthorizationService.cs rename to Svrnty.CQRS.Abstractions/Security/IQueryAuthorizationService.cs index 32e283c..b63d196 100644 --- a/OpenHarbor.CQRS.Abstractions/Security/IQueryAuthorizationService.cs +++ b/Svrnty.CQRS.Abstractions/Security/IQueryAuthorizationService.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace OpenHarbor.CQRS.Abstractions.Security; +namespace Svrnty.CQRS.Abstractions.Security; public interface IQueryAuthorizationService { diff --git a/OpenHarbor.CQRS.Abstractions/ServiceCollectionExtensions.cs b/Svrnty.CQRS.Abstractions/ServiceCollectionExtensions.cs similarity index 96% rename from OpenHarbor.CQRS.Abstractions/ServiceCollectionExtensions.cs rename to Svrnty.CQRS.Abstractions/ServiceCollectionExtensions.cs index efc4fec..5ec1330 100644 --- a/OpenHarbor.CQRS.Abstractions/ServiceCollectionExtensions.cs +++ b/Svrnty.CQRS.Abstractions/ServiceCollectionExtensions.cs @@ -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 { diff --git a/OpenHarbor.CQRS.Abstractions/OpenHarbor.CQRS.Abstractions.csproj b/Svrnty.CQRS.Abstractions/Svrnty.CQRS.Abstractions.csproj similarity index 78% rename from OpenHarbor.CQRS.Abstractions/OpenHarbor.CQRS.Abstractions.csproj rename to Svrnty.CQRS.Abstractions/Svrnty.CQRS.Abstractions.csproj index d9a2e05..377c77a 100644 --- a/OpenHarbor.CQRS.Abstractions/OpenHarbor.CQRS.Abstractions.csproj +++ b/Svrnty.CQRS.Abstractions/Svrnty.CQRS.Abstractions.csproj @@ -1,13 +1,13 @@  - net8.0 + net10.0 true David Lebee, Mathias Beaulieu-Duncan - default - Open Harbor + 14 + Svrnty icon.png README.md - https://git.openharbor.io/Open-Harbor/dotnet-cqrs + https://github.com/svrnty/dotnet-cqrs git true MIT @@ -25,6 +25,6 @@ - + diff --git a/OpenHarbor.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs similarity index 69% rename from OpenHarbor.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs rename to Svrnty.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs index 2637a07..cff96f5 100644 --- a/OpenHarbor.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs +++ b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs @@ -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 diff --git a/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerAuthorizationAttribute.cs b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerAuthorizationAttribute.cs new file mode 100644 index 0000000..73861dd --- /dev/null +++ b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerAuthorizationAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class QueryControllerAuthorizationAttribute : Attribute +{ +} diff --git a/OpenHarbor.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs similarity index 69% rename from OpenHarbor.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs rename to Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs index 5e5d3fa..21340d4 100644 --- a/OpenHarbor.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs +++ b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs @@ -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 diff --git a/OpenHarbor.CQRS.AspNetCore.Abstractions/OpenHarbor.CQRS.AspNetCore.Abstractions.csproj b/Svrnty.CQRS.AspNetCore.Abstractions/Svrnty.CQRS.AspNetCore.Abstractions.csproj similarity index 76% rename from OpenHarbor.CQRS.AspNetCore.Abstractions/OpenHarbor.CQRS.AspNetCore.Abstractions.csproj rename to Svrnty.CQRS.AspNetCore.Abstractions/Svrnty.CQRS.AspNetCore.Abstractions.csproj index be62e0c..6b63613 100644 --- a/OpenHarbor.CQRS.AspNetCore.Abstractions/OpenHarbor.CQRS.AspNetCore.Abstractions.csproj +++ b/Svrnty.CQRS.AspNetCore.Abstractions/Svrnty.CQRS.AspNetCore.Abstractions.csproj @@ -1,13 +1,12 @@ - net8.0 - true + net10.0 false - default - Open Harbor + 14 + Svrnty icon.png README.md - https://git.openharbor.io/Open-Harbor/dotnet-cqrs + https://github.com/svrnty/dotnet-cqrs git true MIT diff --git a/OpenHarbor.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs similarity index 88% rename from OpenHarbor.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs rename to Svrnty.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs index b5eb6b7..13626d1 100644 --- a/OpenHarbor.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; +namespace Svrnty.CQRS.DynamicQuery.Abstractions; public class DynamicQueryInterceptorProvider : IDynamicQueryInterceptorProvider { diff --git a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs similarity index 91% rename from OpenHarbor.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs rename to Svrnty.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs index 1c6b0e6..abdc16b 100644 --- a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; +namespace Svrnty.CQRS.DynamicQuery.Abstractions; public interface IAlterQueryableService { diff --git a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs similarity index 92% rename from OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs rename to Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs index a5a306f..fce0892 100644 --- a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs @@ -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 : IDynamicQuery where TSource : class diff --git a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs similarity index 77% rename from OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs rename to Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs index 2e7fd48..438dfc7 100644 --- a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; +namespace Svrnty.CQRS.DynamicQuery.Abstractions; public interface IDynamicQueryInterceptorProvider { diff --git a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs similarity index 65% rename from OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs rename to Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs index da06839..08323c0 100644 --- a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs @@ -1,4 +1,4 @@ -namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; +namespace Svrnty.CQRS.DynamicQuery.Abstractions; public interface IDynamicQueryParams where TParams : class diff --git a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs similarity index 81% rename from OpenHarbor.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs rename to Svrnty.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs index 6349d1a..2f7635e 100644 --- a/OpenHarbor.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace OpenHarbor.CQRS.DynamicQuery.Abstractions; +namespace Svrnty.CQRS.DynamicQuery.Abstractions; public interface IQueryableProvider { diff --git a/OpenHarbor.CQRS.DynamicQuery.Abstractions/OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj b/Svrnty.CQRS.DynamicQuery.Abstractions/Svrnty.CQRS.DynamicQuery.Abstractions.csproj similarity index 77% rename from OpenHarbor.CQRS.DynamicQuery.Abstractions/OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj rename to Svrnty.CQRS.DynamicQuery.Abstractions/Svrnty.CQRS.DynamicQuery.Abstractions.csproj index d1199f1..183c07b 100644 --- a/OpenHarbor.CQRS.DynamicQuery.Abstractions/OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/Svrnty.CQRS.DynamicQuery.Abstractions.csproj @@ -1,13 +1,13 @@  - netstandard2.1;net8.0 - true + netstandard2.1;net10.0 + true enable - default - Open Harbor + 14 + Svrnty icon.png README.md - https://git.openharbor.io/Open-Harbor/dotnet-cqrs + https://github.com/svrnty/dotnet-cqrs git true MIT diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs similarity index 94% rename from OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs rename to Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs index 2cad0c7..3dc9595 100644 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs @@ -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 : DynamicQuery, IDynamicQuery where TSource : class diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs similarity index 88% rename from OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs rename to Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs index 234bbdd..01418b5 100644 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs @@ -2,7 +2,7 @@ using PoweredSoft.DynamicQuery.Core; using System; -namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore; +namespace Svrnty.CQRS.DynamicQuery.AspNetCore; public class DynamicQueryAggregate { diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs similarity index 97% rename from OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs rename to Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs index 388b06f..ba5f0a8 100644 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs @@ -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 { diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs similarity index 92% rename from OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs rename to Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs index aa3afd3..f6cdb76 100644 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs @@ -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 : Controller diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs similarity index 90% rename from OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs rename to Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs index 70abac4..bd6ac7d 100644 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs @@ -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 { diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs similarity index 94% rename from OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs rename to Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs index 0241040..2694a08 100644 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs @@ -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 diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs new file mode 100644 index 0000000..5bf9bde --- /dev/null +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs @@ -0,0 +1,5 @@ +namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc; + +public class DynamicQueryControllerOptions +{ +} \ No newline at end of file diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs similarity index 87% rename from OpenHarbor.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs rename to Svrnty.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs index 17c0383..5d7bf38 100644 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs @@ -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 { diff --git a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/OpenHarbor.CQRS.DynamicQuery.AspNetCore.csproj b/Svrnty.CQRS.DynamicQuery.AspNetCore/Svrnty.CQRS.DynamicQuery.AspNetCore.csproj similarity index 56% rename from OpenHarbor.CQRS.DynamicQuery.AspNetCore/OpenHarbor.CQRS.DynamicQuery.AspNetCore.csproj rename to Svrnty.CQRS.DynamicQuery.AspNetCore/Svrnty.CQRS.DynamicQuery.AspNetCore.csproj index 080aa0f..db2c20f 100644 --- a/OpenHarbor.CQRS.DynamicQuery.AspNetCore/OpenHarbor.CQRS.DynamicQuery.AspNetCore.csproj +++ b/Svrnty.CQRS.DynamicQuery.AspNetCore/Svrnty.CQRS.DynamicQuery.AspNetCore.csproj @@ -1,11 +1,12 @@ - net8.0 + net10.0 false - Open Harbor + 14 + Svrnty icon.png README.md - https://git.openharbor.io/Open-Harbor/dotnet-cqrs + https://github.com/svrnty/dotnet-cqrs git true MIT @@ -27,10 +28,9 @@ - - - - - + + + + diff --git a/OpenHarbor.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs b/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs similarity index 89% rename from OpenHarbor.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs rename to Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs index 6d4e500..d715f54 100644 --- a/OpenHarbor.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs +++ b/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs @@ -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 { diff --git a/OpenHarbor.CQRS.DynamicQuery/DynamicQueryHandler.cs b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs similarity index 89% rename from OpenHarbor.CQRS.DynamicQuery/DynamicQueryHandler.cs rename to Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs index 1446b1a..3ba2c59 100644 --- a/OpenHarbor.CQRS.DynamicQuery/DynamicQueryHandler.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs @@ -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 : DynamicQueryHandlerBase, - OpenHarbor.CQRS.Abstractions.IQueryHandler, IQueryExecutionResult> + Svrnty.CQRS.Abstractions.IQueryHandler, IQueryExecutionResult> where TSource : class where TDestination : class { @@ -30,7 +30,7 @@ public class DynamicQueryHandler public class DynamicQueryHandler : DynamicQueryHandlerBase, - OpenHarbor.CQRS.Abstractions.IQueryHandler, IQueryExecutionResult> + Svrnty.CQRS.Abstractions.IQueryHandler, IQueryExecutionResult> where TSource : class where TDestination : class where TParams : class diff --git a/OpenHarbor.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs similarity index 97% rename from OpenHarbor.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs rename to Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs index 3188f7c..3dbde90 100644 --- a/OpenHarbor.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs @@ -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 where TSource : class diff --git a/OpenHarbor.CQRS.DynamicQuery/ServiceCollectionExtensions.cs b/Svrnty.CQRS.DynamicQuery/ServiceCollectionExtensions.cs similarity index 97% rename from OpenHarbor.CQRS.DynamicQuery/ServiceCollectionExtensions.cs rename to Svrnty.CQRS.DynamicQuery/ServiceCollectionExtensions.cs index df38e41..50ce55f 100644 --- a/OpenHarbor.CQRS.DynamicQuery/ServiceCollectionExtensions.cs +++ b/Svrnty.CQRS.DynamicQuery/ServiceCollectionExtensions.cs @@ -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 { diff --git a/OpenHarbor.CQRS.DynamicQuery/OpenHarbor.CQRS.DynamicQuery.csproj b/Svrnty.CQRS.DynamicQuery/Svrnty.CQRS.DynamicQuery.csproj similarity index 71% rename from OpenHarbor.CQRS.DynamicQuery/OpenHarbor.CQRS.DynamicQuery.csproj rename to Svrnty.CQRS.DynamicQuery/Svrnty.CQRS.DynamicQuery.csproj index 761dfb2..6620437 100644 --- a/OpenHarbor.CQRS.DynamicQuery/OpenHarbor.CQRS.DynamicQuery.csproj +++ b/Svrnty.CQRS.DynamicQuery/Svrnty.CQRS.DynamicQuery.csproj @@ -1,12 +1,12 @@  - net8.0 + net10.0 true - default - Open Harbor + 14 + Svrnty icon.png README.md - https://git.openharbor.io/Open-Harbor/dotnet-cqrs + https://github.com/svrnty/dotnet-cqrs git true MIT @@ -29,7 +29,7 @@ - - + + diff --git a/OpenHarbor.CQRS.FluentValidation/ServiceCollectionExtensions.cs b/Svrnty.CQRS.FluentValidation/ServiceCollectionExtensions.cs similarity index 96% rename from OpenHarbor.CQRS.FluentValidation/ServiceCollectionExtensions.cs rename to Svrnty.CQRS.FluentValidation/ServiceCollectionExtensions.cs index a2e1d5d..edc00dd 100644 --- a/OpenHarbor.CQRS.FluentValidation/ServiceCollectionExtensions.cs +++ b/Svrnty.CQRS.FluentValidation/ServiceCollectionExtensions.cs @@ -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 { diff --git a/OpenHarbor.CQRS.FluentValidation/OpenHarbor.CQRS.FluentValidation.csproj b/Svrnty.CQRS.FluentValidation/Svrnty.CQRS.FluentValidation.csproj similarity index 71% rename from OpenHarbor.CQRS.FluentValidation/OpenHarbor.CQRS.FluentValidation.csproj rename to Svrnty.CQRS.FluentValidation/Svrnty.CQRS.FluentValidation.csproj index 949c899..d5e1949 100644 --- a/OpenHarbor.CQRS.FluentValidation/OpenHarbor.CQRS.FluentValidation.csproj +++ b/Svrnty.CQRS.FluentValidation/Svrnty.CQRS.FluentValidation.csproj @@ -1,12 +1,12 @@ - net8.0 + net10.0 true - default - Open Harbor + 14 + Svrnty icon.png README.md - https://git.openharbor.io/Open-Harbor/dotnet-cqrs + https://github.com/svrnty/dotnet-cqrs git true MIT @@ -24,10 +24,10 @@ - + - + diff --git a/Svrnty.CQRS.Grpc.Abstractions/Attributes/GrpcIgnoreAttribute.cs b/Svrnty.CQRS.Grpc.Abstractions/Attributes/GrpcIgnoreAttribute.cs new file mode 100644 index 0000000..2015fb4 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Abstractions/Attributes/GrpcIgnoreAttribute.cs @@ -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 +{ +} diff --git a/Svrnty.CQRS.Grpc.Abstractions/Svrnty.CQRS.Grpc.Abstractions.csproj b/Svrnty.CQRS.Grpc.Abstractions/Svrnty.CQRS.Grpc.Abstractions.csproj new file mode 100644 index 0000000..597f8f2 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Abstractions/Svrnty.CQRS.Grpc.Abstractions.csproj @@ -0,0 +1,27 @@ + + + net10.0 + true + David Lebee, Mathias Beaulieu-Duncan + 14 + Svrnty + enable + icon.png + README.md + https://github.com/svrnty/dotnet-cqrs + git + true + MIT + + portable + true + true + true + snupkg + + + + + + + diff --git a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs new file mode 100644 index 0000000..1d1f9c1 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs @@ -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 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(SymbolEqualityComparer.Default); // Command -> Result type (null if no result) + var queryMap = new Dictionary(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 or ICommandHandler + foreach (var iface in typeSymbol.AllInterfaces) + { + if (iface.IsGenericType) + { + // Check for ICommandHandler + 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 + 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 + 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(); + var queries = new List(); + + // 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() + }; + + // 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() + .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() + }; + + // 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() + .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 commands, List 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 commands, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + 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 queries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + 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 commands, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + 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 queries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + 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("// "); + 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(" /// "); + sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static class GrpcServiceRegistrationExtensions"); + sb.AppendLine(" {"); + + if (hasCommands) + { + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers the auto-generated Command gRPC service"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" services.AddSingleton();"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + sb.AppendLine(" endpoints.MapGrpcService();"); + sb.AppendLine(" return endpoints;"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + if (hasQueries) + { + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers the auto-generated Query gRPC service"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" services.AddSingleton();"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + sb.AppendLine(" endpoints.MapGrpcService();"); + sb.AppendLine(" return endpoints;"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + if (hasCommands && hasQueries) + { + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers both Command and Query gRPC services"); + sb.AppendLine(" /// "); + 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();"); + if (hasQueries) + sb.AppendLine(" services.AddSingleton();"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps both Command and Query gRPC service endpoints"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + if (hasCommands) + sb.AppendLine(" endpoints.MapGrpcService();"); + if (hasQueries) + sb.AppendLine(" endpoints.MapGrpcService();"); + 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 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 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 commands, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + 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(" /// "); + sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands"); + sb.AppendLine(" /// "); + 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>();"); + 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 queries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + 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(" /// "); + sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries"); + sb.AppendLine(" /// "); + 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(); + } + } +} diff --git a/Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs b/Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs new file mode 100644 index 0000000..a3fb640 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; + +namespace Svrnty.CQRS.Grpc.Generators.Helpers +{ + internal static class ProtoTypeMapper + { + private static readonly Dictionary TypeMap = new Dictionary + { + // 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("?", ""); + } + } +} diff --git a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs new file mode 100644 index 0000000..d2219ec --- /dev/null +++ b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs @@ -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 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(); + 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; + } + } +} diff --git a/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs new file mode 100644 index 0000000..267d1be --- /dev/null +++ b/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs @@ -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 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(); + ResultType = string.Empty; + ResultFullyQualifiedName = string.Empty; + HandlerInterfaceName = string.Empty; + } + } +} diff --git a/Svrnty.CQRS.Grpc.Generators/Svrnty.CQRS.Grpc.Generators.csproj b/Svrnty.CQRS.Grpc.Generators/Svrnty.CQRS.Grpc.Generators.csproj new file mode 100644 index 0000000..dda5820 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Generators/Svrnty.CQRS.Grpc.Generators.csproj @@ -0,0 +1,39 @@ + + + netstandard2.0 + latest + enable + true + true + true + true + false + true + + + + David Lebee, Mathias Beaulieu-Duncan + Svrnty + icon.png + README.md + https://github.com/svrnty/dotnet-cqrs + git + true + MIT + Source Generator for Svrnty.CQRS.Grpc - generates .proto files and gRPC service implementations from commands and queries + + + + + + + + + + + + + + + + diff --git a/Svrnty.CQRS.Grpc.Sample/AddUserCommand.cs b/Svrnty.CQRS.Grpc.Sample/AddUserCommand.cs new file mode 100644 index 0000000..081435f --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/AddUserCommand.cs @@ -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 +{ + 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 +{ + public Task HandleAsync(AddUserCommand command, CancellationToken cancellationToken = default) + { + // Simulate adding a user and returning ID + return Task.FromResult(123); + } +} diff --git a/Svrnty.CQRS.Grpc.Sample/FetchUserQuery.cs b/Svrnty.CQRS.Grpc.Sample/FetchUserQuery.cs new file mode 100644 index 0000000..b754b5e --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/FetchUserQuery.cs @@ -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 +{ + public Task 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" + }); + } +} diff --git a/Svrnty.CQRS.Grpc.Sample/InternalCommand.cs b/Svrnty.CQRS.Grpc.Sample/InternalCommand.cs new file mode 100644 index 0000000..51892b2 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/InternalCommand.cs @@ -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 +{ + 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; + } +} diff --git a/Svrnty.CQRS.Grpc.Sample/Program.cs b/Svrnty.CQRS.Grpc.Sample/Program.cs new file mode 100644 index 0000000..95f1df3 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/Program.cs @@ -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(); +builder.Services.AddCommand(); + +// Register query handlers with CQRS +builder.Services.AddQuery(); + +// 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(); diff --git a/Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto b/Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto new file mode 100644 index 0000000..1857eeb --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto @@ -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; +} diff --git a/Svrnty.CQRS.Grpc.Sample/RemoveUserCommand.cs b/Svrnty.CQRS.Grpc.Sample/RemoveUserCommand.cs new file mode 100644 index 0000000..d92b0b3 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/RemoveUserCommand.cs @@ -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 +{ + public Task HandleAsync(RemoveUserCommand command, CancellationToken cancellationToken = default) + { + // Simulate removing a user + return Task.CompletedTask; + } +} diff --git a/Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj b/Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj new file mode 100644 index 0000000..f9b3c59 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + true + $(BaseIntermediateOutputPath)Generated + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Svrnty.CQRS.Grpc.Sample/appsettings.Development.json b/Svrnty.CQRS.Grpc.Sample/appsettings.Development.json new file mode 100644 index 0000000..0c27fa0 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/appsettings.Development.json @@ -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" + } + } + } +} diff --git a/Svrnty.CQRS.Grpc.Sample/appsettings.json b/Svrnty.CQRS.Grpc.Sample/appsettings.json new file mode 100644 index 0000000..b42e3a4 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Sample/appsettings.json @@ -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" + } + } +} diff --git a/Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj b/Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj new file mode 100644 index 0000000..21c01c3 --- /dev/null +++ b/Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj @@ -0,0 +1,43 @@ + + + net10.0 + false + David Lebee, Mathias Beaulieu-Duncan + 14 + Svrnty + enable + icon.png + README.md + https://github.com/svrnty/dotnet-cqrs + git + true + MIT + + portable + true + true + true + snupkg + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/Svrnty.CQRS.Grpc/build/Svrnty.CQRS.Grpc.targets b/Svrnty.CQRS.Grpc/build/Svrnty.CQRS.Grpc.targets new file mode 100644 index 0000000..ad40d82 --- /dev/null +++ b/Svrnty.CQRS.Grpc/build/Svrnty.CQRS.Grpc.targets @@ -0,0 +1,14 @@ + + + $(MSBuildProjectDirectory)\Protos + + + + + + + + + + + diff --git a/Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs b/Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..1d6c43b --- /dev/null +++ b/Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs @@ -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(); + var authorizationService = endpoints.ServiceProvider.GetService(); + + foreach (var queryMeta in queryDiscovery.GetQueries()) + { + var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute(); + 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(); + var authorizationService = endpoints.ServiceProvider.GetService(); + + foreach (var commandMeta in commandDiscovery.GetCommands()) + { + var ignoreAttribute = commandMeta.CommandType.GetCustomAttribute(); + 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); + } +} diff --git a/OpenHarbor.CQRS.AspNetCore/OpenHarbor.CQRS.AspNetCore.csproj b/Svrnty.CQRS.MinimalApi/Svrnty.CQRS.MinimalApi.csproj similarity index 63% rename from OpenHarbor.CQRS.AspNetCore/OpenHarbor.CQRS.AspNetCore.csproj rename to Svrnty.CQRS.MinimalApi/Svrnty.CQRS.MinimalApi.csproj index 7ce3ff7..75c3a9e 100644 --- a/OpenHarbor.CQRS.AspNetCore/OpenHarbor.CQRS.AspNetCore.csproj +++ b/Svrnty.CQRS.MinimalApi/Svrnty.CQRS.MinimalApi.csproj @@ -1,13 +1,14 @@ - - + - net8.0 + net10.0 false + 14 + enable David Lebee, Mathias Beaulieu-Duncan - Open Harbor + Svrnty icon.png README.md - https://git.openharbor.io/Open-Harbor/dotnet-cqrs + https://github.com/svrnty/dotnet-cqrs git true MIT @@ -29,12 +30,7 @@ - - + + - - - - - diff --git a/Svrnty.CQRS.sln b/Svrnty.CQRS.sln new file mode 100644 index 0000000..3564654 --- /dev/null +++ b/Svrnty.CQRS.sln @@ -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 diff --git a/OpenHarbor.CQRS/.DS_Store b/Svrnty.CQRS/.DS_Store similarity index 100% rename from OpenHarbor.CQRS/.DS_Store rename to Svrnty.CQRS/.DS_Store diff --git a/OpenHarbor.CQRS/Discovery/CommandDiscovery.cs b/Svrnty.CQRS/Discovery/CommandDiscovery.cs similarity index 90% rename from OpenHarbor.CQRS/Discovery/CommandDiscovery.cs rename to Svrnty.CQRS/Discovery/CommandDiscovery.cs index da91c9c..570605f 100644 --- a/OpenHarbor.CQRS/Discovery/CommandDiscovery.cs +++ b/Svrnty.CQRS/Discovery/CommandDiscovery.cs @@ -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 { diff --git a/OpenHarbor.CQRS/Discovery/QueryDiscovery.cs b/Svrnty.CQRS/Discovery/QueryDiscovery.cs similarity index 89% rename from OpenHarbor.CQRS/Discovery/QueryDiscovery.cs rename to Svrnty.CQRS/Discovery/QueryDiscovery.cs index 08670fe..8098e40 100644 --- a/OpenHarbor.CQRS/Discovery/QueryDiscovery.cs +++ b/Svrnty.CQRS/Discovery/QueryDiscovery.cs @@ -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 { diff --git a/OpenHarbor.CQRS/ServiceCollectionExtensions.cs b/Svrnty.CQRS/ServiceCollectionExtensions.cs similarity index 84% rename from OpenHarbor.CQRS/ServiceCollectionExtensions.cs rename to Svrnty.CQRS/ServiceCollectionExtensions.cs index c335420..b806aa4 100644 --- a/OpenHarbor.CQRS/ServiceCollectionExtensions.cs +++ b/Svrnty.CQRS/ServiceCollectionExtensions.cs @@ -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 { diff --git a/OpenHarbor.CQRS/OpenHarbor.CQRS.csproj b/Svrnty.CQRS/Svrnty.CQRS.csproj similarity index 74% rename from OpenHarbor.CQRS/OpenHarbor.CQRS.csproj rename to Svrnty.CQRS/Svrnty.CQRS.csproj index 88e6142..39e9465 100644 --- a/OpenHarbor.CQRS/OpenHarbor.CQRS.csproj +++ b/Svrnty.CQRS/Svrnty.CQRS.csproj @@ -1,13 +1,13 @@  - net8.0 + net10.0 true David Lebee, Mathias Beaulieu-Duncan - default - Open Harbor + 14 + Svrnty icon.png README.md - https://git.openharbor.io/Open-Harbor/dotnet-cqrs + https://github.com/svrnty/dotnet-cqrs git true MIT @@ -25,6 +25,6 @@ - + diff --git a/TestClient.csx b/TestClient.csx new file mode 100644 index 0000000..549d685 --- /dev/null +++ b/TestClient.csx @@ -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}"); +} diff --git a/TestGrpcClient/Program.cs b/TestGrpcClient/Program.cs new file mode 100644 index 0000000..1180f3a --- /dev/null +++ b/TestGrpcClient/Program.cs @@ -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!"); diff --git a/TestGrpcClient/Protos/cqrs_services.proto b/TestGrpcClient/Protos/cqrs_services.proto new file mode 100644 index 0000000..1857eeb --- /dev/null +++ b/TestGrpcClient/Protos/cqrs_services.proto @@ -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; +} diff --git a/TestGrpcClient/TestGrpcClient.csproj b/TestGrpcClient/TestGrpcClient.csproj new file mode 100644 index 0000000..d7a801f --- /dev/null +++ b/TestGrpcClient/TestGrpcClient.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + +