mega cleanup :D

This commit is contained in:
Mathias Beaulieu-Duncan 2025-11-03 16:00:13 -05:00
parent ed01f58a0c
commit facc8d7851
35 changed files with 650 additions and 370 deletions

View File

@ -30,7 +30,8 @@
"Bash(python3:*)",
"Bash(grpcurl:*)",
"Bash(lsof:*)",
"Bash(xargs kill -9)"
"Bash(xargs kill -9)",
"Bash(dotnet run:*)"
],
"deny": [],
"ask": []

View File

@ -0,0 +1,11 @@
using System;
namespace Svrnty.CQRS.Abstractions.Attributes;
/// <summary>
/// Marks a command to be ignored by all endpoint generators (AspNetCore MVC, MinimalApi, gRPC)
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class IgnoreCommandAttribute : Attribute
{
}

View File

@ -0,0 +1,11 @@
using System;
namespace Svrnty.CQRS.Abstractions.Attributes;
/// <summary>
/// Marks a query to be ignored by all endpoint generators (AspNetCore MVC, MinimalApi, gRPC)
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class IgnoreQueryAttribute : Attribute
{
}

View File

@ -1,8 +0,0 @@
using System;
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CommandControllerIgnoreAttribute : Attribute
{
}

View File

@ -1,8 +0,0 @@
using System;
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class QueryControllerAuthorizationAttribute : Attribute
{
}

View File

@ -1,8 +0,0 @@
using System;
namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class QueryControllerIgnoreAttribute : Attribute
{
}

View File

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible>
<LangVersion>14</LangVersion>
<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>
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
<IncludeSymbols>true</IncludeSymbols>
<IncludeSource>true</IncludeSource>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>

View File

@ -1,61 +0,0 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
using Svrnty.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.DynamicQuery.Core;
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
[ApiController, Route("api/query/[controller]")]
public class DynamicQueryController<TUnderlyingQuery, TSource, TDestination> : Controller
where TSource : class
where TDestination : class
{
[HttpPost, QueryControllerAuthorization]
public async Task<IQueryExecutionResult<TDestination>> HandleAsync(
[FromBody] DynamicQuery<TSource, TDestination> query,
[FromServices]IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>> queryHandler
)
{
var result = await queryHandler.HandleAsync(query, HttpContext.RequestAborted);
return result;
}
[HttpGet, QueryControllerAuthorization]
public async Task<IQueryExecutionResult<TDestination>> HandleGetAsync(
[FromQuery] DynamicQuery<TSource, TDestination> query,
[FromServices] IQueryHandler<IDynamicQuery<TSource, TDestination>, IQueryExecutionResult<TDestination>> queryHandler
)
{
var result = await queryHandler.HandleAsync(query, HttpContext.RequestAborted);
return result;
}
}
[ApiController, Route("api/query/[controller]")]
public class DynamicQueryController<TUnderlyingQuery, TSource, TDestination, TParams> : Controller
where TSource : class
where TDestination : class
where TParams : class
{
[HttpPost, QueryControllerAuthorization]
public async Task<IQueryExecutionResult<TDestination>> HandleAsync(
[FromBody] DynamicQuery<TSource, TDestination, TParams> query,
[FromServices] IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>> queryHandler
)
{
var result = await queryHandler.HandleAsync(query, HttpContext.RequestAborted);
return result;
}
[HttpGet, QueryControllerAuthorization]
public async Task<IQueryExecutionResult<TDestination>> HandleGetAsync(
[FromQuery] DynamicQuery<TSource, TDestination, TParams> query,
[FromServices] IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>> queryHandler
)
{
var result = await queryHandler.HandleAsync(query, HttpContext.RequestAborted);
return result;
}
}

View File

@ -1,27 +0,0 @@
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions.Discovery;
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
public class DynamicQueryControllerConvention : IControllerModelConvention
{
private readonly IServiceProvider _serviceProvider;
public DynamicQueryControllerConvention(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Apply(ControllerModel controller)
{
if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("DynamicQueryController") && controller.ControllerType.Assembly == typeof(DynamicQueryControllerConvention).Assembly)
{
var genericType = controller.ControllerType.GenericTypeArguments[0];
var queryDiscovery = _serviceProvider.GetRequiredService<IQueryDiscovery>();
var query = queryDiscovery.FindQuery(genericType);
controller.ControllerName = query.LowerCamelCaseName;
}
}
}

View File

@ -1,80 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions.Discovery;
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
using Svrnty.CQRS.DynamicQuery.Discover;
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider)
: IApplicationFeatureProvider<ControllerFeature>
{
/**
* public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
var queryDiscovery = this.serviceProvider.GetRequiredService<IQueryDiscovery>();
foreach (var f in queryDiscovery.GetQueries())
{
var ignoreAttribute = f.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
if (ignoreAttribute != null)
continue;
if (f.Category != "DynamicQuery")
continue;
if (f is DynamicQueryMeta dynamicQueryMeta)
{
if (dynamicQueryMeta.ParamsType == null)
{
var controllerType = typeof(DynamicQueryController<,,>).MakeGenericType(f.QueryType, dynamicQueryMeta.SourceType, dynamicQueryMeta.DestinationType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
else
{
var controllerType = typeof(DynamicQueryController<,,,>).MakeGenericType(f.QueryType, dynamicQueryMeta.SourceType, dynamicQueryMeta.DestinationType, dynamicQueryMeta.ParamsType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
}
}
*/
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 != "DynamicQuery")
continue;
if (queryMeta is DynamicQueryMeta dynamicQueryMeta)
{
// todo: add better error output for the user
if (dynamicQueryMeta.ParamsType == null)
{
// todo: not aot friendly
var controllerType = typeof(DynamicQueryController<,,>).MakeGenericType(queryMeta.QueryType, dynamicQueryMeta.SourceType, dynamicQueryMeta.DestinationType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
else
{
// todo: not aot friendly
var controllerType = typeof(DynamicQueryController<,,,>).MakeGenericType(queryMeta.QueryType, dynamicQueryMeta.SourceType, dynamicQueryMeta.DestinationType, dynamicQueryMeta.ParamsType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
}
}
}
}

View File

@ -1,5 +0,0 @@
namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
public class DynamicQueryControllerOptions
{
}

View File

@ -1,19 +0,0 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc;
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
public static class MvcBuilderExtensions
{
public static IMvcBuilder AddOpenHarborDynamicQueries(this IMvcBuilder builder, Action<DynamicQueryControllerOptions> configuration = null)
{
var options = new DynamicQueryControllerOptions();
configuration?.Invoke(options);
var services = builder.Services;
var serviceProvider = services.BuildServiceProvider();
builder.AddMvcOptions(o => o.Conventions.Add(new DynamicQueryControllerConvention(serviceProvider)));
builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new DynamicQueryControllerFeatureProvider(serviceProvider)));
return builder;
}
}

View File

@ -1,36 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible>
<LangVersion>14</LangVersion>
<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>
<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>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
</ItemGroup>
</Project>

View File

@ -9,11 +9,11 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.Abstractions.Attributes;
using Svrnty.CQRS.Abstractions.Discovery;
using Svrnty.CQRS.Abstractions.Security;
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
using Svrnty.CQRS.DynamicQuery;
using Svrnty.CQRS.DynamicQuery.Abstractions;
using Svrnty.CQRS.DynamicQuery.AspNetCore;
using Svrnty.CQRS.DynamicQuery.Discover;
using PoweredSoft.DynamicQuery.Core;
@ -32,7 +32,7 @@ public static class EndpointRouteBuilderExtensions
if (queryMeta.Category != "DynamicQuery")
continue;
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<IgnoreQueryAttribute>();
if (ignoreAttribute != null)
continue;

View File

@ -32,6 +32,5 @@
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.AspNetCore\Svrnty.CQRS.DynamicQuery.AspNetCore.csproj" />
</ItemGroup>
</Project>

View File

@ -4,14 +4,10 @@ using Svrnty.CQRS.Abstractions.Discovery;
namespace Svrnty.CQRS.DynamicQuery.Discover;
public class DynamicQueryMeta : QueryMeta
public class DynamicQueryMeta(Type queryType, Type serviceType, Type queryResultType)
: QueryMeta(queryType, serviceType, queryResultType)
{
public DynamicQueryMeta(Type queryType, Type serviceType, Type queryResultType) : base(queryType, serviceType, queryResultType)
{
}
public Type SourceType => QueryType.GetGenericArguments()[0];
public Type SourceType => QueryType.GetGenericArguments()[0];
public Type DestinationType => QueryType.GetGenericArguments()[1];
public override string Category => "DynamicQuery";
public override string Name

View File

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

View File

@ -1,8 +1,8 @@
using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery.Core;
using System;
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
namespace Svrnty.CQRS.DynamicQuery;
public class DynamicQueryAggregate
{

View File

@ -1,12 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery.Core;
namespace Svrnty.CQRS.DynamicQuery.AspNetCore;
namespace Svrnty.CQRS.DynamicQuery;
public class DynamicQueryFilter
{
@ -17,7 +16,6 @@ public class DynamicQueryFilter
public string Path { get; set; }
public object Value { get; set; }
[FromQuery(Name ="value")]
public string QueryValue
{
get

View File

@ -0,0 +1,69 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.Configuration;
namespace Svrnty.CQRS.FluentValidation;
/// <summary>
/// Extension methods for CqrsBuilder to support FluentValidation
/// </summary>
public static class CqrsBuilderExtensions
{
/// <summary>
/// Adds a command handler with FluentValidation validator to the CQRS pipeline
/// </summary>
public static CqrsBuilder AddCommand<TCommand, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TCommandHandler, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TValidator>(
this CqrsBuilder builder)
where TCommand : class
where TCommandHandler : class, ICommandHandler<TCommand>
where TValidator : class, IValidator<TCommand>
{
// Add the command handler
builder.AddCommand<TCommand, TCommandHandler>();
// Add the validator
builder.Services.AddTransient<IValidator<TCommand>, TValidator>();
return builder;
}
/// <summary>
/// Adds a command handler with result and FluentValidation validator to the CQRS pipeline
/// </summary>
public static CqrsBuilder AddCommand<TCommand, TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TCommandHandler, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TValidator>(
this CqrsBuilder builder)
where TCommand : class
where TCommandHandler : class, ICommandHandler<TCommand, TResult>
where TValidator : class, IValidator<TCommand>
{
// Add the command handler
builder.AddCommand<TCommand, TResult, TCommandHandler>();
// Add the validator
builder.Services.AddTransient<IValidator<TCommand>, TValidator>();
return builder;
}
/// <summary>
/// Adds a query handler with FluentValidation validator to the CQRS pipeline
/// </summary>
public static CqrsBuilder AddQuery<TQuery, TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TQueryHandler, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TValidator>(
this CqrsBuilder builder)
where TQuery : class
where TQueryHandler : class, IQueryHandler<TQuery, TResult>
where TValidator : class, IValidator<TQuery>
{
// Add the query handler
builder.AddQuery<TQuery, TResult, TQueryHandler>();
// Add the validator
builder.Services.AddTransient<IValidator<TQuery>, TValidator>();
return builder;
}
}

View File

@ -785,6 +785,72 @@ namespace Svrnty.CQRS.Grpc.Generators
sb.AppendLine(" endpoints.MapGrpcService<DynamicQueryServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
// Add configuration-based methods
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers gRPC services based on configuration");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcFromConfiguration(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" var config = services.BuildServiceProvider().GetService<Svrnty.CQRS.Configuration.CqrsConfiguration>();");
sb.AppendLine(" var grpcOptions = config?.GetConfiguration<Svrnty.CQRS.Grpc.GrpcCqrsOptions>();");
sb.AppendLine(" if (grpcOptions != null)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)");
sb.AppendLine(" services.AddGrpcReflection();");
sb.AppendLine();
if (hasCommands)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())");
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
}
if (hasQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
}
if (hasDynamicQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" services.AddSingleton<DynamicQueryServiceImpl>();");
}
sb.AppendLine(" }");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps gRPC service endpoints based on configuration");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcFromConfiguration(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" var config = endpoints.ServiceProvider.GetService<Svrnty.CQRS.Configuration.CqrsConfiguration>();");
sb.AppendLine(" var grpcOptions = config?.GetConfiguration<Svrnty.CQRS.Grpc.GrpcCqrsOptions>();");
sb.AppendLine(" if (grpcOptions != null)");
sb.AppendLine(" {");
if (hasCommands)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())");
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
}
if (hasQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
}
if (hasDynamicQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" endpoints.MapGrpcService<DynamicQueryServiceImpl>();");
}
sb.AppendLine();
sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)");
sb.AppendLine(" endpoints.MapGrpcReflectionService();");
sb.AppendLine(" }");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
}
sb.AppendLine(" }");
@ -1318,11 +1384,11 @@ namespace Svrnty.CQRS.Grpc.Generators
// Build the dynamic query object
if (dynamicQuery.HasParams)
{
sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>");
sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>");
}
else
{
sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>");
sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>");
}
sb.AppendLine(" {");
sb.AppendLine(" Page = request.Page > 0 ? request.Page : null,");
@ -1362,15 +1428,15 @@ namespace Svrnty.CQRS.Grpc.Generators
}
// Add helper methods for converting proto messages to AspNetCore types
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter>? ConvertFilters(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>? ConvertFilters(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" if (protoFilters == null || protoFilters.Count == 0)");
sb.AppendLine(" return null;");
sb.AppendLine();
sb.AppendLine(" var filters = new List<Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter>();");
sb.AppendLine(" var filters = new List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>();");
sb.AppendLine(" foreach (var protoFilter in protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter");
sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter");
sb.AppendLine(" {");
sb.AppendLine(" Path = protoFilter.Path,");
sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)protoFilter.Type).ToString(),");
@ -1395,12 +1461,12 @@ namespace Svrnty.CQRS.Grpc.Generators
sb.AppendLine(" return filters;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter> ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter> ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" var result = new List<Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter>();");
sb.AppendLine(" var result = new List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>();");
sb.AppendLine(" foreach (var pf in protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter");
sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter");
sb.AppendLine(" {");
sb.AppendLine(" Path = pf.Path,");
sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)pf.Type).ToString(),");
@ -1447,12 +1513,12 @@ namespace Svrnty.CQRS.Grpc.Generators
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryAggregate>? ConvertAggregates(Google.Protobuf.Collections.RepeatedField<DynamicQueryAggregate> protoAggregates)");
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.DynamicQueryAggregate>? ConvertAggregates(Google.Protobuf.Collections.RepeatedField<DynamicQueryAggregate> protoAggregates)");
sb.AppendLine(" {");
sb.AppendLine(" if (protoAggregates == null || protoAggregates.Count == 0)");
sb.AppendLine(" return null;");
sb.AppendLine();
sb.AppendLine(" return protoAggregates.Select(a => new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryAggregate");
sb.AppendLine(" return protoAggregates.Select(a => new Svrnty.CQRS.DynamicQuery.DynamicQueryAggregate");
sb.AppendLine(" {");
sb.AppendLine(" Path = a.Path,");
sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.AggregateType)a.Type).ToString()");

View File

@ -0,0 +1,72 @@
#nullable enable
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Configuration;
namespace Svrnty.CQRS.Grpc;
/// <summary>
/// Extension methods for CqrsBuilder to add gRPC support
/// </summary>
public static class CqrsBuilderExtensions
{
/// <summary>
/// Adds gRPC support to the CQRS pipeline
/// </summary>
/// <param name="builder">The CQRS builder</param>
/// <param name="configure">Optional configuration for gRPC endpoints</param>
/// <returns>The CQRS builder for method chaining</returns>
public static CqrsBuilder AddGrpc(this CqrsBuilder builder, Action<GrpcCqrsOptions>? configure = null)
{
var options = new GrpcCqrsOptions();
configure?.Invoke(options);
builder.Configuration.SetConfiguration(options);
// Try to find and call the generated AddGrpcFromConfiguration method
var addGrpcMethod = FindExtensionMethod("AddGrpcFromConfiguration");
if (addGrpcMethod != null)
{
addGrpcMethod.Invoke(null, new object[] { builder.Services });
}
else
{
Console.WriteLine("Warning: AddGrpcFromConfiguration not found. gRPC services were not registered.");
Console.WriteLine("Make sure your project has source generators enabled and references Svrnty.CQRS.Grpc.Generators.");
}
return builder;
}
private static MethodInfo? FindExtensionMethod(string methodName)
{
// Search through all loaded assemblies for the extension method
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
var types = assembly.GetTypes()
.Where(t => t.IsClass && t.IsSealed && !t.IsGenericType && t.IsPublic);
foreach (var type in types)
{
var method = type.GetMethod(methodName,
BindingFlags.Static | BindingFlags.Public,
null,
new[] { typeof(IServiceCollection) },
null);
if (method != null)
return method;
}
}
catch
{
// Skip assemblies that can't be inspected
}
}
return null;
}
}

View File

@ -0,0 +1,68 @@
#nullable enable
namespace Svrnty.CQRS.Grpc;
/// <summary>
/// Configuration options for gRPC CQRS endpoints
/// </summary>
public class GrpcCqrsOptions
{
/// <summary>
/// Gets whether reflection should be enabled
/// </summary>
public bool ShouldEnableReflection { get; private set; }
private bool ShouldMapCommands { get; set; }
private bool ShouldMapQueries { get; set; }
private bool WasMappingMethodCalled { get; set; }
/// <summary>
/// Enables gRPC reflection for the service
/// </summary>
public GrpcCqrsOptions EnableReflection()
{
ShouldEnableReflection = true;
return this;
}
/// <summary>
/// Maps command endpoints
/// </summary>
public GrpcCqrsOptions MapCommands()
{
WasMappingMethodCalled = true;
ShouldMapCommands = true;
return this;
}
/// <summary>
/// Maps query endpoints (includes dynamic queries)
/// </summary>
public GrpcCqrsOptions MapQueries()
{
WasMappingMethodCalled = true;
ShouldMapQueries = true;
return this;
}
/// <summary>
/// Maps both command and query endpoints
/// </summary>
public GrpcCqrsOptions MapCommandsAndQueries()
{
WasMappingMethodCalled = true;
ShouldMapCommands = true;
ShouldMapQueries = true;
return this;
}
/// <summary>
/// Gets whether commands should be mapped (defaults to true if no mapping methods were called)
/// </summary>
public bool GetShouldMapCommands() => WasMappingMethodCalled ? ShouldMapCommands : true;
/// <summary>
/// Gets whether queries should be mapped (defaults to true if no mapping methods were called)
/// </summary>
public bool GetShouldMapQueries() => WasMappingMethodCalled ? ShouldMapQueries : true;
}

View File

@ -2,10 +2,10 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>false</IsAotCompatible>
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
<LangVersion>14</LangVersion>
<Company>Svrnty</Company>
<Nullable>enable</Nullable>
<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>
@ -23,21 +23,13 @@
<ItemGroup>
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
<None Include="build\Svrnty.CQRS.Grpc.targets" Pack="true" PackagePath="build\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
<PackageReference Include="Grpc.Tools" Version="2.76.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="Grpc.AspNetCore" Version="2.68.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" />
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@
#nullable enable
using System;
using Svrnty.CQRS.Configuration;
namespace Svrnty.CQRS.MinimalApi;
/// <summary>
/// Extension methods for CqrsBuilder to add MinimalApi support
/// </summary>
public static class CqrsBuilderExtensions
{
/// <summary>
/// Adds MinimalApi support to the CQRS pipeline
/// </summary>
/// <param name="builder">The CQRS builder</param>
/// <param name="configure">Optional configuration for MinimalApi endpoints</param>
/// <returns>The CQRS builder for method chaining</returns>
public static CqrsBuilder AddMinimalApi(this CqrsBuilder builder, Action<MinimalApiCqrsOptions>? configure = null)
{
var options = new MinimalApiCqrsOptions();
configure?.Invoke(options);
builder.Configuration.SetConfiguration(options);
return builder;
}
}

View File

@ -8,9 +8,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.Abstractions.Attributes;
using Svrnty.CQRS.Abstractions.Discovery;
using Svrnty.CQRS.Abstractions.Security;
using Svrnty.CQRS.AspNetCore.Abstractions.Attributes;
namespace Svrnty.CQRS.MinimalApi;
@ -23,7 +23,7 @@ public static class EndpointRouteBuilderExtensions
foreach (var queryMeta in queryDiscovery.GetQueries())
{
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<IgnoreQueryAttribute>();
if (ignoreAttribute != null)
continue;
@ -157,7 +157,7 @@ public static class EndpointRouteBuilderExtensions
foreach (var commandMeta in commandDiscovery.GetCommands())
{
var ignoreAttribute = commandMeta.CommandType.GetCustomAttribute<CommandControllerIgnoreAttribute>();
var ignoreAttribute = commandMeta.CommandType.GetCustomAttribute<IgnoreCommandAttribute>();
if (ignoreAttribute != null)
continue;

View File

@ -0,0 +1,39 @@
#nullable enable
namespace Svrnty.CQRS.MinimalApi;
/// <summary>
/// Configuration options for MinimalApi CQRS endpoints
/// </summary>
public class MinimalApiCqrsOptions
{
/// <summary>
/// Whether to map command endpoints (default: true)
/// </summary>
public bool MapCommands { get; set; } = true;
/// <summary>
/// Whether to map query endpoints (default: true)
/// </summary>
public bool MapQueries { get; set; } = true;
/// <summary>
/// Whether to map dynamic query endpoints (default: true)
/// </summary>
public bool MapDynamicQueries { get; set; } = true;
/// <summary>
/// Route prefix for command endpoints (default: "api/command")
/// </summary>
public string CommandRoutePrefix { get; set; } = "api/command";
/// <summary>
/// Route prefix for query endpoints (default: "api/query")
/// </summary>
public string QueryRoutePrefix { get; set; } = "api/query";
/// <summary>
/// Route prefix for dynamic query endpoints (default: "api/query")
/// </summary>
public string DynamicQueryRoutePrefix { get; set; } = "api/query";
}

View File

@ -35,6 +35,6 @@
<ItemGroup>
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj" />
<ProjectReference Include="..\Svrnty.CQRS\Svrnty.CQRS.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,101 @@
#nullable enable
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Configuration;
using System;
using System.Linq;
using System.Reflection;
namespace Svrnty.CQRS.MinimalApi;
public static class WebApplicationExtensions
{
/// <summary>
/// Maps Svrnty CQRS endpoints based on configuration (supports both gRPC and MinimalApi)
/// </summary>
public static WebApplication UseSvrntyCqrs(this WebApplication app)
{
var config = app.Services.GetService<CqrsConfiguration>();
// Handle gRPC configuration if available
// Note: GrpcCqrsOptions type is from Svrnty.CQRS.Grpc package
var grpcOptionsType = Type.GetType("Svrnty.CQRS.Grpc.GrpcCqrsOptions, Svrnty.CQRS.Grpc");
if (grpcOptionsType != null && config != null)
{
var getConfigMethod = typeof(CqrsConfiguration).GetMethod("GetConfiguration")!.MakeGenericMethod(grpcOptionsType);
var grpcOptions = getConfigMethod.Invoke(config, null);
if (grpcOptions != null)
{
// Try to find and call MapGrpcFromConfiguration extension method via reflection
// This is generated by the source generator in consumer projects
var grpcMethod = FindExtensionMethod("MapGrpcFromConfiguration");
if (grpcMethod != null)
{
grpcMethod.Invoke(null, new object[] { app });
}
else
{
Console.WriteLine("Warning: MapGrpcFromConfiguration not found. gRPC endpoints were not mapped.");
Console.WriteLine("Make sure your project references Svrnty.CQRS.Grpc and has source generators enabled.");
}
}
}
// Handle MinimalApi configuration if available
var minimalApiOptions = config?.GetConfiguration<MinimalApiCqrsOptions>();
if (minimalApiOptions != null)
{
if (minimalApiOptions.MapCommands)
{
app.MapSvrntyCommands(minimalApiOptions.CommandRoutePrefix);
}
if (minimalApiOptions.MapQueries)
{
app.MapSvrntyQueries(minimalApiOptions.QueryRoutePrefix);
}
// TODO: Add dynamic query mapping when available
// if (minimalApiOptions.MapDynamicQueries)
// {
// app.MapSvrntyDynamicQueries(minimalApiOptions.DynamicQueryRoutePrefix);
// }
}
return app;
}
private static MethodInfo? FindExtensionMethod(string methodName)
{
// Search through all loaded assemblies for the extension method
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
var types = assembly.GetTypes()
.Where(t => t.IsClass && t.IsSealed && !t.IsGenericType && t.IsPublic);
foreach (var type in types)
{
var method = type.GetMethod(methodName,
BindingFlags.Static | BindingFlags.Public,
null,
new[] { typeof(IEndpointRouteBuilder) },
null);
if (method != null)
return method;
}
}
catch
{
// Skip assemblies that can't be inspected
}
}
return null;
}
}

View File

@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.Abstractions",
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
@ -17,8 +15,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
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}"
@ -69,18 +65,6 @@ Global
{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
@ -93,18 +77,6 @@ Global
{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

View File

@ -0,0 +1,77 @@
#nullable enable
using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.Discovery;
namespace Svrnty.CQRS.Configuration;
/// <summary>
/// Builder for configuring CQRS services
/// </summary>
public class CqrsBuilder
{
/// <summary>
/// Gets the service collection for manual service registration
/// </summary>
public IServiceCollection Services { get; }
/// <summary>
/// Gets the CQRS configuration object (used by extension packages)
/// </summary>
public CqrsConfiguration Configuration { get; }
internal CqrsBuilder(IServiceCollection services)
{
Services = services;
Configuration = new CqrsConfiguration();
// Add discovery services by default
services.AddDefaultCommandDiscovery();
services.AddDefaultQueryDiscovery();
// Register configuration as singleton so it can be accessed later
services.AddSingleton(Configuration);
}
/// <summary>
/// Completes the builder configuration
/// </summary>
internal void Build()
{
// Configuration is now handled by extension methods in respective packages
}
/// <summary>
/// Adds a command handler to the CQRS pipeline
/// </summary>
public CqrsBuilder AddCommand<TCommand, TCommandHandler>()
where TCommand : class
where TCommandHandler : class, ICommandHandler<TCommand>
{
Services.AddCommand<TCommand, TCommandHandler>();
return this;
}
/// <summary>
/// Adds a command handler with result to the CQRS pipeline
/// </summary>
public CqrsBuilder AddCommand<TCommand, TResult, TCommandHandler>()
where TCommand : class
where TCommandHandler : class, ICommandHandler<TCommand, TResult>
{
Services.AddCommand<TCommand, TResult, TCommandHandler>();
return this;
}
/// <summary>
/// Adds a query handler to the CQRS pipeline
/// </summary>
public CqrsBuilder AddQuery<TQuery, TResult, TQueryHandler>()
where TQuery : class
where TQueryHandler : class, IQueryHandler<TQuery, TResult>
{
Services.AddQuery<TQuery, TResult, TQueryHandler>();
return this;
}
}

View File

@ -0,0 +1,38 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace Svrnty.CQRS.Configuration;
/// <summary>
/// Configuration for CQRS services and endpoints.
/// Supports extension by third-party packages through generic configuration storage.
/// </summary>
public class CqrsConfiguration
{
private readonly Dictionary<Type, object> _configurations = new();
/// <summary>
/// Sets a configuration object for a specific type
/// </summary>
public void SetConfiguration<T>(T config) where T : class
{
_configurations[typeof(T)] = config;
}
/// <summary>
/// Gets a configuration object for a specific type
/// </summary>
public T? GetConfiguration<T>() where T : class
{
return _configurations.TryGetValue(typeof(T), out var config) ? config as T : null;
}
/// <summary>
/// Checks if a configuration exists for a specific type
/// </summary>
public bool HasConfiguration<T>() where T : class
{
return _configurations.ContainsKey(typeof(T));
}
}

View File

@ -1,12 +1,27 @@
using Microsoft.Extensions.DependencyInjection;
#nullable enable
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Svrnty.CQRS.Abstractions.Discovery;
using Svrnty.CQRS.Configuration;
using Svrnty.CQRS.Discovery;
namespace Svrnty.CQRS;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds Svrnty CQRS services with fluent configuration
/// </summary>
public static IServiceCollection AddSvrntyCqrs(this IServiceCollection services, Action<CqrsBuilder>? configure = null)
{
var builder = new CqrsBuilder(services);
configure?.Invoke(builder);
builder.Build(); // Execute deferred registrations
return services;
}
public static IServiceCollection AddDefaultQueryDiscovery(this IServiceCollection services)
{
services.TryAddTransient<IQueryDiscovery, QueryDiscovery>();

View File

@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Svrnty.CQRS;
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.FluentValidation;
using Svrnty.CQRS.Grpc;
using Svrnty.Sample;
using Svrnty.Sample.Grpc.Extensions;
using Svrnty.CQRS.MinimalApi;
using Svrnty.CQRS.DynamicQuery;
using Svrnty.CQRS.DynamicQuery.MinimalApi;
@ -19,37 +18,44 @@ builder.WebHost.ConfigureKestrel(options =>
options.ListenLocalhost(6001, o => o.Protocols = HttpProtocols.Http1);
});
builder.Services.AddCommand<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
// IMPORTANT: Register dynamic query dependencies FIRST
// (before AddSvrntyCqrs, so gRPC services can find the handlers)
builder.Services.AddTransient<PoweredSoft.Data.Core.IAsyncQueryableService, SimpleAsyncQueryableService>();
builder.Services.AddTransient<PoweredSoft.DynamicQuery.Core.IQueryHandlerAsync, PoweredSoft.DynamicQuery.QueryHandlerAsync>();
builder.Services.AddDynamicQueryWithProvider<User, UserQueryableProvider>();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
// Configure CQRS with fluent API
builder.Services.AddSvrntyCqrs(cqrs =>
{
// Register commands and queries with validators
cqrs.AddCommand<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
cqrs.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
cqrs.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
// Enable gRPC endpoints with reflection
cqrs.AddGrpc(grpc =>
{
grpc.EnableReflection();
});
// Enable MinimalApi endpoints
cqrs.AddMinimalApi();
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
builder.Services.AddGrpcCommandsAndQueries();
app.MapGrpcCommandsAndQueries();
app.MapGrpcReflectionService();
// Map all configured CQRS endpoints (gRPC and MinimalApi)
app.UseSvrntyCqrs();
// Map dynamic queries manually for now
app.MapSvrntyDynamicQueries();
app.UseSwagger();
app.UseSwaggerUI();
app.MapSvrntyCommands();
app.MapSvrntyQueries();
app.MapSvrntyDynamicQueries();
Console.WriteLine("Auto-Generated gRPC Server with Reflection, Validation, MinimalApi and Swagger");
Console.WriteLine("gRPC (HTTP/2): http://localhost:6000");

View File

@ -32,6 +32,7 @@
<ProjectReference Include="..\Svrnty.CQRS.MinimalApi\Svrnty.CQRS.MinimalApi.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.MinimalApi\Svrnty.CQRS.DynamicQuery.MinimalApi.csproj" />
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Abstractions\Svrnty.CQRS.Grpc.Abstractions.csproj" />
</ItemGroup>
<!-- Import the proto generation targets for testing (in production this would come from the NuGet package) -->