cat on a spaceship
This commit is contained in:
parent
747fa227a1
commit
f6dccf46d7
26
.claude/settings.local.json
Normal file
26
.claude/settings.local.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dotnet clean:*)",
|
||||
"Bash(dotnet run)",
|
||||
"Bash(dotnet add:*)",
|
||||
"Bash(timeout 5 dotnet run:*)",
|
||||
"Bash(dotnet remove:*)",
|
||||
"Bash(netstat:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"WebSearch",
|
||||
"Bash(dotnet tool install:*)",
|
||||
"Bash(protogen:*)",
|
||||
"Bash(timeout 15 dotnet run:*)",
|
||||
"Bash(where:*)",
|
||||
"Bash(timeout 30 dotnet run:*)",
|
||||
"Bash(timeout 60 dotnet run:*)",
|
||||
"Bash(timeout 120 dotnet run:*)",
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
314
CLAUDE.md
Normal file
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
|
||||
137
README.md
137
README.md
@ -10,42 +10,141 @@ Our implementation of query and command responsibility segregation (CQRS).
|
||||
|
||||
| Package Name | NuGet | NuGet Install |
|
||||
|-----------------------------------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------:|
|
||||
| OpenHarbor.CQRS | [](https://www.nuget.org/packages/OpenHarbor.CQRS/) | ```dotnet add package OpenHarbor.CQRS ``` |
|
||||
| OpenHarbor.CQRS.AspNetCore | [](https://www.nuget.org/packages/OpenHarbor.CQRS.AspNetCore/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore ``` |
|
||||
| OpenHarbor.CQRS.FluentValidation | [](https://www.nuget.org/packages/OpenHarbor.CQRS.FluentValidation/) | ```dotnet add package OpenHarbor.CQRS.FluentValidation ``` |
|
||||
| OpenHarbor.CQRS.DynamicQuery | [](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery/) | ```dotnet add package OpenHarbor.CQRS.DynamicQuery ``` |
|
||||
| OpenHarbor.CQRS.DynamicQuery.AspNetCore | [](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery.AspNetCore/) | ```dotnet add package OpenHarbor.CQRS.DynamicQuery.AspNetCore ``` |
|
||||
| Svrnty.CQRS | [](https://www.nuget.org/packages/Svrnty.CQRS/) | ```dotnet add package Svrnty.CQRS ``` |
|
||||
| Svrnty.CQRS.MinimalApi | [](https://www.nuget.org/packages/Svrnty.CQRS.MinimalApi/) | ```dotnet add package Svrnty.CQRS.MinimalApi ``` |
|
||||
| Svrnty.CQRS.AspNetCore | [](https://www.nuget.org/packages/Svrnty.CQRS.AspNetCore/) | ```dotnet add package Svrnty.CQRS.AspNetCore ``` |
|
||||
| Svrnty.CQRS.FluentValidation | [](https://www.nuget.org/packages/Svrnty.CQRS.FluentValidation/) | ```dotnet add package Svrnty.CQRS.FluentValidation ``` |
|
||||
| Svrnty.CQRS.DynamicQuery | [](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery/) | ```dotnet add package Svrnty.CQRS.DynamicQuery ``` |
|
||||
| Svrnty.CQRS.DynamicQuery.AspNetCore | [](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery.AspNetCore/) | ```dotnet add package Svrnty.CQRS.DynamicQuery.AspNetCore ``` |
|
||||
| Svrnty.CQRS.Grpc | [](https://www.nuget.org/packages/Svrnty.CQRS.Grpc/) | ```dotnet add package Svrnty.CQRS.Grpc ``` |
|
||||
|
||||
> Abstractions Packages.
|
||||
|
||||
| Package Name | NuGet | NuGet Install |
|
||||
| ---------------------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------------------------------: |
|
||||
| OpenHarbor.CQRS.Abstractions | [](https://www.nuget.org/packages/OpenHarbor.CQRS.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.Abstractions ``` |
|
||||
| OpenHarbor.CQRS.AspNetCore.Abstractions | [](https://www.nuget.org/packages/OpenHarbor.CQRS.AspNetCore.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore.Abstractions ``` |
|
||||
| OpenHarbor.CQRS.DynamicQuery.Abstractions | [](https://www.nuget.org/packages/OpenHarbor.CQRS.DynamicQuery.Abstractions/) | ```dotnet add package OpenHarbor.CQRS.AspNetCore.Abstractions ``` |
|
||||
| Svrnty.CQRS.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.Abstractions/) | ```dotnet add package Svrnty.CQRS.Abstractions ``` |
|
||||
| Svrnty.CQRS.AspNetCore.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.AspNetCore.Abstractions/) | ```dotnet add package Svrnty.CQRS.AspNetCore.Abstractions ``` |
|
||||
| Svrnty.CQRS.DynamicQuery.Abstractions | [](https://www.nuget.org/packages/Svrnty.CQRS.DynamicQuery.Abstractions/) | ```dotnet add package Svrnty.CQRS.AspNetCore.Abstractions ``` |
|
||||
|
||||
|
||||
## Sample of startup code for aspnetcore MVC
|
||||
## Sample of startup code for Minimal API (Recommended)
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register CQRS core services
|
||||
builder.Services.AddSvrntyCQRS();
|
||||
builder.Services.AddDefaultCommandDiscovery();
|
||||
builder.Services.AddDefaultQueryDiscovery();
|
||||
|
||||
// Add your commands and queries
|
||||
AddQueries(builder.Services);
|
||||
AddCommands(builder.Services);
|
||||
|
||||
// Add Swagger (optional)
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
// Map CQRS endpoints - automatically creates routes for all commands and queries
|
||||
app.MapSvrntyCommands(); // Creates POST /api/command/{commandName} endpoints
|
||||
app.MapSvrntyQueries(); // Creates POST/GET /api/query/{queryName} endpoints
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
## Sample of startup code for ASP.NET Core MVC (Legacy)
|
||||
|
||||
```csharp
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// make sure to add your queries and commands before configuring MvCBuilder with .AddOpenHarborCommands and .AddOpenHarborQueries
|
||||
// make sure to add your queries and commands before configuring MvcBuilder with .AddSvrntyCommands and .AddSvrntyQueries
|
||||
AddQueries(services);
|
||||
AddCommands(services);
|
||||
|
||||
// adds the non related to aspnet core features.
|
||||
services.AddOpenHarborCQRS();
|
||||
services.AddSvrntyCQRS();
|
||||
|
||||
services
|
||||
.AddControllers()
|
||||
.AddOpenHarborQueries() // adds queries to aspnetcore mvc.(you can make it configurable to load balance only commands on a instance)
|
||||
.AddOpenHarborCommands() // adds commands to aspnetcore mvc. (you can make it configurable to load balance only commands on a instance)
|
||||
.AddSvrntyQueries() // adds queries to aspnetcore mvc.(you can make it configurable to load balance only commands on a instance)
|
||||
.AddSvrntyCommands() // adds commands to aspnetcore mvc. (you can make it configurable to load balance only commands on a instance)
|
||||
.AddFluentValidation();
|
||||
|
||||
services.AddSwaggerGen();
|
||||
}
|
||||
```
|
||||
|
||||
## Sample of startup code for gRPC
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register CQRS core services
|
||||
builder.Services.AddSvrntyCQRS();
|
||||
builder.Services.AddDefaultCommandDiscovery();
|
||||
builder.Services.AddDefaultQueryDiscovery();
|
||||
|
||||
// Add your commands and queries
|
||||
AddQueries(builder.Services);
|
||||
AddCommands(builder.Services);
|
||||
|
||||
// Add gRPC support
|
||||
builder.Services.AddSvrntyCqrsGrpc();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Map gRPC endpoints
|
||||
app.MapSvrntyGrpcCommands();
|
||||
app.MapSvrntyGrpcQueries();
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
### Important: protobuf-net Requirements for gRPC
|
||||
|
||||
To use gRPC, your commands and queries must be annotated with protobuf-net attributes:
|
||||
|
||||
```csharp
|
||||
using ProtoBuf;
|
||||
|
||||
[ProtoContract]
|
||||
public class CreatePersonCommand
|
||||
{
|
||||
[ProtoMember(1)]
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
|
||||
[ProtoMember(2)]
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
|
||||
[ProtoMember(3)]
|
||||
public int Age { get; set; }
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class Person
|
||||
{
|
||||
[ProtoMember(1)]
|
||||
public int Id { get; set; }
|
||||
|
||||
[ProtoMember(2)]
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Add `[ProtoContract]` to each command/query/result class
|
||||
- Add `[ProtoMember(n)]` to each property with sequential numbers starting from 1
|
||||
- These attributes don't interfere with JSON serialization or FluentValidation
|
||||
- You can use both HTTP REST (MinimalApi/MVC) and gRPC simultaneously
|
||||
|
||||
> Example how to add your queries and commands.
|
||||
|
||||
```csharp
|
||||
@ -68,7 +167,7 @@ private void AddQueries(IServiceCollection services)
|
||||
|
||||
We use fluent validation in all of our projects, but we don't want it to be enforced.
|
||||
|
||||
If you install ```OpenHarbor.CQRS.FluentValidation``` you can use this way of registrating your commands.
|
||||
If you install ```Svrnty.CQRS.FluentValidation``` you can use this way of registrating your commands.
|
||||
|
||||
```csharp
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
@ -80,19 +179,21 @@ public void ConfigureServices(IServiceCollection services)
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// with OpenHarbor.CQRS.FluentValidation package.
|
||||
// with Svrnty.CQRS.FluentValidation package.
|
||||
services.AddCommand<EchoCommand, string, EchoCommandHandler, EchoCommandValidator>();
|
||||
}
|
||||
```
|
||||
|
||||
# 2024 Roadmap
|
||||
# 2024-2025 Roadmap
|
||||
|
||||
| Task | Description | Status |
|
||||
|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------|
|
||||
| Support .NET 8 | Ensure compatibility with .NET 8. | ✅ |
|
||||
| Support .NET 10 | Upgrade to .NET 10 with C# 14 language support. | ✅ |
|
||||
| Update FluentValidation | Upgrade FluentValidation to version 11.x for .NET 10 compatibility. | ✅ |
|
||||
| Add gRPC Support with protobuf-net | Implement gRPC endpoints with binary protobuf serialization for high-performance scenarios. | ✅ |
|
||||
| Create a new demo project as an example | Develop a new demo project to serve as an example for users. | ⬜️ |
|
||||
| New Independent Module for MVC | Develop a standalone module, independent of MVC, to enhance framework flexibility. | ⬜️ |
|
||||
| Implement .NET Native Compilation (AOT) | Enable Ahead-of-Time (AOT) compilation support for .NET 8. | ⬜️ |
|
||||
| Update FluentValidation | Upgrade FluentValidation to the latest version, addressing potential breaking changes. | ⬜️ |
|
||||
| Implement .NET Native Compilation (AOT) | Enable full Ahead-of-Time (AOT) compilation support (blocked by third-party dependencies). | ⬜️ |
|
||||
| Create a website for the Framework | Develop a website to host comprehensive documentation for the framework. | ⬜️ |
|
||||
| Re-add support for GraphQL | Re-integrate support for GraphQL, exploring lightweight solutions. | ⬜️ |
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Attributes;
|
||||
namespace Svrnty.CQRS.Abstractions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class CommandNameAttribute : Attribute
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Attributes;
|
||||
namespace Svrnty.CQRS.Abstractions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class QueryNameAttribute : Attribute
|
||||
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using OpenHarbor.CQRS.Abstractions.Attributes;
|
||||
using Svrnty.CQRS.Abstractions.Attributes;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public sealed class CommandMeta : ICommandMeta
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public interface ICommandMeta
|
||||
{
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public interface IQueryDiscovery
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public interface IQueryMeta
|
||||
{
|
||||
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using OpenHarbor.CQRS.Abstractions.Attributes;
|
||||
using Svrnty.CQRS.Abstractions.Attributes;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public class QueryMeta : IQueryMeta
|
||||
{
|
||||
@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions;
|
||||
namespace Svrnty.CQRS.Abstractions;
|
||||
|
||||
public interface ICommandHandler<in TCommand>
|
||||
where TCommand : class
|
||||
@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions;
|
||||
namespace Svrnty.CQRS.Abstractions;
|
||||
|
||||
public interface IQueryHandler<in TQuery, TQueryResult>
|
||||
where TQuery : class
|
||||
@ -1,4 +1,4 @@
|
||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
||||
namespace Svrnty.CQRS.Abstractions.Security;
|
||||
|
||||
public enum AuthorizationResult
|
||||
{
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
||||
namespace Svrnty.CQRS.Abstractions.Security;
|
||||
|
||||
public interface ICommandAuthorizationService
|
||||
{
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions.Security;
|
||||
namespace Svrnty.CQRS.Abstractions.Security;
|
||||
|
||||
public interface IQueryAuthorizationService
|
||||
{
|
||||
@ -1,8 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
namespace OpenHarbor.CQRS.Abstractions;
|
||||
namespace Svrnty.CQRS.Abstractions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -25,6 +25,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0-rc.1.23419.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class CommandControllerIgnoreAttribute : Attribute
|
||||
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public class QueryControllerAuthorizationAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class QueryControllerIgnoreAttribute : Attribute
|
||||
@ -1,13 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public class DynamicQueryInterceptorProvider<TSource, TDestination> : IDynamicQueryInterceptorProvider<TSource, TDestination>
|
||||
{
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IAlterQueryableService<TSource, TDestination>
|
||||
{
|
||||
@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IDynamicQuery<TSource, TDestination> : IDynamicQuery
|
||||
where TSource : class
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IDynamicQueryInterceptorProvider<TSource, TDestination>
|
||||
{
|
||||
@ -1,4 +1,4 @@
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IDynamicQueryParams<out TParams>
|
||||
where TParams : class
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
public interface IQueryableProvider<TSource>
|
||||
{
|
||||
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
|
||||
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
|
||||
<TargetFrameworks>netstandard2.1;net10.0</TargetFrameworks>
|
||||
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">true</IsAotCompatible>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -1,10 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.DynamicQuery;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||
|
||||
public class DynamicQuery<TSource, TDestination> : DynamicQuery, IDynamicQuery<TSource, TDestination>
|
||||
where TSource : class
|
||||
@ -2,7 +2,7 @@
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
using System;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||
|
||||
public class DynamicQueryAggregate
|
||||
{
|
||||
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using PoweredSoft.DynamicQuery;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||
|
||||
public class DynamicQueryFilter
|
||||
{
|
||||
@ -1,11 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
using OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
[ApiController, Route("api/query/[controller]")]
|
||||
public class DynamicQueryController<TUnderlyingQuery, TSource, TDestination> : Controller
|
||||
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
public class DynamicQueryControllerConvention : IControllerModelConvention
|
||||
{
|
||||
@ -4,11 +4,11 @@ using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using OpenHarbor.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Discover;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
using Svrnty.CQRS.DynamicQuery.Discover;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider)
|
||||
: IApplicationFeatureProvider<ControllerFeature>
|
||||
@ -0,0 +1,5 @@
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
public class DynamicQueryControllerOptions
|
||||
{
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
using Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
||||
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
|
||||
|
||||
public static class MvcBuilderExtensions
|
||||
{
|
||||
@ -1,11 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -27,10 +28,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore\OpenHarbor.CQRS.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery\OpenHarbor.CQRS.DynamicQuery.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using Pluralize.NET;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery.Discover;
|
||||
namespace Svrnty.CQRS.DynamicQuery.Discover;
|
||||
|
||||
public class DynamicQueryMeta : QueryMeta
|
||||
{
|
||||
@ -1,4 +1,4 @@
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -6,11 +6,11 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery;
|
||||
namespace Svrnty.CQRS.DynamicQuery;
|
||||
|
||||
public class DynamicQueryHandler<TSource, TDestination>
|
||||
: DynamicQueryHandlerBase<TSource, TDestination>,
|
||||
OpenHarbor.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>>
|
||||
Svrnty.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>>
|
||||
where TSource : class
|
||||
where TDestination : class
|
||||
{
|
||||
@ -30,7 +30,7 @@ public class DynamicQueryHandler<TSource, TDestination>
|
||||
|
||||
public class DynamicQueryHandler<TSource, TDestination, TParams>
|
||||
: DynamicQueryHandlerBase<TSource, TDestination>,
|
||||
OpenHarbor.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>
|
||||
Svrnty.CQRS.Abstractions.IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>
|
||||
where TSource : class
|
||||
where TDestination : class
|
||||
where TParams : class
|
||||
@ -3,11 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.DynamicQuery;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery;
|
||||
namespace Svrnty.CQRS.DynamicQuery;
|
||||
|
||||
public abstract class DynamicQueryHandlerBase<TSource, TDestination>
|
||||
where TSource : class
|
||||
@ -1,13 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Discover;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||
using Svrnty.CQRS.DynamicQuery.Discover;
|
||||
using PoweredSoft.DynamicQuery.Core;
|
||||
|
||||
namespace OpenHarbor.CQRS.DynamicQuery;
|
||||
namespace Svrnty.CQRS.DynamicQuery;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -29,7 +29,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.DynamicQuery.Abstractions\OpenHarbor.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -1,9 +1,9 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
|
||||
namespace OpenHarbor.CQRS.FluentValidation;
|
||||
namespace Svrnty.CQRS.FluentValidation;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -24,10 +24,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="10.4.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS\OpenHarbor.CQRS.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Abstractions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class GrpcIgnoreAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
<DebugType>portable</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<IncludeSource>true</IncludeSource>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
852
Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs
Normal file
852
Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs
Normal file
@ -0,0 +1,852 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Svrnty.CQRS.Grpc.Generators.Helpers;
|
||||
using Svrnty.CQRS.Grpc.Generators.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Svrnty.CQRS.Grpc.Generators
|
||||
{
|
||||
[Generator]
|
||||
public class GrpcGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// Find all types that might be commands or queries
|
||||
var typeDeclarations = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: static (node, _) => node is TypeDeclarationSyntax,
|
||||
transform: static (ctx, _) => GetTypeSymbol(ctx))
|
||||
.Where(static symbol => symbol is not null);
|
||||
|
||||
// Combine with compilation
|
||||
var compilationAndTypes = context.CompilationProvider.Combine(typeDeclarations.Collect());
|
||||
|
||||
// Register source output
|
||||
context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right!, spc));
|
||||
}
|
||||
|
||||
private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context)
|
||||
{
|
||||
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
|
||||
var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration);
|
||||
return symbol as INamedTypeSymbol;
|
||||
}
|
||||
|
||||
private static void Execute(Compilation compilation, IEnumerable<INamedTypeSymbol?> types, SourceProductionContext context)
|
||||
{
|
||||
var grpcIgnoreAttribute = compilation.GetTypeByMetadataName("Svrnty.CQRS.Grpc.Abstractions.Attributes.GrpcIgnoreAttribute");
|
||||
var commandHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`1");
|
||||
var commandHandlerWithResultInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`2");
|
||||
var queryHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.IQueryHandler`2");
|
||||
|
||||
if (commandHandlerInterface == null || queryHandlerInterface == null)
|
||||
{
|
||||
return; // Handler interfaces not found
|
||||
}
|
||||
|
||||
var commandMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol?>(SymbolEqualityComparer.Default); // Command -> Result type (null if no result)
|
||||
var queryMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol>(SymbolEqualityComparer.Default); // Query -> Result type
|
||||
|
||||
// Find all command and query types by looking at handler implementations
|
||||
foreach (var typeSymbol in types)
|
||||
{
|
||||
if (typeSymbol == null || typeSymbol.IsAbstract || typeSymbol.IsStatic)
|
||||
continue;
|
||||
|
||||
// Check if this type implements ICommandHandler<T> or ICommandHandler<T, TResult>
|
||||
foreach (var iface in typeSymbol.AllInterfaces)
|
||||
{
|
||||
if (iface.IsGenericType)
|
||||
{
|
||||
// Check for ICommandHandler<TCommand>
|
||||
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerInterface) && iface.TypeArguments.Length == 1)
|
||||
{
|
||||
var commandType = iface.TypeArguments[0] as INamedTypeSymbol;
|
||||
if (commandType != null && !commandMap.ContainsKey(commandType))
|
||||
commandMap[commandType] = null; // No result type
|
||||
}
|
||||
// Check for ICommandHandler<TCommand, TResult>
|
||||
else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerWithResultInterface) && iface.TypeArguments.Length == 2)
|
||||
{
|
||||
var commandType = iface.TypeArguments[0] as INamedTypeSymbol;
|
||||
var resultType = iface.TypeArguments[1] as INamedTypeSymbol;
|
||||
if (commandType != null && resultType != null)
|
||||
commandMap[commandType] = resultType;
|
||||
}
|
||||
// Check for IQueryHandler<TQuery, TResult>
|
||||
else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryHandlerInterface) && iface.TypeArguments.Length == 2)
|
||||
{
|
||||
var queryType = iface.TypeArguments[0] as INamedTypeSymbol;
|
||||
var resultType = iface.TypeArguments[1] as INamedTypeSymbol;
|
||||
if (queryType != null && resultType != null)
|
||||
queryMap[queryType] = resultType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var commands = new List<CommandInfo>();
|
||||
var queries = new List<QueryInfo>();
|
||||
|
||||
// Process discovered command types
|
||||
foreach (var kvp in commandMap)
|
||||
{
|
||||
var commandType = kvp.Key;
|
||||
var resultType = kvp.Value;
|
||||
|
||||
// Skip if marked with [GrpcIgnore]
|
||||
if (grpcIgnoreAttribute != null && HasAttribute(commandType, grpcIgnoreAttribute))
|
||||
continue;
|
||||
|
||||
var commandInfo = ExtractCommandInfo(commandType, resultType);
|
||||
if (commandInfo != null)
|
||||
commands.Add(commandInfo);
|
||||
}
|
||||
|
||||
// Process discovered query types
|
||||
foreach (var kvp in queryMap)
|
||||
{
|
||||
var queryType = kvp.Key;
|
||||
var resultType = kvp.Value;
|
||||
|
||||
// Skip if marked with [GrpcIgnore]
|
||||
if (grpcIgnoreAttribute != null && HasAttribute(queryType, grpcIgnoreAttribute))
|
||||
continue;
|
||||
|
||||
var queryInfo = ExtractQueryInfo(queryType, resultType);
|
||||
if (queryInfo != null)
|
||||
queries.Add(queryInfo);
|
||||
}
|
||||
|
||||
// Generate services if we found any commands or queries
|
||||
if (commands.Any() || queries.Any())
|
||||
{
|
||||
GenerateProtoAndServices(context, commands, queries, compilation);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
|
||||
{
|
||||
return typeSymbol.GetAttributes().Any(attr =>
|
||||
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol));
|
||||
}
|
||||
|
||||
private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? interfaceSymbol)
|
||||
{
|
||||
if (interfaceSymbol == null)
|
||||
return false;
|
||||
|
||||
return typeSymbol.AllInterfaces.Any(i =>
|
||||
SymbolEqualityComparer.Default.Equals(i, interfaceSymbol));
|
||||
}
|
||||
|
||||
private static bool ImplementsGenericInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? genericInterfaceSymbol)
|
||||
{
|
||||
if (genericInterfaceSymbol == null)
|
||||
return false;
|
||||
|
||||
return typeSymbol.AllInterfaces.Any(i =>
|
||||
i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, genericInterfaceSymbol));
|
||||
}
|
||||
|
||||
private static CommandInfo? ExtractCommandInfo(INamedTypeSymbol commandType, INamedTypeSymbol? resultType)
|
||||
{
|
||||
var commandInfo = new CommandInfo
|
||||
{
|
||||
Name = commandType.Name,
|
||||
FullyQualifiedName = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
Namespace = commandType.ContainingNamespace.ToDisplayString(),
|
||||
Properties = new List<PropertyInfo>()
|
||||
};
|
||||
|
||||
// Set result type if provided
|
||||
if (resultType != null)
|
||||
{
|
||||
commandInfo.ResultType = resultType.Name;
|
||||
commandInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandType.Name}, {resultType.Name}>";
|
||||
}
|
||||
else
|
||||
{
|
||||
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandType.Name}>";
|
||||
}
|
||||
|
||||
// Extract properties
|
||||
var properties = commandType.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
|
||||
.ToList();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
|
||||
|
||||
commandInfo.Properties.Add(new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
Type = propertyType,
|
||||
ProtoType = protoType,
|
||||
FieldNumber = fieldNumber++
|
||||
});
|
||||
}
|
||||
|
||||
return commandInfo;
|
||||
}
|
||||
|
||||
private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType)
|
||||
{
|
||||
var queryInfo = new QueryInfo
|
||||
{
|
||||
Name = queryType.Name,
|
||||
FullyQualifiedName = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
Namespace = queryType.ContainingNamespace.ToDisplayString(),
|
||||
Properties = new List<PropertyInfo>()
|
||||
};
|
||||
|
||||
// Set result type
|
||||
queryInfo.ResultType = resultType.Name;
|
||||
queryInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryType.Name}, {resultType.Name}>";
|
||||
|
||||
// Extract properties
|
||||
var properties = queryType.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
|
||||
.ToList();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
|
||||
|
||||
queryInfo.Properties.Add(new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
Type = propertyType,
|
||||
ProtoType = protoType,
|
||||
FieldNumber = fieldNumber++
|
||||
});
|
||||
}
|
||||
|
||||
return queryInfo;
|
||||
}
|
||||
|
||||
private static void GenerateProtoAndServices(SourceProductionContext context, List<CommandInfo> commands, List<QueryInfo> queries, Compilation compilation)
|
||||
{
|
||||
// Get root namespace from compilation
|
||||
var rootNamespace = compilation.AssemblyName ?? "Application";
|
||||
|
||||
// Generate service implementations for commands
|
||||
if (commands.Any())
|
||||
{
|
||||
var commandService = GenerateCommandServiceImpl(commands, rootNamespace);
|
||||
context.AddSource("CommandServiceImpl.g.cs", commandService);
|
||||
}
|
||||
|
||||
// Generate service implementations for queries
|
||||
if (queries.Any())
|
||||
{
|
||||
var queryService = GenerateQueryServiceImpl(queries, rootNamespace);
|
||||
context.AddSource("QueryServiceImpl.g.cs", queryService);
|
||||
}
|
||||
|
||||
// Generate registration extensions
|
||||
var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), rootNamespace);
|
||||
context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions);
|
||||
}
|
||||
|
||||
private static string GenerateCommandMessages(List<CommandInfo> commands, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using System.Runtime.Serialization;");
|
||||
sb.AppendLine("using ProtoBuf;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages");
|
||||
sb.AppendLine("{");
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
// Generate command DTO
|
||||
sb.AppendLine(" [ProtoContract]");
|
||||
sb.AppendLine(" [DataContract]");
|
||||
sb.AppendLine($" public sealed class {command.Name}Dto");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" [ProtoMember({prop.FieldNumber})]");
|
||||
sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]");
|
||||
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate result DTO if command has a result
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine(" [ProtoContract]");
|
||||
sb.AppendLine(" [DataContract]");
|
||||
sb.AppendLine($" public sealed class {command.Name}ResultDto");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" [ProtoMember(1)]");
|
||||
sb.AppendLine(" [DataMember(Order = 1)]");
|
||||
sb.AppendLine($" public {command.ResultFullyQualifiedName} Result {{ get; set; }}");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateQueryMessages(List<QueryInfo> queries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using System.Runtime.Serialization;");
|
||||
sb.AppendLine("using ProtoBuf;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages");
|
||||
sb.AppendLine("{");
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
// Generate query DTO
|
||||
sb.AppendLine(" [ProtoContract]");
|
||||
sb.AppendLine(" [DataContract]");
|
||||
sb.AppendLine($" public sealed class {query.Name}Dto");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" [ProtoMember({prop.FieldNumber})]");
|
||||
sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]");
|
||||
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate result DTO
|
||||
sb.AppendLine(" [ProtoContract]");
|
||||
sb.AppendLine(" [DataContract]");
|
||||
sb.AppendLine($" public sealed class {query.Name}ResultDto");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" [ProtoMember(1)]");
|
||||
sb.AppendLine(" [DataMember(Order = 1)]");
|
||||
sb.AppendLine($" public {query.ResultFullyQualifiedName} Result {{ get; set; }}");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateCommandService(List<CommandInfo> commands, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using System.ServiceModel;");
|
||||
sb.AppendLine("using System.Threading;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc.Messages;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
sb.AppendLine("using ProtoBuf.Grpc;");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate service interface
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" [ServiceContract]");
|
||||
sb.AppendLine(" public interface ICommandService");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine($" [OperationContract]");
|
||||
sb.AppendLine($" Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" [OperationContract]");
|
||||
sb.AppendLine($" Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate service implementation
|
||||
sb.AppendLine(" public sealed class CommandService : ICommandService");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public CommandService(IServiceProvider serviceProvider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine($" public async Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
|
||||
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {command.Name}ResultDto {{ Result = result }};");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" public async Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
|
||||
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateQueryService(List<QueryInfo> queries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using System.ServiceModel;");
|
||||
sb.AppendLine("using System.Threading;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc.Messages;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
sb.AppendLine("using ProtoBuf.Grpc;");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate service interface
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" [ServiceContract]");
|
||||
sb.AppendLine(" public interface IQueryService");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
sb.AppendLine($" [OperationContract]");
|
||||
sb.AppendLine($" Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default);");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate service implementation
|
||||
sb.AppendLine(" public sealed class QueryService : IQueryService");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public QueryService(IServiceProvider serviceProvider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
sb.AppendLine($" public async Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();");
|
||||
sb.AppendLine($" var query = new {query.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {query.Name}ResultDto {{ Result = result }};");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("using Microsoft.AspNetCore.Builder;");
|
||||
sb.AppendLine("using Microsoft.AspNetCore.Routing;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc.Services;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Extensions");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static class GrpcServiceRegistrationExtensions");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
if (hasCommands)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Registers the auto-generated Command gRPC service");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" services.AddGrpc();");
|
||||
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
|
||||
sb.AppendLine(" return services;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
|
||||
sb.AppendLine(" return endpoints;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (hasQueries)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Registers the auto-generated Query gRPC service");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" services.AddGrpc();");
|
||||
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
|
||||
sb.AppendLine(" return services;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
|
||||
sb.AppendLine(" return endpoints;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (hasCommands && hasQueries)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Registers both Command and Query gRPC services");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IServiceCollection AddGrpcCommandsAndQueries(this IServiceCollection services)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" services.AddGrpc();");
|
||||
sb.AppendLine(" services.AddGrpcReflection();");
|
||||
if (hasCommands)
|
||||
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
|
||||
if (hasQueries)
|
||||
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
|
||||
sb.AppendLine(" return services;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Maps both Command and Query gRPC service endpoints");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)");
|
||||
sb.AppendLine(" {");
|
||||
if (hasCommands)
|
||||
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
|
||||
if (hasQueries)
|
||||
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
|
||||
sb.AppendLine(" return endpoints;");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ToCamelCase(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str) || char.IsLower(str[0]))
|
||||
return str;
|
||||
|
||||
return char.ToLowerInvariant(str[0]) + str.Substring(1);
|
||||
}
|
||||
|
||||
// New methods for standard gRPC generation
|
||||
|
||||
private static string GenerateCommandsProto(List<CommandInfo> commands, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("syntax = \"proto3\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("package cqrs;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("// Command service for CQRS operations");
|
||||
sb.AppendLine("service CommandService {");
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
var methodName = command.Name.Replace("Command", "");
|
||||
sb.AppendLine($" // {command.Name}");
|
||||
sb.AppendLine($" rpc {methodName} ({command.Name}Request) returns ({command.Name}Response);");
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate message types
|
||||
foreach (var command in commands)
|
||||
{
|
||||
// Request message
|
||||
sb.AppendLine($"message {command.Name}Request {{");
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Response message
|
||||
sb.AppendLine($"message {command.Name}Response {{");
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(command.ResultFullyQualifiedName!, out _, out _)} result = 1;");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateQueriesProto(List<QueryInfo> queries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("syntax = \"proto3\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("package cqrs;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("// Query service for CQRS operations");
|
||||
sb.AppendLine("service QueryService {");
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
var methodName = query.Name.Replace("Query", "");
|
||||
sb.AppendLine($" // {query.Name}");
|
||||
sb.AppendLine($" rpc {methodName} ({query.Name}Request) returns ({query.Name}Response);");
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Generate message types
|
||||
foreach (var query in queries)
|
||||
{
|
||||
// Request message
|
||||
sb.AppendLine($"message {query.Name}Request {{");
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Response message
|
||||
sb.AppendLine($"message {query.Name}Response {{");
|
||||
sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(query.ResultFullyQualifiedName, out _, out _)} result = 1;");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateCommandServiceImpl(List<CommandInfo> commands, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using Grpc.Core;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using System.Linq;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine("using FluentValidation;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public sealed class CommandServiceImpl : CommandService.CommandServiceBase");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public CommandServiceImpl(IServiceProvider serviceProvider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
var methodName = command.Name.Replace("Command", "");
|
||||
var requestType = $"{command.Name}Request";
|
||||
var responseType = $"{command.Name}Response";
|
||||
|
||||
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
|
||||
sb.AppendLine($" {requestType} request,");
|
||||
sb.AppendLine(" ServerCallContext context)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in command.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" // Validate command if validator is registered");
|
||||
sb.AppendLine($" var validator = _serviceProvider.GetService<IValidator<{command.FullyQualifiedName}>>();");
|
||||
sb.AppendLine(" if (validator != null)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" var validationResult = await validator.ValidateAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine(" if (!validationResult.IsValid)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" var errors = string.Join(\", \", validationResult.Errors.Select(e => e.ErrorMessage));");
|
||||
sb.AppendLine(" throw new RpcException(new Status(StatusCode.InvalidArgument, $\"Validation failed: {errors}\"));");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
|
||||
|
||||
if (command.HasResult)
|
||||
{
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {responseType} {{ Result = result }};");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {responseType}();");
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateQueryServiceImpl(List<QueryInfo> queries, string rootNamespace)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using Grpc.Core;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" public sealed class QueryServiceImpl : QueryService.QueryServiceBase");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public QueryServiceImpl(IServiceProvider serviceProvider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
var methodName = query.Name.Replace("Query", "");
|
||||
var requestType = $"{query.Name}Request";
|
||||
var responseType = $"{query.Name}Response";
|
||||
|
||||
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
|
||||
sb.AppendLine($" {requestType} request,");
|
||||
sb.AppendLine(" ServerCallContext context)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();");
|
||||
sb.AppendLine($" var query = new {query.FullyQualifiedName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
|
||||
sb.AppendLine($" return new {responseType} {{ Result = result }};");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IsPackable>true</IsPackable>
|
||||
<DevelopmentDependency>true</DevelopmentDependency>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>Source Generator for Svrnty.CQRS.Grpc - generates .proto files and gRPC service implementations from commands and queries</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
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;
|
||||
}
|
||||
}
|
||||
29
Svrnty.CQRS.Grpc.Sample/Program.cs
Normal file
29
Svrnty.CQRS.Grpc.Sample/Program.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.FluentValidation;
|
||||
using Svrnty.CQRS.Grpc.Sample;
|
||||
using Svrnty.CQRS.Grpc.Sample.Grpc.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register command handlers with CQRS and FluentValidation
|
||||
builder.Services.AddCommand<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
|
||||
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
|
||||
|
||||
// Register query handlers with CQRS
|
||||
builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
|
||||
|
||||
// Auto-generated: Register gRPC services for both commands and queries (includes reflection)
|
||||
builder.Services.AddGrpcCommandsAndQueries();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Auto-generated: Map gRPC endpoints for both commands and queries
|
||||
app.MapGrpcCommandsAndQueries();
|
||||
|
||||
// Map gRPC reflection service
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
Console.WriteLine("Auto-Generated gRPC Server with Reflection and Validation");
|
||||
Console.WriteLine("http://localhost:5000");
|
||||
|
||||
app.Run();
|
||||
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;
|
||||
}
|
||||
}
|
||||
32
Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj
Normal file
32
Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj
Normal file
@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos\*.proto" GrpcServices="Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.70.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.70.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc\Svrnty.CQRS.Grpc.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.FluentValidation\Svrnty.CQRS.FluentValidation.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
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.70.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.70.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Abstractions\Svrnty.CQRS.Grpc.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
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>
|
||||
254
Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs
Normal file
254
Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs
Normal file
@ -0,0 +1,254 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.Abstractions.Security;
|
||||
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
|
||||
|
||||
namespace Svrnty.CQRS.MinimalApi;
|
||||
|
||||
public static class EndpointRouteBuilderExtensions
|
||||
{
|
||||
public static IEndpointRouteBuilder MapOpenHarborQueries(this IEndpointRouteBuilder endpoints, string routePrefix = "api/query")
|
||||
{
|
||||
var queryDiscovery = endpoints.ServiceProvider.GetRequiredService<IQueryDiscovery>();
|
||||
var authorizationService = endpoints.ServiceProvider.GetService<IQueryAuthorizationService>();
|
||||
|
||||
foreach (var queryMeta in queryDiscovery.GetQueries())
|
||||
{
|
||||
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
|
||||
if (ignoreAttribute != null)
|
||||
continue;
|
||||
|
||||
var route = $"{routePrefix}/{queryMeta.LowerCamelCaseName}";
|
||||
|
||||
MapQueryPost(endpoints, route, queryMeta, authorizationService);
|
||||
MapQueryGet(endpoints, route, queryMeta, authorizationService);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static void MapQueryPost(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
IQueryMeta queryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
{
|
||||
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
|
||||
|
||||
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryMeta.QueryType, cancellationToken);
|
||||
if (authorizationResult == AuthorizationResult.Forbidden)
|
||||
return Results.StatusCode(403);
|
||||
if (authorizationResult == AuthorizationResult.Unauthorized)
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var query = await context.Request.ReadFromJsonAsync(queryMeta.QueryType, cancellationToken);
|
||||
if (query == null)
|
||||
return Results.BadRequest("Invalid query payload");
|
||||
|
||||
var handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync");
|
||||
if (handleMethod == null)
|
||||
return Results.Problem("Handler method not found");
|
||||
|
||||
var task = (Task)handleMethod.Invoke(handler, [query, cancellationToken])!;
|
||||
await task;
|
||||
|
||||
var resultProperty = task.GetType().GetProperty("Result");
|
||||
var result = resultProperty?.GetValue(task);
|
||||
|
||||
return Results.Ok(result);
|
||||
})
|
||||
.WithName($"Query_{queryMeta.LowerCamelCaseName}_Post")
|
||||
.WithTags("Queries")
|
||||
.Produces(200, queryMeta.QueryResultType)
|
||||
.Produces(400)
|
||||
.Produces(401)
|
||||
.Produces(403);
|
||||
}
|
||||
|
||||
private static void MapQueryGet(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
IQueryMeta queryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
{
|
||||
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
|
||||
|
||||
endpoints.MapGet(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryMeta.QueryType, cancellationToken);
|
||||
if (authorizationResult == AuthorizationResult.Forbidden)
|
||||
return Results.StatusCode(403);
|
||||
if (authorizationResult == AuthorizationResult.Unauthorized)
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var query = Activator.CreateInstance(queryMeta.QueryType);
|
||||
if (query == null)
|
||||
return Results.BadRequest("Could not create query instance");
|
||||
|
||||
foreach (var property in queryMeta.QueryType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (!property.CanWrite)
|
||||
continue;
|
||||
|
||||
var queryStringValue = context.Request.Query[property.Name].FirstOrDefault();
|
||||
if (queryStringValue != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var convertedValue = Convert.ChangeType(queryStringValue, property.PropertyType);
|
||||
property.SetValue(query, convertedValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync");
|
||||
if (handleMethod == null)
|
||||
return Results.Problem("Handler method not found");
|
||||
|
||||
var task = (Task)handleMethod.Invoke(handler, [query, cancellationToken])!;
|
||||
await task;
|
||||
|
||||
var resultProperty = task.GetType().GetProperty("Result");
|
||||
var result = resultProperty?.GetValue(task);
|
||||
|
||||
return Results.Ok(result);
|
||||
})
|
||||
.WithName($"Query_{queryMeta.LowerCamelCaseName}_Get")
|
||||
.WithTags("Queries")
|
||||
.Produces(200, queryMeta.QueryResultType)
|
||||
.Produces(400)
|
||||
.Produces(401)
|
||||
.Produces(403);
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapOpenHarborCommands(this IEndpointRouteBuilder endpoints, string routePrefix = "api/command")
|
||||
{
|
||||
var commandDiscovery = endpoints.ServiceProvider.GetRequiredService<ICommandDiscovery>();
|
||||
var authorizationService = endpoints.ServiceProvider.GetService<ICommandAuthorizationService>();
|
||||
|
||||
foreach (var commandMeta in commandDiscovery.GetCommands())
|
||||
{
|
||||
var ignoreAttribute = commandMeta.CommandType.GetCustomAttribute<CommandControllerIgnoreAttribute>();
|
||||
if (ignoreAttribute != null)
|
||||
continue;
|
||||
|
||||
var route = $"{routePrefix}/{commandMeta.LowerCamelCaseName}";
|
||||
|
||||
if (commandMeta.CommandResultType == null)
|
||||
{
|
||||
MapCommandWithoutResult(endpoints, route, commandMeta, authorizationService);
|
||||
}
|
||||
else
|
||||
{
|
||||
MapCommandWithResult(endpoints, route, commandMeta, authorizationService);
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static void MapCommandWithoutResult(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
ICommandMeta commandMeta,
|
||||
ICommandAuthorizationService? authorizationService)
|
||||
{
|
||||
var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandMeta.CommandType);
|
||||
|
||||
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(commandMeta.CommandType, cancellationToken);
|
||||
if (authorizationResult == AuthorizationResult.Forbidden)
|
||||
return Results.StatusCode(403);
|
||||
if (authorizationResult == AuthorizationResult.Unauthorized)
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var command = await context.Request.ReadFromJsonAsync(commandMeta.CommandType, cancellationToken);
|
||||
if (command == null)
|
||||
return Results.BadRequest("Invalid command payload");
|
||||
|
||||
var handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync");
|
||||
if (handleMethod == null)
|
||||
return Results.Problem("Handler method not found");
|
||||
|
||||
await (Task)handleMethod.Invoke(handler, [command, cancellationToken])!;
|
||||
return Results.Ok();
|
||||
})
|
||||
.WithName($"Command_{commandMeta.LowerCamelCaseName}")
|
||||
.WithTags("Commands")
|
||||
.Produces(200)
|
||||
.Produces(400)
|
||||
.Produces(401)
|
||||
.Produces(403);
|
||||
}
|
||||
|
||||
private static void MapCommandWithResult(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
ICommandMeta commandMeta,
|
||||
ICommandAuthorizationService? authorizationService)
|
||||
{
|
||||
var handlerType = typeof(ICommandHandler<,>).MakeGenericType(commandMeta.CommandType, commandMeta.CommandResultType!);
|
||||
|
||||
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(commandMeta.CommandType, cancellationToken);
|
||||
if (authorizationResult == AuthorizationResult.Forbidden)
|
||||
return Results.StatusCode(403);
|
||||
if (authorizationResult == AuthorizationResult.Unauthorized)
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var command = await context.Request.ReadFromJsonAsync(commandMeta.CommandType, cancellationToken);
|
||||
if (command == null)
|
||||
return Results.BadRequest("Invalid command payload");
|
||||
|
||||
var handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync");
|
||||
if (handleMethod == null)
|
||||
return Results.Problem("Handler method not found");
|
||||
|
||||
var task = (Task)handleMethod.Invoke(handler, [command, cancellationToken])!;
|
||||
await task;
|
||||
|
||||
var resultProperty = task.GetType().GetProperty("Result");
|
||||
var result = resultProperty?.GetValue(task);
|
||||
|
||||
return Results.Ok(result);
|
||||
})
|
||||
.WithName($"Command_{commandMeta.LowerCamelCaseName}")
|
||||
.WithTags("Commands")
|
||||
.Produces(200, commandMeta.CommandResultType)
|
||||
.Produces(400)
|
||||
.Produces(401)
|
||||
.Produces(403);
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<Company>Open Harbor</Company>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -29,12 +30,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.AspNetCore.Abstractions\OpenHarbor.CQRS.AspNetCore.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
197
Svrnty.CQRS.sln
Normal file
197
Svrnty.CQRS.sln
Normal file
@ -0,0 +1,197 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30907.101
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.Abstractions", "Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj", "{ED78E19D-31D4-4783-AE9E-2844A8541277}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS", "Svrnty.CQRS\Svrnty.CQRS.csproj", "{7069B98F-8736-4114-8AF5-1ACE094E6238}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.AspNetCore.Abstractions", "Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj", "{4C466827-31D3-4081-A751-C2FC7C381D7E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{617BA357-1A1F-40C5-B19A-A65A960E6142}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
LICENSE = LICENSE
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.DynamicQuery", "Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj", "{A38CE930-191F-417C-B5BE-8CC62DB47513}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.DynamicQuery.AspNetCore", "Svrnty.CQRS.DynamicQuery.AspNetCore\Svrnty.CQRS.DynamicQuery.AspNetCore.csproj", "{0829B99A-0A20-4CAC-A91E-FB67E18444DE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.FluentValidation", "Svrnty.CQRS.FluentValidation\Svrnty.CQRS.FluentValidation.csproj", "{70BD37C4-7497-474D-9A40-A701203971D8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.DynamicQuery.Abstractions", "Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj", "{8B9F8ACE-10EA-4215-9776-DE29EC93B020}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.MinimalApi", "Svrnty.CQRS.MinimalApi\Svrnty.CQRS.MinimalApi.csproj", "{E0756B59-F436-441A-8A98-37B6712D4B96}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc", "Svrnty.CQRS.Grpc\Svrnty.CQRS.Grpc.csproj", "{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Abstractions", "Svrnty.CQRS.Grpc.Abstractions\Svrnty.CQRS.Grpc.Abstractions.csproj", "{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Generators", "Svrnty.CQRS.Grpc.Generators\Svrnty.CQRS.Grpc.Generators.csproj", "{11537382-592C-4FE5-9103-272F358F0CAA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Grpc.Sample", "Svrnty.CQRS.Grpc.Sample\Svrnty.CQRS.Grpc.Sample.csproj", "{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x64.Build.0 = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{ED78E19D-31D4-4783-AE9E-2844A8541277}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{70BD37C4-7497-474D-9A40-A701203971D8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8B9F8ACE-10EA-4215-9776-DE29EC93B020}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E0756B59-F436-441A-8A98-37B6712D4B96}.Release|x86.Build.0 = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{144EE6AD-BA53-4DF4-A6D7-CE2F62CC60D8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AC7ED941-9E1D-4FC2-B716-50FC0FD02B98}.Release|x86.Build.0 = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{11537382-592C-4FE5-9103-272F358F0CAA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B7CEE5D0-A7CA-4C2C-AE42-923A8CF9B854}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D6D431EA-C04F-462B-8033-60F510FEB49E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
namespace OpenHarbor.CQRS.Discovery;
|
||||
namespace Svrnty.CQRS.Discovery;
|
||||
|
||||
public sealed class CommandDiscovery : ICommandDiscovery
|
||||
{
|
||||
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
namespace OpenHarbor.CQRS.Discovery;
|
||||
namespace Svrnty.CQRS.Discovery;
|
||||
|
||||
public sealed class QueryDiscovery : IQueryDiscovery
|
||||
{
|
||||
@ -1,9 +1,9 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using OpenHarbor.CQRS.Discovery;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
using Svrnty.CQRS.Discovery;
|
||||
|
||||
namespace OpenHarbor.CQRS;
|
||||
namespace Svrnty.CQRS;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Open Harbor</Company>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/Open-Harbor/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
@ -25,6 +25,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenHarbor.CQRS.Abstractions\OpenHarbor.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
36
TestClient.csx
Normal file
36
TestClient.csx
Normal file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env dotnet-script
|
||||
#r "nuget: Grpc.Net.Client, 2.70.0"
|
||||
#r "nuget: Google.Protobuf, 3.28.3"
|
||||
#r "nuget: Grpc.Tools, 2.70.0"
|
||||
|
||||
using Grpc.Net.Client;
|
||||
using Grpc.Core;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// We'll use reflection/dynamic to call the gRPC service
|
||||
// This is a simple HTTP/2 test
|
||||
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
};
|
||||
|
||||
using var channel = GrpcChannel.ForAddress("http://localhost:5000", new GrpcChannelOptions
|
||||
{
|
||||
HttpHandler = handler
|
||||
});
|
||||
|
||||
Console.WriteLine("Connected to gRPC server at http://localhost:5000");
|
||||
Console.WriteLine("Channel state: " + channel.State);
|
||||
|
||||
// Test basic connectivity
|
||||
try
|
||||
{
|
||||
await channel.ConnectAsync();
|
||||
Console.WriteLine("Successfully connected!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Connection failed: {ex.Message}");
|
||||
}
|
||||
100
TestGrpcClient/Program.cs
Normal file
100
TestGrpcClient/Program.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Svrnty.CQRS.Grpc.Sample.Grpc;
|
||||
|
||||
Console.WriteLine("=== gRPC Client Validation Test ===");
|
||||
Console.WriteLine();
|
||||
|
||||
// Create a gRPC channel
|
||||
using var channel = GrpcChannel.ForAddress("http://localhost:5000");
|
||||
|
||||
// Create the gRPC client
|
||||
var client = new CommandService.CommandServiceClient(channel);
|
||||
|
||||
// Test 1: Valid request
|
||||
Console.WriteLine("Test 1: Valid AddUser request...");
|
||||
var validRequest = new AddUserCommandRequest
|
||||
{
|
||||
Name = "John Doe",
|
||||
Email = "john.doe@example.com",
|
||||
Age = 30
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.AddUserAsync(validRequest);
|
||||
Console.WriteLine($"✓ Success! User added with ID: {response.Result}");
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Console.WriteLine($"✗ Unexpected error: {ex.Status.Detail}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Test 2: Invalid email (empty)
|
||||
Console.WriteLine("Test 2: Invalid email (empty)...");
|
||||
var invalidEmailRequest = new AddUserCommandRequest
|
||||
{
|
||||
Name = "Jane Doe",
|
||||
Email = "",
|
||||
Age = 25
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.AddUserAsync(invalidEmailRequest);
|
||||
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
|
||||
Console.WriteLine($" Message: {ex.Status.Detail}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Test 3: Invalid email format
|
||||
Console.WriteLine("Test 3: Invalid email format...");
|
||||
var badEmailRequest = new AddUserCommandRequest
|
||||
{
|
||||
Name = "Bob Smith",
|
||||
Email = "not-an-email",
|
||||
Age = 40
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.AddUserAsync(badEmailRequest);
|
||||
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
|
||||
Console.WriteLine($" Message: {ex.Status.Detail}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Test 4: Invalid age (0)
|
||||
Console.WriteLine("Test 4: Invalid age (0)...");
|
||||
var invalidAgeRequest = new AddUserCommandRequest
|
||||
{
|
||||
Name = "Alice Brown",
|
||||
Email = "alice@example.com",
|
||||
Age = 0
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.AddUserAsync(invalidAgeRequest);
|
||||
Console.WriteLine($"✗ Unexpected success! Validation should have failed.");
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Console.WriteLine($"✓ Validation caught! Status: {ex.StatusCode}");
|
||||
Console.WriteLine($" Message: {ex.Status.Detail}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("All tests completed!");
|
||||
58
TestGrpcClient/Protos/cqrs_services.proto
Normal file
58
TestGrpcClient/Protos/cqrs_services.proto
Normal file
@ -0,0 +1,58 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Svrnty.CQRS.Grpc.Sample.Grpc";
|
||||
|
||||
package cqrs;
|
||||
|
||||
// Command service for CQRS operations
|
||||
service CommandService {
|
||||
// Adds a new user and returns the user ID
|
||||
rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse);
|
||||
|
||||
// Removes a user
|
||||
rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse);
|
||||
}
|
||||
|
||||
// Query service for CQRS operations
|
||||
service QueryService {
|
||||
// Fetches a user by ID
|
||||
rpc FetchUser (FetchUserQueryRequest) returns (FetchUserQueryResponse);
|
||||
}
|
||||
|
||||
// Request message for adding a user
|
||||
message AddUserCommandRequest {
|
||||
string name = 1;
|
||||
string email = 2;
|
||||
int32 age = 3;
|
||||
}
|
||||
|
||||
// Response message containing the added user ID
|
||||
message AddUserCommandResponse {
|
||||
int32 result = 1;
|
||||
}
|
||||
|
||||
// Request message for removing a user
|
||||
message RemoveUserCommandRequest {
|
||||
int32 user_id = 1;
|
||||
}
|
||||
|
||||
// Response message for remove user (empty)
|
||||
message RemoveUserCommandResponse {
|
||||
}
|
||||
|
||||
// Request message for fetching a user
|
||||
message FetchUserQueryRequest {
|
||||
int32 user_id = 1;
|
||||
}
|
||||
|
||||
// Response message containing the user
|
||||
message FetchUserQueryResponse {
|
||||
User result = 1;
|
||||
}
|
||||
|
||||
// User entity
|
||||
message User {
|
||||
int32 id = 1;
|
||||
string name = 2;
|
||||
string email = 3;
|
||||
}
|
||||
23
TestGrpcClient/TestGrpcClient.csproj
Normal file
23
TestGrpcClient/TestGrpcClient.csproj
Normal file
@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos\*.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.76.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue
Block a user