Compare commits

...

15 Commits

Author SHA1 Message Date
55f1324286 Merge pull request 'feat/claude-code-harness' (#2) from feat/claude-code-harness into main
All checks were successful
Publish NuGets / build (release) Successful in 34s
Reviewed-on: #2
2026-03-12 06:44:11 -04:00
Mathias Beaulieu-Duncan
b34bf874b4 Remove Claude harness — replaced by claude-cqrs-plugin
The in-repo .claude/ harness (rules, skills, settings) is superseded by
the standalone claude-cqrs-plugin which provides the same guidance as a
reusable plugin across all Svrnty.CQRS projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 06:42:50 -04:00
Mathias Beaulieu-Duncan
c6de10b98b Move UseSvrntyCqrs() from MinimalApi to core Svrnty.CQRS package
gRPC-only projects couldn't call app.UseSvrntyCqrs() without adding the
MinimalApi package. The method only calls ExecuteMappingCallbacks() which
is already in core — it had no MinimalApi dependency. Adds ASP.NET Core
FrameworkReference to the core package.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 06:38:26 -04:00
Mathias Beaulieu-Duncan
3945c1a158 Add project-init agent for scaffolding new CQRS projects
Scaffolds a complete Svrnty.CQRS project from a natural language
description — creates solution, web project, DAL with PostgreSQL,
entities, Program.cs, first feature, proto file, and .editorconfig.
Defaults to gRPC-only; MinimalApi added only on request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 06:38:26 -04:00
7614f68512 Merge pull request 'feat/claude-code-harness' (#1) from feat/claude-code-harness into main
All checks were successful
Publish NuGets / build (release) Successful in 32s
Reviewed-on: #1
2026-03-12 03:35:26 -04:00
Mathias Beaulieu-Duncan
fdee02c960 Apply dotnet format with new editorconfig rules
Automated formatting: BOM removal, using sort order, final newlines,
whitespace normalization across all projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 03:30:50 -04:00
Mathias Beaulieu-Duncan
a4525bad6a Add Claude Code harness: rules, skills, hooks, and editorconfig
- Add path-specific rules for commands/queries, dynamic queries, validation, and gRPC
- Add /add-command, /add-query, /add-dynamic-query scaffolding skills
- Add project settings with post-edit formatting, proto validation, and build-gate hooks
- Add .editorconfig codifying existing code style conventions
- Trim CLAUDE.md from 414 to 130 lines (domain details moved to rules)
- Add .harness-version tracking for the shared claude-harness repo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 03:30:27 -04:00
Svrnty
3df094b9e7 docs: sanitise product references, add "Where This Fits" to README
Co-Authored-By: Svrnty Inc. <jp@svrnty.io, mathias@svrnty.io>
2026-02-27 13:08:17 -05:00
6aece5a769
Handle generic types in proto message name generation
All checks were successful
Publish NuGets / build (release) Successful in 39s
Generic types like Translation<T> now produce qualified message names
(e.g. TranslationOfFaqTranslationQueryItem) to avoid duplicate message
definitions in generated .proto files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:56:37 -05:00
b372805c4e
Fix string filter values not converting to correct CLR types for DynamicQuery
All checks were successful
Publish NuGets / build (release) Successful in 41s
Convert string filter values (e.g. from gRPC transport) to their actual
property types (DateTime, DateTimeOffset) so PoweredSoft.DynamicLinq can
build valid LINQ expressions. Also removes filters with null values and
recurses into composite filters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 11:28:09 -05:00
89ccbe990f
add AND / OR support when filtering
All checks were successful
Publish NuGets / build (release) Successful in 34s
2026-02-02 17:53:43 -05:00
433b852a43
Refactor proto generation from source generator to MSBuild task
All checks were successful
Publish NuGets / build (release) Successful in 40s
Replace ProtoFileSourceGenerator and WriteProtoFileTask with a new
GenerateProtoFileTask that creates its own Roslyn compilation. This
solves the timing issue where source generators run too late for
Grpc.Tools to process the generated proto files.

The new task runs after ResolveAssemblyReferences but before
_gRPC_GetProtoc and CoreCompile, ensuring the proto file exists
when Grpc.Tools needs it.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 14:35:56 -05:00
03041721ca
Preserve existing proto files instead of overwriting
All checks were successful
Publish NuGets / build (release) Successful in 39s
If a proto file already exists (committed to repo), don't overwrite it
with a placeholder. This allows first-time builds to work correctly
when the proto file is already in the repository.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 12:05:45 -05:00
05449b9a28
Add empty service definitions to placeholder proto
Grpc.Tools needs service definitions to generate the base classes
(CommandService+CommandServiceBase, etc.) that GrpcGenerator looks for.
Without these, the service bases wouldn't exist on first build.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 02:21:43 -05:00
dfbef9d161
Fix placeholder proto namespace to use project's actual namespace
The WriteProtoFileTask now receives RootNamespace and AssemblyName from
MSBuild and uses them for the placeholder proto's csharp_namespace instead
of hardcoded "Generated.Grpc". This ensures GrpcGenerator can find the
service base types on first build, enabling gRPC service registration
to work without requiring a second build.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 02:19:50 -05:00
50 changed files with 3908 additions and 3744 deletions

View File

@ -1,50 +0,0 @@
{
"permissions": {
"allow": [
"Bash(dotnet clean:*)",
"Bash(dotnet run)",
"Bash(dotnet add:*)",
"Bash(timeout 5 dotnet run:*)",
"Bash(dotnet remove:*)",
"Bash(netstat:*)",
"Bash(findstr:*)",
"Bash(cat:*)",
"Bash(taskkill:*)",
"WebSearch",
"Bash(dotnet tool install:*)",
"Bash(protogen:*)",
"Bash(timeout 15 dotnet run:*)",
"Bash(where:*)",
"Bash(timeout 30 dotnet run:*)",
"Bash(timeout 60 dotnet run:*)",
"Bash(timeout 120 dotnet run:*)",
"Bash(git add:*)",
"Bash(curl:*)",
"Bash(timeout 3 cmd:*)",
"Bash(timeout:*)",
"Bash(tasklist:*)",
"Bash(dotnet build:*)",
"Bash(dotnet --list-sdks:*)",
"Bash(dotnet sln:*)",
"Bash(pkill:*)",
"Bash(python3:*)",
"Bash(grpcurl:*)",
"Bash(lsof:*)",
"Bash(xargs kill -9)",
"Bash(dotnet run:*)",
"Bash(find:*)",
"Bash(dotnet pack:*)",
"Bash(unzip:*)",
"WebFetch(domain:andrewlock.net)",
"WebFetch(domain:github.com)",
"WebFetch(domain:stackoverflow.com)",
"WebFetch(domain:www.kenmuse.com)",
"WebFetch(domain:blog.rsuter.com)",
"WebFetch(domain:natemcmaster.com)",
"WebFetch(domain:www.nuget.org)",
"Bash(mkdir:*)"
],
"deny": [],
"ask": []
}
}

96
.editorconfig Normal file
View File

@ -0,0 +1,96 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{csproj,props,targets,xml}]
indent_size = 2
[*.{json,yml,yaml}]
indent_size = 2
[*.proto]
indent_size = 2
[*.cs]
# Namespace
csharp_style_namespace_declarations = file_scoped:warning
# Braces — Allman style
csharp_new_line_before_open_brace = all
# Usings
dotnet_sort_system_directives_first = true
csharp_using_directive_placement = outside_namespace:warning
# var preferences — use var when type is apparent
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
# Expression bodies — prefer for simple members
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = false:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_lambdas = true:suggestion
# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
# Null checking
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences — exclude interface members (netstandard2.1 compat)
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
# Field naming — _camelCase for private fields
dotnet_naming_rule.private_fields_should_be_camel_case.severity = warning
dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields
dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_underscore
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_style.camel_case_underscore.required_prefix = _
dotnet_naming_style.camel_case_underscore.capitalization = camel_case
# Constants — PascalCase
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
dotnet_naming_rule.constants_should_be_pascal_case.style = pascal_case
dotnet_naming_symbols.constants.applicable_kinds = field
dotnet_naming_symbols.constants.required_modifiers = const
dotnet_naming_style.pascal_case.capitalization = pascal_case
# Interfaces — I prefix
dotnet_naming_rule.interfaces_should_begin_with_i.severity = warning
dotnet_naming_rule.interfaces_should_begin_with_i.symbols = interfaces
dotnet_naming_rule.interfaces_should_begin_with_i.style = begins_with_i
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.capitalization = pascal_case
# Async methods — Async suffix
dotnet_naming_rule.async_methods_should_end_with_async.severity = suggestion
dotnet_naming_rule.async_methods_should_end_with_async.symbols = async_methods
dotnet_naming_rule.async_methods_should_end_with_async.style = ends_with_async
dotnet_naming_symbols.async_methods.applicable_kinds = method
dotnet_naming_symbols.async_methods.required_modifiers = async
dotnet_naming_style.ends_with_async.required_suffix = Async
dotnet_naming_style.ends_with_async.capitalization = pascal_case

View File

@ -1,6 +1,6 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to AI agents when working with code in this repository.
## Project Overview ## Project Overview

View File

@ -4,6 +4,15 @@
Our implementation of query and command responsibility segregation (CQRS). Our implementation of query and command responsibility segregation (CQRS).
## Where This Fits
This is a backend framework of the [Svrnty Agent System](../README.md).
**Layer**: Framework
**Depends on**: Nothing (standalone .NET framework)
**Depended on by**: a-gent-app (backend services), flutter_cqrs_datasource (client)
**Git**: [git.openharbor.io/svrnty/dotnet-cqrs](https://git.openharbor.io/svrnty/dotnet-cqrs)
## Getting Started ## Getting Started
> Install nuget package to your awesome project. > Install nuget package to your awesome project.

View File

@ -1,4 +1,4 @@
using System; using System;
namespace Svrnty.CQRS.Abstractions.Attributes; namespace Svrnty.CQRS.Abstractions.Attributes;

View File

@ -1,4 +1,4 @@
using System; using System;
namespace Svrnty.CQRS.Abstractions.Attributes; namespace Svrnty.CQRS.Abstractions.Attributes;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Reflection; using System.Reflection;
using Svrnty.CQRS.Abstractions.Attributes; using Svrnty.CQRS.Abstractions.Attributes;

View File

@ -1,4 +1,4 @@
using System; using System;
namespace Svrnty.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.Abstractions.Discovery;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Svrnty.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.Abstractions.Discovery;

View File

@ -1,4 +1,4 @@
using System; using System;
namespace Svrnty.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.Abstractions.Discovery;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Reflection; using System.Reflection;
using Svrnty.CQRS.Abstractions.Attributes; using Svrnty.CQRS.Abstractions.Attributes;

View File

@ -1,4 +1,4 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Svrnty.CQRS.Abstractions; namespace Svrnty.CQRS.Abstractions;

View File

@ -1,4 +1,4 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Svrnty.CQRS.Abstractions; namespace Svrnty.CQRS.Abstractions;

View File

@ -1,4 +1,4 @@
namespace Svrnty.CQRS.Abstractions.Security; namespace Svrnty.CQRS.Abstractions.Security;
public enum AuthorizationResult public enum AuthorizationResult
{ {

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Svrnty.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;

View File

@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
namespace Svrnty.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Svrnty.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;

View File

@ -1,4 +1,4 @@
namespace Svrnty.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery.Abstractions;
public interface IDynamicQueryParams<out TParams> public interface IDynamicQueryParams<out TParams>
where TParams : class where TParams : class

View File

@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using PoweredSoft.DynamicQuery.Core;
using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.Abstractions.Attributes; using Svrnty.CQRS.Abstractions.Attributes;
using Svrnty.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;
@ -14,7 +15,6 @@ using Svrnty.CQRS.Abstractions.Security;
using Svrnty.CQRS.DynamicQuery; using Svrnty.CQRS.DynamicQuery;
using Svrnty.CQRS.DynamicQuery.Abstractions; using Svrnty.CQRS.DynamicQuery.Abstractions;
using Svrnty.CQRS.DynamicQuery.Discover; using Svrnty.CQRS.DynamicQuery.Discover;
using PoweredSoft.DynamicQuery.Core;
namespace Svrnty.CQRS.DynamicQuery.MinimalApi; namespace Svrnty.CQRS.DynamicQuery.MinimalApi;

View File

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

View File

@ -1,8 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Svrnty.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
using Svrnty.CQRS.DynamicQuery.Abstractions;
namespace Svrnty.CQRS.DynamicQuery; namespace Svrnty.CQRS.DynamicQuery;

View File

@ -1,6 +1,6 @@
using System;
using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
using System;
namespace Svrnty.CQRS.DynamicQuery; namespace Svrnty.CQRS.DynamicQuery;

View File

@ -1,10 +1,10 @@
using Svrnty.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.DynamicQuery.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using PoweredSoft.DynamicQuery.Core;
using Svrnty.CQRS.DynamicQuery.Abstractions;
namespace Svrnty.CQRS.DynamicQuery; namespace Svrnty.CQRS.DynamicQuery;
@ -49,7 +49,7 @@ public class DynamicQueryHandler<TSource, TDestination, TParams>
protected override async Task<IQueryable<TSource>> AlterSourceAsync(IQueryable<TSource> source, IDynamicQuery query, CancellationToken cancellationToken) protected override async Task<IQueryable<TSource>> AlterSourceAsync(IQueryable<TSource> source, IDynamicQuery query, CancellationToken cancellationToken)
{ {
source = await base.AlterSourceAsync(source, query, cancellationToken); source = await base.AlterSourceAsync(source, query, cancellationToken);
if (query is IDynamicQueryParams<TParams> withParams) if (query is IDynamicQueryParams<TParams> withParams)
{ {

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Svrnty.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery;
using PoweredSoft.DynamicQuery.Core; using PoweredSoft.DynamicQuery.Core;
using Svrnty.CQRS.DynamicQuery.Abstractions;
namespace Svrnty.CQRS.DynamicQuery; namespace Svrnty.CQRS.DynamicQuery;
@ -16,7 +19,10 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
private readonly IQueryHandlerAsync _queryHandlerAsync; private readonly IQueryHandlerAsync _queryHandlerAsync;
private readonly IEnumerable<IQueryableProvider<TSource>> _queryableProviders; private readonly IEnumerable<IQueryableProvider<TSource>> _queryableProviders;
private readonly IEnumerable<IAlterQueryableService<TSource, TDestination>> _alterQueryableServices; private readonly IEnumerable<IAlterQueryableService<TSource, TDestination>> _alterQueryableServices;
private readonly IEnumerable<IDynamicQueryInterceptorProvider<TSource, TDestination>> _dynamicQueryInterceptorProviders;
private readonly IEnumerable<IDynamicQueryInterceptorProvider<TSource, TDestination>>
_dynamicQueryInterceptorProviders;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
public DynamicQueryHandlerBase(IQueryHandlerAsync queryHandlerAsync, public DynamicQueryHandlerBase(IQueryHandlerAsync queryHandlerAsync,
@ -32,7 +38,8 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
} }
protected virtual Task<IQueryable<TSource>> GetQueryableAsync(IDynamicQuery query, CancellationToken cancellationToken = default) protected virtual Task<IQueryable<TSource>> GetQueryableAsync(IDynamicQuery query,
CancellationToken cancellationToken = default)
{ {
if (_queryableProviders.Any()) if (_queryableProviders.Any())
{ {
@ -56,7 +63,8 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
yield return _serviceProvider.GetService(type) as IQueryInterceptor; yield return _serviceProvider.GetService(type) as IQueryInterceptor;
} }
protected async Task<IQueryExecutionResult<TDestination>> ProcessQueryAsync(IDynamicQuery query, CancellationToken cancellationToken = default) protected async Task<IQueryExecutionResult<TDestination>> ProcessQueryAsync(IDynamicQuery query,
CancellationToken cancellationToken = default)
{ {
var source = await GetQueryableAsync(query, cancellationToken); var source = await GetQueryableAsync(query, cancellationToken);
source = await AlterSourceAsync(source, query, cancellationToken); source = await AlterSourceAsync(source, query, cancellationToken);
@ -67,11 +75,13 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
_queryHandlerAsync.AddInterceptor(interceptor); _queryHandlerAsync.AddInterceptor(interceptor);
var criteria = CreateCriteriaFromQuery(query); var criteria = CreateCriteriaFromQuery(query);
var result = await _queryHandlerAsync.ExecuteAsync<TSource, TDestination>(source, criteria, options, cancellationToken); var result =
await _queryHandlerAsync.ExecuteAsync<TSource, TDestination>(source, criteria, options, cancellationToken);
return result; return result;
} }
protected virtual async Task<IQueryable<TSource>> AlterSourceAsync(IQueryable<TSource> source, IDynamicQuery query, CancellationToken cancellationToken) protected virtual async Task<IQueryable<TSource>> AlterSourceAsync(IQueryable<TSource> source, IDynamicQuery query,
CancellationToken cancellationToken)
{ {
foreach (var t in _alterQueryableServices) foreach (var t in _alterQueryableServices)
source = await t.AlterQueryableAsync(source, query, cancellationToken); source = await t.AlterQueryableAsync(source, query, cancellationToken);
@ -81,16 +91,94 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
protected virtual IQueryCriteria CreateCriteriaFromQuery(IDynamicQuery query) protected virtual IQueryCriteria CreateCriteriaFromQuery(IDynamicQuery query)
{ {
var filters = query?.GetFilters() ?? new List<IFilter>();
ConvertFilterValuesToPropertyTypes(filters);
var criteria = new QueryCriteria var criteria = new QueryCriteria
{ {
Page = query?.GetPage(), Page = query?.GetPage(),
PageSize = query?.GetPageSize(), PageSize = query?.GetPageSize(),
Filters = query?.GetFilters() ?? new List<IFilter>(), Filters = filters,
Sorts = query?.GetSorts() ?? new List<ISort>(), Sorts = query?.GetSorts() ?? new List<ISort>(),
Groups = query?.GetGroups() ?? new List<IGroup>(), Groups = query?.GetGroups() ?? new List<IGroup>(),
Aggregates = query?.GetAggregates() ?? new List<IAggregate>() Aggregates = query?.GetAggregates() ?? new List<IAggregate>()
}; };
return criteria; return criteria;
} }
/// <summary>
/// Converts string filter values to the correct CLR type based on TSource property types.
/// This handles the case where transport layers (e.g. gRPC) pass all values as strings,
/// but PoweredSoft.DynamicLinq needs the actual type to build LINQ expressions.
/// </summary>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087",
Justification = "TSource properties are preserved by EF Core and DynamicLinq usage")]
private static void ConvertFilterValuesToPropertyTypes(List<IFilter> filters)
{
for (var i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
if (filter is SimpleFilter simpleFilter)
{
if (simpleFilter.Value == null)
{
filters.RemoveAt(i);
continue;
}
if (simpleFilter.Value is string strValue && !string.IsNullOrEmpty(strValue))
{
var propertyType = ResolvePropertyType(typeof(TSource), simpleFilter.Path);
if (propertyType == null)
continue;
var targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
if (targetType == typeof(DateTime))
{
if (DateTime.TryParse(strValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal,
out var dt))
{
simpleFilter.Value = DateTime.SpecifyKind(dt, DateTimeKind.Utc);
}
}
else if (targetType == typeof(DateTimeOffset))
{
if (DateTimeOffset.TryParse(strValue, CultureInfo.InvariantCulture, DateTimeStyles.None,
out var dto))
{
simpleFilter.Value = dto;
}
}
}
}
else if (filter is CompositeFilter compositeFilter && compositeFilter.Filters != null)
{
ConvertFilterValuesToPropertyTypes(compositeFilter.Filters);
}
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070",
Justification = "Property types are preserved by EF Core and DynamicLinq usage")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075",
Justification = "Nested property type resolution is inherently dynamic")]
static Type? ResolvePropertyType(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, string? path)
{
if (string.IsNullOrEmpty(path))
return null;
var currentType = type;
foreach (var part in path.Split('.'))
{
var property = currentType.GetProperty(part,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (property == null)
return null;
currentType = property.PropertyType;
}
return currentType;
}
}
} }

View File

@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using PoweredSoft.Data.Core; using PoweredSoft.Data.Core;
@ -91,10 +91,10 @@ public static class ServiceCollectionExtensions
where TParams : class where TParams : class
=> AddDynamicQueryWithParams<TSourceAndDestination, TSourceAndDestination, TParams>(services, name: name); => AddDynamicQueryWithParams<TSourceAndDestination, TSourceAndDestination, TParams>(services, name: name);
public static IServiceCollection AddDynamicQueryWithParams<TSource, TDestination, TParams>(this IServiceCollection services, string name = null) public static IServiceCollection AddDynamicQueryWithParams<TSource, TDestination, TParams>(this IServiceCollection services, string name = null)
where TSource : class where TSource : class
where TDestination : class where TDestination : class
where TParams : class where TParams : class
{ {
// add query handler. // add query handler.
services.AddTransient<IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>, DynamicQueryHandler<TSource, TDestination, TParams>>(); services.AddTransient<IQueryHandler<IDynamicQuery<TSource, TDestination, TParams>, IQueryExecutionResult<TDestination>>, DynamicQueryHandler<TSource, TDestination, TParams>>();
@ -133,7 +133,7 @@ public static class ServiceCollectionExtensions
where TParams : class where TParams : class
where TService : class, IAlterQueryableService<TSourceAndTDestination, TSourceAndTDestination, TParams> where TService : class, IAlterQueryableService<TSourceAndTDestination, TSourceAndTDestination, TParams>
{ {
return services.AddTransient<IAlterQueryableService< TSourceAndTDestination, TSourceAndTDestination, TParams>, TService>(); return services.AddTransient<IAlterQueryableService<TSourceAndTDestination, TSourceAndTDestination, TParams>, TService>();
} }
public static IServiceCollection AddAlterQueryableWithParams<TSource, TDestination, TParams, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TService> public static IServiceCollection AddAlterQueryableWithParams<TSource, TDestination, TParams, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TService>

View File

@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.Abstractions;

View File

@ -0,0 +1,256 @@
#pragma warning disable RS1035 // Do not use APIs banned for analyzers - This is an MSBuild task, not an analyzer
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Svrnty.CQRS.Grpc.Generators;
/// <summary>
/// MSBuild task that generates .proto files by creating its own Roslyn compilation.
/// This runs BEFORE CoreCompile to solve the source generator timing issue.
/// </summary>
public class GenerateProtoFileTask : Task
{
/// <summary>
/// The project directory
/// </summary>
[Required]
public string ProjectDirectory { get; set; } = string.Empty;
/// <summary>
/// The output directory where the proto file should be written (typically Protos/)
/// </summary>
[Required]
public string OutputDirectory { get; set; } = string.Empty;
/// <summary>
/// The name of the proto file to generate (typically cqrs_services.proto)
/// </summary>
[Required]
public string ProtoFileName { get; set; } = string.Empty;
/// <summary>
/// The C# source files to compile (from @(Compile) ItemGroup)
/// </summary>
[Required]
public ITaskItem[] SourceFiles { get; set; } = Array.Empty<ITaskItem>();
/// <summary>
/// The assembly references (from @(ReferencePath) ItemGroup)
/// </summary>
[Required]
public ITaskItem[] References { get; set; } = Array.Empty<ITaskItem>();
/// <summary>
/// The root namespace of the project
/// </summary>
public string RootNamespace { get; set; } = string.Empty;
/// <summary>
/// The assembly name of the project
/// </summary>
public string AssemblyName { get; set; } = string.Empty;
public override bool Execute()
{
try
{
Log.LogMessage(MessageImportance.High,
"Svrnty.CQRS.Grpc: Generating proto file via MSBuild task...");
// Determine the namespace for the proto file
var projectNamespace = !string.IsNullOrEmpty(RootNamespace) ? RootNamespace
: !string.IsNullOrEmpty(AssemblyName) ? AssemblyName
: "Generated";
var grpcNamespace = $"{projectNamespace}.Grpc";
var packageName = "cqrs";
// Create the compilation
var compilation = CreateCompilation();
if (compilation == null)
{
Log.LogWarning("Svrnty.CQRS.Grpc: Could not create compilation. Writing placeholder proto file.");
WritePlaceholderProto(grpcNamespace);
return true;
}
// Check for compilation errors that would prevent proper analysis
var diagnostics = compilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.ToList();
if (diagnostics.Count > 0)
{
Log.LogMessage(MessageImportance.Normal,
$"Svrnty.CQRS.Grpc: Compilation has {diagnostics.Count} errors. Attempting to generate proto anyway...");
// Log first few errors for debugging
foreach (var diag in diagnostics.Take(5))
{
Log.LogMessage(MessageImportance.Low, $" {diag.GetMessage()}");
}
}
// Use the ProtoFileGenerator to generate content
var generator = new ProtoFileGenerator(compilation);
var protoContent = generator.Generate(packageName, grpcNamespace);
// Check if we got meaningful content
if (string.IsNullOrWhiteSpace(protoContent) || !protoContent.Contains("rpc "))
{
Log.LogMessage(MessageImportance.High,
"Svrnty.CQRS.Grpc: No commands/queries/notifications found. Writing minimal proto file.");
WritePlaceholderProto(grpcNamespace);
return true;
}
// Ensure output directory exists
var fullOutputPath = Path.IsPathRooted(OutputDirectory)
? OutputDirectory
: Path.Combine(ProjectDirectory, OutputDirectory);
Directory.CreateDirectory(fullOutputPath);
// Write the proto file
var protoFilePath = Path.Combine(fullOutputPath, ProtoFileName);
File.WriteAllText(protoFilePath, protoContent);
Log.LogMessage(MessageImportance.High,
$"Svrnty.CQRS.Grpc: Successfully generated proto file at {protoFilePath}");
return true;
}
catch (Exception ex)
{
Log.LogErrorFromException(ex, true);
return false;
}
}
private CSharpCompilation? CreateCompilation()
{
try
{
// Parse all source files into syntax trees
var syntaxTrees = new List<SyntaxTree>();
foreach (var sourceFile in SourceFiles)
{
var filePath = sourceFile.ItemSpec;
if (!Path.IsPathRooted(filePath))
{
filePath = Path.Combine(ProjectDirectory, filePath);
}
if (!File.Exists(filePath))
{
Log.LogMessage(MessageImportance.Low, $"Source file not found: {filePath}");
continue;
}
try
{
var sourceText = File.ReadAllText(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(
sourceText,
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest),
path: filePath);
syntaxTrees.Add(syntaxTree);
}
catch (Exception ex)
{
Log.LogMessage(MessageImportance.Low, $"Failed to parse {filePath}: {ex.Message}");
}
}
if (syntaxTrees.Count == 0)
{
Log.LogWarning("Svrnty.CQRS.Grpc: No source files could be parsed.");
return null;
}
Log.LogMessage(MessageImportance.Normal,
$"Svrnty.CQRS.Grpc: Parsed {syntaxTrees.Count} source files");
// Create metadata references from the References
var metadataReferences = new List<MetadataReference>();
foreach (var reference in References)
{
var refPath = reference.ItemSpec;
if (!File.Exists(refPath))
{
Log.LogMessage(MessageImportance.Low, $"Reference not found: {refPath}");
continue;
}
try
{
var metadataRef = MetadataReference.CreateFromFile(refPath);
metadataReferences.Add(metadataRef);
}
catch (Exception ex)
{
Log.LogMessage(MessageImportance.Low, $"Failed to load reference {refPath}: {ex.Message}");
}
}
Log.LogMessage(MessageImportance.Normal,
$"Svrnty.CQRS.Grpc: Loaded {metadataReferences.Count} references");
// Create the compilation
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithNullableContextOptions(NullableContextOptions.Enable);
var assemblyName = !string.IsNullOrEmpty(AssemblyName) ? AssemblyName : "TempCompilation";
var compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees,
metadataReferences,
compilationOptions);
return compilation;
}
catch (Exception ex)
{
Log.LogMessage(MessageImportance.High,
$"Svrnty.CQRS.Grpc: Failed to create compilation: {ex.Message}");
return null;
}
}
private void WritePlaceholderProto(string grpcNamespace)
{
var placeholderProto = $@"syntax = ""proto3"";
option csharp_namespace = ""{grpcNamespace}"";
package cqrs;
// Placeholder proto file - will be regenerated when commands/queries are available
// Using namespace: {grpcNamespace}
// Empty service definitions so Grpc.Tools generates base classes
service CommandService {{
}}
service QueryService {{
}}
service DynamicQueryService {{
}}
";
var fullOutputPath = Path.IsPathRooted(OutputDirectory)
? OutputDirectory
: Path.Combine(ProjectDirectory, OutputDirectory);
Directory.CreateDirectory(fullOutputPath);
var protoFilePath = Path.Combine(fullOutputPath, ProtoFileName);
File.WriteAllText(protoFilePath, placeholderProto);
Log.LogMessage(MessageImportance.High,
$"Svrnty.CQRS.Grpc: Wrote placeholder proto file at {protoFilePath}");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,102 +1,101 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Svrnty.CQRS.Grpc.Generators.Helpers namespace Svrnty.CQRS.Grpc.Generators.Helpers;
internal static class ProtoTypeMapper
{ {
internal static class ProtoTypeMapper private static readonly Dictionary<string, string> TypeMap = new Dictionary<string, string>
{ {
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 byte[] as bytes proto type (NOT repeated uint32)
if (csharpType == "System.Byte[]" || csharpType == "byte[]" || csharpType == "Byte[]")
{ {
// Primitives return "bytes";
{ "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 byte[] as bytes proto type (NOT repeated uint32)
if (csharpType == "System.Byte[]" || csharpType == "byte[]" || csharpType == "Byte[]")
{
return "bytes";
}
// 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("?", "");
} }
// 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("?", "");
} }
} }

View File

@ -1,83 +1,82 @@
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Svrnty.CQRS.Grpc.Generators.Models namespace Svrnty.CQRS.Grpc.Generators.Models;
public class CommandInfo
{ {
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 List<PropertyInfo> ResultProperties { get; set; }
public bool IsResultPrimitiveType { get; set; }
public CommandInfo()
{ {
public string Name { get; set; } Name = string.Empty;
public string FullyQualifiedName { get; set; } FullyQualifiedName = string.Empty;
public string Namespace { get; set; } Namespace = string.Empty;
public List<PropertyInfo> Properties { get; set; } Properties = new List<PropertyInfo>();
public string? ResultType { get; set; } HandlerInterfaceName = string.Empty;
public string? ResultFullyQualifiedName { get; set; } ResultProperties = new List<PropertyInfo>();
public bool HasResult => ResultType != null; IsResultPrimitiveType = false;
public string HandlerInterfaceName { get; set; } }
public List<PropertyInfo> ResultProperties { get; set; } }
public bool IsResultPrimitiveType { get; set; }
public class PropertyInfo
public CommandInfo() {
{ public string Name { get; set; }
Name = string.Empty; public string Type { get; set; }
FullyQualifiedName = string.Empty; public string FullyQualifiedType { get; set; }
Namespace = string.Empty; public string ProtoType { get; set; }
Properties = new List<PropertyInfo>(); public int FieldNumber { get; set; }
HandlerInterfaceName = string.Empty; public bool IsComplexType { get; set; }
ResultProperties = new List<PropertyInfo>(); public List<PropertyInfo> NestedProperties { get; set; }
IsResultPrimitiveType = false;
} // Type conversion metadata
} public bool IsEnum { get; set; }
public bool IsList { get; set; }
public class PropertyInfo public bool IsNullable { get; set; }
{ public bool IsDecimal { get; set; }
public string Name { get; set; } public bool IsDateTime { get; set; }
public string Type { get; set; } public bool IsDateTimeOffset { get; set; }
public string FullyQualifiedType { get; set; } public bool IsGuid { get; set; }
public string ProtoType { get; set; } public bool IsJsonElement { get; set; }
public int FieldNumber { get; set; } public bool IsBinaryType { get; set; } // Stream, byte[], MemoryStream
public bool IsComplexType { get; set; } public bool IsStream { get; set; } // Specifically Stream types (not byte[])
public List<PropertyInfo> NestedProperties { get; set; } public bool IsReadOnly { get; set; } // Read-only/computed properties should be skipped
public bool IsValueTypeCollection { get; set; } // Value types that implement IList<T> (like NpgsqlPolygon)
// Type conversion metadata public string? ElementType { get; set; }
public bool IsEnum { get; set; } public bool IsElementComplexType { get; set; }
public bool IsList { get; set; } public bool IsElementGuid { get; set; }
public bool IsNullable { get; set; } public List<PropertyInfo>? ElementNestedProperties { get; set; }
public bool IsDecimal { get; set; }
public bool IsDateTime { get; set; } public PropertyInfo()
public bool IsDateTimeOffset { get; set; } {
public bool IsGuid { get; set; } Name = string.Empty;
public bool IsJsonElement { get; set; } Type = string.Empty;
public bool IsBinaryType { get; set; } // Stream, byte[], MemoryStream FullyQualifiedType = string.Empty;
public bool IsStream { get; set; } // Specifically Stream types (not byte[]) ProtoType = string.Empty;
public bool IsReadOnly { get; set; } // Read-only/computed properties should be skipped IsComplexType = false;
public bool IsValueTypeCollection { get; set; } // Value types that implement IList<T> (like NpgsqlPolygon) NestedProperties = new List<PropertyInfo>();
public string? ElementType { get; set; } IsEnum = false;
public bool IsElementComplexType { get; set; } IsList = false;
public bool IsElementGuid { get; set; } IsNullable = false;
public List<PropertyInfo>? ElementNestedProperties { get; set; } IsDecimal = false;
IsDateTime = false;
public PropertyInfo() IsDateTimeOffset = false;
{ IsGuid = false;
Name = string.Empty; IsJsonElement = false;
Type = string.Empty; IsBinaryType = false;
FullyQualifiedType = string.Empty; IsStream = false;
ProtoType = string.Empty; IsReadOnly = false;
IsComplexType = false; IsValueTypeCollection = false;
NestedProperties = new List<PropertyInfo>(); IsElementComplexType = false;
IsEnum = false; IsElementGuid = false;
IsList = false;
IsNullable = false;
IsDecimal = false;
IsDateTime = false;
IsDateTimeOffset = false;
IsGuid = false;
IsJsonElement = false;
IsBinaryType = false;
IsStream = false;
IsReadOnly = false;
IsValueTypeCollection = false;
IsElementComplexType = false;
IsElementGuid = false;
}
} }
} }

View File

@ -1,28 +1,27 @@
namespace Svrnty.CQRS.Grpc.Generators.Models namespace Svrnty.CQRS.Grpc.Generators.Models;
{
public class DynamicQueryInfo
{
public string Name { get; set; }
public string SourceType { get; set; }
public string SourceTypeFullyQualified { get; set; }
public string DestinationType { get; set; }
public string DestinationTypeFullyQualified { get; set; }
public string? ParamsType { get; set; }
public string? ParamsTypeFullyQualified { get; set; }
public string HandlerInterfaceName { get; set; }
public string QueryInterfaceName { get; set; }
public bool HasParams { get; set; }
public DynamicQueryInfo() public class DynamicQueryInfo
{ {
Name = string.Empty; public string Name { get; set; }
SourceType = string.Empty; public string SourceType { get; set; }
SourceTypeFullyQualified = string.Empty; public string SourceTypeFullyQualified { get; set; }
DestinationType = string.Empty; public string DestinationType { get; set; }
DestinationTypeFullyQualified = string.Empty; public string DestinationTypeFullyQualified { get; set; }
HandlerInterfaceName = string.Empty; public string? ParamsType { get; set; }
QueryInterfaceName = string.Empty; public string? ParamsTypeFullyQualified { get; set; }
HasParams = false; public string HandlerInterfaceName { get; set; }
} public string QueryInterfaceName { get; set; }
public bool HasParams { get; set; }
public DynamicQueryInfo()
{
Name = string.Empty;
SourceType = string.Empty;
SourceTypeFullyQualified = string.Empty;
DestinationType = string.Empty;
DestinationTypeFullyQualified = string.Empty;
HandlerInterfaceName = string.Empty;
QueryInterfaceName = string.Empty;
HasParams = false;
} }
} }

View File

@ -1,50 +1,49 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Svrnty.CQRS.Grpc.Generators.Models namespace Svrnty.CQRS.Grpc.Generators.Models;
/// <summary>
/// Represents a discovered streaming notification type for proto/gRPC generation.
/// </summary>
public class NotificationInfo
{ {
/// <summary> /// <summary>
/// Represents a discovered streaming notification type for proto/gRPC generation. /// The notification type name (e.g., "InventoryChangeNotification").
/// </summary> /// </summary>
public class NotificationInfo public string Name { get; set; }
/// <summary>
/// The fully qualified type name including namespace.
/// </summary>
public string FullyQualifiedName { get; set; }
/// <summary>
/// The namespace of the notification type.
/// </summary>
public string Namespace { get; set; }
/// <summary>
/// The property name used as the subscription key (from [StreamingNotification] attribute).
/// </summary>
public string SubscriptionKeyProperty { get; set; }
/// <summary>
/// The subscription key property info.
/// </summary>
public PropertyInfo SubscriptionKeyInfo { get; set; }
/// <summary>
/// All properties of the notification type.
/// </summary>
public List<PropertyInfo> Properties { get; set; }
public NotificationInfo()
{ {
/// <summary> Name = string.Empty;
/// The notification type name (e.g., "InventoryChangeNotification"). FullyQualifiedName = string.Empty;
/// </summary> Namespace = string.Empty;
public string Name { get; set; } SubscriptionKeyProperty = string.Empty;
SubscriptionKeyInfo = new PropertyInfo();
/// <summary> Properties = new List<PropertyInfo>();
/// The fully qualified type name including namespace.
/// </summary>
public string FullyQualifiedName { get; set; }
/// <summary>
/// The namespace of the notification type.
/// </summary>
public string Namespace { get; set; }
/// <summary>
/// The property name used as the subscription key (from [StreamingNotification] attribute).
/// </summary>
public string SubscriptionKeyProperty { get; set; }
/// <summary>
/// The subscription key property info.
/// </summary>
public PropertyInfo SubscriptionKeyInfo { get; set; }
/// <summary>
/// All properties of the notification type.
/// </summary>
public List<PropertyInfo> Properties { get; set; }
public NotificationInfo()
{
Name = string.Empty;
FullyQualifiedName = string.Empty;
Namespace = string.Empty;
SubscriptionKeyProperty = string.Empty;
SubscriptionKeyInfo = new PropertyInfo();
Properties = new List<PropertyInfo>();
}
} }
} }

View File

@ -1,30 +1,29 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Svrnty.CQRS.Grpc.Generators.Models 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 List<PropertyInfo> ResultProperties { get; set; }
public bool IsResultPrimitiveType { get; set; }
public QueryInfo() public class QueryInfo
{ {
Name = string.Empty; public string Name { get; set; }
FullyQualifiedName = string.Empty; public string FullyQualifiedName { get; set; }
Namespace = string.Empty; public string Namespace { get; set; }
Properties = new List<PropertyInfo>(); public List<PropertyInfo> Properties { get; set; }
ResultType = string.Empty; public string ResultType { get; set; }
ResultFullyQualifiedName = string.Empty; public string ResultFullyQualifiedName { get; set; }
HandlerInterfaceName = string.Empty; public string HandlerInterfaceName { get; set; }
ResultProperties = new List<PropertyInfo>(); public List<PropertyInfo> ResultProperties { get; set; }
IsResultPrimitiveType = false; public bool IsResultPrimitiveType { 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;
ResultProperties = new List<PropertyInfo>();
IsResultPrimitiveType = false;
} }
} }

View File

@ -413,17 +413,21 @@ internal class ProtoFileGenerator
private void GenerateComplexTypeMessage(INamedTypeSymbol? type) private void GenerateComplexTypeMessage(INamedTypeSymbol? type)
{ {
if (type == null || _generatedMessages.Contains(type.Name)) if (type == null)
return;
var messageName = ProtoFileTypeMapper.GetProtoMessageName(type);
if (_generatedMessages.Contains(messageName))
return; return;
// Don't generate messages for system types or primitives // Don't generate messages for system types or primitives
if (type.ContainingNamespace?.ToString().StartsWith("System") == true) if (type.ContainingNamespace?.ToString().StartsWith("System") == true)
return; return;
_generatedMessages.Add(type.Name); _generatedMessages.Add(messageName);
_messagesBuilder.AppendLine($"// {type.Name} entity"); _messagesBuilder.AppendLine($"// {messageName} entity");
_messagesBuilder.AppendLine($"message {type.Name} {{"); _messagesBuilder.AppendLine($"message {messageName} {{");
// Collect nested complex types to generate after closing this message // Collect nested complex types to generate after closing this message
var nestedComplexTypes = new List<INamedTypeSymbol>(); var nestedComplexTypes = new List<INamedTypeSymbol>();

View File

@ -1,124 +0,0 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Svrnty.CQRS.Grpc.Generators;
/// <summary>
/// Incremental source generator that generates .proto files from C# commands and queries
/// </summary>
[Generator]
public class ProtoFileSourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Register a post-initialization output to generate the proto file
context.RegisterPostInitializationOutput(ctx =>
{
// Generate a placeholder - the actual proto will be generated in the source output
});
// Collect type declarations to trigger generation
// We use any type declaration as a trigger since ProtoFileGenerator scans all assemblies
var typeDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is TypeDeclarationSyntax,
transform: static (ctx, _) => GetTypeSymbol(ctx))
.Where(static m => m is not null)
.Collect();
// Combine with compilation to have access to it
var compilationAndTypes = context.CompilationProvider.Combine(typeDeclarations);
// Generate proto file when commands/queries change
context.RegisterSourceOutput(compilationAndTypes, (spc, source) =>
{
var (compilation, types) = source;
// Note: We no longer bail out early since ProtoFileGenerator now scans all referenced assemblies
// The types from source are just a trigger - the generator will find types from all assemblies
try
{
// Get the root namespace from the compilation - this matches what GrpcGenerator does
var rootNamespace = compilation.AssemblyName ?? "Generated";
var packageName = "cqrs";
var csharpNamespace = $"{rootNamespace}.Grpc";
// Generate the proto file content
var generator = new ProtoFileGenerator(compilation);
var protoContent = generator.Generate(packageName, csharpNamespace);
// Output as an embedded resource that can be extracted
var protoFileName = "cqrs_services.proto";
// Generate a C# class that contains the proto content
// This allows build tools to extract it if needed
var csContent = $$"""
// <auto-generated />
#nullable enable
namespace Svrnty.CQRS.Grpc.Generated
{
/// <summary>
/// Contains the auto-generated Protocol Buffer definition
/// </summary>
public static class GeneratedProtoFile
{
public const string FileName = "{{protoFileName}}";
public const string Content = @"{{protoContent.Replace("\"", "\"\"")}}";
}
}
""";
spc.AddSource("GeneratedProtoFile.g.cs", csContent);
// Report that we generated the proto content
var descriptor = new DiagnosticDescriptor(
"CQRSGRPC002",
"Proto file generated",
"Generated proto file content in GeneratedProtoFile class",
"Svrnty.CQRS.Grpc",
DiagnosticSeverity.Info,
isEnabledByDefault: true);
spc.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None));
}
catch (Exception ex)
{
// Report diagnostic if generation fails
var descriptor = new DiagnosticDescriptor(
"CQRSGRPC001",
"Proto file generation failed",
"Failed to generate proto file: {0}",
"Svrnty.CQRS.Grpc",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
spc.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None, ex.Message));
}
});
}
private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context)
{
var typeDecl = (TypeDeclarationSyntax)context.Node;
var symbol = context.SemanticModel.GetDeclaredSymbol(typeDecl) as INamedTypeSymbol;
// Skip if it has GrpcIgnore attribute
if (symbol?.GetAttributes().Any(a => a.AttributeClass?.Name == "GrpcIgnoreAttribute") == true)
return null;
return symbol;
}
private static string? GetBuildProperty(SourceProductionContext context, string propertyName)
{
// Try to get build properties from the compilation options
// This is a simplified approach - in practice, you might need analyzer config
return null; // Will use defaults
}
}

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Svrnty.CQRS.Grpc.Generators; namespace Svrnty.CQRS.Grpc.Generators;
@ -151,13 +152,27 @@ internal static class ProtoFileTypeMapper
// Complex types (classes/records) become message types // Complex types (classes/records) become message types
if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.TypeKind == TypeKind.Struct) if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.TypeKind == TypeKind.Struct)
{ {
return typeName; // Reference the message type by name return GetProtoMessageName(typeSymbol); // Reference the message type by name (handles generics)
} }
// Fallback // Fallback
return "string"; // Default to string for unknown types return "string"; // Default to string for unknown types
} }
/// <summary>
/// Gets the proto message name for a type, handling generic types by qualifying
/// with type arguments. e.g. Translation&lt;FaqTranslationQueryItem&gt; becomes TranslationOfFaqTranslationQueryItem.
/// </summary>
public static string GetProtoMessageName(ITypeSymbol typeSymbol)
{
if (typeSymbol is INamedTypeSymbol namedType && namedType.IsGenericType && namedType.TypeArguments.Length > 0)
{
var typeArgs = string.Join("And", namedType.TypeArguments.Select(t => GetProtoMessageName(t)));
return $"{namedType.Name}Of{typeArgs}";
}
return typeSymbol.Name;
}
/// <summary> /// <summary>
/// Converts C# PascalCase property name to proto snake_case field name. /// Converts C# PascalCase property name to proto snake_case field name.
/// Uses simple conversion: add underscore before each uppercase letter (except first). /// Uses simple conversion: add underscore before each uppercase letter (except first).

View File

@ -1,150 +0,0 @@
#pragma warning disable RS1035 // Do not use APIs banned for analyzers - This is an MSBuild task, not an analyzer
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Svrnty.CQRS.Grpc.Generators;
/// <summary>
/// MSBuild task that extracts the auto-generated proto file content from the source generator
/// output and writes it to disk so Grpc.Tools can process it
/// </summary>
public class WriteProtoFileTask : Task
{
/// <summary>
/// The project directory where we should look for generated files
/// </summary>
[Required]
public string ProjectDirectory { get; set; } = string.Empty;
/// <summary>
/// The intermediate output path (typically obj/Debug/net10.0)
/// </summary>
[Required]
public string IntermediateOutputPath { get; set; } = string.Empty;
/// <summary>
/// The output directory where the proto file should be written (typically Protos/)
/// </summary>
[Required]
public string OutputDirectory { get; set; } = string.Empty;
/// <summary>
/// The name of the proto file to generate (typically cqrs_services.proto)
/// </summary>
[Required]
public string ProtoFileName { get; set; } = string.Empty;
public override bool Execute()
{
try
{
Log.LogMessage(MessageImportance.High,
"Svrnty.CQRS.Grpc: Extracting auto-generated proto file...");
// Look for the generated C# file containing the proto content
// Source generators output to obj/Generated, not IntermediateOutputPath/Generated
var generatedFilePath = Path.Combine(
ProjectDirectory,
"obj",
"Generated",
"Svrnty.CQRS.Grpc.Generators",
"Svrnty.CQRS.Grpc.Generators.ProtoFileSourceGenerator",
"GeneratedProtoFile.g.cs"
);
if (!File.Exists(generatedFilePath))
{
Log.LogWarning(
$"Generated proto file not found at {generatedFilePath}. " +
"The proto file may not have been generated yet. This is normal on first build.");
// Write a minimal placeholder proto file so Grpc.Tools doesn't fail
// The real content will be generated on the next build
var placeholderProto = @"syntax = ""proto3"";
option csharp_namespace = ""Generated.Grpc"";
package cqrs;
// Placeholder proto file - will be regenerated on next build
";
var placeholderOutputPath = Path.Combine(ProjectDirectory, OutputDirectory);
Directory.CreateDirectory(placeholderOutputPath);
var placeholderProtoFilePath = Path.Combine(placeholderOutputPath, ProtoFileName);
File.WriteAllText(placeholderProtoFilePath, placeholderProto);
Log.LogMessage(MessageImportance.High,
$"Svrnty.CQRS.Grpc: Wrote placeholder proto file at {placeholderProtoFilePath}. " +
"Run build again to generate the actual proto content.");
return true;
}
// Read the generated C# file
var csContent = File.ReadAllText(generatedFilePath);
// Extract the proto content using a more robust approach
// Looking for: public const string Content = @"...";
var startMarker = "public const string Content = @\"";
var startIndex = csContent.IndexOf(startMarker);
if (startIndex < 0)
{
Log.LogError($"Could not find Content property in {generatedFilePath}");
return false;
}
startIndex += startMarker.Length;
// Find the closing "; - We need the LAST occurrence because the content contains escaped quotes
// The pattern is: Content = @"...content...";
// where content has "" for literal quotes
var endMarker = "\";";
// Find where the next field starts or class ends to limit our search
var nextFieldOrEnd = csContent.IndexOf("\n }", startIndex); // End of class
if (nextFieldOrEnd < 0)
{
nextFieldOrEnd = csContent.Length;
}
var endIndex = csContent.LastIndexOf(endMarker, nextFieldOrEnd, nextFieldOrEnd - startIndex);
if (endIndex < 0 || endIndex < startIndex)
{
Log.LogError($"Could not find end of Content property in {generatedFilePath}");
return false;
}
// Extract and unescape doubled quotes
var protoContent = csContent.Substring(startIndex, endIndex - startIndex);
protoContent = protoContent.Replace("\"\"", "\"");
Log.LogMessage(MessageImportance.High,
$"Extracted proto content length: {protoContent.Length} characters");
// Ensure output directory exists
var fullOutputPath = Path.Combine(ProjectDirectory, OutputDirectory);
Directory.CreateDirectory(fullOutputPath);
// Write the proto file
var protoFilePath = Path.Combine(fullOutputPath, ProtoFileName);
File.WriteAllText(protoFilePath, protoContent);
Log.LogMessage(MessageImportance.High,
$"Svrnty.CQRS.Grpc: Successfully generated proto file at {protoFilePath}");
return true;
}
catch (Exception ex)
{
Log.LogErrorFromException(ex, true);
return false;
}
}
}

View File

@ -9,29 +9,48 @@
<!-- Determine the assembly path (different for NuGet package vs project reference) --> <!-- Determine the assembly path (different for NuGet package vs project reference) -->
<PropertyGroup> <PropertyGroup>
<_GeneratorsAssemblyPath Condition="Exists('$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath> <_GeneratorsAssemblyPath Condition="Exists('$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath>
<_GeneratorsAssemblyPath Condition="'$(_GeneratorsAssemblyPath)' == '' AND Exists('$(MSBuildThisFileDirectory)Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath>
<_GeneratorsAssemblyPath Condition="'$(_GeneratorsAssemblyPath)' == '' AND Exists('$(MSBuildThisFileDirectory)\..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath> <_GeneratorsAssemblyPath Condition="'$(_GeneratorsAssemblyPath)' == '' AND Exists('$(MSBuildThisFileDirectory)\..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath>
<_GeneratorsAssemblyPath Condition="'$(_GeneratorsAssemblyPath)' == '' AND Exists('$(MSBuildThisFileDirectory)\..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath> <_GeneratorsAssemblyPath Condition="'$(_GeneratorsAssemblyPath)' == '' AND Exists('$(MSBuildThisFileDirectory)\..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath>
</PropertyGroup> </PropertyGroup>
<!-- Load the WriteProtoFileTask from the generator assembly --> <!-- Load the GenerateProtoFileTask from the generator assembly -->
<UsingTask TaskName="Svrnty.CQRS.Grpc.Generators.WriteProtoFileTask" <UsingTask TaskName="Svrnty.CQRS.Grpc.Generators.GenerateProtoFileTask"
AssemblyFile="$(_GeneratorsAssemblyPath)" AssemblyFile="$(_GeneratorsAssemblyPath)"
Condition="'$(_GeneratorsAssemblyPath)' != ''" /> Condition="'$(_GeneratorsAssemblyPath)' != ''" />
<!-- This target ensures the Protos directory exists before the generator runs --> <!-- This target ensures the Protos directory exists -->
<Target Name="EnsureProtosDirectory" BeforeTargets="CoreCompile"> <Target Name="EnsureProtosDirectory" BeforeTargets="SvrntyGenerateProtoFile">
<MakeDir Directories="$(ProtoOutputDirectory)" Condition="!Exists('$(ProtoOutputDirectory)')" /> <MakeDir Directories="$(ProtoOutputDirectory)" Condition="!Exists('$(ProtoOutputDirectory)')" />
</Target> </Target>
<!-- Extract the proto file from the source generator output BEFORE Grpc.Tools processes protos --> <!--
<!-- Runs before CoreCompile, after source generators have been executed --> Generate the proto file BEFORE Grpc.Tools processes protos and BEFORE CoreCompile.
<Target Name="SvrntyExtractProtoFile" BeforeTargets="CoreCompile" AfterTargets="ResolveProjectReferences" DependsOnTargets="EnsureProtosDirectory" Condition="'$(GenerateProtoFile)' == 'true'"> This runs AFTER ResolveAssemblyReferences so we have access to @(ReferencePath).
<Message Text="Svrnty.CQRS.Grpc: Extracting auto-generated proto file to $(ProtoOutputDirectory)\$(GeneratedProtoFileName)" Importance="high" />
<WriteProtoFileTask Key timing:
- AfterTargets="ResolveAssemblyReferences" ensures we have all references resolved
- BeforeTargets="_gRPC_GetProtoc;CoreCompile" ensures proto is generated before:
1. Grpc.Tools compiles the proto into C# (_gRPC_GetProtoc is Grpc.Tools' entry point)
2. CoreCompile compiles the project
-->
<Target Name="SvrntyGenerateProtoFile"
BeforeTargets="_gRPC_GetProtoc;CoreCompile"
AfterTargets="ResolveAssemblyReferences"
DependsOnTargets="EnsureProtosDirectory"
Condition="'$(GenerateProtoFile)' == 'true' AND '$(_GeneratorsAssemblyPath)' != ''">
<Message Text="Svrnty.CQRS.Grpc: Generating proto file from $(MSBuildProjectName)..." Importance="high" />
<Message Text="Svrnty.CQRS.Grpc: Source files count: @(Compile->Count())" Importance="normal" />
<Message Text="Svrnty.CQRS.Grpc: References count: @(ReferencePath->Count())" Importance="normal" />
<GenerateProtoFileTask
ProjectDirectory="$(MSBuildProjectDirectory)" ProjectDirectory="$(MSBuildProjectDirectory)"
IntermediateOutputPath="$(IntermediateOutputPath)"
OutputDirectory="$(ProtoOutputDirectory)" OutputDirectory="$(ProtoOutputDirectory)"
ProtoFileName="$(GeneratedProtoFileName)" /> ProtoFileName="$(GeneratedProtoFileName)"
SourceFiles="@(Compile)"
References="@(ReferencePath)"
RootNamespace="$(RootNamespace)"
AssemblyName="$(AssemblyName)" />
</Target> </Target>
</Project> </Project>

View File

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.Discovery; using Svrnty.CQRS.Discovery;
@ -43,7 +44,7 @@ public class CqrsBuilder
/// <summary> /// <summary>
/// Adds a command handler to the CQRS pipeline /// Adds a command handler to the CQRS pipeline
/// </summary> /// </summary>
public CqrsBuilder AddCommand<TCommand, TCommandHandler>() public CqrsBuilder AddCommand<TCommand, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TCommandHandler>()
where TCommand : class where TCommand : class
where TCommandHandler : class, ICommandHandler<TCommand> where TCommandHandler : class, ICommandHandler<TCommand>
{ {
@ -54,7 +55,7 @@ public class CqrsBuilder
/// <summary> /// <summary>
/// Adds a command handler with result to the CQRS pipeline /// Adds a command handler with result to the CQRS pipeline
/// </summary> /// </summary>
public CqrsBuilder AddCommand<TCommand, TResult, TCommandHandler>() public CqrsBuilder AddCommand<TCommand, TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TCommandHandler>()
where TCommand : class where TCommand : class
where TCommandHandler : class, ICommandHandler<TCommand, TResult> where TCommandHandler : class, ICommandHandler<TCommand, TResult>
{ {

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Svrnty.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Svrnty.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Discovery;

View File

@ -26,6 +26,10 @@
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" /> <None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" /> <ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Svrnty.CQRS.Configuration; using Svrnty.CQRS.Configuration;
namespace Svrnty.CQRS.MinimalApi; namespace Svrnty.CQRS;
public static class WebApplicationExtensions public static class WebApplicationExtensions
{ {

View File

@ -1,11 +1,11 @@
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Svrnty.CQRS; using Svrnty.CQRS;
using Svrnty.CQRS.Abstractions;
using Svrnty.CQRS.DynamicQuery;
using Svrnty.CQRS.FluentValidation; using Svrnty.CQRS.FluentValidation;
using Svrnty.CQRS.Grpc; using Svrnty.CQRS.Grpc;
using Svrnty.Sample;
using Svrnty.CQRS.MinimalApi; using Svrnty.CQRS.MinimalApi;
using Svrnty.CQRS.DynamicQuery; using Svrnty.Sample;
using Svrnty.CQRS.Abstractions;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);

View File

@ -1,5 +1,5 @@
using PoweredSoft.Data.Core;
using System.Linq.Expressions; using System.Linq.Expressions;
using PoweredSoft.Data.Core;
namespace Svrnty.Sample; namespace Svrnty.Sample;