Compare commits
5 Commits
main
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
| ccfaa35c1d | |||
| 6735261f21 | |||
| 4824c0d31d | |||
| f6dccf46d7 | |||
| 747fa227a1 |
30
.claude/settings.local.json
Normal file
30
.claude/settings.local.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"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:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(timeout 3 cmd:*)",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(tasklist:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
314
CLAUDE.md
Normal file
314
CLAUDE.md
Normal file
@ -0,0 +1,314 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is Svrnty.CQRS, a modern implementation of Command Query Responsibility Segregation (CQRS) for .NET 10. It was forked from PoweredSoft.CQRS and provides:
|
||||
|
||||
- Automatic REST endpoint generation from command/query handlers
|
||||
- Dynamic query capabilities (filtering, sorting, grouping, aggregation)
|
||||
- ASP.NET Core MVC integration
|
||||
- FluentValidation support
|
||||
- AOT (Ahead-of-Time) compilation compatibility for core packages
|
||||
|
||||
## Solution Structure
|
||||
|
||||
The solution contains 9 projects organized by responsibility:
|
||||
|
||||
**Abstractions (interfaces and contracts only):**
|
||||
- `Svrnty.CQRS.Abstractions` - Core interfaces (ICommandHandler, IQueryHandler, discovery contracts)
|
||||
- `Svrnty.CQRS.AspNetCore.Abstractions` - ASP.NET Core attributes
|
||||
- `Svrnty.CQRS.DynamicQuery.Abstractions` - Dynamic query interfaces (multi-targets netstandard2.1 and net10.0)
|
||||
|
||||
**Implementation:**
|
||||
- `Svrnty.CQRS` - Core discovery and registration logic
|
||||
- `Svrnty.CQRS.AspNetCore` - MVC controller generation (legacy/backward compatibility)
|
||||
- `Svrnty.CQRS.MinimalApi` - Minimal API endpoint mapping (recommended for new projects)
|
||||
- `Svrnty.CQRS.DynamicQuery` - PoweredSoft.DynamicQuery integration
|
||||
- `Svrnty.CQRS.DynamicQuery.AspNetCore` - Dynamic query controllers
|
||||
- `Svrnty.CQRS.FluentValidation` - Validation integration helpers
|
||||
|
||||
**Key Design Principle:** Abstractions projects contain ONLY interfaces/attributes with minimal dependencies. Implementation projects depend on abstractions. This allows consumers to reference abstractions without pulling in heavy implementation dependencies.
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Restore dependencies
|
||||
dotnet restore
|
||||
|
||||
# Build entire solution
|
||||
dotnet build
|
||||
|
||||
# Build in Release mode
|
||||
dotnet build -c Release
|
||||
|
||||
# Create NuGet packages (with version)
|
||||
dotnet pack -c Release -o ./artifacts -p:Version=1.0.0
|
||||
|
||||
# Build specific project
|
||||
dotnet build Svrnty.CQRS/Svrnty.CQRS.csproj
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
This repository does not currently contain test projects. When adding tests:
|
||||
- Place them in a `tests/` directory or alongside source projects
|
||||
- Name them with `.Tests` suffix (e.g., `Svrnty.CQRS.Tests`)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core CQRS Pattern
|
||||
|
||||
The framework uses handler interfaces that follow this pattern:
|
||||
|
||||
```csharp
|
||||
// Command with no result
|
||||
ICommandHandler<TCommand>
|
||||
Task HandleAsync(TCommand command, CancellationToken cancellationToken = default)
|
||||
|
||||
// Command with result
|
||||
ICommandHandler<TCommand, TResult>
|
||||
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default)
|
||||
|
||||
// Query (always returns result)
|
||||
IQueryHandler<TQuery, TResult>
|
||||
Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default)
|
||||
```
|
||||
|
||||
### Metadata-Driven Discovery
|
||||
|
||||
The framework uses a **metadata pattern** for runtime discovery:
|
||||
|
||||
1. When you register a handler using `services.AddCommand<TCommand, THandler>()`, it:
|
||||
- Registers the handler in DI as `ICommandHandler<TCommand, THandler>`
|
||||
- Creates metadata (`ICommandMeta`) describing the command type, handler type, and result type
|
||||
- Stores metadata as singleton in DI
|
||||
|
||||
2. Discovery services (`ICommandDiscovery`, `IQueryDiscovery`) implemented in `Svrnty.CQRS`:
|
||||
- Query all registered metadata from DI container
|
||||
- Provide lookup methods: `GetCommand(string name)`, `GetCommands()`, etc.
|
||||
|
||||
3. ASP.NET Core feature providers use discovery to:
|
||||
- Enumerate all registered commands/queries
|
||||
- Dynamically generate generic controller instances at startup
|
||||
- Apply naming conventions (convert to lowerCamelCase)
|
||||
|
||||
**Key Files:**
|
||||
- `Svrnty.CQRS.Abstractions/Discovery/` - Metadata interfaces
|
||||
- `Svrnty.CQRS/Discovery/` - Discovery implementations
|
||||
- `Svrnty.CQRS.AspNetCore/Mvc/*FeatureProvider.cs` - Dynamic controller generation
|
||||
- `Svrnty.CQRS.AspNetCore/Mvc/*Convention.cs` - Naming conventions
|
||||
|
||||
### ASP.NET Core Integration Options
|
||||
|
||||
There are two options for integrating commands and queries with ASP.NET Core:
|
||||
|
||||
#### Option 1: Minimal API (Recommended)
|
||||
|
||||
The **Svrnty.CQRS.MinimalApi** package provides modern Minimal API endpoints:
|
||||
|
||||
**Registration:**
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register CQRS services
|
||||
builder.Services.AddSvrntyCQRS();
|
||||
builder.Services.AddDefaultCommandDiscovery();
|
||||
builder.Services.AddDefaultQueryDiscovery();
|
||||
|
||||
// Add your commands and queries
|
||||
builder.Services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>();
|
||||
builder.Services.AddQuery<PersonQuery, IQueryable<Person>, PersonQueryHandler>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Map endpoints (this creates routes automatically)
|
||||
app.MapSvrntyCommands(); // Maps all commands to POST /api/command/{name}
|
||||
app.MapSvrntyQueries(); // Maps all queries to POST/GET /api/query/{name}
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
**How It Works:**
|
||||
1. Extension methods iterate through `ICommandDiscovery` and `IQueryDiscovery`
|
||||
2. For each command/query, creates Minimal API endpoints using `MapPost()`/`MapGet()`
|
||||
3. Applies same naming conventions as MVC (lowerCamelCase)
|
||||
4. Respects `[CommandControllerIgnore]` and `[QueryControllerIgnore]` attributes
|
||||
5. Integrates with `ICommandAuthorizationService` and `IQueryAuthorizationService`
|
||||
6. Supports OpenAPI/Swagger documentation
|
||||
|
||||
**Features:**
|
||||
- Queries support both POST (with JSON body) and GET (with query string parameters)
|
||||
- Commands only support POST with JSON body
|
||||
- Authorization via authorization services (returns 401/403 status codes)
|
||||
- Customizable route prefixes: `MapSvrntyCommands("my-prefix")`
|
||||
- Automatic OpenAPI tags: "Commands" and "Queries"
|
||||
|
||||
**Key Files:**
|
||||
- `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs` - Main implementation
|
||||
|
||||
#### Option 2: MVC Controllers (Legacy)
|
||||
|
||||
The AspNetCore packages use **dynamic controller generation** (maintained for backward compatibility):
|
||||
|
||||
1. **Feature Providers** (`CommandControllerFeatureProvider`, `QueryControllerFeatureProvider`):
|
||||
- Called during MVC startup
|
||||
- Discover handlers via `ICommandDiscovery`/`IQueryDiscovery`
|
||||
- Create generic controller types: `CommandController<TCommand>`, `QueryController<TQuery, TResult>`
|
||||
- Skip handlers marked with `[CommandControllerIgnore]` or `[QueryControllerIgnore]`
|
||||
|
||||
2. **Conventions** (`CommandControllerConvention`, `QueryControllerConvention`):
|
||||
- Apply naming to generated controllers
|
||||
- Default: Type name without suffix, converted to lowerCamelCase
|
||||
- Custom: Use `[CommandName]` or `[QueryName]` attribute
|
||||
|
||||
3. **Controllers** expose automatic endpoints:
|
||||
- Commands: `POST /api/command/{name}`
|
||||
- Queries: `POST /api/query/{name}` or `GET /api/query/{name}`
|
||||
|
||||
**Registration Pattern:**
|
||||
```csharp
|
||||
services
|
||||
.AddControllers()
|
||||
.AddSvrntyQueries() // Registers query feature provider
|
||||
.AddSvrntyCommands(); // Registers command feature provider
|
||||
```
|
||||
|
||||
### Dynamic Query System
|
||||
|
||||
Dynamic queries provide OData-like filtering capabilities:
|
||||
|
||||
**Core Components:**
|
||||
- `IDynamicQuery<TSource, TDestination>` - Interface with GetFilters(), GetSorts(), GetGroups(), GetAggregates()
|
||||
- `IQueryableProvider<TSource>` - Provides base IQueryable to query against
|
||||
- `IAlterQueryableService<TSource, TDestination>` - Middleware to modify queries (e.g., security filters)
|
||||
- `DynamicQueryHandler<TSource, TDestination>` - Executes queries using PoweredSoft.DynamicQuery
|
||||
|
||||
**Request Flow:**
|
||||
1. HTTP request with filters/sorts/aggregates
|
||||
2. DynamicQueryController receives request
|
||||
3. DynamicQueryHandler gets base queryable from IQueryableProvider
|
||||
4. Applies alterations from all registered IAlterQueryableService instances
|
||||
5. Builds PoweredSoft query criteria
|
||||
6. Executes and returns IQueryExecutionResult
|
||||
|
||||
**Registration Example:**
|
||||
```csharp
|
||||
services.AddDynamicQuery<Person, PersonDto>()
|
||||
.AddDynamicQueryWithProvider<Person, PersonQueryableProvider>()
|
||||
.AddAlterQueryable<Person, PersonDto, SecurityFilter>();
|
||||
```
|
||||
|
||||
**Key Files:**
|
||||
- `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs`
|
||||
- `Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs` - Request models
|
||||
|
||||
## Package Configuration
|
||||
|
||||
All projects target .NET 10.0 and use C# 14, sharing common configuration:
|
||||
|
||||
- **Target Framework**: `net10.0` (except DynamicQuery.Abstractions which multi-targets `netstandard2.1;net10.0`)
|
||||
- **Language Version**: C# 14
|
||||
- **IsAotCompatible**: Currently set but not enforced (many dependencies are not AOT-compatible yet)
|
||||
- **Symbols**: Portable debug symbols with source, published as `.snupkg`
|
||||
- **NuGet metadata**: Icon, README, license (MIT), and repository URL included in packages
|
||||
- **Authors**: David Lebee, Mathias Beaulieu-Duncan
|
||||
- **Repository**: https://git.openharbor.io/Open-Harbor/dotnet-cqrs
|
||||
|
||||
### Package Dependencies
|
||||
|
||||
- **Microsoft.Extensions.DependencyInjection.Abstractions**: 10.0.0-rc.2.25502.107 (will update to stable when .NET 10 is released)
|
||||
- **FluentValidation**: 11.11.0
|
||||
- **Microsoft.CodeAnalysis.CSharp**: 4.13.0
|
||||
- **PoweredSoft.DynamicQuery**: 3.0.1
|
||||
- **Pluralize.NET**: 1.0.2
|
||||
|
||||
## Publishing
|
||||
|
||||
NuGet packages are published automatically via GitHub Actions when a release is created:
|
||||
|
||||
**Workflow:** `.github/workflows/publish-nugets.yml`
|
||||
1. Triggered on release publication
|
||||
2. Extracts version from release tag
|
||||
3. Runs `dotnet pack -c Release -p:Version={tag}`
|
||||
4. Pushes to NuGet.org using `NUGET_API_KEY` secret
|
||||
|
||||
**Manual publish:**
|
||||
```bash
|
||||
# Create packages with specific version
|
||||
dotnet pack -c Release -o ./artifacts -p:Version=1.2.3
|
||||
|
||||
# Push to NuGet
|
||||
dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key YOUR_KEY
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
**Adding a New Command/Query Handler:**
|
||||
|
||||
1. Create command/query POCO in consumer project
|
||||
2. Implement handler: `ICommandHandler<TCommand, TResult>`
|
||||
3. Register in DI: `services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>()`
|
||||
4. (Optional) Add validator: `services.AddTransient<IValidator<CreatePersonCommand>, Validator>()`
|
||||
5. Controller endpoint is automatically generated
|
||||
|
||||
**Adding a New Feature to Framework:**
|
||||
|
||||
1. Add interface to appropriate Abstractions project
|
||||
2. Implement in corresponding implementation project
|
||||
3. Update ServiceCollectionExtensions with registration method
|
||||
4. Ensure all projects maintain AOT compatibility (unless AspNetCore-specific)
|
||||
5. Update package version and release notes
|
||||
|
||||
**Naming Conventions:**
|
||||
|
||||
- Commands/Queries: Use `[CommandName]` or `[QueryName]` attribute for custom names
|
||||
- Default naming: Strips "Command"/"Query" suffix, converts to lowerCamelCase
|
||||
- Example: `CreatePersonCommand` -> `createPerson` endpoint
|
||||
|
||||
## C# 14 Language Features
|
||||
|
||||
The project now uses C# 14, which introduces several new features. Be aware of these breaking changes:
|
||||
|
||||
**Potential Breaking Changes:**
|
||||
- **`field` keyword**: New contextual keyword in property accessors for implicit backing fields
|
||||
- **`extension` keyword**: Reserved for extension containers; use `@extension` for identifiers
|
||||
- **`partial` return type**: Cannot use `partial` as return type without escaping
|
||||
- **Span<T> overload resolution**: New implicit conversions may select different overloads
|
||||
- **`scoped` as lambda modifier**: Always treated as modifier in lambda parameters
|
||||
|
||||
**New Features Available:**
|
||||
- Extension members (static extension members and extension properties)
|
||||
- Implicit span conversions
|
||||
- Unbound generic types with `nameof`
|
||||
- Lambda parameter modifiers without type specification
|
||||
- Partial instance constructors and events
|
||||
- Null-conditional assignment (`?.=` and `?[]=`)
|
||||
|
||||
The codebase currently compiles without warnings on C# 14.
|
||||
|
||||
## Important Implementation Notes
|
||||
|
||||
1. **AOT Compatibility**: Currently not enforced. The `IsAotCompatible` property is set on some projects but many dependencies (including FluentValidation, PoweredSoft.DynamicQuery) are not AOT-compatible. Future work may address this.
|
||||
|
||||
2. **Async Everywhere**: All handlers are async. Always support CancellationToken.
|
||||
|
||||
3. **Generic Type Safety**: Framework relies heavily on generics for compile-time safety. When adding features, maintain strong typing.
|
||||
|
||||
4. **Metadata Pattern**: When extending discovery, always create corresponding metadata classes (implement ICommandMeta/IQueryMeta).
|
||||
|
||||
5. **Feature Provider Timing**: Controller generation happens during MVC startup. Discovery services must be registered before calling AddSvrntyCommands/Queries.
|
||||
|
||||
6. **FluentValidation**: This framework only REGISTERS validators. Actual validation execution requires separate middleware/filters in consumer applications.
|
||||
|
||||
7. **DynamicQuery Interceptors**: Support up to 5 interceptors per query type. Interceptors modify PoweredSoft DynamicQuery behavior.
|
||||
|
||||
## Common Code Locations
|
||||
|
||||
- Handler interfaces: `Svrnty.CQRS.Abstractions/ICommandHandler.cs`, `IQueryHandler.cs`
|
||||
- Discovery implementations: `Svrnty.CQRS/Discovery/`
|
||||
- Service registration: `*/ServiceCollectionExtensions.cs` in each project
|
||||
- Controller templates: `Svrnty.CQRS.AspNetCore/Mvc/*Controller.cs`
|
||||
- Dynamic query logic: `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs`
|
||||
- Naming/routing: `Svrnty.CQRS.AspNetCore/Mvc/*Convention.cs`
|
||||
@ -1,38 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
[Produces("application/json")]
|
||||
[ApiController, Route("api/command/[controller]")]
|
||||
public class CommandController<TCommand> : Controller
|
||||
where TCommand : class
|
||||
{
|
||||
[HttpPost, CommandControllerAuthorization]
|
||||
public async Task<IActionResult> Handle([FromServices] ICommandHandler<TCommand> handler,
|
||||
[FromBody] TCommand command)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
await handler.HandleAsync(command, this.Request.HttpContext.RequestAborted);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
[Produces("application/json")]
|
||||
[ApiController, Route("api/command/[controller]")]
|
||||
public class CommandController<TCommand, TTCommandResult> : Controller
|
||||
where TCommand : class
|
||||
{
|
||||
[HttpPost, CommandControllerAuthorization]
|
||||
public async Task<ActionResult<TTCommandResult>> Handle([FromServices] ICommandHandler<TCommand, TTCommandResult> handler,
|
||||
[FromBody] TCommand command)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
return Ok(await handler.HandleAsync(command, this.Request.HttpContext.RequestAborted));
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Reflection;
|
||||
using OpenHarbor.CQRS.Abstractions.Security;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public class CommandControllerAsyncAuthorizationFilter : IAsyncAuthorizationFilter
|
||||
{
|
||||
private readonly ICommandAuthorizationService _authorizationService;
|
||||
|
||||
public CommandControllerAsyncAuthorizationFilter(IServiceProvider serviceProvider)
|
||||
{
|
||||
_authorizationService = serviceProvider.GetService<ICommandAuthorizationService>();
|
||||
}
|
||||
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
if (_authorizationService == null)
|
||||
return;
|
||||
|
||||
var action = context.ActionDescriptor as Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor;
|
||||
if (action == null)
|
||||
throw new Exception("Only Supports controller action descriptor");
|
||||
|
||||
var attribute = action.MethodInfo.GetCustomAttribute<CommandControllerAuthorizationAttribute>();
|
||||
Type commandType;
|
||||
if (attribute?.CommandType != null)
|
||||
commandType = attribute.CommandType;
|
||||
else
|
||||
commandType = action.ControllerTypeInfo.GenericTypeArguments.First();
|
||||
|
||||
var ar = await _authorizationService.IsAllowedAsync(commandType);
|
||||
if (ar == AuthorizationResult.Forbidden)
|
||||
context.Result = new StatusCodeResult(403);
|
||||
else if(ar == AuthorizationResult.Unauthorized)
|
||||
context.Result = new StatusCodeResult(401);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public class CommandControllerConvention : IControllerModelConvention
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public CommandControllerConvention(IServiceProvider serviceProvider)
|
||||
{
|
||||
this._serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
if (!controller.ControllerType.IsGenericType)
|
||||
return;
|
||||
|
||||
if (!controller.ControllerType.Name.Contains("CommandController"))
|
||||
return;
|
||||
|
||||
if (controller.ControllerType.Assembly != typeof(CommandControllerConvention).Assembly)
|
||||
return;
|
||||
|
||||
var genericType = controller.ControllerType.GenericTypeArguments[0];
|
||||
var commandDiscovery = this._serviceProvider.GetRequiredService<ICommandDiscovery>();
|
||||
var command = commandDiscovery.FindCommand(genericType);
|
||||
controller.ControllerName = command.LowerCamelCaseName;
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public class CommandControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
|
||||
{
|
||||
private readonly ServiceProvider _serviceProvider;
|
||||
|
||||
public CommandControllerFeatureProvider(ServiceProvider serviceProvider)
|
||||
{
|
||||
this._serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
|
||||
{
|
||||
var commandDiscovery = this._serviceProvider.GetRequiredService<ICommandDiscovery>();
|
||||
foreach (var f in commandDiscovery.GetCommands())
|
||||
{
|
||||
var ignoreAttribute = f.CommandType.GetCustomAttribute<CommandControllerIgnoreAttribute>();
|
||||
if (ignoreAttribute != null)
|
||||
continue;
|
||||
|
||||
if (f.CommandResultType == null)
|
||||
{
|
||||
var controllerType = typeof(CommandController<>).MakeGenericType(f.CommandType);
|
||||
var controllerTypeInfo = controllerType.GetTypeInfo();
|
||||
feature.Controllers.Add(controllerTypeInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
var controllerType = typeof(CommandController<,>).MakeGenericType(f.CommandType, f.CommandResultType);
|
||||
var controllerTypeInfo = controllerType.GetTypeInfo();
|
||||
feature.Controllers.Add(controllerTypeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public class CommandControllerOptions
|
||||
{
|
||||
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public static class MvcBuilderExtensions
|
||||
{
|
||||
public static IMvcBuilder AddOpenHarborQueries(this IMvcBuilder builder, Action<QueryControllerOptions> configuration = null)
|
||||
{
|
||||
var options = new QueryControllerOptions();
|
||||
configuration?.Invoke(options);
|
||||
var services = builder.Services;
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
builder.AddMvcOptions(o => o.Conventions.Add(new QueryControllerConvention(serviceProvider)));
|
||||
builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new QueryControllerFeatureProvider(serviceProvider)));
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IMvcBuilder AddOpenHarborCommands(this IMvcBuilder builder)
|
||||
{
|
||||
var services = builder.Services;
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
builder.AddMvcOptions(o => o.Conventions.Add(new CommandControllerConvention(serviceProvider)));
|
||||
builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new CommandControllerFeatureProvider(serviceProvider)));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
[Produces("application/json")]
|
||||
[ApiController, Route("api/query/[controller]")]
|
||||
public class QueryController<TQuery, TQueryResult> : Controller
|
||||
where TQuery : class
|
||||
{
|
||||
[HttpPost, QueryControllerAuthorization]
|
||||
public async Task<ActionResult<TQueryResult>> Handle([FromServices] IQueryHandler<TQuery, TQueryResult> handler,
|
||||
[FromBody] TQuery query)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
|
||||
return Ok(await handler.HandleAsync(query, this.Request.HttpContext.RequestAborted));
|
||||
}
|
||||
|
||||
[HttpGet, QueryControllerAuthorization]
|
||||
public async Task<ActionResult<TQueryResult>> HandleGet([FromServices] IQueryHandler<TQuery, TQueryResult> handler,
|
||||
[FromQuery] TQuery query)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
|
||||
return Ok(await handler.HandleAsync(query, this.Request.HttpContext.RequestAborted));
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Reflection;
|
||||
using OpenHarbor.CQRS.Abstractions.Security;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public class QueryControllerAsyncAuthorizationFilter : IAsyncAuthorizationFilter
|
||||
{
|
||||
private readonly IQueryAuthorizationService _authorizationService;
|
||||
|
||||
public QueryControllerAsyncAuthorizationFilter(IServiceProvider serviceProvider)
|
||||
{
|
||||
_authorizationService = serviceProvider.GetService<IQueryAuthorizationService>();
|
||||
}
|
||||
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
if (_authorizationService == null)
|
||||
return;
|
||||
|
||||
var action = context.ActionDescriptor as Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor;
|
||||
if (action == null)
|
||||
throw new Exception("Only Supports controller action descriptor");
|
||||
|
||||
var attribute = action.MethodInfo.GetCustomAttribute<QueryControllerAuthorizationAttribute>();
|
||||
Type queryType;
|
||||
if (attribute?.QueryType != null)
|
||||
queryType = attribute.QueryType;
|
||||
else
|
||||
queryType = action.ControllerTypeInfo.GenericTypeArguments.First();
|
||||
|
||||
var ar = await _authorizationService.IsAllowedAsync(queryType);
|
||||
if (ar == AuthorizationResult.Forbidden)
|
||||
context.Result = new StatusCodeResult(403);
|
||||
else if (ar == AuthorizationResult.Unauthorized)
|
||||
context.Result = new StatusCodeResult(401);
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public class QueryControllerConvention : IControllerModelConvention
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public QueryControllerConvention(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("QueryController") && controller.ControllerType.Assembly == typeof(QueryControllerConvention).Assembly)
|
||||
{
|
||||
var genericType = controller.ControllerType.GenericTypeArguments[0];
|
||||
var queryDiscovery = _serviceProvider.GetRequiredService<IQueryDiscovery>();
|
||||
var query = queryDiscovery.FindQuery(genericType);
|
||||
controller.ControllerName = query.LowerCamelCaseName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public class QueryControllerFeatureProvider(ServiceProvider serviceProvider)
|
||||
: IApplicationFeatureProvider<ControllerFeature>
|
||||
{
|
||||
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
|
||||
{
|
||||
var queryDiscovery = serviceProvider.GetRequiredService<IQueryDiscovery>();
|
||||
foreach (var queryMeta in queryDiscovery.GetQueries())
|
||||
{
|
||||
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
|
||||
if (ignoreAttribute != null)
|
||||
continue;
|
||||
|
||||
if (queryMeta.Category != "BasicQuery")
|
||||
continue;
|
||||
|
||||
var controllerType = typeof(QueryController<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
|
||||
var controllerTypeInfo = controllerType.GetTypeInfo();
|
||||
feature.Controllers.Add(controllerTypeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
|
||||
public class QueryControllerOptions
|
||||
{
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
public class DynamicQueryControllerOptions
|
||||
{
|
||||
}
|
||||
@ -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
|
||||
278
README.md
278
README.md
@ -10,42 +10,211 @@ Our implementation of query and command responsibility segregation (CQRS).
|
||||
|
||||
| Package Name | NuGet | NuGet Install |
|
||||
|-----------------------------------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------:|
|
||||
| OpenHarbor.CQRS | [](https://www.nuget.org/packages/OpenHarbor.CQRS/) | ```dotnet add package OpenHarbor.CQRS ``` |
|
||||
| OpenHarbor.CQRS.AspNetCore | [](https://www.nuget.org/packages/OpenHarbor.CQRS.AspNetCore/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore ``` |
|
||||
| OpenHarbor.CQRS.FluentValidation | [](https://www.nuget.org/packages/OpenHarbor.CQRS.FluentValidation/) | ```dotnet add package OpenHarbor.CQRS.FluentValidation ``` |
|
||||
| OpenHarbor.CQRS.DynamicQuery | [](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery/) | ```dotnet add package OpenHarbor.CQRS.DynamicQuery ``` |
|
||||
| OpenHarbor.CQRS.DynamicQuery.AspNetCore | [](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery.AspNetCore/) | ```dotnet add package OpenHarbor.CQRS.DynamicQuery.AspNetCore ``` |
|
||||
| Svrnty.CQRS | [](https://www.nuget.org/packages/Svrnty.CQRS/) | ```dotnet add package Svrnty.CQRS ``` |
|
||||
| Svrnty.CQRS.MinimalApi | [](https://www.nuget.org/packages/Svrnty.CQRS.MinimalApi/) | ```dotnet add package Svrnty.CQRS.MinimalApi ``` |
|
||||
| Svrnty.CQRS.AspNetCore | [](https://www.nuget.org/packages/Svrnty.CQRS.AspNetCore/) | ```dotnet add package Svrnty.CQRS.AspNetCore ``` |
|
||||
| Svrnty.CQRS.FluentValidation | [](https://www.nuget.org/packages/Svrnty.CQRS.FluentValidation/) | ```dotnet add package Svrnty.CQRS.FluentValidation ``` |
|
||||
| Svrnty.CQRS.DynamicQuery | [](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery/) | ```dotnet add package Svrnty.CQRS.DynamicQuery ``` |
|
||||
| Svrnty.CQRS.DynamicQuery.AspNetCore | [](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery.AspNetCore/) | ```dotnet add package Svrnty.CQRS.DynamicQuery.AspNetCore ``` |
|
||||
| Svrnty.CQRS.Grpc | [](https://www.nuget.org/packages/Svrnty.CQRS.Grpc/) | ```dotnet add package Svrnty.CQRS.Grpc ``` |
|
||||
| Svrnty.CQRS.Grpc.Generators | [](https://www.nuget.org/packages/Svrnty.CQRS.Grpc.Generators/) | ```dotnet add package Svrnty.CQRS.Grpc.Generators ``` |
|
||||
|
||||
> Abstractions Packages.
|
||||
|
||||
| Package Name | NuGet | NuGet Install |
|
||||
| ---------------------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------------------------------: |
|
||||
| OpenHarbor.CQRS.Abstractions | [](https://www.nuget.org/packages/OpenHarbor.CQRS.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.Abstractions ``` |
|
||||
| OpenHarbor.CQRS.AspNetCore.Abstractions | [](https://www.nuget.org/packages/OpenHarbor.CQRS.AspNetCore.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore.Abstractions ``` |
|
||||
| OpenHarbor.CQRS.DynamicQuery.Abstractions | [](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore.Abstractions ``` |
|
||||
| Svrnty.CQRS.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.Abstractions/) | ```dotnet add package Svrnty.CQRS.Abstractions ``` |
|
||||
| Svrnty.CQRS.AspNetCore.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.AspNetCore.Abstractions/) | ```dotnet add package Svrnty.CQRS.AspNetCore.Abstractions ``` |
|
||||
| Svrnty.CQRS.DynamicQuery.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery.Abstractions/) | ```dotnet add package Svrnty.CQRS.DynamicQuery.Abstractions ``` |
|
||||
| Svrnty.CQRS.Grpc.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.Grpc.Abstractions/) | ```dotnet add package Svrnty.CQRS.Grpc.Abstractions ``` |
|
||||
|
||||
|
||||
## Sample of startup code for aspnetcore MVC
|
||||
## Sample of startup code for gRPC (Recommended)
|
||||
|
||||
```csharp
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// make sure to add your queries and commands before configuring MvCBuilder with .AddOpenHarborCommands and .AddOpenHarborQueries
|
||||
AddQueries(services);
|
||||
AddCommands(services);
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// adds the non related to aspnet core features.
|
||||
services.AddOpenHarborCQRS();
|
||||
// Register CQRS core services
|
||||
builder.Services.AddSvrntyCQRS();
|
||||
builder.Services.AddDefaultCommandDiscovery();
|
||||
builder.Services.AddDefaultQueryDiscovery();
|
||||
|
||||
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)
|
||||
.AddFluentValidation();
|
||||
// Add your commands and queries
|
||||
AddQueries(builder.Services);
|
||||
AddCommands(builder.Services);
|
||||
|
||||
services.AddSwaggerGen();
|
||||
// Add gRPC support
|
||||
builder.Services.AddGrpc();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Map auto-generated gRPC service implementations
|
||||
app.MapGrpcService<CommandServiceImpl>();
|
||||
app.MapGrpcService<QueryServiceImpl>();
|
||||
|
||||
// Enable gRPC reflection for tools like grpcurl
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
### Important: gRPC Requirements
|
||||
|
||||
The gRPC implementation uses **Grpc.Tools** with `.proto` files and **source generators** for automatic service implementation:
|
||||
|
||||
#### 1. Install required packages:
|
||||
|
||||
```bash
|
||||
dotnet add package Grpc.AspNetCore
|
||||
dotnet add package Grpc.AspNetCore.Server.Reflection
|
||||
dotnet add package Grpc.StatusProto # For Rich Error Model validation
|
||||
```
|
||||
|
||||
#### 2. Add the source generator as an analyzer:
|
||||
|
||||
```bash
|
||||
dotnet add package Svrnty.CQRS.Grpc.Generators
|
||||
```
|
||||
|
||||
The source generator is automatically configured as an analyzer when installed via NuGet and will generate the gRPC service implementations at compile time.
|
||||
|
||||
#### 3. Define your proto files in `Protos/` directory:
|
||||
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
service CommandService {
|
||||
rpc AddUser(AddUserCommandRequest) returns (AddUserCommandResponse);
|
||||
rpc RemoveUser(RemoveUserCommandRequest) returns (google.protobuf.Empty);
|
||||
}
|
||||
|
||||
message AddUserCommandRequest {
|
||||
string name = 1;
|
||||
string email = 2;
|
||||
int32 age = 3;
|
||||
}
|
||||
|
||||
message AddUserCommandResponse {
|
||||
int32 result = 1;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Define your C# commands matching the proto structure:
|
||||
|
||||
```csharp
|
||||
public record AddUserCommand
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string Email { get; init; }
|
||||
public int Age { get; init; }
|
||||
}
|
||||
|
||||
public record RemoveUserCommand
|
||||
{
|
||||
public int UserId { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- The source generator automatically creates `CommandServiceImpl` and `QueryServiceImpl` implementations
|
||||
- Property names in C# commands must match proto field names (case-insensitive)
|
||||
- FluentValidation is automatically integrated with **Google Rich Error Model** for structured validation errors
|
||||
- Validation errors return `google.rpc.Status` with `BadRequest` containing `FieldViolations`
|
||||
- Use `record` types for commands/queries (immutable, value-based equality, more concise)
|
||||
- No need for protobuf-net attributes
|
||||
|
||||
## Sample of startup code for Minimal API (Traditional HTTP)
|
||||
|
||||
For traditional HTTP/REST scenarios, you can use the Minimal API approach:
|
||||
|
||||
```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();
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- FluentValidation is automatically integrated with **RFC 7807 Problem Details** for structured validation errors
|
||||
- Use `record` types for commands/queries (immutable, value-based equality, more concise)
|
||||
- Supports both POST and GET (for queries) endpoints
|
||||
- Automatically generates Swagger/OpenAPI documentation
|
||||
|
||||
## Sample enabling both gRPC and HTTP
|
||||
|
||||
You can enable both gRPC and traditional HTTP endpoints simultaneously, allowing clients to choose their preferred protocol:
|
||||
|
||||
```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.AddGrpc();
|
||||
|
||||
// Add HTTP/REST support with Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
// Map gRPC endpoints
|
||||
app.MapGrpcService<CommandServiceImpl>();
|
||||
app.MapGrpcService<QueryServiceImpl>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
// Map HTTP/REST endpoints
|
||||
app.MapSvrntyCommands();
|
||||
app.MapSvrntyQueries();
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Single codebase supports multiple protocols
|
||||
- gRPC for high-performance, low-latency scenarios (microservices, internal APIs)
|
||||
- HTTP/REST for web browsers, legacy clients, and public APIs
|
||||
- Same commands, queries, and validation logic for both protocols
|
||||
- Swagger UI available for HTTP endpoints, gRPC reflection for gRPC clients
|
||||
|
||||
> Example how to add your queries and commands.
|
||||
|
||||
```csharp
|
||||
@ -66,33 +235,62 @@ private void AddQueries(IServiceCollection services)
|
||||
|
||||
# Fluent Validation
|
||||
|
||||
We use fluent validation in all of our projects, but we don't want it to be enforced.
|
||||
FluentValidation is optional but recommended for command and query validation. The `Svrnty.CQRS.FluentValidation` package provides extension methods to simplify validator registration.
|
||||
|
||||
If you install ```OpenHarbor.CQRS.FluentValidation``` you can use this way of registrating your commands.
|
||||
## Without Svrnty.CQRS.FluentValidation
|
||||
|
||||
You need to register commands and validators separately:
|
||||
|
||||
```csharp
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// without Package.
|
||||
services.AddCommand<EchoCommand, string, EchoCommandHandler>();
|
||||
services.AddTransient<IValidator<EchoCommand>, EchoCommandValidator>();
|
||||
}
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using FluentValidation;
|
||||
using Svrnty.CQRS;
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
private void AddCommands(IServiceCollection services)
|
||||
{
|
||||
// with OpenHarbor.CQRS.FluentValidation package.
|
||||
services.AddCommand<EchoCommand, string, EchoCommandHandler, EchoCommandValidator>();
|
||||
// Register command handler
|
||||
services.AddCommand<EchoCommand, string, EchoCommandHandler>();
|
||||
|
||||
// Manually register validator
|
||||
services.AddTransient<IValidator<EchoCommand>, EchoCommandValidator>();
|
||||
}
|
||||
```
|
||||
|
||||
# 2024 Roadmap
|
||||
## With Svrnty.CQRS.FluentValidation (Recommended)
|
||||
|
||||
The package exposes extension method overloads that accept the validator as a generic parameter:
|
||||
|
||||
```bash
|
||||
dotnet add package Svrnty.CQRS.FluentValidation
|
||||
```
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Svrnty.CQRS.FluentValidation; // Extension methods for validator registration
|
||||
|
||||
private void AddCommands(IServiceCollection services)
|
||||
{
|
||||
// Command without result - validator included in generics
|
||||
services.AddCommand<EchoCommand, string, EchoCommandHandler, EchoCommandValidator>();
|
||||
|
||||
// Command with result - validator as last generic parameter
|
||||
services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler, CreatePersonCommandValidator>();
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Single line registration** - Handler and validator registered together
|
||||
- **Type safety** - Compiler ensures validator matches command type
|
||||
- **Less boilerplate** - No need for separate `AddTransient<IValidator<T>>()` calls
|
||||
- **Cleaner code** - Clear intent that validation is part of command pipeline
|
||||
|
||||
# 2024-2025 Roadmap
|
||||
|
||||
| Task | Description | Status |
|
||||
|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------|
|
||||
| Support .NET 8 | Ensure compatibility with .NET 8. | ✅ |
|
||||
| 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. | ⬜️ |
|
||||
| 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. | ⬜️ |
|
||||
| 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 source generators | Implement gRPC endpoints with source generators and Google Rich Error Model for validation. | ✅ |
|
||||
| Create a demo project (Svrnty.CQRS.Grpc.Sample) | Develop a comprehensive demo project showcasing gRPC and HTTP endpoints. | ✅ |
|
||||
| Create a website for the Framework | Develop a website to host comprehensive documentation for the framework. | ⬜️ |
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public interface ICommandMeta
|
||||
{
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public interface IQueryDiscovery
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public interface IQueryMeta
|
||||
{
|
||||
@ -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
|
||||
{
|
||||
@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions;
|
||||
namespace Svrnty.CQRS.Abstractions;
|
||||
|
||||
public interface ICommandHandler<in TCommand>
|
||||
where TCommand : class
|
||||
@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions;
|
||||
namespace Svrnty.CQRS.Abstractions;
|
||||
|
||||
public interface IQueryHandler<in TQuery, TQueryResult>
|
||||
where TQuery : class
|
||||
@ -1,4 +1,4 @@
|
||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
||||
namespace Svrnty.CQRS.Abstractions.Security;
|
||||
|
||||
public enum AuthorizationResult
|
||||
{
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
||||
namespace Svrnty.CQRS.Abstractions.Security;
|
||||
|
||||
public interface ICommandAuthorizationService
|
||||
{
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
||||
namespace Svrnty.CQRS.Abstractions.Security;
|
||||
|
||||
public interface IQueryAuthorizationService
|
||||
{
|
||||
@ -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
|
||||
{
|
||||
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -25,6 +25,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0-rc.1.23419.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -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
|
||||
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public class QueryControllerAuthorizationAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@ -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
|
||||
@ -1,13 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public class DynamicQueryInterceptorProvider<TSource, TDestination> : IDynamicQueryInterceptorProvider<TSource, TDestination>
|
||||
{
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IAlterQueryableService<TSource, TDestination>
|
||||
{
|
||||
@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IDynamicQuery<TSource, TDestination> : IDynamicQuery
|
||||
where TSource : class
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IDynamicQueryInterceptorProvider<TSource, TDestination>
|
||||
{
|
||||
@ -1,4 +1,4 @@
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IDynamicQueryParams<out TParams>
|
||||
where TParams : class
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IQueryableProvider<TSource>
|
||||
{
|
||||
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
|
||||
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
|
||||
<TargetFrameworks>netstandard2.1;net10.0</TargetFrameworks>
|
||||
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">true</IsAotCompatible>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -1,10 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.DynamicQuery;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||
|
||||
public class DynamicQuery<TSource, TDestination> : DynamicQuery, IDynamicQuery<TSource, TDestination>
|
||||
where TSource : class
|
||||
@ -2,7 +2,7 @@
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||
|
||||
public class DynamicQueryAggregate
|
||||
{
|
||||
@ -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
|
||||
{
|
||||
@ -1,11 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
using OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
[ApiController, Route("api/query/[controller]")]
|
||||
public class DynamicQueryController<TUnderlyingQuery, TSource, TDestination> : Controller
|
||||
@ -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
|
||||
{
|
||||
@ -4,11 +4,11 @@ using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Discover;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
using Svrnty.CQRS.DynamicQuery.Discover;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider)
|
||||
: IApplicationFeatureProvider<ControllerFeature>
|
||||
@ -0,0 +1,5 @@
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
public class DynamicQueryControllerOptions
|
||||
{
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
@ -1,11 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -27,10 +28,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore\OpenHarbor.CQRS.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery\OpenHarbor.CQRS.DynamicQuery.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -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
|
||||
{
|
||||
@ -1,4 +1,4 @@
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -6,11 +6,11 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery;
|
||||
namespace Svrnty.CQRS.DynamicQuery;
|
||||
|
||||
public class DynamicQueryHandler<TSource, TDestination>
|
||||
: DynamicQueryHandlerBase<TSource, TDestination>,
|
||||
OpenHarbor.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>>
|
||||
Svrnty.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>>
|
||||
where TSource : class
|
||||
where TDestination : class
|
||||
{
|
||||
@ -30,7 +30,7 @@ public class DynamicQueryHandler<TSource, TDestination>
|
||||
|
||||
public class DynamicQueryHandler<TSource, TDestination, TParams>
|
||||
: DynamicQueryHandlerBase<TSource, TDestination>,
|
||||
OpenHarbor.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>
|
||||
Svrnty.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>
|
||||
where TSource : class
|
||||
where TDestination : class
|
||||
where TParams : class
|
||||
@ -3,11 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.DynamicQuery;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery;
|
||||
namespace Svrnty.CQRS.DynamicQuery;
|
||||
|
||||
public abstract class DynamicQueryHandlerBase<TSource, TDestination>
|
||||
where TSource : class
|
||||
@ -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
|
||||
{
|
||||
@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -29,7 +29,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -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
|
||||
{
|
||||
@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -24,10 +24,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="10.4.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -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
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
<DebugType>portable</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<IncludeSource>true</IncludeSource>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
871
Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs
Normal file
871
Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs
Normal file
@ -0,0 +1,871 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Svrnty.CQRS.Grpc.Generators.Helpers;
|
||||
using Svrnty.CQRS.Grpc.Generators.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generators
|
||||
{
|
||||
[Generator]
|
||||
public class GrpcGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// Find all types that might be commands or queries
|
||||
var typeDeclarations = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: static (node, _) => node is TypeDeclarationSyntax,
|
||||
transform: static (ctx, _) => GetTypeSymbol(ctx))
|
||||
.Where(static symbol => symbol is not null);
|
||||
|
||||
// Combine with compilation
|
||||
var compilationAndTypes = context.CompilationProvider.Combine(typeDeclarations.Collect());
|
||||
|
||||
// Register source output
|
||||
context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right!, spc));
|
||||
}
|
||||
|
||||
private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context)
|
||||
{
|
||||
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
|
||||
var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration);
|
||||
return symbol as INamedTypeSymbol;
|
||||
}
|
||||
|
||||
private static void Execute(Compilation compilation, IEnumerable<INamedTypeSymbol?> types, SourceProductionContext context)
|
||||
{
|
||||
var grpcIgnoreAttribute = compilation.GetTypeByMetadataName("Svrnty.CQRS.Grpc.Abstractions.Attributes.GrpcIgnoreAttribute");
|
||||
var commandHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`1");
|
||||
var commandHandlerWithResultInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`2");
|
||||
var queryHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.IQueryHandler`2");
|
||||
|
||||
if (commandHandlerInterface == null || queryHandlerInterface == null)
|
||||
{
|
||||
return; // Handler interfaces not found
|
||||
}
|
||||
|
||||
var commandMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol?>(SymbolEqualityComparer.Default); // Command -> Result type (null if no result)
|
||||
var queryMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol>(SymbolEqualityComparer.Default); // Query -> Result type
|
||||
|
||||
// Find all command and query types by looking at handler implementations
|
||||
foreach (var typeSymbol in types)
|
||||
{
|
||||
if (typeSymbol == null || typeSymbol.IsAbstract || typeSymbol.IsStatic)
|
||||
continue;
|
||||
|
||||
// Check if this type implements ICommandHandler<T> or ICommandHandler<T, TResult>
|
||||
foreach (var iface in typeSymbol.AllInterfaces)
|
||||
{
|
||||
if (iface.IsGenericType)
|
||||
{
|
||||
// Check for ICommandHandler<TCommand>
|
||||
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerInterface) && iface.TypeArguments.Length == 1)
|
||||
{
|
||||
var commandType = iface.TypeArguments[0] as INamedTypeSymbol;
|
||||
if (commandType != null && !commandMap.ContainsKey(commandType))
|
||||
commandMap[commandType] = null; // No result type
|
||||
}
|
||||
// Check for ICommandHandler<TCommand, TResult>
|
||||
else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerWithResultInterface) && iface.TypeArguments.Length == 2)
|
||||
{
|
||||
var commandType = iface.TypeArguments[0] as INamedTypeSymbol;
|
||||
var resultType = iface.TypeArguments[1] as INamedTypeSymbol;
|
||||
if (commandType != null && resultType != null)
|
||||
commandMap[commandType] = resultType;
|
||||
}
|
||||
// Check for IQueryHandler<TQuery, TResult>
|
||||
else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryHandlerInterface) && iface.TypeArguments.Length == 2)
|
||||
{
|
||||
var queryType = iface.TypeArguments[0] as INamedTypeSymbol;
|
||||
var resultType = iface.TypeArguments[1] as INamedTypeSymbol;
|
||||
if (queryType != null && resultType != null)
|
||||
queryMap[queryType] = resultType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var commands = new List<CommandInfo>();
|
||||
var queries = new List<QueryInfo>();
|
||||
|
||||
// Process discovered command types
|
||||
foreach (var kvp in commandMap)
|
||||
{
|
||||
var commandType = kvp.Key;
|
||||
var resultType = kvp.Value;
|
||||
|
||||
// Skip if marked with [GrpcIgnore]
|
||||
if (grpcIgnoreAttribute != null && HasAttribute(commandType, grpcIgnoreAttribute))
|
||||
continue;
|
||||
|
||||
var commandInfo = ExtractCommandInfo(commandType, resultType);
|
||||
if (commandInfo != null)
|
||||
commands.Add(commandInfo);
|
||||
}
|
||||
|
||||
// Process discovered query types
|
||||
foreach (var kvp in queryMap)
|
||||
{
|
||||
var queryType = kvp.Key;
|
||||
var resultType = kvp.Value;
|
||||
|
||||
// Skip if marked with [GrpcIgnore]
|
||||
if (grpcIgnoreAttribute != null && HasAttribute(queryType, grpcIgnoreAttribute))
|
||||
continue;
|
||||
|
||||
var queryInfo = ExtractQueryInfo(queryType, resultType);
|
||||
if (queryInfo != null)
|
||||
queries.Add(queryInfo);
|
||||
}
|
||||
|
||||
// Generate services if we found any commands or queries
|
||||
if (commands.Any() || queries.Any())
|
||||
{
|
||||
GenerateProtoAndServices(context, commands, queries, compilation);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
|
||||
{
|
||||
return typeSymbol.GetAttributes().Any(attr =>
|
||||
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol));
|
||||
}
|
||||
|
||||
private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? interfaceSymbol)
|
||||
{
|
||||
if (interfaceSymbol == null)
|
||||
return false;
|
||||
|
||||
return typeSymbol.AllInterfaces.Any(i =>
|
||||
SymbolEqualityComparer.Default.Equals(i, interfaceSymbol));
|
||||
}
|
||||
|
||||
private static bool ImplementsGenericInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? genericInterfaceSymbol)
|
||||
{
|
||||
if (genericInterfaceSymbol == null)
|
||||
return false;
|
||||
|
||||
return typeSymbol.AllInterfaces.Any(i =>
|
||||
i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, genericInterfaceSymbol));
|
||||
}
|
||||
|
||||
private static CommandInfo? ExtractCommandInfo(INamedTypeSymbol commandType, INamedTypeSymbol? resultType)
|
||||
{
|
||||
var commandInfo = new CommandInfo
|
||||
{
|
||||
Name = commandType.Name,
|
||||
FullyQualifiedName = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
Namespace = commandType.ContainingNamespace.ToDisplayString(),
|
||||
Properties = new List<PropertyInfo>()
|
||||
};
|
||||
|
||||
// Set result type if provided
|
||||
if (resultType != null)
|
||||
{
|
||||
commandInfo.ResultType = resultType.Name;
|
||||
commandInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandType.Name}, {resultType.Name}>";
|
||||
}
|
||||
else
|
||||
{
|
||||
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandType.Name}>";
|
||||
}
|
||||
|
||||
// Extract properties
|
||||
var properties = commandType.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
|
||||
.ToList();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
|
||||
|
||||
commandInfo.Properties.Add(new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
Type = propertyType,
|
||||
ProtoType = protoType,
|
||||
FieldNumber = fieldNumber++
|
||||
});
|
||||
}
|
||||
|
||||
return commandInfo;
|
||||
}
|
||||
|
||||
private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType)
|
||||
{
|
||||
var queryInfo = new QueryInfo
|
||||
{
|
||||
Name = queryType.Name,
|
||||
FullyQualifiedName = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
Namespace = queryType.ContainingNamespace.ToDisplayString(),
|
||||
Properties = new List<PropertyInfo>()
|
||||
};
|
||||
|
||||
// Set result type
|
||||
queryInfo.ResultType = resultType.Name;
|
||||
queryInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryType.Name}, {resultType.Name}>";
|
||||
|
||||
// Extract properties
|
||||
var properties = queryType.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
|
||||
.ToList();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
|
||||
|
||||
queryInfo.Properties.Add(new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
Type = propertyType,
|
||||
ProtoType = protoType,
|
||||
FieldNumber = fieldNumber++
|
||||
});
|
||||
}
|
||||
|
||||
return queryInfo;
|
||||
}
|
||||
|
||||
private static void GenerateProtoAndServices(SourceProductionContext context, List<CommandInfo> commands, List<QueryInfo> queries, Compilation compilation)
|
||||
{
|
||||
// Get root namespace from compilation
|
||||
var rootNamespace = compilation.AssemblyName ?? "Application";
|
||||
|
||||
// Generate service implementations for commands
|
||||
if (commands.Any())
|
||||
{
|
||||
var commandService = GenerateCommandServiceImpl(commands, rootNamespace);
|
||||
context.AddSource("CommandServiceImpl.g.cs", commandService);
|
||||
}
|
||||
|
||||
// Generate service implementations for queries
|
||||
if (queries.Any())
|
||||
{
|
||||
var queryService = GenerateQueryServiceImpl(queries, rootNamespace);
|
||||
context.AddSource("QueryServiceImpl.g.cs", queryService);
|
||||
}
|
||||
|
||||
// Generate registration extensions
|
||||
var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), rootNamespace);
|
||||
context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions);
|
||||
}
|
||||
|
||||
private static string GenerateCommandMessages(List<CommandInfo> commands, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using System.Runtime.Serialization;");
|
||||
sb.AppendLine("using ProtoBuf;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages");
|
||||
sb.AppendLine("{");
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
// Generate command DTO
|
||||
sb.AppendLine(" [ProtoContract]");
|
||||
sb.AppendLine(" [DataContract]");
|
||||
sb.AppendLine($" public sealed class {command.Name}Dto");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" [ProtoMember({prop.FieldNumber})]");
|
||||
sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]");
|
||||
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate result DTO if command has a result
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine(" [ProtoContract]");
|
||||
sb.AppendLine(" [DataContract]");
|
||||
sb.AppendLine($" public sealed class {command.Name}ResultDto");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" [ProtoMember(1)]");
|
||||
sb.AppendLine(" [DataMember(Order = 1)]");
|
||||
sb.AppendLine($" public {command.ResultFullyQualifiedName} Result {{ get; set; }}");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateQueryMessages(List<QueryInfo> queries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using System.Runtime.Serialization;");
|
||||
sb.AppendLine("using ProtoBuf;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages");
|
||||
sb.AppendLine("{");
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
// Generate query DTO
|
||||
sb.AppendLine(" [ProtoContract]");
|
||||
sb.AppendLine(" [DataContract]");
|
||||
sb.AppendLine($" public sealed class {query.Name}Dto");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" [ProtoMember({prop.FieldNumber})]");
|
||||
sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]");
|
||||
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate result DTO
|
||||
sb.AppendLine(" [ProtoContract]");
|
||||
sb.AppendLine(" [DataContract]");
|
||||
sb.AppendLine($" public sealed class {query.Name}ResultDto");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" [ProtoMember(1)]");
|
||||
sb.AppendLine(" [DataMember(Order = 1)]");
|
||||
sb.AppendLine($" public {query.ResultFullyQualifiedName} Result {{ get; set; }}");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateCommandService(List<CommandInfo> commands, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using System.ServiceModel;");
|
||||
sb.AppendLine("using System.Threading;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc.Messages;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
sb.AppendLine("using ProtoBuf.Grpc;");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate service interface
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" [ServiceContract]");
|
||||
sb.AppendLine(" public interface ICommandService");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine($" [OperationContract]");
|
||||
sb.AppendLine($" Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" [OperationContract]");
|
||||
sb.AppendLine($" Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate service implementation
|
||||
sb.AppendLine(" public sealed class CommandService : ICommandService");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public CommandService(IServiceProvider serviceProvider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine($" public async Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
|
||||
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {command.Name}ResultDto {{ Result = result }};");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" public async Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
|
||||
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateQueryService(List<QueryInfo> queries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using System.ServiceModel;");
|
||||
sb.AppendLine("using System.Threading;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc.Messages;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
sb.AppendLine("using ProtoBuf.Grpc;");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate service interface
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" [ServiceContract]");
|
||||
sb.AppendLine(" public interface IQueryService");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
sb.AppendLine($" [OperationContract]");
|
||||
sb.AppendLine($" Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default);");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate service implementation
|
||||
sb.AppendLine(" public sealed class QueryService : IQueryService");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public QueryService(IServiceProvider serviceProvider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
sb.AppendLine($" public async Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();");
|
||||
sb.AppendLine($" var query = new {query.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {query.Name}ResultDto {{ Result = result }};");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("using Microsoft.AspNetCore.Builder;");
|
||||
sb.AppendLine("using Microsoft.AspNetCore.Routing;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc.Services;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Extensions");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static class GrpcServiceRegistrationExtensions");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
if (hasCommands)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Registers the auto-generated Command gRPC service");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" services.AddGrpc();");
|
||||
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
|
||||
sb.AppendLine(" return services;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
|
||||
sb.AppendLine(" return endpoints;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (hasQueries)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Registers the auto-generated Query gRPC service");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" services.AddGrpc();");
|
||||
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
|
||||
sb.AppendLine(" return services;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
|
||||
sb.AppendLine(" return endpoints;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (hasCommands && hasQueries)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Registers both Command and Query gRPC services");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IServiceCollection AddGrpcCommandsAndQueries(this IServiceCollection services)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" services.AddGrpc();");
|
||||
sb.AppendLine(" services.AddGrpcReflection();");
|
||||
if (hasCommands)
|
||||
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
|
||||
if (hasQueries)
|
||||
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
|
||||
sb.AppendLine(" return services;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Maps both Command and Query gRPC service endpoints");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)");
|
||||
sb.AppendLine(" {");
|
||||
if (hasCommands)
|
||||
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
|
||||
if (hasQueries)
|
||||
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
|
||||
sb.AppendLine(" return endpoints;");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ToCamelCase(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str) || char.IsLower(str[0]))
|
||||
return str;
|
||||
|
||||
return char.ToLowerInvariant(str[0]) + str.Substring(1);
|
||||
}
|
||||
|
||||
// New methods for standard gRPC generation
|
||||
|
||||
private static string GenerateCommandsProto(List<CommandInfo> commands, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("syntax = \"proto3\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("package cqrs;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("// Command service for CQRS operations");
|
||||
sb.AppendLine("service CommandService {");
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
var methodName = command.Name.Replace("Command", "");
|
||||
sb.AppendLine($" // {command.Name}");
|
||||
sb.AppendLine($" rpc {methodName} ({command.Name}Request) returns ({command.Name}Response);");
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate message types
|
||||
foreach (var command in commands)
|
||||
{
|
||||
// Request message
|
||||
sb.AppendLine($"message {command.Name}Request {{");
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Response message
|
||||
sb.AppendLine($"message {command.Name}Response {{");
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(command.ResultFullyQualifiedName!, out _, out _)} result = 1;");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateQueriesProto(List<QueryInfo> queries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("syntax = \"proto3\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("package cqrs;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("// Query service for CQRS operations");
|
||||
sb.AppendLine("service QueryService {");
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
var methodName = query.Name.Replace("Query", "");
|
||||
sb.AppendLine($" // {query.Name}");
|
||||
sb.AppendLine($" rpc {methodName} ({query.Name}Request) returns ({query.Name}Response);");
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate message types
|
||||
foreach (var query in queries)
|
||||
{
|
||||
// Request message
|
||||
sb.AppendLine($"message {query.Name}Request {{");
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Response message
|
||||
sb.AppendLine($"message {query.Name}Response {{");
|
||||
sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(query.ResultFullyQualifiedName, out _, out _)} result = 1;");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateCommandServiceImpl(List<CommandInfo> commands, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using Grpc.Core;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using System.Linq;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine("using FluentValidation;");
|
||||
sb.AppendLine("using Google.Rpc;");
|
||||
sb.AppendLine("using Google.Protobuf.WellKnownTypes;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public sealed class CommandServiceImpl : CommandService.CommandServiceBase");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public CommandServiceImpl(IServiceProvider serviceProvider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
var methodName = command.Name.Replace("Command", "");
|
||||
var requestType = $"{command.Name}Request";
|
||||
var responseType = $"{command.Name}Response";
|
||||
|
||||
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
|
||||
sb.AppendLine($" {requestType} request,");
|
||||
sb.AppendLine(" ServerCallContext context)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" // Validate command if validator is registered");
|
||||
sb.AppendLine($" var validator = _serviceProvider.GetService<IValidator<{command.FullyQualifiedName}>>();");
|
||||
sb.AppendLine(" if (validator != null)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" var validationResult = await validator.ValidateAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine(" if (!validationResult.IsValid)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" // Create Rich Error Model with structured field violations");
|
||||
sb.AppendLine(" var badRequest = new BadRequest();");
|
||||
sb.AppendLine(" foreach (var error in validationResult.Errors)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" Field = error.PropertyName,");
|
||||
sb.AppendLine(" Description = error.ErrorMessage");
|
||||
sb.AppendLine(" });");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" var status = new Google.Rpc.Status");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" Code = (int)Code.InvalidArgument,");
|
||||
sb.AppendLine(" Message = \"Validation failed\",");
|
||||
sb.AppendLine(" Details = { Any.Pack(badRequest) }");
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" throw status.ToRpcException();");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
|
||||
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {responseType} {{ Result = result }};");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {responseType}();");
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateQueryServiceImpl(List<QueryInfo> queries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using Grpc.Core;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public sealed class QueryServiceImpl : QueryService.QueryServiceBase");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public QueryServiceImpl(IServiceProvider serviceProvider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
var methodName = query.Name.Replace("Query", "");
|
||||
var requestType = $"{query.Name}Request";
|
||||
var responseType = $"{query.Name}Response";
|
||||
|
||||
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
|
||||
sb.AppendLine($" {requestType} request,");
|
||||
sb.AppendLine(" ServerCallContext context)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();");
|
||||
sb.AppendLine($" var query = new {query.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {responseType} {{ Result = result }};");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
96
Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs
Normal file
96
Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generators.Helpers
|
||||
{
|
||||
internal static class ProtoTypeMapper
|
||||
{
|
||||
private static readonly Dictionary<string, string> TypeMap = new Dictionary<string, string>
|
||||
{
|
||||
// Primitives
|
||||
{ "System.String", "string" },
|
||||
{ "System.Boolean", "bool" },
|
||||
{ "System.Int32", "int32" },
|
||||
{ "System.Int64", "int64" },
|
||||
{ "System.UInt32", "uint32" },
|
||||
{ "System.UInt64", "uint64" },
|
||||
{ "System.Single", "float" },
|
||||
{ "System.Double", "double" },
|
||||
{ "System.Byte", "uint32" },
|
||||
{ "System.SByte", "int32" },
|
||||
{ "System.Int16", "int32" },
|
||||
{ "System.UInt16", "uint32" },
|
||||
{ "System.Decimal", "string" }, // Decimal as string to preserve precision
|
||||
{ "System.DateTime", "int64" }, // Unix timestamp
|
||||
{ "System.DateTimeOffset", "int64" }, // Unix timestamp
|
||||
{ "System.Guid", "string" },
|
||||
{ "System.TimeSpan", "int64" }, // Ticks
|
||||
|
||||
// Nullable variants
|
||||
{ "System.Boolean?", "bool" },
|
||||
{ "System.Int32?", "int32" },
|
||||
{ "System.Int64?", "int64" },
|
||||
{ "System.UInt32?", "uint32" },
|
||||
{ "System.UInt64?", "uint64" },
|
||||
{ "System.Single?", "float" },
|
||||
{ "System.Double?", "double" },
|
||||
{ "System.Byte?", "uint32" },
|
||||
{ "System.SByte?", "int32" },
|
||||
{ "System.Int16?", "int32" },
|
||||
{ "System.UInt16?", "uint32" },
|
||||
{ "System.Decimal?", "string" },
|
||||
{ "System.DateTime?", "int64" },
|
||||
{ "System.DateTimeOffset?", "int64" },
|
||||
{ "System.Guid?", "string" },
|
||||
{ "System.TimeSpan?", "int64" },
|
||||
};
|
||||
|
||||
public static string MapToProtoType(string csharpType, out bool isRepeated, out bool isOptional)
|
||||
{
|
||||
isRepeated = false;
|
||||
isOptional = false;
|
||||
|
||||
// Handle arrays
|
||||
if (csharpType.EndsWith("[]"))
|
||||
{
|
||||
isRepeated = true;
|
||||
var elementType = csharpType.Substring(0, csharpType.Length - 2);
|
||||
return MapToProtoType(elementType, out _, out _);
|
||||
}
|
||||
|
||||
// Handle generic collections
|
||||
if (csharpType.StartsWith("System.Collections.Generic.List<") ||
|
||||
csharpType.StartsWith("System.Collections.Generic.IList<") ||
|
||||
csharpType.StartsWith("System.Collections.Generic.IEnumerable<") ||
|
||||
csharpType.StartsWith("System.Collections.Generic.ICollection<"))
|
||||
{
|
||||
isRepeated = true;
|
||||
var startIndex = csharpType.IndexOf('<') + 1;
|
||||
var endIndex = csharpType.LastIndexOf('>');
|
||||
var elementType = csharpType.Substring(startIndex, endIndex - startIndex);
|
||||
return MapToProtoType(elementType, out _, out _);
|
||||
}
|
||||
|
||||
// Handle nullable value types
|
||||
if (csharpType.EndsWith("?"))
|
||||
{
|
||||
isOptional = true;
|
||||
}
|
||||
|
||||
// Check if it's a known primitive type
|
||||
if (TypeMap.TryGetValue(csharpType, out var protoType))
|
||||
{
|
||||
return protoType;
|
||||
}
|
||||
|
||||
// For unknown types, assume it's a custom message type
|
||||
// Extract just the type name without namespace
|
||||
var lastDot = csharpType.LastIndexOf('.');
|
||||
if (lastDot >= 0)
|
||||
{
|
||||
return csharpType.Substring(lastDot + 1).Replace("?", "");
|
||||
}
|
||||
|
||||
return csharpType.Replace("?", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs
Normal file
41
Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
{
|
||||
public class CommandInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FullyQualifiedName { get; set; }
|
||||
public string Namespace { get; set; }
|
||||
public List<PropertyInfo> Properties { get; set; }
|
||||
public string? ResultType { get; set; }
|
||||
public string? ResultFullyQualifiedName { get; set; }
|
||||
public bool HasResult => ResultType != null;
|
||||
public string HandlerInterfaceName { get; set; }
|
||||
|
||||
public CommandInfo()
|
||||
{
|
||||
Name = string.Empty;
|
||||
FullyQualifiedName = string.Empty;
|
||||
Namespace = string.Empty;
|
||||
Properties = new List<PropertyInfo>();
|
||||
HandlerInterfaceName = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public class PropertyInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string ProtoType { get; set; }
|
||||
public int FieldNumber { get; set; }
|
||||
|
||||
public PropertyInfo()
|
||||
{
|
||||
Name = string.Empty;
|
||||
Type = string.Empty;
|
||||
ProtoType = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs
Normal file
26
Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
{
|
||||
public class QueryInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FullyQualifiedName { get; set; }
|
||||
public string Namespace { get; set; }
|
||||
public List<PropertyInfo> Properties { get; set; }
|
||||
public string ResultType { get; set; }
|
||||
public string ResultFullyQualifiedName { get; set; }
|
||||
public string HandlerInterfaceName { get; set; }
|
||||
|
||||
public QueryInfo()
|
||||
{
|
||||
Name = string.Empty;
|
||||
FullyQualifiedName = string.Empty;
|
||||
Namespace = string.Empty;
|
||||
Properties = new List<PropertyInfo>();
|
||||
ResultType = string.Empty;
|
||||
ResultFullyQualifiedName = string.Empty;
|
||||
HandlerInterfaceName = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
338
Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs
Normal file
338
Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs
Normal file
@ -0,0 +1,338 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Protocol Buffer (.proto) files from C# Command and Query types
|
||||
/// </summary>
|
||||
internal class ProtoFileGenerator
|
||||
{
|
||||
private readonly Compilation _compilation;
|
||||
private readonly HashSet<string> _requiredImports = new HashSet<string>();
|
||||
private readonly HashSet<string> _generatedMessages = new HashSet<string>();
|
||||
private readonly StringBuilder _messagesBuilder = new StringBuilder();
|
||||
|
||||
public ProtoFileGenerator(Compilation compilation)
|
||||
{
|
||||
_compilation = compilation;
|
||||
}
|
||||
|
||||
public string Generate(string packageName, string csharpNamespace)
|
||||
{
|
||||
var commands = DiscoverCommands();
|
||||
var queries = DiscoverQueries();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Header
|
||||
sb.AppendLine("syntax = \"proto3\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"option csharp_namespace = \"{csharpNamespace}\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"package {packageName};");
|
||||
sb.AppendLine();
|
||||
|
||||
// Imports (will be added later if needed)
|
||||
var importsPlaceholder = sb.Length;
|
||||
|
||||
// Command Service
|
||||
if (commands.Any())
|
||||
{
|
||||
sb.AppendLine("// Command service for CQRS operations");
|
||||
sb.AppendLine("service CommandService {");
|
||||
foreach (var command in commands)
|
||||
{
|
||||
var methodName = command.Name.Replace("Command", "");
|
||||
var requestType = $"{command.Name}Request";
|
||||
var responseType = $"{command.Name}Response";
|
||||
|
||||
sb.AppendLine($" // {GetXmlDocSummary(command)}");
|
||||
sb.AppendLine($" rpc {methodName} ({requestType}) returns ({responseType});");
|
||||
sb.AppendLine();
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Query Service
|
||||
if (queries.Any())
|
||||
{
|
||||
sb.AppendLine("// Query service for CQRS operations");
|
||||
sb.AppendLine("service QueryService {");
|
||||
foreach (var query in queries)
|
||||
{
|
||||
var methodName = query.Name.Replace("Query", "");
|
||||
var requestType = $"{query.Name}Request";
|
||||
var responseType = $"{query.Name}Response";
|
||||
|
||||
sb.AppendLine($" // {GetXmlDocSummary(query)}");
|
||||
sb.AppendLine($" rpc {methodName} ({requestType}) returns ({responseType});");
|
||||
sb.AppendLine();
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Generate messages for commands
|
||||
foreach (var command in commands)
|
||||
{
|
||||
GenerateRequestMessage(command);
|
||||
GenerateResponseMessage(command);
|
||||
}
|
||||
|
||||
// Generate messages for queries
|
||||
foreach (var query in queries)
|
||||
{
|
||||
GenerateRequestMessage(query);
|
||||
GenerateResponseMessage(query);
|
||||
}
|
||||
|
||||
// Append all generated messages
|
||||
sb.Append(_messagesBuilder);
|
||||
|
||||
// Insert imports if any were needed
|
||||
if (_requiredImports.Any())
|
||||
{
|
||||
var imports = new StringBuilder();
|
||||
foreach (var import in _requiredImports.OrderBy(i => i))
|
||||
{
|
||||
imports.AppendLine($"import \"{import}\";");
|
||||
}
|
||||
imports.AppendLine();
|
||||
sb.Insert(importsPlaceholder, imports.ToString());
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private List<INamedTypeSymbol> DiscoverCommands()
|
||||
{
|
||||
return _compilation.GetSymbolsWithName(
|
||||
name => name.EndsWith("Command"),
|
||||
SymbolFilter.Type)
|
||||
.OfType<INamedTypeSymbol>()
|
||||
.Where(t => !HasGrpcIgnoreAttribute(t))
|
||||
.Where(t => t.TypeKind == TypeKind.Class || t.TypeKind == TypeKind.Struct)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<INamedTypeSymbol> DiscoverQueries()
|
||||
{
|
||||
return _compilation.GetSymbolsWithName(
|
||||
name => name.EndsWith("Query"),
|
||||
SymbolFilter.Type)
|
||||
.OfType<INamedTypeSymbol>()
|
||||
.Where(t => !HasGrpcIgnoreAttribute(t))
|
||||
.Where(t => t.TypeKind == TypeKind.Class || t.TypeKind == TypeKind.Struct)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private bool HasGrpcIgnoreAttribute(INamedTypeSymbol type)
|
||||
{
|
||||
return type.GetAttributes().Any(attr =>
|
||||
attr.AttributeClass?.Name == "GrpcIgnoreAttribute");
|
||||
}
|
||||
|
||||
private void GenerateRequestMessage(INamedTypeSymbol type)
|
||||
{
|
||||
var messageName = $"{type.Name}Request";
|
||||
if (_generatedMessages.Contains(messageName))
|
||||
return;
|
||||
|
||||
_generatedMessages.Add(messageName);
|
||||
|
||||
_messagesBuilder.AppendLine($"// Request message for {type.Name}");
|
||||
_messagesBuilder.AppendLine($"message {messageName} {{");
|
||||
|
||||
var properties = type.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
||||
.ToList();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
{
|
||||
// Skip unsupported types and add a comment
|
||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
||||
|
||||
// If this is a complex type, generate its message too
|
||||
if (IsComplexType(prop.Type))
|
||||
{
|
||||
GenerateComplexTypeMessage(prop.Type as INamedTypeSymbol);
|
||||
}
|
||||
|
||||
fieldNumber++;
|
||||
}
|
||||
|
||||
_messagesBuilder.AppendLine("}");
|
||||
_messagesBuilder.AppendLine();
|
||||
}
|
||||
|
||||
private void GenerateResponseMessage(INamedTypeSymbol type)
|
||||
{
|
||||
var messageName = $"{type.Name}Response";
|
||||
if (_generatedMessages.Contains(messageName))
|
||||
return;
|
||||
|
||||
_generatedMessages.Add(messageName);
|
||||
|
||||
_messagesBuilder.AppendLine($"// Response message for {type.Name}");
|
||||
_messagesBuilder.AppendLine($"message {messageName} {{");
|
||||
|
||||
// Determine the result type from ICommandHandler<T, TResult> or IQueryHandler<T, TResult>
|
||||
var resultType = GetResultType(type);
|
||||
|
||||
if (resultType != null)
|
||||
{
|
||||
var protoType = ProtoFileTypeMapper.MapType(resultType, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
|
||||
_messagesBuilder.AppendLine($" {protoType} result = 1;");
|
||||
}
|
||||
// If no result type, leave message empty (void return)
|
||||
|
||||
_messagesBuilder.AppendLine("}");
|
||||
_messagesBuilder.AppendLine();
|
||||
|
||||
// Generate complex type message after closing the response message
|
||||
if (resultType != null && IsComplexType(resultType))
|
||||
{
|
||||
GenerateComplexTypeMessage(resultType as INamedTypeSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateComplexTypeMessage(INamedTypeSymbol? type)
|
||||
{
|
||||
if (type == null || _generatedMessages.Contains(type.Name))
|
||||
return;
|
||||
|
||||
// Don't generate messages for system types or primitives
|
||||
if (type.ContainingNamespace?.ToString().StartsWith("System") == true)
|
||||
return;
|
||||
|
||||
_generatedMessages.Add(type.Name);
|
||||
|
||||
_messagesBuilder.AppendLine($"// {type.Name} entity");
|
||||
_messagesBuilder.AppendLine($"message {type.Name} {{");
|
||||
|
||||
var properties = type.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
||||
.ToList();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
{
|
||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
||||
|
||||
// Recursively generate nested complex types
|
||||
if (IsComplexType(prop.Type))
|
||||
{
|
||||
GenerateComplexTypeMessage(prop.Type as INamedTypeSymbol);
|
||||
}
|
||||
|
||||
fieldNumber++;
|
||||
}
|
||||
|
||||
_messagesBuilder.AppendLine("}");
|
||||
_messagesBuilder.AppendLine();
|
||||
}
|
||||
|
||||
private ITypeSymbol? GetResultType(INamedTypeSymbol commandOrQueryType)
|
||||
{
|
||||
// Scan for handler classes that implement ICommandHandler<T, TResult> or IQueryHandler<T, TResult>
|
||||
var handlerInterfaceName = commandOrQueryType.Name.EndsWith("Command")
|
||||
? "ICommandHandler"
|
||||
: "IQueryHandler";
|
||||
|
||||
// Find all types in the compilation
|
||||
var allTypes = _compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type)
|
||||
.OfType<INamedTypeSymbol>();
|
||||
|
||||
foreach (var type in allTypes)
|
||||
{
|
||||
// Check if this type implements the handler interface
|
||||
foreach (var @interface in type.AllInterfaces)
|
||||
{
|
||||
if (@interface.Name == handlerInterfaceName && @interface.TypeArguments.Length >= 1)
|
||||
{
|
||||
// Check if the first type argument matches our command/query
|
||||
var firstArg = @interface.TypeArguments[0];
|
||||
if (SymbolEqualityComparer.Default.Equals(firstArg, commandOrQueryType))
|
||||
{
|
||||
// Found the handler! Return the result type (second type argument) if it exists
|
||||
if (@interface.TypeArguments.Length == 2)
|
||||
{
|
||||
return @interface.TypeArguments[1];
|
||||
}
|
||||
// If only one type argument, it's a void command (ICommandHandler<T>)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No handler found
|
||||
}
|
||||
|
||||
private bool IsComplexType(ITypeSymbol type)
|
||||
{
|
||||
// Check if it's a user-defined class/struct (not a primitive or system type)
|
||||
if (type.TypeKind != TypeKind.Class && type.TypeKind != TypeKind.Struct)
|
||||
return false;
|
||||
|
||||
var fullName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
return !fullName.Contains("System.");
|
||||
}
|
||||
|
||||
private string GetXmlDocSummary(INamedTypeSymbol type)
|
||||
{
|
||||
var xml = type.GetDocumentationCommentXml();
|
||||
if (string.IsNullOrEmpty(xml))
|
||||
return $"{type.Name} operation";
|
||||
|
||||
// Simple extraction - could be enhanced
|
||||
// xml is guaranteed non-null after IsNullOrEmpty check above
|
||||
var summaryStart = xml!.IndexOf("<summary>");
|
||||
var summaryEnd = xml.IndexOf("</summary>");
|
||||
if (summaryStart >= 0 && summaryEnd > summaryStart)
|
||||
{
|
||||
var summary = xml.Substring(summaryStart + 9, summaryEnd - summaryStart - 9).Trim();
|
||||
return summary;
|
||||
}
|
||||
|
||||
return $"{type.Name} operation";
|
||||
}
|
||||
}
|
||||
131
Svrnty.CQRS.Grpc.Generators/ProtoFileSourceGenerator.cs
Normal file
131
Svrnty.CQRS.Grpc.Generators/ProtoFileSourceGenerator.cs
Normal file
@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// Incremental source generator that generates .proto files from C# commands and queries
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public class ProtoFileSourceGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// Register a post-initialization output to generate the proto file
|
||||
context.RegisterPostInitializationOutput(ctx =>
|
||||
{
|
||||
// Generate a placeholder - the actual proto will be generated in the source output
|
||||
});
|
||||
|
||||
// Collect all command and query types
|
||||
var commandsAndQueries = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: static (s, _) => IsCommandOrQuery(s),
|
||||
transform: static (ctx, _) => GetTypeSymbol(ctx))
|
||||
.Where(static m => m is not null)
|
||||
.Collect();
|
||||
|
||||
// Combine with compilation to have access to it
|
||||
var compilationAndTypes = context.CompilationProvider.Combine(commandsAndQueries);
|
||||
|
||||
// Generate proto file when commands/queries change
|
||||
context.RegisterSourceOutput(compilationAndTypes, (spc, source) =>
|
||||
{
|
||||
var (compilation, types) = source;
|
||||
|
||||
if (types.IsDefaultOrEmpty)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Get build properties for configuration
|
||||
var packageName = GetBuildProperty(spc, "RootNamespace") ?? "cqrs";
|
||||
var csharpNamespace = GetBuildProperty(spc, "RootNamespace") ?? "Generated.Grpc";
|
||||
|
||||
// Generate the proto file content
|
||||
var generator = new ProtoFileGenerator(compilation);
|
||||
var protoContent = generator.Generate(packageName, csharpNamespace);
|
||||
|
||||
// Output as an embedded resource that can be extracted
|
||||
var protoFileName = "cqrs_services.proto";
|
||||
|
||||
// Generate a C# class that contains the proto content
|
||||
// This allows build tools to extract it if needed
|
||||
var csContent = $$"""
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generated
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the auto-generated Protocol Buffer definition
|
||||
/// </summary>
|
||||
internal static class GeneratedProtoFile
|
||||
{
|
||||
public const string FileName = "{{protoFileName}}";
|
||||
|
||||
public const string Content = @"{{protoContent.Replace("\"", "\"\"")}}";
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
spc.AddSource("GeneratedProtoFile.g.cs", csContent);
|
||||
|
||||
// Report that we generated the proto content
|
||||
var descriptor = new DiagnosticDescriptor(
|
||||
"CQRSGRPC002",
|
||||
"Proto file generated",
|
||||
"Generated proto file content in GeneratedProtoFile class",
|
||||
"Svrnty.CQRS.Grpc",
|
||||
DiagnosticSeverity.Info,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
spc.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Report diagnostic if generation fails
|
||||
var descriptor = new DiagnosticDescriptor(
|
||||
"CQRSGRPC001",
|
||||
"Proto file generation failed",
|
||||
"Failed to generate proto file: {0}",
|
||||
"Svrnty.CQRS.Grpc",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
spc.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None, ex.Message));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static bool IsCommandOrQuery(SyntaxNode node)
|
||||
{
|
||||
if (node is not TypeDeclarationSyntax typeDecl)
|
||||
return false;
|
||||
|
||||
var name = typeDecl.Identifier.Text;
|
||||
return name.EndsWith("Command") || name.EndsWith("Query");
|
||||
}
|
||||
|
||||
private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context)
|
||||
{
|
||||
var typeDecl = (TypeDeclarationSyntax)context.Node;
|
||||
var symbol = context.SemanticModel.GetDeclaredSymbol(typeDecl) as INamedTypeSymbol;
|
||||
|
||||
// Skip if it has GrpcIgnore attribute
|
||||
if (symbol?.GetAttributes().Any(a => a.AttributeClass?.Name == "GrpcIgnoreAttribute") == true)
|
||||
return null;
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
private static string? GetBuildProperty(SourceProductionContext context, string propertyName)
|
||||
{
|
||||
// Try to get build properties from the compilation options
|
||||
// This is a simplified approach - in practice, you might need analyzer config
|
||||
return null; // Will use defaults
|
||||
}
|
||||
}
|
||||
191
Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs
Normal file
191
Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs
Normal file
@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// Maps C# types to Protocol Buffer types for proto file generation
|
||||
/// </summary>
|
||||
internal static class ProtoFileTypeMapper
|
||||
{
|
||||
public static string MapType(ITypeSymbol typeSymbol, out bool needsImport, out string? importPath)
|
||||
{
|
||||
needsImport = false;
|
||||
importPath = null;
|
||||
|
||||
// Handle special name (fully qualified name)
|
||||
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var typeName = typeSymbol.Name;
|
||||
|
||||
// Nullable types - unwrap
|
||||
if (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated && typeSymbol is INamedTypeSymbol namedType && namedType.TypeArguments.Length > 0)
|
||||
{
|
||||
return MapType(namedType.TypeArguments[0], out needsImport, out importPath);
|
||||
}
|
||||
|
||||
// Basic types
|
||||
switch (typeName)
|
||||
{
|
||||
case "String":
|
||||
return "string";
|
||||
case "Int32":
|
||||
return "int32";
|
||||
case "UInt32":
|
||||
return "uint32";
|
||||
case "Int64":
|
||||
return "int64";
|
||||
case "UInt64":
|
||||
return "uint64";
|
||||
case "Int16":
|
||||
return "int32"; // Proto has no int16
|
||||
case "UInt16":
|
||||
return "uint32"; // Proto has no uint16
|
||||
case "Byte":
|
||||
return "uint32"; // Proto has no byte
|
||||
case "SByte":
|
||||
return "int32"; // Proto has no sbyte
|
||||
case "Boolean":
|
||||
return "bool";
|
||||
case "Single":
|
||||
return "float";
|
||||
case "Double":
|
||||
return "double";
|
||||
case "Byte[]":
|
||||
return "bytes";
|
||||
}
|
||||
|
||||
// Special types that need imports
|
||||
if (fullTypeName.Contains("System.DateTime"))
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/timestamp.proto";
|
||||
return "google.protobuf.Timestamp";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.TimeSpan"))
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/duration.proto";
|
||||
return "google.protobuf.Duration";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.Guid"))
|
||||
{
|
||||
// Guid serialized as string
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.Decimal"))
|
||||
{
|
||||
// Decimal serialized as string (no native decimal in proto)
|
||||
return "string";
|
||||
}
|
||||
|
||||
// Collections
|
||||
if (typeSymbol is INamedTypeSymbol collectionType)
|
||||
{
|
||||
// List, IEnumerable, Array, etc.
|
||||
if (collectionType.TypeArguments.Length == 1)
|
||||
{
|
||||
var elementType = collectionType.TypeArguments[0];
|
||||
var protoElementType = MapType(elementType, out needsImport, out importPath);
|
||||
return $"repeated {protoElementType}";
|
||||
}
|
||||
|
||||
// Dictionary<K, V>
|
||||
if (collectionType.TypeArguments.Length == 2 &&
|
||||
(typeName.Contains("Dictionary") || typeName.Contains("IDictionary")))
|
||||
{
|
||||
var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath);
|
||||
var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath);
|
||||
|
||||
// Set import flags if either key or value needs imports
|
||||
if (keyNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = keyImportPath;
|
||||
}
|
||||
if (valueNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = valueImportPath; // Note: This only captures last import, may need improvement
|
||||
}
|
||||
|
||||
return $"map<{keyType}, {valueType}>";
|
||||
}
|
||||
}
|
||||
|
||||
// Enums
|
||||
if (typeSymbol.TypeKind == TypeKind.Enum)
|
||||
{
|
||||
return typeName; // Use the enum name directly
|
||||
}
|
||||
|
||||
// Complex types (classes/records) become message types
|
||||
if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.TypeKind == TypeKind.Struct)
|
||||
{
|
||||
return typeName; // Reference the message type by name
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return "string"; // Default to string for unknown types
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts C# PascalCase property name to proto snake_case field name
|
||||
/// </summary>
|
||||
public static string ToSnakeCase(string pascalCase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pascalCase))
|
||||
return pascalCase;
|
||||
|
||||
var result = new System.Text.StringBuilder();
|
||||
result.Append(char.ToLowerInvariant(pascalCase[0]));
|
||||
|
||||
for (int i = 1; i < pascalCase.Length; i++)
|
||||
{
|
||||
var c = pascalCase[i];
|
||||
if (char.IsUpper(c))
|
||||
{
|
||||
// Handle sequences of uppercase letters (e.g., "APIKey" -> "api_key")
|
||||
if (i + 1 < pascalCase.Length && char.IsUpper(pascalCase[i + 1]))
|
||||
{
|
||||
result.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append('_');
|
||||
result.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a type should be skipped/ignored for proto generation
|
||||
/// </summary>
|
||||
public static bool IsUnsupportedType(ITypeSymbol typeSymbol)
|
||||
{
|
||||
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
|
||||
// Skip these types - they should trigger a warning/error
|
||||
if (fullTypeName.Contains("System.IO.Stream") ||
|
||||
fullTypeName.Contains("System.Threading.CancellationToken") ||
|
||||
fullTypeName.Contains("System.Threading.Tasks.Task") ||
|
||||
fullTypeName.Contains("System.Collections.Generic.IAsyncEnumerable") ||
|
||||
fullTypeName.Contains("System.Func") ||
|
||||
fullTypeName.Contains("System.Action") ||
|
||||
fullTypeName.Contains("System.Delegate"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IsPackable>true</IsPackable>
|
||||
<DevelopmentDependency>true</DevelopmentDependency>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>Source Generator for Svrnty.CQRS.Grpc - generates .proto files and gRPC service implementations from commands and queries</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0-2.final" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.0.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Package as analyzer -->
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
|
||||
<!-- Also package as build task -->
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="build" Visible="false" />
|
||||
<None Include="build\Svrnty.CQRS.Grpc.Generators.targets" Pack="true" PackagePath="build" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -0,0 +1,22 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Set default values for proto generation -->
|
||||
<GenerateProtoFile Condition="'$(GenerateProtoFile)' == ''">true</GenerateProtoFile>
|
||||
<ProtoOutputDirectory Condition="'$(ProtoOutputDirectory)' == ''">$(MSBuildProjectDirectory)\Protos</ProtoOutputDirectory>
|
||||
<GeneratedProtoFileName Condition="'$(GeneratedProtoFileName)' == ''">cqrs_services.proto</GeneratedProtoFileName>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="SvrntyGenerateProtoInfo" BeforeTargets="CoreCompile">
|
||||
<Message Text="Svrnty.CQRS.Grpc.Generators: Proto file will be auto-generated to $(ProtoOutputDirectory)\$(GeneratedProtoFileName)" Importance="normal" />
|
||||
</Target>
|
||||
|
||||
<!-- This target ensures the Protos directory exists before the generator runs -->
|
||||
<Target Name="EnsureProtosDirectory" BeforeTargets="CoreCompile">
|
||||
<MakeDir Directories="$(ProtoOutputDirectory)" Condition="!Exists('$(ProtoOutputDirectory)')" />
|
||||
</Target>
|
||||
|
||||
<!-- Set environment variable so the source generator can find the project directory -->
|
||||
<PropertyGroup>
|
||||
<MSBuildProjectDirectory>$(MSBuildProjectDirectory)</MSBuildProjectDirectory>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
40
Svrnty.CQRS.Grpc.Sample/AddUserCommand.cs
Normal file
40
Svrnty.CQRS.Grpc.Sample/AddUserCommand.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using FluentValidation;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Sample;
|
||||
|
||||
public record AddUserCommand
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public int Age { get; set; }
|
||||
}
|
||||
|
||||
public class AddUserCommandValidator : AbstractValidator<AddUserCommand>
|
||||
{
|
||||
public AddUserCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty()
|
||||
.WithMessage("Email is required")
|
||||
.EmailAddress()
|
||||
.WithMessage("Email must be a valid email address");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Name is required");
|
||||
|
||||
RuleFor(x => x.Age)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("Age must be greater than 0");
|
||||
}
|
||||
}
|
||||
|
||||
public class AddUserCommandHandler : ICommandHandler<AddUserCommand, int>
|
||||
{
|
||||
public Task<int> HandleAsync(AddUserCommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Simulate adding a user and returning ID
|
||||
return Task.FromResult(123);
|
||||
}
|
||||
}
|
||||
29
Svrnty.CQRS.Grpc.Sample/FetchUserQuery.cs
Normal file
29
Svrnty.CQRS.Grpc.Sample/FetchUserQuery.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Sample;
|
||||
|
||||
public record User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public record FetchUserQuery
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
}
|
||||
|
||||
public class FetchUserQueryHandler : IQueryHandler<FetchUserQuery, User>
|
||||
{
|
||||
public Task<User> HandleAsync(FetchUserQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Simulate fetching a user
|
||||
return Task.FromResult(new User
|
||||
{
|
||||
Id = query.UserId,
|
||||
Name = "John Doe",
|
||||
Email = "john@example.com"
|
||||
});
|
||||
}
|
||||
}
|
||||
21
Svrnty.CQRS.Grpc.Sample/InternalCommand.cs
Normal file
21
Svrnty.CQRS.Grpc.Sample/InternalCommand.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.Grpc.Abstractions.Attributes;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Sample;
|
||||
|
||||
// This command is marked with [GrpcIgnore] so it won't be exposed via gRPC
|
||||
[GrpcIgnore]
|
||||
public record InternalCommand
|
||||
{
|
||||
public string Data { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class InternalCommandHandler : ICommandHandler<InternalCommand>
|
||||
{
|
||||
public Task HandleAsync(InternalCommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// This is an internal command that should not be exposed via gRPC
|
||||
Console.WriteLine($"Processing internal command: {command.Data}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
60
Svrnty.CQRS.Grpc.Sample/Program.cs
Normal file
60
Svrnty.CQRS.Grpc.Sample/Program.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Svrnty.CQRS;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.FluentValidation;
|
||||
using Svrnty.CQRS.Grpc.Sample;
|
||||
using Svrnty.CQRS.Grpc.Sample.Grpc.Extensions;
|
||||
using Svrnty.CQRS.MinimalApi;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Configure Kestrel to support both HTTP/1.1 (for REST APIs) and HTTP/2 (for gRPC)
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
// Port 6000: HTTP/2 for gRPC
|
||||
options.ListenLocalhost(6000, o => o.Protocols = HttpProtocols.Http2);
|
||||
// Port 6001: HTTP/1.1 for REST API
|
||||
options.ListenLocalhost(6001, o => o.Protocols = HttpProtocols.Http1);
|
||||
});
|
||||
|
||||
// Register command handlers with CQRS and FluentValidation
|
||||
builder.Services.AddCommand<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
|
||||
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
|
||||
|
||||
// Register query handlers with CQRS
|
||||
builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
|
||||
|
||||
// Register discovery services for MinimalApi
|
||||
builder.Services.AddDefaultCommandDiscovery();
|
||||
builder.Services.AddDefaultQueryDiscovery();
|
||||
|
||||
// Auto-generated: Register gRPC services for both commands and queries (includes reflection)
|
||||
builder.Services.AddGrpcCommandsAndQueries();
|
||||
|
||||
// Add Swagger/OpenAPI support
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Auto-generated: Map gRPC endpoints for both commands and queries
|
||||
app.MapGrpcCommandsAndQueries();
|
||||
|
||||
// Map gRPC reflection service
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
// Enable Swagger middleware
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
// Map MinimalApi endpoints for commands and queries
|
||||
app.MapSvrntyCommands();
|
||||
app.MapSvrntyQueries();
|
||||
|
||||
|
||||
Console.WriteLine("Auto-Generated gRPC Server with Reflection, Validation, MinimalApi and Swagger");
|
||||
Console.WriteLine("gRPC (HTTP/2): http://localhost:6000");
|
||||
Console.WriteLine("HTTP API (HTTP/1.1): http://localhost:6001/api/command/* and http://localhost:6001/api/query/*");
|
||||
Console.WriteLine("Swagger UI: http://localhost:6001/swagger");
|
||||
|
||||
app.Run();
|
||||
58
Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto
Normal file
58
Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto
Normal file
@ -0,0 +1,58 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Svrnty.CQRS.Grpc.Sample.Grpc";
|
||||
|
||||
package cqrs;
|
||||
|
||||
// Command service for CQRS operations
|
||||
service CommandService {
|
||||
// Adds a new user and returns the user ID
|
||||
rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse);
|
||||
|
||||
// Removes a user
|
||||
rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse);
|
||||
}
|
||||
|
||||
// Query service for CQRS operations
|
||||
service QueryService {
|
||||
// Fetches a user by ID
|
||||
rpc FetchUser (FetchUserQueryRequest) returns (FetchUserQueryResponse);
|
||||
}
|
||||
|
||||
// Request message for adding a user
|
||||
message AddUserCommandRequest {
|
||||
string name = 1;
|
||||
string email = 2;
|
||||
int32 age = 3;
|
||||
}
|
||||
|
||||
// Response message containing the added user ID
|
||||
message AddUserCommandResponse {
|
||||
int32 result = 1;
|
||||
}
|
||||
|
||||
// Request message for removing a user
|
||||
message RemoveUserCommandRequest {
|
||||
int32 user_id = 1;
|
||||
}
|
||||
|
||||
// Response message for remove user (empty)
|
||||
message RemoveUserCommandResponse {
|
||||
}
|
||||
|
||||
// Request message for fetching a user
|
||||
message FetchUserQueryRequest {
|
||||
int32 user_id = 1;
|
||||
}
|
||||
|
||||
// Response message containing the user
|
||||
message FetchUserQueryResponse {
|
||||
User result = 1;
|
||||
}
|
||||
|
||||
// User entity
|
||||
message User {
|
||||
int32 id = 1;
|
||||
string name = 2;
|
||||
string email = 3;
|
||||
}
|
||||
17
Svrnty.CQRS.Grpc.Sample/RemoveUserCommand.cs
Normal file
17
Svrnty.CQRS.Grpc.Sample/RemoveUserCommand.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Sample;
|
||||
|
||||
public record RemoveUserCommand
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
}
|
||||
|
||||
public class RemoveUserCommandHandler : ICommandHandler<RemoveUserCommand>
|
||||
{
|
||||
public Task HandleAsync(RemoveUserCommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Simulate removing a user
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
35
Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj
Normal file
35
Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj
Normal file
@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos\*.proto" GrpcServices="Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.76.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Grpc.StatusProto" Version="2.71.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc\Svrnty.CQRS.Grpc.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.FluentValidation\Svrnty.CQRS.FluentValidation.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.MinimalApi\Svrnty.CQRS.MinimalApi.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
17
Svrnty.CQRS.Grpc.Sample/appsettings.Development.json
Normal file
17
Svrnty.CQRS.Grpc.Sample/appsettings.Development.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.AspNetCore.Server.Kestrel": "Information"
|
||||
}
|
||||
},
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://localhost:5000",
|
||||
"Protocols": "Http2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Svrnty.CQRS.Grpc.Sample/appsettings.json
Normal file
24
Svrnty.CQRS.Grpc.Sample/appsettings.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://localhost:5000",
|
||||
"Protocols": "Http2"
|
||||
},
|
||||
"Https": {
|
||||
"Url": "https://localhost:5001",
|
||||
"Protocols": "Http2"
|
||||
}
|
||||
},
|
||||
"EndpointDefaults": {
|
||||
"Protocols": "Http2"
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj
Normal file
43
Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj
Normal file
@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
<DebugType>portable</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<IncludeSource>true</IncludeSource>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
<None Include="build\Svrnty.CQRS.Grpc.targets" Pack="true" PackagePath="build\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.76.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Abstractions\Svrnty.CQRS.Grpc.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
14
Svrnty.CQRS.Grpc/build/Svrnty.CQRS.Grpc.targets
Normal file
14
Svrnty.CQRS.Grpc/build/Svrnty.CQRS.Grpc.targets
Normal file
@ -0,0 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<GrpcProtosDirectory>$(MSBuildProjectDirectory)\Protos</GrpcProtosDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Auto-include generated proto files -->
|
||||
<Protobuf Include="$(GrpcProtosDirectory)\*.proto" GrpcServices="Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="EnsureProtosDirectory" BeforeTargets="BeforeBuild">
|
||||
<MakeDir Directories="$(GrpcProtosDirectory)" Condition="!Exists('$(GrpcProtosDirectory)')" />
|
||||
</Target>
|
||||
</Project>
|
||||
263
Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs
Normal file
263
Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs
Normal file
@ -0,0 +1,263 @@
|
||||
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 MapSvrntyQueries(this IEndpointRouteBuilder endpoints, string routePrefix = "api/query")
|
||||
{
|
||||
var queryDiscovery = endpoints.ServiceProvider.GetRequiredService<IQueryDiscovery>();
|
||||
var authorizationService = endpoints.ServiceProvider.GetService<IQueryAuthorizationService>();
|
||||
|
||||
foreach (var queryMeta in queryDiscovery.GetQueries())
|
||||
{
|
||||
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
|
||||
if (ignoreAttribute != null)
|
||||
continue;
|
||||
|
||||
var route = $"{routePrefix}/{queryMeta.LowerCamelCaseName}";
|
||||
|
||||
MapQueryPost(endpoints, route, queryMeta, authorizationService);
|
||||
MapQueryGet(endpoints, route, queryMeta, authorizationService);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static void MapQueryPost(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
IQueryMeta queryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
{
|
||||
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
|
||||
|
||||
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryMeta.QueryType, cancellationToken);
|
||||
if (authorizationResult == AuthorizationResult.Forbidden)
|
||||
return Results.StatusCode(403);
|
||||
if (authorizationResult == AuthorizationResult.Unauthorized)
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
// Retrieve already-deserialized and validated query from HttpContext.Items
|
||||
var query = context.Items[ValidationFilter<object>.ValidatedObjectKey];
|
||||
if (query == null || !queryMeta.QueryType.IsInstanceOfType(query))
|
||||
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);
|
||||
})
|
||||
.AddEndpointFilter((IEndpointFilter)Activator.CreateInstance(typeof(ValidationFilter<>).MakeGenericType(queryMeta.QueryType))!)
|
||||
.WithName($"Query_{queryMeta.LowerCamelCaseName}_Post")
|
||||
.WithTags("Queries")
|
||||
.Accepts(queryMeta.QueryType, "application/json")
|
||||
.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 MapSvrntyCommands(this IEndpointRouteBuilder endpoints, string routePrefix = "api/command")
|
||||
{
|
||||
var commandDiscovery = endpoints.ServiceProvider.GetRequiredService<ICommandDiscovery>();
|
||||
var authorizationService = endpoints.ServiceProvider.GetService<ICommandAuthorizationService>();
|
||||
|
||||
foreach (var commandMeta in commandDiscovery.GetCommands())
|
||||
{
|
||||
var ignoreAttribute = commandMeta.CommandType.GetCustomAttribute<CommandControllerIgnoreAttribute>();
|
||||
if (ignoreAttribute != null)
|
||||
continue;
|
||||
|
||||
var route = $"{routePrefix}/{commandMeta.LowerCamelCaseName}";
|
||||
|
||||
if (commandMeta.CommandResultType == null)
|
||||
{
|
||||
MapCommandWithoutResult(endpoints, route, commandMeta, authorizationService);
|
||||
}
|
||||
else
|
||||
{
|
||||
MapCommandWithResult(endpoints, route, commandMeta, authorizationService);
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static void MapCommandWithoutResult(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
ICommandMeta commandMeta,
|
||||
ICommandAuthorizationService? authorizationService)
|
||||
{
|
||||
var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandMeta.CommandType);
|
||||
|
||||
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(commandMeta.CommandType, cancellationToken);
|
||||
if (authorizationResult == AuthorizationResult.Forbidden)
|
||||
return Results.StatusCode(403);
|
||||
if (authorizationResult == AuthorizationResult.Unauthorized)
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
// Retrieve already-deserialized and validated command from HttpContext.Items
|
||||
var command = context.Items[ValidationFilter<object>.ValidatedObjectKey];
|
||||
if (command == null || !commandMeta.CommandType.IsInstanceOfType(command))
|
||||
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();
|
||||
})
|
||||
.AddEndpointFilter((IEndpointFilter)Activator.CreateInstance(typeof(ValidationFilter<>).MakeGenericType(commandMeta.CommandType))!)
|
||||
.WithName($"Command_{commandMeta.LowerCamelCaseName}")
|
||||
.WithTags("Commands")
|
||||
.Accepts(commandMeta.CommandType, "application/json")
|
||||
.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();
|
||||
}
|
||||
|
||||
// Retrieve already-deserialized and validated command from HttpContext.Items
|
||||
var command = context.Items[ValidationFilter<object>.ValidatedObjectKey];
|
||||
if (command == null || !commandMeta.CommandType.IsInstanceOfType(command))
|
||||
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);
|
||||
})
|
||||
.AddEndpointFilter((IEndpointFilter)Activator.CreateInstance(typeof(ValidationFilter<>).MakeGenericType(commandMeta.CommandType))!)
|
||||
.WithName($"Command_{commandMeta.LowerCamelCaseName}")
|
||||
.WithTags("Commands")
|
||||
.Accepts(commandMeta.CommandType, "application/json")
|
||||
.Produces(200, commandMeta.CommandResultType)
|
||||
.Produces(400)
|
||||
.Produces(401)
|
||||
.Produces(403);
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<Company>Open Harbor</Company>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -29,12 +30,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
37
Svrnty.CQRS.MinimalApi/ValidationFilter.cs
Normal file
37
Svrnty.CQRS.MinimalApi/ValidationFilter.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Svrnty.CQRS.MinimalApi;
|
||||
|
||||
public class ValidationFilter<T> : IEndpointFilter where T : class
|
||||
{
|
||||
public const string ValidatedObjectKey = "ValidatedObject";
|
||||
|
||||
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
||||
{
|
||||
// Deserialize the request body
|
||||
var obj = await context.HttpContext.Request.ReadFromJsonAsync<T>(context.HttpContext.RequestAborted);
|
||||
|
||||
if (obj == null)
|
||||
return Results.BadRequest("Invalid request payload");
|
||||
|
||||
// Store the deserialized object for the lambda to retrieve
|
||||
context.HttpContext.Items[ValidatedObjectKey] = obj;
|
||||
|
||||
// Validate if validator is registered
|
||||
var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
|
||||
if (validator != null)
|
||||
{
|
||||
var validationResult = await validator.ValidateAsync(obj, context.HttpContext.RequestAborted);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
return Results.ValidationProblem(validationResult.ToDictionary());
|
||||
}
|
||||
}
|
||||
|
||||
return await next(context);
|
||||
}
|
||||
}
|
||||
197
Svrnty.CQRS.sln
Normal file
197
Svrnty.CQRS.sln
Normal file
@ -0,0 +1,197 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30907.101
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.Abstractions", "Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj", "{ED78E19D-31D4-4783-AE9E-2844A8541277}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS", "Svrnty.CQRS\Svrnty.CQRS.csproj", "{7069B98F-8736-4114-8AF5-1ACE094E6238}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.AspNetCore.Abstractions", "Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj", "{4C466827-31D3-4081-A751-C2FC7C381D7E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{617BA357-1A1F-40C5-B19A-A65A960E6142}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
LICENSE = LICENSE
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.DynamicQuery", "Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj", "{A38CE930-191F-417C-B5BE-8CC62DB47513}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.DynamicQuery.AspNetCore", "Svrnty.CQRS.DynamicQuery.AspNetCore\Svrnty.CQRS.DynamicQuery.AspNetCore.csproj", "{0829B99A-0A20-4CAC-A91E-FB67E18444DE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.FluentValidation", "Svrnty.CQRS.FluentValidation\Svrnty.CQRS.FluentValidation.csproj", "{70BD37C4-7497-474D-9A40-A701203971D8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.DynamicQuery.Abstractions", "Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj", "{8B9F8ACE-10EA-4215-9776-DE29EC93B020}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.MinimalApi", "Svrnty.CQRS.MinimalApi\Svrnty.CQRS.MinimalApi.csproj", "{E0756B59-F436-441A-8A98-37B6712D4B96}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc", "Svrnty.CQRS.Grpc\Svrnty.CQRS.Grpc.csproj", "{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Abstractions", "Svrnty.CQRS.Grpc.Abstractions\Svrnty.CQRS.Grpc.Abstractions.csproj", "{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Generators", "Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj", "{11537382-592C-4FE5-9103-272F358F0CAA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Sample", "Svrnty.CQRS.Grpc.Sample\Svrnty.CQRS.Grpc.Sample.csproj", "{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x64.Build.0 = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x86.Build.0 = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x86.Build.0 = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D6D431EA-C04F-462B-8033-60F510FEB49E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@ -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
|
||||
{
|
||||
@ -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
|
||||
{
|
||||
@ -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
|
||||
{
|
||||
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -25,6 +25,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
36
TestClient.csx
Normal file
36
TestClient.csx
Normal file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env dotnet-script
|
||||
#r "nuget: Grpc.Net.Client, 2.70.0"
|
||||
#r "nuget: Google.Protobuf, 3.28.3"
|
||||
#r "nuget: Grpc.Tools, 2.70.0"
|
||||
|
||||
using Grpc.Net.Client;
|
||||
using Grpc.Core;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// We'll use reflection/dynamic to call the gRPC service
|
||||
// This is a simple HTTP/2 test
|
||||
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
};
|
||||
|
||||
using var channel = GrpcChannel.ForAddress("http://localhost:5000", new GrpcChannelOptions
|
||||
{
|
||||
HttpHandler = handler
|
||||
});
|
||||
|
||||
Console.WriteLine("Connected to gRPC server at http://localhost:5000");
|
||||
Console.WriteLine("Channel state: " + channel.State);
|
||||
|
||||
// Test basic connectivity
|
||||
try
|
||||
{
|
||||
await channel.ConnectAsync();
|
||||
Console.WriteLine("Successfully connected!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Connection failed: {ex.Message}");
|
||||
}
|
||||
100
TestGrpcClient/Program.cs
Normal file
100
TestGrpcClient/Program.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Svrnty.CQRS.Grpc.Sample.Grpc;
|
||||
|
||||
Console.WriteLine("=== gRPC Client Validation Test ===");
|
||||
Console.WriteLine();
|
||||
|
||||
// Create a gRPC channel
|
||||
using var channel = GrpcChannel.ForAddress("http://localhost:5000");
|
||||
|
||||
// Create the gRPC client
|
||||
var client = new CommandService.CommandServiceClient(channel);
|
||||
|
||||
// Test 1: Valid request
|
||||
Console.WriteLine("Test 1: Valid AddUser request...");
|
||||
var validRequest = new AddUserCommandRequest
|
||||
{
|
||||
Name = "John Doe",
|
||||
Email = "john.doe@example.com",
|
||||
Age = 30
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.AddUserAsync(validRequest);
|
||||
Console.WriteLine($"✓ Success! User added with ID: {response.Result}");
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Console.WriteLine($"✗ Unexpected error: {ex.Status.Detail}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Test 2: Invalid email (empty)
|
||||
Console.WriteLine("Test 2: Invalid email (empty)...");
|
||||
var invalidEmailRequest = new AddUserCommandRequest
|
||||
{
|
||||
Name = "Jane Doe",
|
||||
Email = "",
|
||||
Age = 25
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.AddUserAsync(invalidEmailRequest);
|
||||
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
|
||||
Console.WriteLine($" Message: {ex.Status.Detail}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Test 3: Invalid email format
|
||||
Console.WriteLine("Test 3: Invalid email format...");
|
||||
var badEmailRequest = new AddUserCommandRequest
|
||||
{
|
||||
Name = "Bob Smith",
|
||||
Email = "not-an-email",
|
||||
Age = 40
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.AddUserAsync(badEmailRequest);
|
||||
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
|
||||
Console.WriteLine($" Message: {ex.Status.Detail}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Test 4: Invalid age (0)
|
||||
Console.WriteLine("Test 4: Invalid age (0)...");
|
||||
var invalidAgeRequest = new AddUserCommandRequest
|
||||
{
|
||||
Name = "Alice Brown",
|
||||
Email = "alice@example.com",
|
||||
Age = 0
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.AddUserAsync(invalidAgeRequest);
|
||||
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
|
||||
Console.WriteLine($" Message: {ex.Status.Detail}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("All tests completed!");
|
||||
58
TestGrpcClient/Protos/cqrs_services.proto
Normal file
58
TestGrpcClient/Protos/cqrs_services.proto
Normal file
@ -0,0 +1,58 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Svrnty.CQRS.Grpc.Sample.Grpc";
|
||||
|
||||
package cqrs;
|
||||
|
||||
// Command service for CQRS operations
|
||||
service CommandService {
|
||||
// Adds a new user and returns the user ID
|
||||
rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse);
|
||||
|
||||
// Removes a user
|
||||
rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse);
|
||||
}
|
||||
|
||||
// Query service for CQRS operations
|
||||
service QueryService {
|
||||
// Fetches a user by ID
|
||||
rpc FetchUser (FetchUserQueryRequest) returns (FetchUserQueryResponse);
|
||||
}
|
||||
|
||||
// Request message for adding a user
|
||||
message AddUserCommandRequest {
|
||||
string name = 1;
|
||||
string email = 2;
|
||||
int32 age = 3;
|
||||
}
|
||||
|
||||
// Response message containing the added user ID
|
||||
message AddUserCommandResponse {
|
||||
int32 result = 1;
|
||||
}
|
||||
|
||||
// Request message for removing a user
|
||||
message RemoveUserCommandRequest {
|
||||
int32 user_id = 1;
|
||||
}
|
||||
|
||||
// Response message for remove user (empty)
|
||||
message RemoveUserCommandResponse {
|
||||
}
|
||||
|
||||
// Request message for fetching a user
|
||||
message FetchUserQueryRequest {
|
||||
int32 user_id = 1;
|
||||
}
|
||||
|
||||
// Response message containing the user
|
||||
message FetchUserQueryResponse {
|
||||
User result = 1;
|
||||
}
|
||||
|
||||
// User entity
|
||||
message User {
|
||||
int32 id = 1;
|
||||
string name = 2;
|
||||
string email = 3;
|
||||
}
|
||||
23
TestGrpcClient/TestGrpcClient.csproj
Normal file
23
TestGrpcClient/TestGrpcClient.csproj
Normal file
@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos\*.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.76.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,49 +0,0 @@
|
||||
# Starter pipeline
|
||||
# Start with a minimal pipeline that you can customize to build and deploy your code.
|
||||
# Add steps that build, run tests, deploy, and more:
|
||||
# https://aka.ms/yaml
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- refs/tags/*
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
|
||||
- task: gitversion/setup@0
|
||||
inputs:
|
||||
versionSpec: '5.6.0'
|
||||
|
||||
- task: gitversion/execute@0
|
||||
displayName: 'Git Version'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
inputs:
|
||||
command: 'restore'
|
||||
projects: '**/*.csproj'
|
||||
feedsToUse: 'select'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: '**/*.csproj'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
inputs:
|
||||
command: 'pack'
|
||||
packagesToPack: '**/*.csproj'
|
||||
includesymbols: true
|
||||
includesource: true
|
||||
versioningScheme: 'byEnvVar'
|
||||
versionEnvVar: 'GitVersion.NuGetVersion'
|
||||
|
||||
- task: NuGetCommand@2
|
||||
inputs:
|
||||
command: 'push'
|
||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg'
|
||||
nuGetFeedType: 'external'
|
||||
publishFeedCredentials: 'PoweredSoftNuget'
|
||||
Loading…
Reference in New Issue
Block a user