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 |
|
| Package Name | NuGet | NuGet Install |
|
||||||
|-----------------------------------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------:|
|
|-----------------------------------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------:|
|
||||||
| OpenHarbor.CQRS | [](https://www.nuget.org/packages/OpenHarbor.CQRS/) | ```dotnet add package OpenHarbor.CQRS ``` |
|
| Svrnty.CQRS | [](https://www.nuget.org/packages/Svrnty.CQRS/) | ```dotnet add package Svrnty.CQRS ``` |
|
||||||
| OpenHarbor.CQRS.AspNetCore | [](https://www.nuget.org/packages/OpenHarbor.CQRS.AspNetCore/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore ``` |
|
| Svrnty.CQRS.MinimalApi | [](https://www.nuget.org/packages/Svrnty.CQRS.MinimalApi/) | ```dotnet add package Svrnty.CQRS.MinimalApi ``` |
|
||||||
| OpenHarbor.CQRS.FluentValidation | [](https://www.nuget.org/packages/OpenHarbor.CQRS.FluentValidation/) | ```dotnet add package OpenHarbor.CQRS.FluentValidation ``` |
|
| Svrnty.CQRS.AspNetCore | [](https://www.nuget.org/packages/Svrnty.CQRS.AspNetCore/) | ```dotnet add package Svrnty.CQRS.AspNetCore ``` |
|
||||||
| OpenHarbor.CQRS.DynamicQuery | [](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery/) | ```dotnet add package OpenHarbor.CQRS.DynamicQuery ``` |
|
| Svrnty.CQRS.FluentValidation | [](https://www.nuget.org/packages/Svrnty.CQRS.FluentValidation/) | ```dotnet add package Svrnty.CQRS.FluentValidation ``` |
|
||||||
| OpenHarbor.CQRS.DynamicQuery.AspNetCore | [](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery.AspNetCore/) | ```dotnet add package OpenHarbor.CQRS.DynamicQuery.AspNetCore ``` |
|
| 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.
|
> Abstractions Packages.
|
||||||
|
|
||||||
| Package Name | NuGet | NuGet Install |
|
| Package Name | NuGet | NuGet Install |
|
||||||
| ---------------------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------------------------------: |
|
| ---------------------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------------------------------: |
|
||||||
| OpenHarbor.CQRS.Abstractions | [](https://www.nuget.org/packages/OpenHarbor.CQRS.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.Abstractions ``` |
|
| Svrnty.CQRS.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.Abstractions/) | ```dotnet add package Svrnty.CQRS.Abstractions ``` |
|
||||||
| OpenHarbor.CQRS.AspNetCore.Abstractions | [](https://www.nuget.org/packages/OpenHarbor.CQRS.AspNetCore.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore.Abstractions ``` |
|
| Svrnty.CQRS.AspNetCore.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.AspNetCore.Abstractions/) | ```dotnet add package Svrnty.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.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
|
```csharp
|
||||||
public void ConfigureServices(IServiceCollection services)
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
{
|
|
||||||
// make sure to add your queries and commands before configuring MvCBuilder with .AddOpenHarborCommands and .AddOpenHarborQueries
|
|
||||||
AddQueries(services);
|
|
||||||
AddCommands(services);
|
|
||||||
|
|
||||||
// adds the non related to aspnet core features.
|
// Register CQRS core services
|
||||||
services.AddOpenHarborCQRS();
|
builder.Services.AddSvrntyCQRS();
|
||||||
|
builder.Services.AddDefaultCommandDiscovery();
|
||||||
|
builder.Services.AddDefaultQueryDiscovery();
|
||||||
|
|
||||||
services
|
// Add your commands and queries
|
||||||
.AddControllers()
|
AddQueries(builder.Services);
|
||||||
.AddOpenHarborQueries() // adds queries to aspnetcore mvc.(you can make it configurable to load balance only commands on a instance)
|
AddCommands(builder.Services);
|
||||||
.AddOpenHarborCommands() // adds commands to aspnetcore mvc. (you can make it configurable to load balance only commands on a instance)
|
|
||||||
.AddFluentValidation();
|
|
||||||
|
|
||||||
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.
|
> Example how to add your queries and commands.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@ -66,33 +235,62 @@ private void AddQueries(IServiceCollection services)
|
|||||||
|
|
||||||
# Fluent Validation
|
# 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
|
```csharp
|
||||||
public void ConfigureServices(IServiceCollection services)
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
{
|
using FluentValidation;
|
||||||
// without Package.
|
using Svrnty.CQRS;
|
||||||
services.AddCommand<EchoCommand, string, EchoCommandHandler>();
|
|
||||||
services.AddTransient<IValidator<EchoCommand>, EchoCommandValidator>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
private void AddCommands(IServiceCollection services)
|
||||||
{
|
{
|
||||||
// with OpenHarbor.CQRS.FluentValidation package.
|
// Register command handler
|
||||||
services.AddCommand<EchoCommand, string, EchoCommandHandler, EchoCommandValidator>();
|
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 |
|
| Task | Description | Status |
|
||||||
|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------|
|
|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------|
|
||||||
| Support .NET 8 | Ensure compatibility with .NET 8. | ✅ |
|
| 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. | ⬜️ |
|
| Support .NET 10 | Upgrade to .NET 10 with C# 14 language support. | ✅ |
|
||||||
| New Independent Module for MVC | Develop a standalone module, independent of MVC, to enhance framework flexibility. | ⬜️ |
|
| Update FluentValidation | Upgrade FluentValidation to version 11.x for .NET 10 compatibility. | ✅ |
|
||||||
| Implement .NET Native Compilation (AOT) | Enable Ahead-of-Time (AOT) compilation support for .NET 8. | ⬜️ |
|
| Add gRPC Support with source generators | Implement gRPC endpoints with source generators and Google Rich Error Model for validation. | ✅ |
|
||||||
| Update FluentValidation | Upgrade FluentValidation to the latest version, addressing potential breaking changes. | ⬜️ |
|
| 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. | ⬜️ |
|
| 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. | ⬜️ |
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Attributes;
|
namespace Svrnty.CQRS.Abstractions.Attributes;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public class CommandNameAttribute : Attribute
|
public class CommandNameAttribute : Attribute
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Attributes;
|
namespace Svrnty.CQRS.Abstractions.Attributes;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public class QueryNameAttribute : Attribute
|
public class QueryNameAttribute : Attribute
|
||||||
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using OpenHarbor.CQRS.Abstractions.Attributes;
|
using Svrnty.CQRS.Abstractions.Attributes;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
public sealed class CommandMeta : ICommandMeta
|
public sealed class CommandMeta : ICommandMeta
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
public interface ICommandMeta
|
public interface ICommandMeta
|
||||||
{
|
{
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
public interface IQueryDiscovery
|
public interface IQueryDiscovery
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
public interface IQueryMeta
|
public interface IQueryMeta
|
||||||
{
|
{
|
||||||
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using OpenHarbor.CQRS.Abstractions.Attributes;
|
using Svrnty.CQRS.Abstractions.Attributes;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
public class QueryMeta : IQueryMeta
|
public class QueryMeta : IQueryMeta
|
||||||
{
|
{
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions;
|
namespace Svrnty.CQRS.Abstractions;
|
||||||
|
|
||||||
public interface ICommandHandler<in TCommand>
|
public interface ICommandHandler<in TCommand>
|
||||||
where TCommand : class
|
where TCommand : class
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions;
|
namespace Svrnty.CQRS.Abstractions;
|
||||||
|
|
||||||
public interface IQueryHandler<in TQuery, TQueryResult>
|
public interface IQueryHandler<in TQuery, TQueryResult>
|
||||||
where TQuery : class
|
where TQuery : class
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
namespace Svrnty.CQRS.Abstractions.Security;
|
||||||
|
|
||||||
public enum AuthorizationResult
|
public enum AuthorizationResult
|
||||||
{
|
{
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
namespace Svrnty.CQRS.Abstractions.Security;
|
||||||
|
|
||||||
public interface ICommandAuthorizationService
|
public interface ICommandAuthorizationService
|
||||||
{
|
{
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
namespace Svrnty.CQRS.Abstractions.Security;
|
||||||
|
|
||||||
public interface IQueryAuthorizationService
|
public interface IQueryAuthorizationService
|
||||||
{
|
{
|
||||||
@ -1,8 +1,8 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
using Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Abstractions;
|
namespace Svrnty.CQRS.Abstractions;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
@ -1,13 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||||
<LangVersion>default</LangVersion>
|
<LangVersion>14</LangVersion>
|
||||||
<Company>Open Harbor</Company>
|
<Company>Svrnty</Company>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
@ -25,6 +25,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0-rc.1.23419.4" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public class CommandControllerIgnoreAttribute : Attribute
|
public class CommandControllerIgnoreAttribute : Attribute
|
||||||
@ -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;
|
using System;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public class QueryControllerIgnoreAttribute : Attribute
|
public class QueryControllerIgnoreAttribute : Attribute
|
||||||
@ -1,13 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
|
||||||
<IsAotCompatible>false</IsAotCompatible>
|
<IsAotCompatible>false</IsAotCompatible>
|
||||||
<LangVersion>default</LangVersion>
|
<LangVersion>14</LangVersion>
|
||||||
<Company>Open Harbor</Company>
|
<Company>Svrnty</Company>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
|
|
||||||
public class DynamicQueryInterceptorProvider<TSource, TDestination> : IDynamicQueryInterceptorProvider<TSource, TDestination>
|
public class DynamicQueryInterceptorProvider<TSource, TDestination> : IDynamicQueryInterceptorProvider<TSource, TDestination>
|
||||||
{
|
{
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
|
|
||||||
public interface IAlterQueryableService<TSource, TDestination>
|
public interface IAlterQueryableService<TSource, TDestination>
|
||||||
{
|
{
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
|
|
||||||
public interface IDynamicQuery<TSource, TDestination> : IDynamicQuery
|
public interface IDynamicQuery<TSource, TDestination> : IDynamicQuery
|
||||||
where TSource : class
|
where TSource : class
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
|
|
||||||
public interface IDynamicQueryInterceptorProvider<TSource, TDestination>
|
public interface IDynamicQueryInterceptorProvider<TSource, TDestination>
|
||||||
{
|
{
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
|
|
||||||
public interface IDynamicQueryParams<out TParams>
|
public interface IDynamicQueryParams<out TParams>
|
||||||
where TParams : class
|
where TParams : class
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
|
|
||||||
public interface IQueryableProvider<TSource>
|
public interface IQueryableProvider<TSource>
|
||||||
{
|
{
|
||||||
@ -1,13 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.1;net10.0</TargetFrameworks>
|
||||||
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
|
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">true</IsAotCompatible>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>default</LangVersion>
|
<LangVersion>14</LangVersion>
|
||||||
<Company>Open Harbor</Company>
|
<Company>Svrnty</Company>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
@ -1,10 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
using PoweredSoft.DynamicQuery;
|
using PoweredSoft.DynamicQuery;
|
||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||||
|
|
||||||
public class DynamicQuery<TSource, TDestination> : DynamicQuery, IDynamicQuery<TSource, TDestination>
|
public class DynamicQuery<TSource, TDestination> : DynamicQuery, IDynamicQuery<TSource, TDestination>
|
||||||
where TSource : class
|
where TSource : class
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||||
|
|
||||||
public class DynamicQueryAggregate
|
public class DynamicQueryAggregate
|
||||||
{
|
{
|
||||||
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using PoweredSoft.DynamicQuery;
|
using PoweredSoft.DynamicQuery;
|
||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||||
|
|
||||||
public class DynamicQueryFilter
|
public class DynamicQueryFilter
|
||||||
{
|
{
|
||||||
@ -1,11 +1,11 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using OpenHarbor.CQRS.Abstractions;
|
using Svrnty.CQRS.Abstractions;
|
||||||
using OpenHarbor.CQRS.AspNetCore.Mvc;
|
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||||
|
|
||||||
[ApiController, Route("api/query/[controller]")]
|
[ApiController, Route("api/query/[controller]")]
|
||||||
public class DynamicQueryController<TUnderlyingQuery, TSource, TDestination> : Controller
|
public class DynamicQueryController<TUnderlyingQuery, TSource, TDestination> : Controller
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
using Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||||
|
|
||||||
public class DynamicQueryControllerConvention : IControllerModelConvention
|
public class DynamicQueryControllerConvention : IControllerModelConvention
|
||||||
{
|
{
|
||||||
@ -4,11 +4,11 @@ using System.Reflection;
|
|||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
using Svrnty.CQRS.Abstractions.Discovery;
|
||||||
using OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||||
using OpenHarbor.CQRS.DynamicQuery.Discover;
|
using Svrnty.CQRS.DynamicQuery.Discover;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||||
|
|
||||||
public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider)
|
public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider)
|
||||||
: IApplicationFeatureProvider<ControllerFeature>
|
: IApplicationFeatureProvider<ControllerFeature>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
public class DynamicQueryControllerOptions
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
using Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||||
|
|
||||||
public static class MvcBuilderExtensions
|
public static class MvcBuilderExtensions
|
||||||
{
|
{
|
||||||
@ -1,11 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsAotCompatible>false</IsAotCompatible>
|
<IsAotCompatible>false</IsAotCompatible>
|
||||||
<Company>Open Harbor</Company>
|
<LangVersion>14</LangVersion>
|
||||||
|
<Company>Svrnty</Company>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
@ -27,10 +28,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore\OpenHarbor.CQRS.AspNetCore.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery\OpenHarbor.CQRS.DynamicQuery.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using Pluralize.NET;
|
using Pluralize.NET;
|
||||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
using Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery.Discover;
|
namespace Svrnty.CQRS.DynamicQuery.Discover;
|
||||||
|
|
||||||
public class DynamicQueryMeta : QueryMeta
|
public class DynamicQueryMeta : QueryMeta
|
||||||
{
|
{
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -6,11 +6,11 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery;
|
namespace Svrnty.CQRS.DynamicQuery;
|
||||||
|
|
||||||
public class DynamicQueryHandler<TSource, TDestination>
|
public class DynamicQueryHandler<TSource, TDestination>
|
||||||
: DynamicQueryHandlerBase<TSource, TDestination>,
|
: DynamicQueryHandlerBase<TSource, TDestination>,
|
||||||
OpenHarbor.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>>
|
Svrnty.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>>
|
||||||
where TSource : class
|
where TSource : class
|
||||||
where TDestination : class
|
where TDestination : class
|
||||||
{
|
{
|
||||||
@ -30,7 +30,7 @@ public class DynamicQueryHandler<TSource, TDestination>
|
|||||||
|
|
||||||
public class DynamicQueryHandler<TSource, TDestination, TParams>
|
public class DynamicQueryHandler<TSource, TDestination, TParams>
|
||||||
: DynamicQueryHandlerBase<TSource, TDestination>,
|
: DynamicQueryHandlerBase<TSource, TDestination>,
|
||||||
OpenHarbor.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>
|
Svrnty.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>
|
||||||
where TSource : class
|
where TSource : class
|
||||||
where TDestination : class
|
where TDestination : class
|
||||||
where TParams : class
|
where TParams : class
|
||||||
@ -3,11 +3,11 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
using PoweredSoft.DynamicQuery;
|
using PoweredSoft.DynamicQuery;
|
||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery;
|
namespace Svrnty.CQRS.DynamicQuery;
|
||||||
|
|
||||||
public abstract class DynamicQueryHandlerBase<TSource, TDestination>
|
public abstract class DynamicQueryHandlerBase<TSource, TDestination>
|
||||||
where TSource : class
|
where TSource : class
|
||||||
@ -1,13 +1,13 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using OpenHarbor.CQRS.Abstractions;
|
using Svrnty.CQRS.Abstractions;
|
||||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
using Svrnty.CQRS.Abstractions.Discovery;
|
||||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
using OpenHarbor.CQRS.DynamicQuery.Discover;
|
using Svrnty.CQRS.DynamicQuery.Discover;
|
||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.DynamicQuery;
|
namespace Svrnty.CQRS.DynamicQuery;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
@ -1,12 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
<LangVersion>default</LangVersion>
|
<LangVersion>14</LangVersion>
|
||||||
<Company>Open Harbor</Company>
|
<Company>Svrnty</Company>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OpenHarbor.CQRS.Abstractions;
|
using Svrnty.CQRS.Abstractions;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.FluentValidation;
|
namespace Svrnty.CQRS.FluentValidation;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
@ -1,12 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
<LangVersion>default</LangVersion>
|
<LangVersion>14</LangVersion>
|
||||||
<Company>Open Harbor</Company>
|
<Company>Svrnty</Company>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
@ -24,10 +24,10 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation" Version="10.4.0" />
|
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -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>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsAotCompatible>false</IsAotCompatible>
|
<IsAotCompatible>false</IsAotCompatible>
|
||||||
|
<LangVersion>14</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||||
<Company>Open Harbor</Company>
|
<Company>Svrnty</Company>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
@ -29,12 +30,11 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
using Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Discovery;
|
namespace Svrnty.CQRS.Discovery;
|
||||||
|
|
||||||
public sealed class CommandDiscovery : ICommandDiscovery
|
public sealed class CommandDiscovery : ICommandDiscovery
|
||||||
{
|
{
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
using Svrnty.CQRS.Abstractions.Discovery;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS.Discovery;
|
namespace Svrnty.CQRS.Discovery;
|
||||||
|
|
||||||
public sealed class QueryDiscovery : IQueryDiscovery
|
public sealed class QueryDiscovery : IQueryDiscovery
|
||||||
{
|
{
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
using Svrnty.CQRS.Abstractions.Discovery;
|
||||||
using OpenHarbor.CQRS.Discovery;
|
using Svrnty.CQRS.Discovery;
|
||||||
|
|
||||||
namespace OpenHarbor.CQRS;
|
namespace Svrnty.CQRS;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
@ -1,13 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||||
<LangVersion>default</LangVersion>
|
<LangVersion>14</LangVersion>
|
||||||
<Company>Open Harbor</Company>
|
<Company>Svrnty</Company>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
@ -25,6 +25,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
36
TestClient.csx
Normal file
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