diff --git a/.claude/.harness-version b/.claude/.harness-version new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/.claude/.harness-version @@ -0,0 +1 @@ +1.0.0 diff --git a/.claude/rules/commands-queries.md b/.claude/rules/commands-queries.md new file mode 100644 index 0000000..19a62eb --- /dev/null +++ b/.claude/rules/commands-queries.md @@ -0,0 +1,100 @@ +--- +paths: + - "*Command*.cs" + - "*Query*.cs" + - "*Handler*.cs" + - "**/Program.cs" + - "**/ServiceCollectionExtensions.cs" +--- + +# Commands & Queries + +## Commands Are Domain Actions, Not CRUD + +Commands express **user intent**. Name them as the business action being performed. + +- `PlaceOrderCommand` not `CreateOrderCommand` +- `ApproveExpenseCommand` not `UpdateExpenseCommand` +- `DeactivateAccountCommand` not `DeleteAccountCommand` + +One CRUD "Update" often becomes multiple distinct commands, each with its own validation and side effects. + +## File Structure + +Command, handler, and validator live in the **same file**, organized by feature: + +```csharp +// File: Features/Orders/PlaceOrderCommand.cs +public record PlaceOrderCommand +{ + public string CustomerId { get; set; } = string.Empty; + public List Items { get; set; } = []; +} + +public class PlaceOrderCommandValidator : AbstractValidator +{ + public PlaceOrderCommandValidator() + { + RuleFor(x => x.CustomerId).NotEmpty(); + RuleFor(x => x.Items).NotEmpty(); + } +} + +public class PlaceOrderCommandHandler : ICommandHandler +{ + public Task HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken = default) + { + // implementation + } +} +``` + +## Handler Interfaces + +```csharp +// Command with no result +ICommandHandler + Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + +// Command with result +ICommandHandler + Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + +// Query (always returns result) — only for single-entity lookups or non-queryable data +IQueryHandler + Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) +``` + +## Registration + +```csharp +// Command without result +services.AddCommand(); + +// Command with result +services.AddCommand(); + +// Command with result + validator (from Svrnty.CQRS.FluentValidation) +services.AddCommand(); + +// Regular query — ONLY for single-entity lookups or non-queryable results +services.AddQuery(); +``` + +## When to Use Regular IQueryHandler vs Dynamic Query + +**Use `IQueryHandler`** (rare): +- Single entity by ID: `FetchOrderByIdQuery` +- Non-entity results: `GetDashboardStatsQuery` +- Complex aggregation not expressible as IQueryable + +**Use Dynamic Query** (default — see dynamic-query.md rule): +- Any list/collection query +- Anything that needs pagination, filtering, or sorting + +## Rules + +- Always use `CancellationToken` — never omit it +- Commands/queries are `record` types with `{ get; set; }` properties +- Default string properties to `string.Empty`, collections to `[]` +- Naming: endpoint name is auto-derived by stripping `Command`/`Query` suffix and converting to lowerCamelCase diff --git a/.claude/rules/dynamic-query.md b/.claude/rules/dynamic-query.md new file mode 100644 index 0000000..5cb3fec --- /dev/null +++ b/.claude/rules/dynamic-query.md @@ -0,0 +1,97 @@ +--- +paths: + - "*QueryableProvider*.cs" + - "*AlterQueryable*.cs" + - "*DynamicQuery*.cs" + - "*Interceptor*.cs" +--- + +# Dynamic Queries — The Default Query Pattern + +**99% of queries should be dynamic queries.** They provide pagination, filtering, sorting, grouping, and aggregation for free. + +## The Standard Pattern: IQueryableProviderOverride + +```csharp +// File: Features/Orders/OrderQueryableProvider.cs +public class OrderQueryableProvider : IQueryableProviderOverride +{ + private readonly AppDbContext _db; + public OrderQueryableProvider(AppDbContext db) => _db = db; + + public Task> GetQueryableAsync(object query, CancellationToken ct = default) + => Task.FromResult(_db.Orders.AsQueryable()); +} +``` + +Registration — one line: +```csharp +builder.Services.AddDynamicQueryWithProvider(); +``` + +This automatically creates endpoints with full filtering, sorting, and pagination support. + +## Required Dependencies + +These must be registered before `AddSvrntyCqrs`: +```csharp +builder.Services.AddTransient(); +builder.Services.AddTransient(); +``` + +## Alter Queryable Services (Security Filters, Tenant Isolation) + +Use `IAlterQueryableService` to modify the queryable before execution — for security filters, tenant isolation, default ordering, etc. + +```csharp +public class OrderTenantFilter : IAlterQueryableService +{ + private readonly ITenantContext _tenant; + public OrderTenantFilter(ITenantContext tenant) => _tenant = tenant; + + public Task> AlterQueryableAsync( + IQueryable query, + IDynamicQuery dynamicQuery, + CancellationToken ct = default) + { + return Task.FromResult(query.Where(o => o.TenantId == _tenant.Id)); + } +} +``` + +Registration: +```csharp +builder.Services.AddAlterQueryable(); +``` + +## Interceptors + +Up to 5 interceptors per query type. These modify the PoweredSoft DynamicQuery criteria at query build time. + +```csharp +builder.Services.AddDynamicQueryInterceptor(); +``` + +## Source to Destination Mapping + +When the entity type differs from the DTO: +```csharp +// Provider returns the source entity queryable +public class OrderQueryableProvider : IQueryableProviderOverride { ... } + +// Registration maps source -> destination +builder.Services.AddDynamicQueryWithProvider(); +``` + +## Key Interfaces + +```csharp +IQueryableProvider + Task> GetQueryableAsync(object query, CancellationToken ct) + +IQueryableProviderOverride : IQueryableProvider + // Marker interface — same method, signals override registration + +IAlterQueryableService + Task> AlterQueryableAsync(IQueryable query, IDynamicQuery dynamicQuery, CancellationToken ct) +``` diff --git a/.claude/rules/grpc.md b/.claude/rules/grpc.md new file mode 100644 index 0000000..a2472ce --- /dev/null +++ b/.claude/rules/grpc.md @@ -0,0 +1,109 @@ +--- +paths: + - "**/*.proto" + - "**/Protos/**" + - "*Grpc*" +--- + +# gRPC Integration + +## Proto File Conventions + +Proto files live in `Protos/` directory. The source generator (`Svrnty.CQRS.Grpc.Generators`) auto-generates service implementations at compile time. + +### Service Structure +```protobuf +service CommandService { + rpc PlaceOrder (PlaceOrderCommandRequest) returns (PlaceOrderCommandResponse); +} + +service QueryService { + rpc FetchUserById (FetchUserByIdQueryRequest) returns (FetchUserByIdQueryResponse); +} + +service DynamicQueryService { + rpc QueryOrders (DynamicQueryOrdersRequest) returns (DynamicQueryOrdersResponse); +} +``` + +### Naming Rules +- RPC method name: command/query name without the `Command`/`Query` suffix +- Request message: `{Name}CommandRequest` or `{Name}QueryRequest` +- Response message: `{Name}CommandResponse` or `{Name}QueryResponse` +- Dynamic query request: `DynamicQuery{PluralEntity}Request` +- Dynamic query response: `DynamicQuery{PluralEntity}Response` +- Fields use `snake_case` (proto convention), mapped case-insensitively to C# `PascalCase` properties + +### Field Mapping +C# property names must match proto field names (case-insensitive): + +| C# Property | Proto Field | +|---------------|-----------------| +| `UserId` | `user_id` | +| `Name` | `name` | +| `EmailAddress`| `email_address` | +| `OrderItems` | `order_items` | + +### Type Mapping +| C# | Proto | +|-------------|-------------| +| `int` | `int32` | +| `long` | `int64` | +| `string` | `string` | +| `bool` | `bool` | +| `double` | `double` | +| `float` | `float` | +| `List` | `repeated T`| +| complex type| `message` | + +### Dynamic Query Messages (standard, reuse across entities) +```protobuf +message DynamicQueryFilter { + string path = 1; + int32 type = 2; // PoweredSoft.DynamicQuery.Core.FilterType + string value = 3; + repeated DynamicQueryFilter and = 4; + repeated DynamicQueryFilter or = 5; +} + +message DynamicQuerySort { + string path = 1; + bool ascending = 2; +} + +message DynamicQueryGroup { + string path = 1; +} + +message DynamicQueryAggregate { + string path = 1; + int32 type = 2; // PoweredSoft.DynamicQuery.Core.AggregateType +} +``` + +## Source Generator Behavior + +The generator in `Svrnty.CQRS.Grpc.Generators` auto-creates: +- `CommandServiceImpl` — implements `CommandService.CommandServiceBase` +- `QueryServiceImpl` — implements `QueryService.QueryServiceBase` +- `DynamicQueryServiceImpl` — implements `DynamicQueryService.DynamicQueryServiceBase` +- `GrpcServiceRegistration` — auto-registration code + +Generated implementations handle: +1. Request-to-POCO property mapping +2. Validator invocation (if registered) with Google Rich Error Model errors +3. Handler invocation with proper `CancellationToken` +4. DI scope management via `IServiceScopeFactory` + +## Validation in gRPC + +Validation errors return `google.rpc.Status` with `BadRequest` detail containing `FieldViolations`. This is automatic — do not manually validate in handlers. + +## Registration + +```csharp +builder.Services.AddSvrntyCqrs(cqrs => +{ + cqrs.AddGrpc(grpc => grpc.EnableReflection()); // reflection for grpcurl/grpcui +}); +``` diff --git a/.claude/rules/validation.md b/.claude/rules/validation.md new file mode 100644 index 0000000..cf89688 --- /dev/null +++ b/.claude/rules/validation.md @@ -0,0 +1,60 @@ +--- +paths: + - "*Validator*.cs" + - "*Validation*" +--- + +# FluentValidation Integration + +## Validator Pattern + +Validators live in the **same file** as their command/query, named `{CommandName}Validator`: + +```csharp +public class PlaceOrderCommandValidator : AbstractValidator +{ + public PlaceOrderCommandValidator() + { + RuleFor(x => x.CustomerId) + .NotEmpty() + .WithMessage("Customer is required"); + + RuleFor(x => x.Items) + .NotEmpty() + .WithMessage("At least one item is required"); + } +} +``` + +## Registration + +Validators are registered alongside their handler using the FluentValidation overloads: + +```csharp +// Command without result + validator +services.AddCommand(); + +// Command with result + validator +services.AddCommand(); + +// Query + validator +services.AddQuery(); +``` + +These come from `Svrnty.CQRS.FluentValidation.ServiceCollectionExtensions` — they call the base `AddCommand`/`AddQuery` then register `IValidator`. + +## Error Formats by Protocol + +The framework handles validation execution automatically. Errors are returned as: + +- **HTTP (Minimal API)**: RFC 7807 Problem Details — `400 Bad Request` with structured error body +- **gRPC**: Google Rich Error Model — `google.rpc.Status` with `BadRequest` detail containing `FieldViolations` + +You do NOT need to manually invoke validation in handlers. The endpoint layer (Minimal API or gRPC source-generated service) handles it. + +## Rules + +- Inherit from `AbstractValidator`, not `IValidator` directly +- Define all rules in the constructor +- Always include `.WithMessage()` for user-facing error messages +- Validator constructor can inject services for async/database validation diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..486f8aa --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,79 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build:*)", + "Bash(dotnet clean:*)", + "Bash(dotnet restore:*)", + "Bash(dotnet run:*)", + "Bash(dotnet test:*)", + "Bash(dotnet format:*)", + "Bash(dotnet add:*)", + "Bash(dotnet remove:*)", + "Bash(dotnet sln:*)", + "Bash(dotnet --list-sdks:*)", + "Bash(dotnet tool install:*)", + "Bash(dotnet ef:*)", + "Bash(grpcurl:*)", + "Bash(protogen:*)", + "Bash(mkdir:*)", + "Bash(ls:*)", + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git diff:*)", + "Bash(git branch:*)", + "Bash(git stash:*)", + "Bash(git add:*)", + "Bash(git checkout:*)", + "WebSearch", + "WebFetch(domain:learn.microsoft.com)", + "WebFetch(domain:github.com)", + "WebFetch(domain:www.nuget.org)", + "WebFetch(domain:stackoverflow.com)" + ], + "ask": [ + "Bash(dotnet publish:*)", + "Bash(dotnet pack:*)", + "Bash(git push:*)", + "Bash(git commit:*)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash -c 'INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r \".tool_input.file_path // empty\"); if [[ -n \"$FILE\" && \"$FILE\" == *.cs ]]; then dotnet format --include \"$FILE\" --no-restore --diagnostics IDE0005 IDE0055 IDE0161 2>/dev/null || true; fi'", + "timeout": 15, + "statusMessage": "Formatting..." + } + ] + }, + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash -c 'INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r \".tool_input.file_path // empty\"); if [[ -n \"$FILE\" && \"$FILE\" == *.proto ]]; then dotnet build --no-restore 2>&1 | grep -E \"(error|warning)\" | head -10; fi'", + "timeout": 30, + "statusMessage": "Validating proto..." + } + ] + } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash -c 'BUILD=$(dotnet build --no-restore 2>&1); if echo \"$BUILD\" | grep -q \"Build FAILED\"; then echo \"BUILD FAILED — errors below:\" >&2; echo \"$BUILD\" | grep -E \"error CS\" >&2; exit 2; fi'", + "timeout": 60, + "statusMessage": "Verifying build..." + } + ] + } + ] + } +} diff --git a/.claude/skills/add-command/SKILL.md b/.claude/skills/add-command/SKILL.md new file mode 100644 index 0000000..fc1f105 --- /dev/null +++ b/.claude/skills/add-command/SKILL.md @@ -0,0 +1,103 @@ +--- +name: add-command +description: Scaffold a new CQRS command with handler, validator, and DI registration. Use when the user wants to add a new command to a project. +argument-hint: +--- + +# Add Command + +Scaffold a new command based on: $ARGUMENTS + +## Instructions + +### 1. Determine the command name + +The name must express **domain intent**, not CRUD. If the user's description sounds like CRUD, translate it: +- "create a user" → `RegisterUserCommand` +- "update the order status" → contextual: `ShipOrderCommand`, `CancelOrderCommand`, `ApproveOrderCommand` +- "delete an account" → `DeactivateAccountCommand` or `CloseAccountCommand` + +Ask the user to clarify if the intent is ambiguous (e.g., "update order" could mean many things). + +### 2. Determine the feature folder + +Place the file in the appropriate feature folder: `Features/{DomainArea}/` +If the folder doesn't exist, create it. If the project doesn't use `Features/` yet, check the existing structure and follow the same pattern. + +### 3. Create the file + +Create a single file `Features/{DomainArea}/{CommandName}Command.cs` containing all three classes: + +```csharp +using FluentValidation; +using Svrnty.CQRS.Abstractions; + +namespace {ProjectNamespace}.Features.{DomainArea}; + +// 1. Command POCO — record type, properties with defaults +public record {CommandName}Command +{ + // Properties based on user description + // Strings default to string.Empty, collections to [] +} + +// 2. Validator — rules in constructor, always include .WithMessage() +public class {CommandName}CommandValidator : AbstractValidator<{CommandName}Command> +{ + public {CommandName}CommandValidator() + { + // Validation rules based on command properties + } +} + +// 3. Handler — always async with CancellationToken +public class {CommandName}CommandHandler : ICommandHandler<{CommandName}Command, {ResultType}> +{ + public Task<{ResultType}> HandleAsync({CommandName}Command command, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} +``` + +If the command has **no return value**, use `ICommandHandler<{CommandName}Command>` (single generic param) with `Task HandleAsync(...)`. + +### 4. Register in DI + +Find the project's service registration (typically `Program.cs` or a dedicated registration method) and add: + +```csharp +// With validator (preferred) +builder.Services.AddCommand<{CommandName}Command, {ResultType}, {CommandName}CommandHandler, {CommandName}CommandValidator>(); + +// Without validator +builder.Services.AddCommand<{CommandName}Command, {ResultType}, {CommandName}CommandHandler>(); + +// No return value + validator +builder.Services.AddCommand<{CommandName}Command, {CommandName}CommandHandler, {CommandName}CommandValidator>(); +``` + +Ensure the `using Svrnty.CQRS.FluentValidation;` namespace is imported if using the validator overload. + +### 5. Proto message (if project uses gRPC) + +If the project has a `Protos/` directory, add the corresponding proto message: + +```protobuf +// In the CommandService definition +rpc {CommandName} ({CommandName}CommandRequest) returns ({CommandName}CommandResponse); + +// Request message +message {CommandName}CommandRequest { + // fields matching command properties, snake_case, numbered sequentially +} + +// Response message +message {CommandName}CommandResponse { + {result_type} result = 1; // omit if command has no return value +} +``` + +### 6. Summary + +After creating everything, list what was created and where. diff --git a/.claude/skills/add-dynamic-query/SKILL.md b/.claude/skills/add-dynamic-query/SKILL.md new file mode 100644 index 0000000..9bc4c67 --- /dev/null +++ b/.claude/skills/add-dynamic-query/SKILL.md @@ -0,0 +1,125 @@ +--- +name: add-dynamic-query +description: Scaffold a dynamic query with IQueryableProviderOverride for automatic pagination, filtering, and sorting. This is the DEFAULT choice for any list/collection query. +argument-hint: +--- + +# Add Dynamic Query + +Scaffold a dynamic query based on: $ARGUMENTS + +**This is the default query pattern.** It gives pagination, filtering, sorting, grouping, and aggregation for free. + +## Instructions + +### 1. Identify the entity + +Determine the source entity type from the user's description. If the project uses a DAL with EF Core, the entity should already exist in the DbContext. + +### 2. Determine the feature folder + +Place the file in: `Features/{DomainArea}/` + +### 3. Create the queryable provider + +Create `Features/{DomainArea}/{Entity}QueryableProvider.cs`: + +```csharp +using Svrnty.CQRS.DynamicQuery.Abstractions; + +namespace {ProjectNamespace}.Features.{DomainArea}; + +public class {Entity}QueryableProvider : IQueryableProviderOverride<{Entity}> +{ + private readonly {DbContextType} _db; + public {Entity}QueryableProvider({DbContextType} db) => _db = db; + + public Task> GetQueryableAsync(object query, CancellationToken cancellationToken = default) + => Task.FromResult(_db.{EntityPlural}.AsQueryable()); +} +``` + +If the entity needs a DTO projection (different shape for the API), note this for the registration step. + +### 4. Create alter queryable service (if needed) + +If the entity needs security filtering, tenant isolation, or default ordering, create `Features/{DomainArea}/{Entity}AlterQueryable.cs`: + +```csharp +using Svrnty.CQRS.DynamicQuery.Abstractions; + +namespace {ProjectNamespace}.Features.{DomainArea}; + +public class {Entity}AlterQueryable : IAlterQueryableService<{Entity}, {EntityOrDto}> +{ + public Task> AlterQueryableAsync( + IQueryable<{Entity}> query, + IDynamicQuery dynamicQuery, + CancellationToken cancellationToken = default) + { + // Add default ordering, security filters, etc. + return Task.FromResult(query.OrderByDescending(x => x.CreatedAt)); + } +} +``` + +### 5. Register in DI + +Find the project's service registration and add: + +```csharp +// Basic — entity returned as-is +builder.Services.AddDynamicQueryWithProvider<{Entity}, {Entity}QueryableProvider>(); + +// With DTO projection — entity mapped to DTO +builder.Services.AddDynamicQueryWithProvider<{Entity}, {EntityDto}, {Entity}QueryableProvider>(); + +// With alter service (add after provider registration) +builder.Services.AddAlterQueryable<{Entity}, {EntityOrDto}, {Entity}AlterQueryable>(); +``` + +### 6. Ensure dynamic query dependencies are registered + +Check that these are registered (typically once per project, before `AddSvrntyCqrs`): + +```csharp +builder.Services.AddTransient(); +builder.Services.AddTransient(); +``` + +If they're already present, don't add duplicates. + +### 7. Proto message (if project uses gRPC) + +```protobuf +// In the DynamicQueryService definition +rpc Query{EntityPlural} (DynamicQuery{EntityPlural}Request) returns (DynamicQuery{EntityPlural}Response); + +// Entity message (if not already defined) +message {Entity} { + // fields matching entity properties +} + +// Request — uses standard dynamic query fields +message DynamicQuery{EntityPlural}Request { + int32 page = 1; + int32 page_size = 2; + repeated DynamicQueryFilter filters = 3; + repeated DynamicQuerySort sorts = 4; + repeated DynamicQueryGroup groups = 5; + repeated DynamicQueryAggregate aggregates = 6; +} + +// Response +message DynamicQuery{EntityPlural}Response { + repeated {Entity} data = 1; + int64 total_records = 2; + int32 number_of_pages = 3; +} +``` + +The standard `DynamicQueryFilter`, `DynamicQuerySort`, `DynamicQueryGroup`, and `DynamicQueryAggregate` messages should already be defined — reuse them. + +### 8. Summary + +After creating everything, list what was created and where. diff --git a/.claude/skills/add-query/SKILL.md b/.claude/skills/add-query/SKILL.md new file mode 100644 index 0000000..9dc4155 --- /dev/null +++ b/.claude/skills/add-query/SKILL.md @@ -0,0 +1,86 @@ +--- +name: add-query +description: Scaffold a new regular CQRS query with handler and DI registration. Use ONLY for single-entity lookups or non-queryable results. For list/collection queries, use /add-dynamic-query instead. +argument-hint: +--- + +# Add Query + +Scaffold a new regular query based on: $ARGUMENTS + +## Important: Is This the Right Skill? + +**This skill is for the rare case.** Most queries should be dynamic queries (`/add-dynamic-query`). + +Only use this for: +- Single entity by ID (e.g., `FetchOrderByIdQuery`) +- Non-entity results (e.g., `GetDashboardStatsQuery`, `CheckAvailabilityQuery`) +- Complex aggregation not expressible as IQueryable + +If the user is asking for a list/collection query with filtering, sorting, or pagination → suggest `/add-dynamic-query` instead. + +## Instructions + +### 1. Determine the query name + +Use descriptive, domain-oriented names: +- `FetchOrderByIdQuery` — fetching a single entity +- `GetSystemHealthQuery` — non-entity result +- `CheckInventoryAvailabilityQuery` — domain-specific check + +### 2. Determine the feature folder + +Place the file in: `Features/{DomainArea}/` + +### 3. Create the file + +Create `Features/{DomainArea}/{QueryName}Query.cs`: + +```csharp +using Svrnty.CQRS.Abstractions; + +namespace {ProjectNamespace}.Features.{DomainArea}; + +// 1. Query POCO +public record {QueryName}Query +{ + // Parameters — e.g., Id for lookups +} + +// 2. Handler +public class {QueryName}QueryHandler : IQueryHandler<{QueryName}Query, {ResultType}> +{ + public Task<{ResultType}> HandleAsync({QueryName}Query query, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} +``` + +### 4. Register in DI + +```csharp +builder.Services.AddQuery<{QueryName}Query, {ResultType}, {QueryName}QueryHandler>(); + +// With validator (if needed) +builder.Services.AddQuery<{QueryName}Query, {ResultType}, {QueryName}QueryHandler, {QueryName}QueryValidator>(); +``` + +### 5. Proto message (if project uses gRPC) + +```protobuf +// In the QueryService definition +rpc {QueryName} ({QueryName}QueryRequest) returns ({QueryName}QueryResponse); + +message {QueryName}QueryRequest { + // fields matching query properties +} + +message {QueryName}QueryResponse { + {ResultMessage} result = 1; +} +``` + +### 6. Summary + +After creating everything, list what was created and where. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..68897d2 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index cb9cfe8..50ac60d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,9 @@ This file provides guidance to AI agents when working with code in this repository. +For domain-specific patterns (commands, queries, validation, gRPC, dynamic queries), see `.claude/rules/`. +For scaffolding commands, see `.claude/skills/` (`/add-command`, `/add-query`, `/add-dynamic-query`). + ## Project Overview This is Svrnty.CQRS, a modern implementation of Command Query Responsibility Segregation (CQRS) for .NET 10. It was forked from PoweredSoft.CQRS and provides: @@ -11,11 +14,10 @@ This is Svrnty.CQRS, a modern implementation of Command Query Responsibility Seg - Automatic gRPC endpoint generation with source generators and Google Rich Error Model validation - Dynamic query capabilities (filtering, sorting, grouping, aggregation) - FluentValidation support with RFC 7807 Problem Details (HTTP) and Google Rich Error Model (gRPC) -- AOT (Ahead-of-Time) compilation compatibility for core packages (where dependencies allow) ## Solution Structure -The solution contains 11 projects organized by responsibility (10 packages + 1 sample project): +The solution contains projects organized by responsibility: **Abstractions (interfaces and contracts only):** - `Svrnty.CQRS.Abstractions` - Core interfaces (ICommandHandler, IQueryHandler, discovery contracts) @@ -24,81 +26,42 @@ The solution contains 11 projects organized by responsibility (10 packages + 1 s **Implementation:** - `Svrnty.CQRS` - Core discovery and registration logic -- `Svrnty.CQRS.MinimalApi` - Minimal API endpoint mapping for commands/queries (recommended for HTTP) +- `Svrnty.CQRS.MinimalApi` - Minimal API endpoint mapping for commands/queries - `Svrnty.CQRS.DynamicQuery` - PoweredSoft.DynamicQuery integration for advanced filtering - `Svrnty.CQRS.DynamicQuery.MinimalApi` - Minimal API endpoint mapping for dynamic queries - `Svrnty.CQRS.FluentValidation` - Validation integration helpers - `Svrnty.CQRS.Grpc` - gRPC service implementation support - `Svrnty.CQRS.Grpc.Generators` - Source generator for .proto files and gRPC service implementations -**Sample Projects:** -- `Svrnty.Sample` - Comprehensive demo project showcasing both HTTP and gRPC endpoints +**Sample:** +- `Svrnty.Sample` - Demo project showcasing both HTTP and gRPC endpoints -**Key Design Principle:** Abstractions projects contain ONLY interfaces/attributes with minimal dependencies. Implementation projects depend on abstractions. This allows consumers to reference abstractions without pulling in heavy implementation dependencies. +**Key Design Principle:** Abstractions projects contain ONLY interfaces/attributes with minimal dependencies. Implementation projects depend on abstractions. ## Build Commands ```bash -# Restore dependencies -dotnet restore - -# Build entire solution -dotnet build - -# Build in Release mode -dotnet build -c Release - -# Create NuGet packages (with version) -dotnet pack -c Release -o ./artifacts -p:Version=1.0.0 - -# Build specific project -dotnet build Svrnty.CQRS/Svrnty.CQRS.csproj +dotnet restore # Restore dependencies +dotnet build # Build entire solution +dotnet build -c Release # Build in Release mode +dotnet pack -c Release -o ./artifacts -p:Version=1.0.0 # Create NuGet packages ``` ## Testing -This repository does not currently contain test projects. When adding tests: -- Place them in a `tests/` directory or alongside source projects +No test projects currently exist. When adding tests: +- Place them in a `tests/` directory - Name them with `.Tests` suffix (e.g., `Svrnty.CQRS.Tests`) ## Architecture -### Core CQRS Pattern - -The framework uses handler interfaces that follow this pattern: - -```csharp -// Command with no result -ICommandHandler - Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) - -// Command with result -ICommandHandler - Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) - -// Query (always returns result) -IQueryHandler - Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) -``` - ### Metadata-Driven Discovery The framework uses a **metadata pattern** for runtime discovery: -1. When you register a handler using `services.AddCommand()`, it: - - Registers the handler in DI as `ICommandHandler` - - Creates metadata (`ICommandMeta`) describing the command type, handler type, and result type - - Stores metadata as singleton in DI - -2. Discovery services (`ICommandDiscovery`, `IQueryDiscovery`) implemented in `Svrnty.CQRS`: - - Query all registered metadata from DI container - - Provide lookup methods: `GetCommand(string name)`, `GetCommands()`, etc. - -3. Endpoint mapping (HTTP and gRPC) uses discovery to: - - Enumerate all registered commands/queries - - Dynamically generate endpoints at application startup - - Apply naming conventions (convert to lowerCamelCase) - - Generate gRPC service implementations via source generators +1. `services.AddCommand()` registers the handler in DI and creates `ICommandMeta` metadata as a singleton +2. Discovery services (`ICommandDiscovery`, `IQueryDiscovery`) query all registered metadata from DI +3. Endpoint mapping (HTTP and gRPC) uses discovery to dynamically generate endpoints at startup **Key Files:** - `Svrnty.CQRS.Abstractions/Discovery/` - Metadata interfaces @@ -107,307 +70,77 @@ The framework uses a **metadata pattern** for runtime discovery: - `Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs` - Dynamic query endpoint generation - `Svrnty.CQRS.Grpc.Generators/` - gRPC service generation via source generators -### Integration Options +### Integration -There are two primary integration options for exposing commands and queries: - -#### Option 1: gRPC (Recommended for performance-critical scenarios) - -The **Svrnty.CQRS.Grpc** package with **Svrnty.CQRS.Grpc.Generators** source generator provides high-performance gRPC endpoints: - -**Registration:** -```csharp -var builder = WebApplication.CreateBuilder(args); - -// Register CQRS services -builder.Services.AddSvrntyCQRS(); -builder.Services.AddDefaultCommandDiscovery(); -builder.Services.AddDefaultQueryDiscovery(); - -// Add your commands and queries -builder.Services.AddCommand(); -builder.Services.AddCommand(); - -// Add gRPC support -builder.Services.AddGrpc(); - -var app = builder.Build(); - -// Map auto-generated gRPC service implementations -app.MapGrpcService(); -app.MapGrpcService(); - -// Enable gRPC reflection for tools like grpcurl -app.MapGrpcReflectionService(); - -app.Run(); -``` - -**How It Works:** -1. Define `.proto` files in `Protos/` directory with your commands/queries as messages -2. Source generator automatically creates `CommandServiceImpl` and `QueryServiceImpl` implementations -3. Property names in C# commands must match proto field names (case-insensitive) -4. FluentValidation is automatically integrated with **Google Rich Error Model** for structured validation errors -5. Validation errors return `google.rpc.Status` with `BadRequest` containing `FieldViolations` - -**Features:** -- High-performance binary protocol -- Automatic service implementation generation at compile time -- Google Rich Error Model for structured validation errors -- Full FluentValidation integration -- gRPC reflection support for development tools -- Suitable for microservices, internal APIs, and low-latency scenarios - -**Key Files:** -- `Svrnty.CQRS.Grpc/` - Runtime support for gRPC services -- `Svrnty.CQRS.Grpc.Generators/` - Source generator for service implementations - -#### Option 2: HTTP via Minimal API (Recommended for web/browser scenarios) - -The **Svrnty.CQRS.MinimalApi** package provides HTTP endpoints for CQRS commands and queries: - -**Registration:** -```csharp -var builder = WebApplication.CreateBuilder(args); - -// Register CQRS services -builder.Services.AddSvrntyCQRS(); -builder.Services.AddDefaultCommandDiscovery(); -builder.Services.AddDefaultQueryDiscovery(); - -// Add your commands and queries -builder.Services.AddCommand(); -builder.Services.AddQuery, PersonQueryHandler>(); - -// Add Swagger (optional) -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -var app = builder.Build(); - -// Map endpoints (this creates routes automatically) -app.MapSvrntyCommands(); // Maps all commands to POST /api/command/{name} -app.MapSvrntyQueries(); // Maps all queries to POST/GET /api/query/{name} - -app.Run(); -``` - -**How It Works:** -1. Extension methods iterate through `ICommandDiscovery` and `IQueryDiscovery` -2. For each command/query, creates Minimal API endpoints using `MapPost()`/`MapGet()` -3. Applies naming conventions (lowerCamelCase) -4. Respects `[CommandControllerIgnore]` and `[QueryControllerIgnore]` attributes -5. Integrates with `ICommandAuthorizationService` and `IQueryAuthorizationService` -6. Supports OpenAPI/Swagger documentation - -**Features:** -- Queries support both POST (with JSON body) and GET (with query string parameters) -- Commands only support POST with JSON body -- Authorization via authorization services (returns 401/403 status codes) -- Customizable route prefixes: `MapSvrntyCommands("my-prefix")` -- Automatic OpenAPI tags: "Commands" and "Queries" -- RFC 7807 Problem Details for validation errors -- Full Swagger/OpenAPI support - -**Key Files:** -- `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs` - Main implementation - -#### Option 3: Both gRPC and HTTP (Dual Protocol Support) - -You can enable both protocols simultaneously, allowing clients to choose their preferred protocol: +Commands and queries can be exposed via HTTP (Minimal API), gRPC, or both simultaneously. The fluent configuration API handles all wiring: ```csharp -var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSvrntyCqrs(cqrs => +{ + cqrs.AddGrpc(grpc => grpc.EnableReflection()); + cqrs.AddMinimalApi(); +}); -// Register CQRS services -builder.Services.AddSvrntyCQRS(); -builder.Services.AddDefaultCommandDiscovery(); -builder.Services.AddDefaultQueryDiscovery(); - -// Add commands and queries -AddCommands(builder.Services); -AddQueries(builder.Services); - -// Add both gRPC and HTTP support -builder.Services.AddGrpc(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -var app = builder.Build(); - -// Map both gRPC and HTTP endpoints -app.MapGrpcService(); -app.MapGrpcService(); -app.MapGrpcReflectionService(); - -app.MapSvrntyCommands(); -app.MapSvrntyQueries(); - -app.Run(); +app.UseSvrntyCqrs(); // Maps all endpoints ``` -**Benefits:** -- Single codebase supports multiple protocols -- gRPC for high-performance, low-latency scenarios (microservices, internal APIs) -- HTTP for web browsers, legacy clients, and public APIs -- Same commands, queries, and validation logic for both protocols -- Swagger UI available for HTTP endpoints, gRPC reflection for gRPC clients - -### Dynamic Query System - -Dynamic queries provide OData-like filtering capabilities: - -**Core Components:** -- `IDynamicQuery` - Interface with GetFilters(), GetSorts(), GetGroups(), GetAggregates() -- `IQueryableProvider` - Provides base IQueryable to query against -- `IAlterQueryableService` - Middleware to modify queries (e.g., security filters) -- `DynamicQueryHandler` - Executes queries using PoweredSoft.DynamicQuery - -**Request Flow:** -1. HTTP request with filters/sorts/aggregates -2. Minimal API endpoint receives request -3. DynamicQueryHandler gets base queryable from IQueryableProvider -4. Applies alterations from all registered IAlterQueryableService instances -5. Builds PoweredSoft query criteria -6. Executes and returns IQueryExecutionResult - -**Registration Example:** -```csharp -// Register dynamic query -services.AddDynamicQuery() - .AddDynamicQueryWithProvider() - .AddAlterQueryable(); - -// Map dynamic query endpoints -app.MapSvrntyDynamicQueries(); // Creates POST/GET /api/query/{queryName} endpoints -``` - -**Key Files:** -- `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs` - Query execution logic -- `Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs` - HTTP endpoint mapping +See `Svrnty.Sample/Program.cs` for a complete working example. ## Package Configuration -All projects target .NET 10.0 and use C# 14, sharing common configuration: - -- **Target Framework**: `net10.0` (except DynamicQuery.Abstractions which multi-targets `netstandard2.1;net10.0`) +- **Target Framework**: `net10.0` (except DynamicQuery.Abstractions: `netstandard2.1;net10.0`) - **Language Version**: C# 14 -- **IsAotCompatible**: Currently set but not enforced (many dependencies are not AOT-compatible yet) -- **Symbols**: Portable debug symbols with source, published as `.snupkg` -- **NuGet metadata**: Icon, README, license (MIT), and repository URL included in packages - **Authors**: David Lebee, Mathias Beaulieu-Duncan - **Repository**: https://git.openharbor.io/svrnty/dotnet-cqrs -### Package Dependencies +### Key Dependencies -**Core Dependencies:** - **Microsoft.Extensions.DependencyInjection.Abstractions**: 10.0.0 - **FluentValidation**: 11.11.0 - **PoweredSoft.DynamicQuery**: 3.0.1 -- **Pluralize.NET**: 1.0.2 - -**gRPC Dependencies (for Svrnty.CQRS.Grpc):** -- **Grpc.AspNetCore**: 2.68.0 or later -- **Grpc.AspNetCore.Server.Reflection**: 2.71.0 or later (optional, for reflection) -- **Grpc.StatusProto**: 2.71.0 or later (for Rich Error Model validation) -- **Grpc.Tools**: 2.76.0 or later (for .proto compilation) - -**Source Generator Dependencies (for Svrnty.CQRS.Grpc.Generators):** -- **Microsoft.CodeAnalysis.CSharp**: 5.0.0-2.final -- **Microsoft.CodeAnalysis.Analyzers**: 3.11.0 -- **Microsoft.Build.Utilities.Core**: 17.0.0 -- Targets: netstandard2.0 (for Roslyn compatibility) +- **Grpc.AspNetCore**: 2.68.0+ +- **Grpc.StatusProto**: 2.71.0+ (Rich Error Model) +- **Microsoft.CodeAnalysis.CSharp**: 5.0.0-2.final (source generators, targets netstandard2.0) ## Publishing -NuGet packages are published automatically via GitHub Actions when a release is created: +NuGet packages publish automatically via GitHub Actions (`.github/workflows/publish-nugets.yml`) when a release is created. Tag becomes the version. -**Workflow:** `.github/workflows/publish-nugets.yml` -1. Triggered on release publication -2. Extracts version from release tag -3. Runs `dotnet pack -c Release -p:Version={tag}` -4. Pushes to NuGet.org using `NUGET_API_KEY` secret - -**Manual publish:** ```bash -# Create packages with specific version +# Manual publish dotnet pack -c Release -o ./artifacts -p:Version=1.2.3 - -# Push to NuGet dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key YOUR_KEY ``` ## Development Workflow -**Adding a New Command/Query Handler:** - -1. Create command/query POCO in consumer project -2. Implement handler: `ICommandHandler` -3. Register in DI: `services.AddCommand()` -4. (Optional) Add validator: `services.AddTransient, Validator>()` -5. Controller endpoint is automatically generated - -**Adding a New Feature to Framework:** +**Adding a New Feature to the Framework:** 1. Add interface to appropriate Abstractions project 2. Implement in corresponding implementation project 3. Update ServiceCollectionExtensions with registration method -4. Ensure all projects maintain AOT compatibility (unless AspNetCore-specific) +4. When extending discovery, create corresponding metadata classes (implement ICommandMeta/IQueryMeta) 5. Update package version and release notes -**Naming Conventions:** - -- Commands/Queries: Use `[CommandName]` or `[QueryName]` attribute for custom names -- Default naming: Strips "Command"/"Query" suffix, converts to lowerCamelCase -- Example: `CreatePersonCommand` -> `createPerson` endpoint - ## C# 14 Language Features -The project now uses C# 14, which introduces several new features. Be aware of these breaking changes: - -**Potential Breaking Changes:** -- **`field` keyword**: New contextual keyword in property accessors for implicit backing fields -- **`extension` keyword**: Reserved for extension containers; use `@extension` for identifiers -- **`partial` return type**: Cannot use `partial` as return type without escaping -- **Span overload resolution**: New implicit conversions may select different overloads -- **`scoped` as lambda modifier**: Always treated as modifier in lambda parameters - -**New Features Available:** -- Extension members (static extension members and extension properties) -- Implicit span conversions -- Unbound generic types with `nameof` -- Lambda parameter modifiers without type specification -- Partial instance constructors and events -- Null-conditional assignment (`?.=` and `?[]=`) - -The codebase currently compiles without warnings on C# 14. +The project uses C# 14. Be aware of these reserved keywords: +- **`field`**: Contextual keyword in property accessors for implicit backing fields +- **`extension`**: Reserved for extension containers; use `@extension` for identifiers ## Important Implementation Notes -1. **AOT Compatibility**: Currently not enforced. The `IsAotCompatible` property is set on some projects but many dependencies (including FluentValidation, PoweredSoft.DynamicQuery) are not AOT-compatible. Future work may address this. - -2. **Async Everywhere**: All handlers are async. Always support CancellationToken. - -3. **Generic Type Safety**: Framework relies heavily on generics for compile-time safety. When adding features, maintain strong typing. - -4. **Metadata Pattern**: When extending discovery, always create corresponding metadata classes (implement ICommandMeta/IQueryMeta). - -5. **Endpoint Mapping Timing**: Endpoints are mapped at application startup. Discovery services must be registered before calling `MapSvrntyCommands()`/`MapSvrntyQueries()` or mapping gRPC services. - -6. **FluentValidation Integration**: - - For HTTP: Validation happens automatically in the Minimal API pipeline. Errors return RFC 7807 Problem Details. - - For gRPC: Validation happens automatically via source-generated services. Errors return Google Rich Error Model with structured FieldViolations. - - The framework REGISTERS validators in DI; actual validation execution is handled by the endpoint implementations. - -7. **DynamicQuery Interceptors**: Support up to 5 interceptors per query type. Interceptors modify PoweredSoft DynamicQuery behavior. +1. **Async Everywhere**: All handlers are async. Always support CancellationToken. +2. **Generic Type Safety**: Framework relies heavily on generics. Maintain strong typing. +3. **Endpoint Mapping Timing**: Discovery services must be registered before calling `UseSvrntyCqrs()`. +4. **AOT Compatibility**: `IsAotCompatible` is set but not enforced — many dependencies are not AOT-compatible. ## Common Code Locations - Handler interfaces: `Svrnty.CQRS.Abstractions/ICommandHandler.cs`, `IQueryHandler.cs` -- Discovery implementations: `Svrnty.CQRS/Discovery/` +- Discovery: `Svrnty.CQRS/Discovery/` - Service registration: `*/ServiceCollectionExtensions.cs` in each project -- HTTP endpoint mapping: `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs` -- Dynamic query logic: `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs` -- Dynamic query endpoints: `Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs` -- gRPC support: `Svrnty.CQRS.Grpc/` runtime, `Svrnty.CQRS.Grpc.Generators/` source generators -- Sample application: `Svrnty.Sample/` - demonstrates both HTTP and gRPC integration +- HTTP endpoints: `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs` +- Dynamic queries: `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs` +- gRPC generators: `Svrnty.CQRS.Grpc.Generators/` +- Sample: `Svrnty.Sample/` diff --git a/Svrnty.CQRS.Abstractions/Attributes/CommandNameAttribute.cs b/Svrnty.CQRS.Abstractions/Attributes/CommandNameAttribute.cs index d08e3f8..4411db3 100644 --- a/Svrnty.CQRS.Abstractions/Attributes/CommandNameAttribute.cs +++ b/Svrnty.CQRS.Abstractions/Attributes/CommandNameAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Svrnty.CQRS.Abstractions.Attributes; diff --git a/Svrnty.CQRS.Abstractions/Attributes/QueryNameAttribute.cs b/Svrnty.CQRS.Abstractions/Attributes/QueryNameAttribute.cs index 628a233..f62e085 100644 --- a/Svrnty.CQRS.Abstractions/Attributes/QueryNameAttribute.cs +++ b/Svrnty.CQRS.Abstractions/Attributes/QueryNameAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Svrnty.CQRS.Abstractions.Attributes; diff --git a/Svrnty.CQRS.Abstractions/Discovery/CommandMeta.cs b/Svrnty.CQRS.Abstractions/Discovery/CommandMeta.cs index d58502b..9f27c56 100644 --- a/Svrnty.CQRS.Abstractions/Discovery/CommandMeta.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/CommandMeta.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using Svrnty.CQRS.Abstractions.Attributes; diff --git a/Svrnty.CQRS.Abstractions/Discovery/ICommandMeta.cs b/Svrnty.CQRS.Abstractions/Discovery/ICommandMeta.cs index bffcd15..c81e02d 100644 --- a/Svrnty.CQRS.Abstractions/Discovery/ICommandMeta.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/ICommandMeta.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Svrnty.CQRS.Abstractions.Discovery; diff --git a/Svrnty.CQRS.Abstractions/Discovery/IQueryDiscovery.cs b/Svrnty.CQRS.Abstractions/Discovery/IQueryDiscovery.cs index 115ae1e..29e419c 100644 --- a/Svrnty.CQRS.Abstractions/Discovery/IQueryDiscovery.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/IQueryDiscovery.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Svrnty.CQRS.Abstractions.Discovery; diff --git a/Svrnty.CQRS.Abstractions/Discovery/IQueryMeta.cs b/Svrnty.CQRS.Abstractions/Discovery/IQueryMeta.cs index 0d51e3b..06258b0 100644 --- a/Svrnty.CQRS.Abstractions/Discovery/IQueryMeta.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/IQueryMeta.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Svrnty.CQRS.Abstractions.Discovery; @@ -10,4 +10,4 @@ public interface IQueryMeta Type QueryResultType { get; } string Category { get; } string LowerCamelCaseName { get; } -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.Abstractions/Discovery/QueryMeta.cs b/Svrnty.CQRS.Abstractions/Discovery/QueryMeta.cs index e6a4a14..b021d04 100644 --- a/Svrnty.CQRS.Abstractions/Discovery/QueryMeta.cs +++ b/Svrnty.CQRS.Abstractions/Discovery/QueryMeta.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using Svrnty.CQRS.Abstractions.Attributes; diff --git a/Svrnty.CQRS.Abstractions/ICommandHandler.cs b/Svrnty.CQRS.Abstractions/ICommandHandler.cs index d40f4b1..f607662 100644 --- a/Svrnty.CQRS.Abstractions/ICommandHandler.cs +++ b/Svrnty.CQRS.Abstractions/ICommandHandler.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Svrnty.CQRS.Abstractions; @@ -13,4 +13,4 @@ public interface ICommandHandler where TCommand : class { Task HandleAsync(TCommand command, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.Abstractions/IQueryHandler.cs b/Svrnty.CQRS.Abstractions/IQueryHandler.cs index 396e2c5..fcc214e 100644 --- a/Svrnty.CQRS.Abstractions/IQueryHandler.cs +++ b/Svrnty.CQRS.Abstractions/IQueryHandler.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Svrnty.CQRS.Abstractions; @@ -7,4 +7,4 @@ public interface IQueryHandler where TQuery : class { Task HandleAsync(TQuery query, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.Abstractions/Security/AuthorizationResult.cs b/Svrnty.CQRS.Abstractions/Security/AuthorizationResult.cs index c1f32e4..9991516 100644 --- a/Svrnty.CQRS.Abstractions/Security/AuthorizationResult.cs +++ b/Svrnty.CQRS.Abstractions/Security/AuthorizationResult.cs @@ -1,8 +1,8 @@ -namespace Svrnty.CQRS.Abstractions.Security; +namespace Svrnty.CQRS.Abstractions.Security; public enum AuthorizationResult { Unauthorized, Forbidden, Allowed -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.Abstractions/Security/ICommandAuthorizationService.cs b/Svrnty.CQRS.Abstractions/Security/ICommandAuthorizationService.cs index 8b75b3b..fafb291 100644 --- a/Svrnty.CQRS.Abstractions/Security/ICommandAuthorizationService.cs +++ b/Svrnty.CQRS.Abstractions/Security/ICommandAuthorizationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -7,4 +7,4 @@ namespace Svrnty.CQRS.Abstractions.Security; public interface ICommandAuthorizationService { Task IsAllowedAsync(Type commandType, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.Abstractions/Security/IQueryAuthorizationService.cs b/Svrnty.CQRS.Abstractions/Security/IQueryAuthorizationService.cs index b63d196..15d160f 100644 --- a/Svrnty.CQRS.Abstractions/Security/IQueryAuthorizationService.cs +++ b/Svrnty.CQRS.Abstractions/Security/IQueryAuthorizationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -7,4 +7,4 @@ namespace Svrnty.CQRS.Abstractions.Security; public interface IQueryAuthorizationService { Task IsAllowedAsync(Type queryType, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.Abstractions/ServiceCollectionExtensions.cs b/Svrnty.CQRS.Abstractions/ServiceCollectionExtensions.cs index 5ec1330..5cac068 100644 --- a/Svrnty.CQRS.Abstractions/ServiceCollectionExtensions.cs +++ b/Svrnty.CQRS.Abstractions/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Svrnty.CQRS.Abstractions.Discovery; @@ -47,4 +47,4 @@ public static class ServiceCollectionExtensions return services; } -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs index 13626d1..147ca0a 100644 --- a/Svrnty.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/DynamicQueryInterceptorProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Svrnty.CQRS.DynamicQuery.Abstractions; diff --git a/Svrnty.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs index abdc16b..f8ecf5a 100644 --- a/Svrnty.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IAlterQueryableService.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -13,4 +13,4 @@ public interface IAlterQueryableService where TParams : class { Task> AlterQueryableAsync(IQueryable query, IDynamicQueryParams dynamicQuery, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs index fce0892..628863a 100644 --- a/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQuery.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using PoweredSoft.DynamicQuery.Core; namespace Svrnty.CQRS.DynamicQuery.Abstractions; @@ -15,7 +15,7 @@ public interface IDynamicQuery : IDynamicQue where TDestination : class where TParams : class { - + } public interface IDynamicQuery @@ -26,4 +26,4 @@ public interface IDynamicQuery List GetAggregates(); int? GetPage(); int? GetPageSize(); -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs index 438dfc7..8f93a64 100644 --- a/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryInterceptorProvider.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; namespace Svrnty.CQRS.DynamicQuery.Abstractions; - + public interface IDynamicQueryInterceptorProvider { IEnumerable GetInterceptorsTypes(); -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs index 08323c0..cbc37a2 100644 --- a/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IDynamicQueryParams.cs @@ -1,4 +1,4 @@ -namespace Svrnty.CQRS.DynamicQuery.Abstractions; +namespace Svrnty.CQRS.DynamicQuery.Abstractions; public interface IDynamicQueryParams where TParams : class diff --git a/Svrnty.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs b/Svrnty.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs index 2f7635e..d667137 100644 --- a/Svrnty.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs +++ b/Svrnty.CQRS.DynamicQuery.Abstractions/IQueryableProvider.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -7,4 +7,4 @@ namespace Svrnty.CQRS.DynamicQuery.Abstractions; public interface IQueryableProvider { Task> GetQueryableAsync(object query, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs b/Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs index 983d305..d7453e0 100644 --- a/Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs +++ b/Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using PoweredSoft.DynamicQuery.Core; using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.Abstractions.Attributes; using Svrnty.CQRS.Abstractions.Discovery; @@ -14,7 +15,6 @@ using Svrnty.CQRS.Abstractions.Security; using Svrnty.CQRS.DynamicQuery; using Svrnty.CQRS.DynamicQuery.Abstractions; using Svrnty.CQRS.DynamicQuery.Discover; -using PoweredSoft.DynamicQuery.Core; namespace Svrnty.CQRS.DynamicQuery.MinimalApi; diff --git a/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs b/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs index 54fc225..42afef0 100644 --- a/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs +++ b/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs @@ -1,4 +1,4 @@ -using System; +using System; using Pluralize.NET; using Svrnty.CQRS.Abstractions.Discovery; @@ -7,7 +7,7 @@ namespace Svrnty.CQRS.DynamicQuery.Discover; public class DynamicQueryMeta(Type queryType, Type serviceType, Type queryResultType) : QueryMeta(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 diff --git a/Svrnty.CQRS.DynamicQuery/DynamicQuery.cs b/Svrnty.CQRS.DynamicQuery/DynamicQuery.cs index fd322eb..eb32d07 100644 --- a/Svrnty.CQRS.DynamicQuery/DynamicQuery.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQuery.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; -using Svrnty.CQRS.DynamicQuery.Abstractions; using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery.Core; +using Svrnty.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery; diff --git a/Svrnty.CQRS.DynamicQuery/DynamicQueryAggregate.cs b/Svrnty.CQRS.DynamicQuery/DynamicQueryAggregate.cs index f04f8e0..29f5c40 100644 --- a/Svrnty.CQRS.DynamicQuery/DynamicQueryAggregate.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQueryAggregate.cs @@ -1,6 +1,6 @@ +using System; using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery.Core; -using System; namespace Svrnty.CQRS.DynamicQuery; diff --git a/Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs index 3ba2c59..960a045 100644 --- a/Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs @@ -1,23 +1,23 @@ -using Svrnty.CQRS.DynamicQuery.Abstractions; -using PoweredSoft.DynamicQuery.Core; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using PoweredSoft.DynamicQuery.Core; +using Svrnty.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery; public class DynamicQueryHandler - : DynamicQueryHandlerBase, + : DynamicQueryHandlerBase, Svrnty.CQRS.Abstractions.IQueryHandler, IQueryExecutionResult> where TSource : class where TDestination : class { - public DynamicQueryHandler(IQueryHandlerAsync queryHandlerAsync, + public DynamicQueryHandler(IQueryHandlerAsync queryHandlerAsync, IEnumerable> queryableProviders, - IEnumerable> alterQueryableServices, - IEnumerable> dynamicQueryInterceptorProviders, + IEnumerable> alterQueryableServices, + IEnumerable> dynamicQueryInterceptorProviders, IServiceProvider serviceProvider) : base(queryHandlerAsync, queryableProviders, alterQueryableServices, dynamicQueryInterceptorProviders, serviceProvider) { } @@ -29,7 +29,7 @@ public class DynamicQueryHandler } public class DynamicQueryHandler - : DynamicQueryHandlerBase, + : DynamicQueryHandlerBase, Svrnty.CQRS.Abstractions.IQueryHandler, IQueryExecutionResult> where TSource : class where TDestination : class @@ -37,10 +37,10 @@ public class DynamicQueryHandler { private readonly IEnumerable> alterQueryableServicesWithParams; - public DynamicQueryHandler(IQueryHandlerAsync queryHandlerAsync, + public DynamicQueryHandler(IQueryHandlerAsync queryHandlerAsync, IEnumerable> queryableProviders, IEnumerable> alterQueryableServices, - IEnumerable> alterQueryableServicesWithParams, + IEnumerable> alterQueryableServicesWithParams, IEnumerable> dynamicQueryInterceptorProviders, IServiceProvider serviceProvider) : base(queryHandlerAsync, queryableProviders, alterQueryableServices, dynamicQueryInterceptorProviders, serviceProvider) { @@ -49,7 +49,7 @@ public class DynamicQueryHandler protected override async Task> AlterSourceAsync(IQueryable source, IDynamicQuery query, CancellationToken cancellationToken) { - source = await base.AlterSourceAsync(source, query, cancellationToken); + source = await base.AlterSourceAsync(source, query, cancellationToken); if (query is IDynamicQueryParams withParams) { diff --git a/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs index 54dee1f..099563f 100644 --- a/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -6,9 +6,9 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Svrnty.CQRS.DynamicQuery.Abstractions; using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery.Core; +using Svrnty.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery; diff --git a/Svrnty.CQRS.DynamicQuery/ServiceCollectionExtensions.cs b/Svrnty.CQRS.DynamicQuery/ServiceCollectionExtensions.cs index 687b9a5..498076c 100644 --- a/Svrnty.CQRS.DynamicQuery/ServiceCollectionExtensions.cs +++ b/Svrnty.CQRS.DynamicQuery/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using PoweredSoft.Data.Core; @@ -91,10 +91,10 @@ public static class ServiceCollectionExtensions where TParams : class => AddDynamicQueryWithParams(services, name: name); - public static IServiceCollection AddDynamicQueryWithParams(this IServiceCollection services, string name = null) - where TSource : class - where TDestination : class - where TParams : class + public static IServiceCollection AddDynamicQueryWithParams(this IServiceCollection services, string name = null) + where TSource : class + where TDestination : class + where TParams : class { // add query handler. services.AddTransient, IQueryExecutionResult>, DynamicQueryHandler>(); @@ -133,7 +133,7 @@ public static class ServiceCollectionExtensions where TParams : class where TService : class, IAlterQueryableService { - return services.AddTransient, TService>(); + return services.AddTransient, TService>(); } public static IServiceCollection AddAlterQueryableWithParams diff --git a/Svrnty.CQRS.FluentValidation/ServiceCollectionExtensions.cs b/Svrnty.CQRS.FluentValidation/ServiceCollectionExtensions.cs index edc00dd..e043a7e 100644 --- a/Svrnty.CQRS.FluentValidation/ServiceCollectionExtensions.cs +++ b/Svrnty.CQRS.FluentValidation/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using FluentValidation; using Microsoft.Extensions.DependencyInjection; using Svrnty.CQRS.Abstractions; @@ -39,7 +39,7 @@ public static class ServiceCollectionExtensions { services.AddQuery() .AddFluentValidator(); - + return services; } -} \ No newline at end of file +} diff --git a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs index f090b30..c6356d2 100644 --- a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs @@ -1,1484 +1,341 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Svrnty.CQRS.Grpc.Generators.Helpers; using Svrnty.CQRS.Grpc.Generators.Models; -using System.Collections.Generic; -using System.Linq; -using System.Text; -namespace Svrnty.CQRS.Grpc.Generators +namespace Svrnty.CQRS.Grpc.Generators; + +[Generator] +public class GrpcGenerator : IIncrementalGenerator { - [Generator] - public class GrpcGenerator : IIncrementalGenerator + public void Initialize(IncrementalGeneratorInitializationContext context) { - public void Initialize(IncrementalGeneratorInitializationContext context) + // Find all types that might be commands or queries from source + var typeDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (node, _) => node is TypeDeclarationSyntax, + transform: static (ctx, _) => GetTypeSymbol(ctx)) + .Where(static symbol => symbol is not null); + + // Combine with compilation + var compilationAndTypes = context.CompilationProvider.Combine(typeDeclarations.Collect()); + + // Register source output + context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right!, spc)); + } + + private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context) + { + var typeDeclaration = (TypeDeclarationSyntax)context.Node; + var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration); + return symbol as INamedTypeSymbol; + } + + /// + /// Collects all types from the compilation and all referenced assemblies + /// + private static IEnumerable GetAllTypesFromCompilation(Compilation compilation) + { + var types = new List(); + + // Get types from the current assembly + CollectTypesFromNamespace(compilation.Assembly.GlobalNamespace, types); + + // Get types from all referenced assemblies + foreach (var reference in compilation.References) { - // Find all types that might be commands or queries from source - var typeDeclarations = context.SyntaxProvider - .CreateSyntaxProvider( - predicate: static (node, _) => node is TypeDeclarationSyntax, - transform: static (ctx, _) => GetTypeSymbol(ctx)) - .Where(static symbol => symbol is not null); - - // Combine with compilation - var compilationAndTypes = context.CompilationProvider.Combine(typeDeclarations.Collect()); - - // Register source output - context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right!, spc)); - } - - private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context) - { - var typeDeclaration = (TypeDeclarationSyntax)context.Node; - var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration); - return symbol as INamedTypeSymbol; - } - - /// - /// Collects all types from the compilation and all referenced assemblies - /// - private static IEnumerable GetAllTypesFromCompilation(Compilation compilation) - { - var types = new List(); - - // Get types from the current assembly - CollectTypesFromNamespace(compilation.Assembly.GlobalNamespace, types); - - // Get types from all referenced assemblies - foreach (var reference in compilation.References) + var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; + if (assemblySymbol != null) { - var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; - if (assemblySymbol != null) + CollectTypesFromNamespace(assemblySymbol.GlobalNamespace, types); + } + } + + return types; + } + + private static void CollectTypesFromNamespace(INamespaceSymbol ns, List types) + { + foreach (var type in ns.GetTypeMembers()) + { + types.Add(type); + // Also collect nested types + CollectNestedTypes(type, types); + } + + foreach (var nestedNs in ns.GetNamespaceMembers()) + { + CollectTypesFromNamespace(nestedNs, types); + } + } + + private static void CollectNestedTypes(INamedTypeSymbol type, List types) + { + foreach (var nestedType in type.GetTypeMembers()) + { + types.Add(nestedType); + CollectNestedTypes(nestedType, types); + } + } + + private static void Execute(Compilation compilation, IEnumerable sourceTypes, SourceProductionContext context) + { + // Get the expected namespace for proto-generated types + var rootNamespace = compilation.AssemblyName ?? "Generated"; + var grpcNamespace = $"{rootNamespace}.Grpc"; + + // Check if proto types are available (from Grpc.Tools compilation of .proto file) + // If not, skip generation - this happens on first build before proto file is compiled + var commandServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.CommandService+CommandServiceBase"); + var queryServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.QueryService+QueryServiceBase"); + var dynamicQueryServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.DynamicQueryService+DynamicQueryServiceBase"); + var notificationServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.NotificationService+NotificationServiceBase"); + + // If none of the service bases exist, the proto hasn't been compiled yet - skip generation + if (commandServiceBase == null && queryServiceBase == null && dynamicQueryServiceBase == null && notificationServiceBase == null) + { + // Report diagnostic for first build + var descriptor = new DiagnosticDescriptor( + "CQRSGRPC003", + "Proto types not yet available", + "gRPC service implementations will be generated on second build after proto file is compiled", + "Svrnty.CQRS.Grpc", + DiagnosticSeverity.Info, + isEnabledByDefault: true); + context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None)); + return; + } + + var grpcIgnoreAttribute = compilation.GetTypeByMetadataName("Svrnty.CQRS.Grpc.Abstractions.Attributes.GrpcIgnoreAttribute"); + var commandHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`1"); + var commandHandlerWithResultInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`2"); + var queryHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.IQueryHandler`2"); + var dynamicQueryInterface2 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`2"); + var dynamicQueryInterface3 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`3"); + var queryableProviderInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IQueryableProvider`1"); + + if (commandHandlerInterface == null || queryHandlerInterface == null) + { + return; // Handler interfaces not found + } + + var commandMap = new Dictionary(SymbolEqualityComparer.Default); // Command -> Result type (null if no result) + var queryMap = new Dictionary(SymbolEqualityComparer.Default); // Query -> Result type + var dynamicQueryMap = new List<(INamedTypeSymbol SourceType, INamedTypeSymbol DestinationType, INamedTypeSymbol? ParamsType)>(); // List of (Source, Destination, Params?) + + // Get all types from the compilation and referenced assemblies + var allTypes = GetAllTypesFromCompilation(compilation); + + // Find all command and query types by looking at handler implementations + foreach (var typeSymbol in allTypes) + { + if (typeSymbol.IsAbstract || typeSymbol.IsStatic) + continue; + + // Check if this type implements ICommandHandler or ICommandHandler + foreach (var iface in typeSymbol.AllInterfaces) + { + if (iface.IsGenericType) { - CollectTypesFromNamespace(assemblySymbol.GlobalNamespace, types); + // Check for ICommandHandler + if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerInterface) && iface.TypeArguments.Length == 1) + { + var commandType = iface.TypeArguments[0] as INamedTypeSymbol; + if (commandType != null && !commandMap.ContainsKey(commandType)) + commandMap[commandType] = null; // No result type + } + // Check for ICommandHandler + else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerWithResultInterface) && iface.TypeArguments.Length == 2) + { + var commandType = iface.TypeArguments[0] as INamedTypeSymbol; + var resultType = iface.TypeArguments[1] as INamedTypeSymbol; + if (commandType != null && resultType != null) + commandMap[commandType] = resultType; + } + // Check for IQueryHandler + else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryHandlerInterface) && iface.TypeArguments.Length == 2) + { + var queryType = iface.TypeArguments[0] as INamedTypeSymbol; + var resultType = iface.TypeArguments[1] as INamedTypeSymbol; + if (queryType != null && resultType != null) + { + // Check if this is a dynamic query handler + if (queryType.IsGenericType && + (SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface2) || + SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface3))) + { + // Extract source, destination, and optional params types + var sourceType = queryType.TypeArguments[0] as INamedTypeSymbol; + var destinationType = queryType.TypeArguments[1] as INamedTypeSymbol; + INamedTypeSymbol? paramsType = null; + + if (queryType.TypeArguments.Length == 3) + { + paramsType = queryType.TypeArguments[2] as INamedTypeSymbol; + } + + if (sourceType != null && destinationType != null) + { + // Check if already added (avoid duplicates) + var exists = dynamicQueryMap.Any(dq => + SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) && + SymbolEqualityComparer.Default.Equals(dq.DestinationType, destinationType) && + (dq.ParamsType == null && paramsType == null || + dq.ParamsType != null && paramsType != null && SymbolEqualityComparer.Default.Equals(dq.ParamsType, paramsType))); + + if (!exists) + { + dynamicQueryMap.Add((sourceType, destinationType, paramsType)); + } + } + } + else + { + queryMap[queryType] = resultType; + } + } + } } } - return types; - } - - private static void CollectTypesFromNamespace(INamespaceSymbol ns, List types) - { - foreach (var type in ns.GetTypeMembers()) + // Check if this type implements IQueryableProvider - this indicates a dynamic query + if (queryableProviderInterface != null) { - types.Add(type); - // Also collect nested types - CollectNestedTypes(type, types); - } - - foreach (var nestedNs in ns.GetNamespaceMembers()) - { - CollectTypesFromNamespace(nestedNs, types); - } - } - - private static void CollectNestedTypes(INamedTypeSymbol type, List types) - { - foreach (var nestedType in type.GetTypeMembers()) - { - types.Add(nestedType); - CollectNestedTypes(nestedType, types); - } - } - - private static void Execute(Compilation compilation, IEnumerable sourceTypes, SourceProductionContext context) - { - // Get the expected namespace for proto-generated types - var rootNamespace = compilation.AssemblyName ?? "Generated"; - var grpcNamespace = $"{rootNamespace}.Grpc"; - - // Check if proto types are available (from Grpc.Tools compilation of .proto file) - // If not, skip generation - this happens on first build before proto file is compiled - var commandServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.CommandService+CommandServiceBase"); - var queryServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.QueryService+QueryServiceBase"); - var dynamicQueryServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.DynamicQueryService+DynamicQueryServiceBase"); - var notificationServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.NotificationService+NotificationServiceBase"); - - // If none of the service bases exist, the proto hasn't been compiled yet - skip generation - if (commandServiceBase == null && queryServiceBase == null && dynamicQueryServiceBase == null && notificationServiceBase == null) - { - // Report diagnostic for first build - var descriptor = new DiagnosticDescriptor( - "CQRSGRPC003", - "Proto types not yet available", - "gRPC service implementations will be generated on second build after proto file is compiled", - "Svrnty.CQRS.Grpc", - DiagnosticSeverity.Info, - isEnabledByDefault: true); - context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None)); - return; - } - - var grpcIgnoreAttribute = compilation.GetTypeByMetadataName("Svrnty.CQRS.Grpc.Abstractions.Attributes.GrpcIgnoreAttribute"); - var commandHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`1"); - var commandHandlerWithResultInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`2"); - var queryHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.IQueryHandler`2"); - var dynamicQueryInterface2 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`2"); - var dynamicQueryInterface3 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`3"); - var queryableProviderInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IQueryableProvider`1"); - - if (commandHandlerInterface == null || queryHandlerInterface == null) - { - return; // Handler interfaces not found - } - - var commandMap = new Dictionary(SymbolEqualityComparer.Default); // Command -> Result type (null if no result) - var queryMap = new Dictionary(SymbolEqualityComparer.Default); // Query -> Result type - var dynamicQueryMap = new List<(INamedTypeSymbol SourceType, INamedTypeSymbol DestinationType, INamedTypeSymbol? ParamsType)>(); // List of (Source, Destination, Params?) - - // Get all types from the compilation and referenced assemblies - var allTypes = GetAllTypesFromCompilation(compilation); - - // Find all command and query types by looking at handler implementations - foreach (var typeSymbol in allTypes) - { - if (typeSymbol.IsAbstract || typeSymbol.IsStatic) - continue; - - // Check if this type implements ICommandHandler or ICommandHandler foreach (var iface in typeSymbol.AllInterfaces) { - if (iface.IsGenericType) + if (iface.IsGenericType && SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryableProviderInterface)) { - // Check for ICommandHandler - if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerInterface) && iface.TypeArguments.Length == 1) + // Extract source type from IQueryableProvider + var sourceType = iface.TypeArguments[0] as INamedTypeSymbol; + if (sourceType != null) { - var commandType = iface.TypeArguments[0] as INamedTypeSymbol; - if (commandType != null && !commandMap.ContainsKey(commandType)) - commandMap[commandType] = null; // No result type - } - // Check for ICommandHandler - else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerWithResultInterface) && iface.TypeArguments.Length == 2) - { - var commandType = iface.TypeArguments[0] as INamedTypeSymbol; - var resultType = iface.TypeArguments[1] as INamedTypeSymbol; - if (commandType != null && resultType != null) - commandMap[commandType] = resultType; - } - // Check for IQueryHandler - else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryHandlerInterface) && iface.TypeArguments.Length == 2) - { - var queryType = iface.TypeArguments[0] as INamedTypeSymbol; - var resultType = iface.TypeArguments[1] as INamedTypeSymbol; - if (queryType != null && resultType != null) + // For IQueryableProvider, we assume TSource = TDestination (no params) + var exists = dynamicQueryMap.Any(dq => + SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) && + SymbolEqualityComparer.Default.Equals(dq.DestinationType, sourceType) && + dq.ParamsType == null); + + if (!exists) { - // Check if this is a dynamic query handler - if (queryType.IsGenericType && - (SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface2) || - SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface3))) - { - // Extract source, destination, and optional params types - var sourceType = queryType.TypeArguments[0] as INamedTypeSymbol; - var destinationType = queryType.TypeArguments[1] as INamedTypeSymbol; - INamedTypeSymbol? paramsType = null; - - if (queryType.TypeArguments.Length == 3) - { - paramsType = queryType.TypeArguments[2] as INamedTypeSymbol; - } - - if (sourceType != null && destinationType != null) - { - // Check if already added (avoid duplicates) - var exists = dynamicQueryMap.Any(dq => - SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) && - SymbolEqualityComparer.Default.Equals(dq.DestinationType, destinationType) && - (dq.ParamsType == null && paramsType == null || - dq.ParamsType != null && paramsType != null && SymbolEqualityComparer.Default.Equals(dq.ParamsType, paramsType))); - - if (!exists) - { - dynamicQueryMap.Add((sourceType, destinationType, paramsType)); - } - } - } - else - { - queryMap[queryType] = resultType; - } - } - } - } - } - - // Check if this type implements IQueryableProvider - this indicates a dynamic query - if (queryableProviderInterface != null) - { - foreach (var iface in typeSymbol.AllInterfaces) - { - if (iface.IsGenericType && SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryableProviderInterface)) - { - // Extract source type from IQueryableProvider - var sourceType = iface.TypeArguments[0] as INamedTypeSymbol; - if (sourceType != null) - { - // For IQueryableProvider, we assume TSource = TDestination (no params) - var exists = dynamicQueryMap.Any(dq => - SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) && - SymbolEqualityComparer.Default.Equals(dq.DestinationType, sourceType) && - dq.ParamsType == null); - - if (!exists) - { - dynamicQueryMap.Add((sourceType, sourceType, null)); - } + dynamicQueryMap.Add((sourceType, sourceType, null)); } } } } } - - var commands = new List(); - var queries = new List(); - - // Process discovered command types - foreach (var kvp in commandMap) - { - var commandType = kvp.Key; - var resultType = kvp.Value; - - // Skip if marked with [GrpcIgnore] - if (HasGrpcIgnoreAttribute(commandType)) - continue; - - var commandInfo = ExtractCommandInfo(commandType, resultType); - if (commandInfo != null) - commands.Add(commandInfo); - } - - // Process discovered query types - foreach (var kvp in queryMap) - { - var queryType = kvp.Key; - var resultType = kvp.Value; - - // Skip if marked with [GrpcIgnore] - if (HasGrpcIgnoreAttribute(queryType)) - continue; - - var queryInfo = ExtractQueryInfo(queryType, resultType); - if (queryInfo != null) - queries.Add(queryInfo); - } - - // Process discovered dynamic query types - var dynamicQueries = new List(); - foreach (var (sourceType, destinationType, paramsType) in dynamicQueryMap) - { - var dynamicQueryInfo = ExtractDynamicQueryInfo(sourceType, destinationType, paramsType); - if (dynamicQueryInfo != null) - dynamicQueries.Add(dynamicQueryInfo); - } - - // Process discovered notification types (marked with [StreamingNotification]) - var notifications = DiscoverNotifications(allTypes, compilation); - - // Generate services if we found any commands, queries, dynamic queries, or notifications - if (commands.Any() || queries.Any() || dynamicQueries.Any() || notifications.Any()) - { - GenerateProtoAndServices(context, commands, queries, dynamicQueries, notifications, compilation); - } } - private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol) + var commands = new List(); + var queries = new List(); + + // Process discovered command types + foreach (var kvp in commandMap) { - return typeSymbol.GetAttributes().Any(attr => - SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol)); + var commandType = kvp.Key; + var resultType = kvp.Value; + + // Skip if marked with [GrpcIgnore] + if (HasGrpcIgnoreAttribute(commandType)) + continue; + + var commandInfo = ExtractCommandInfo(commandType, resultType); + if (commandInfo != null) + commands.Add(commandInfo); } - private static bool HasGrpcIgnoreAttribute(INamedTypeSymbol typeSymbol) + // Process discovered query types + foreach (var kvp in queryMap) { - return typeSymbol.GetAttributes().Any(attr => - attr.AttributeClass?.Name == "GrpcIgnoreAttribute"); + var queryType = kvp.Key; + var resultType = kvp.Value; + + // Skip if marked with [GrpcIgnore] + if (HasGrpcIgnoreAttribute(queryType)) + continue; + + var queryInfo = ExtractQueryInfo(queryType, resultType); + if (queryInfo != null) + queries.Add(queryInfo); } - private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? interfaceSymbol) + // Process discovered dynamic query types + var dynamicQueries = new List(); + foreach (var (sourceType, destinationType, paramsType) in dynamicQueryMap) { - if (interfaceSymbol == null) - return false; - - return typeSymbol.AllInterfaces.Any(i => - SymbolEqualityComparer.Default.Equals(i, interfaceSymbol)); + var dynamicQueryInfo = ExtractDynamicQueryInfo(sourceType, destinationType, paramsType); + if (dynamicQueryInfo != null) + dynamicQueries.Add(dynamicQueryInfo); } - private static bool ImplementsGenericInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? genericInterfaceSymbol) - { - if (genericInterfaceSymbol == null) - return false; + // Process discovered notification types (marked with [StreamingNotification]) + var notifications = DiscoverNotifications(allTypes, compilation); - return typeSymbol.AllInterfaces.Any(i => - i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, genericInterfaceSymbol)); + // Generate services if we found any commands, queries, dynamic queries, or notifications + if (commands.Any() || queries.Any() || dynamicQueries.Any() || notifications.Any()) + { + GenerateProtoAndServices(context, commands, queries, dynamicQueries, notifications, compilation); } + } - private static CommandInfo? ExtractCommandInfo(INamedTypeSymbol commandType, INamedTypeSymbol? resultType) - { - var commandInfo = new CommandInfo - { - Name = commandType.Name, - FullyQualifiedName = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - Namespace = commandType.ContainingNamespace.ToDisplayString(), - Properties = new List() - }; + private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol) + { + return typeSymbol.GetAttributes().Any(attr => + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol)); + } - // Set result type if provided - if (resultType != null) - { - commandInfo.ResultType = resultType.Name; - commandInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - // Use fully qualified names to avoid ambiguity with proto-generated types - var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}, {resultTypeFullyQualified}>"; + private static bool HasGrpcIgnoreAttribute(INamedTypeSymbol typeSymbol) + { + return typeSymbol.GetAttributes().Any(attr => + attr.AttributeClass?.Name == "GrpcIgnoreAttribute"); + } - // Check if result type is primitive - var resultTypeString = resultType.ToDisplayString(); - commandInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString); - - // Extract result type properties if it's a complex type - if (!commandInfo.IsResultPrimitiveType) - { - var resultProperties = resultType.GetMembers().OfType() - .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) - .ToList(); - - foreach (var property in resultProperties) - { - var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var isList = IsListOrCollection(property.Type); - var isComplexType = IsUserDefinedComplexType(property.Type); - - var propInfo = new PropertyInfo - { - Name = property.Name, - Type = property.Type.ToDisplayString(), - FullyQualifiedType = propertyType, - ProtoType = string.Empty, // Not needed for result mapping - FieldNumber = 0, // Not needed for result mapping - IsList = isList, - IsComplexType = isComplexType, - IsNullable = IsNullableType(property.Type), - IsEnum = IsEnumType(property.Type), - IsDecimal = IsDecimalType(property.Type), - IsDateTime = IsDateTimeType(property.Type), - IsDateTimeOffset = IsDateTimeOffsetType(property.Type), - IsGuid = IsGuidType(property.Type), - IsJsonElement = IsJsonElementType(property.Type), - IsValueTypeCollection = IsValueTypeCollection(property.Type), - IsBinaryType = IsBinaryType(property.Type), - IsStream = IsStreamType(property.Type), - }; - - // If it's a list, extract element type info - if (isList) - { - var elementType = GetListElementType(property.Type); - if (elementType != null) - { - propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); - propInfo.IsElementGuid = IsGuidType(elementType); - } - } - - commandInfo.ResultProperties.Add(propInfo); - } - } - } - else - { - var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}>"; - } - - // Extract properties - var properties = commandType.GetMembers().OfType() - .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) - .ToList(); - - int fieldNumber = 1; - foreach (var property in properties) - { - var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); - - var propInfo = new PropertyInfo - { - Name = property.Name, - Type = propertyType, - FullyQualifiedType = propertyType, - ProtoType = protoType, - FieldNumber = fieldNumber++, - IsComplexType = IsUserDefinedComplexType(property.Type), - // New type metadata fields - IsNullable = IsNullableType(property.Type), - IsEnum = IsEnumType(property.Type), - IsDecimal = IsDecimalType(property.Type), - IsDateTime = IsDateTimeType(property.Type), - IsDateTimeOffset = IsDateTimeOffsetType(property.Type), - IsGuid = IsGuidType(property.Type), - IsJsonElement = IsJsonElementType(property.Type), - IsList = IsListOrCollection(property.Type), - IsValueTypeCollection = IsValueTypeCollection(property.Type), - IsBinaryType = IsBinaryType(property.Type), - IsStream = IsStreamType(property.Type), - }; - - // If it's a list, extract element type info - if (propInfo.IsList) - { - var elementType = GetListElementType(property.Type); - if (elementType != null) - { - propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); - propInfo.IsElementGuid = IsGuidType(elementType); - - // If element is complex, extract nested properties - if (propInfo.IsElementComplexType) - { - var unwrappedElement = UnwrapNullableType(elementType); - if (unwrappedElement is INamedTypeSymbol namedElementType) - { - propInfo.ElementNestedProperties = new List(); - ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); - } - } - } - } - // If it's a complex type (not list), extract nested properties - else if (propInfo.IsComplexType) - { - var unwrapped = UnwrapNullableType(property.Type); - if (unwrapped is INamedTypeSymbol namedType) - { - ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties); - } - } - - commandInfo.Properties.Add(propInfo); - } - - return commandInfo; - } - - private static bool IsUserDefinedComplexType(ITypeSymbol type) - { - if (type == null) - return false; - - // Unwrap nullable first - var unwrapped = UnwrapNullableType(type); - - if (unwrapped.TypeKind != TypeKind.Class && unwrapped.TypeKind != TypeKind.Struct) - return false; - - var fullName = unwrapped.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - // Exclude system types and primitives - if (fullName.StartsWith("global::System.")) - return false; - if (IsPrimitiveType(fullName)) - return false; - - return true; - } - - private static ITypeSymbol UnwrapNullableType(ITypeSymbol type) - { - // Handle Nullable (value type nullability) - if (type is INamedTypeSymbol namedType && - namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && - namedType.TypeArguments.Length == 1) - { - return namedType.TypeArguments[0]; - } - return type; - } - - private static bool IsNullableType(ITypeSymbol type) - { - // Check for Nullable (value type nullability) - if (type is INamedTypeSymbol namedType && - namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - return true; - } - // Check for reference type nullability (C# 8.0+) - if (type.NullableAnnotation == NullableAnnotation.Annotated) - { - return true; - } + private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? interfaceSymbol) + { + if (interfaceSymbol == null) return false; - } - private static bool IsDecimalType(ITypeSymbol type) - { - var unwrapped = UnwrapNullableType(type); - return unwrapped.SpecialType == SpecialType.System_Decimal; - } + return typeSymbol.AllInterfaces.Any(i => + SymbolEqualityComparer.Default.Equals(i, interfaceSymbol)); + } - private static bool IsDateTimeType(ITypeSymbol type) - { - var unwrapped = UnwrapNullableType(type); - return unwrapped.SpecialType == SpecialType.System_DateTime; - } - - private static bool IsDateTimeOffsetType(ITypeSymbol type) - { - var unwrapped = UnwrapNullableType(type); - return unwrapped.ToDisplayString() == "System.DateTimeOffset"; - } - - private static bool IsEnumType(ITypeSymbol type) - { - var unwrapped = UnwrapNullableType(type); - return unwrapped.TypeKind == TypeKind.Enum; - } - - private static bool IsGuidType(ITypeSymbol type) - { - var unwrapped = UnwrapNullableType(type); - return unwrapped.ToDisplayString() == "System.Guid"; - } - - private static bool IsJsonElementType(ITypeSymbol type) - { - var unwrapped = UnwrapNullableType(type); - return unwrapped.ToDisplayString() == "System.Text.Json.JsonElement"; - } - - private static bool IsBinaryType(ITypeSymbol type) - { - return ProtoFileTypeMapper.IsBinaryType(type); - } - - private static bool IsStreamType(ITypeSymbol type) - { - var fullTypeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - if (fullTypeName.Contains("System.IO.Stream") || - fullTypeName.Contains("System.IO.MemoryStream") || - fullTypeName.Contains("System.IO.FileStream")) - { - return true; - } - var typeName = type.Name; - return typeName == "Stream" || typeName == "MemoryStream" || typeName == "FileStream"; - } - - private static bool IsListOrCollection(ITypeSymbol type) - { - // Unwrap nullable first to handle NpgsqlPolygon? - var unwrapped = UnwrapNullableType(type); - - if (unwrapped is IArrayTypeSymbol arrayType) - { - // byte[] is binary data, not a collection - if (arrayType.ElementType.SpecialType == SpecialType.System_Byte) - return false; - return true; - } - - if (unwrapped is INamedTypeSymbol namedType) - { - // Check if it's a generic collection type - if (namedType.IsGenericType) - { - var typeName = namedType.OriginalDefinition.ToDisplayString(); - if (typeName.StartsWith("System.Collections.Generic.List<") || - typeName.StartsWith("System.Collections.Generic.IList<") || - typeName.StartsWith("System.Collections.Generic.ICollection<") || - typeName.StartsWith("System.Collections.Generic.IEnumerable<")) - { - return true; - } - } - - // Check if it implements IList, ICollection, or IEnumerable (handles types like NpgsqlPolygon) - // Skip string which implements IEnumerable - if (namedType.SpecialType == SpecialType.System_String) - return false; - - foreach (var iface in namedType.AllInterfaces) - { - if (iface.IsGenericType && iface.TypeArguments.Length == 1) - { - var ifaceName = iface.OriginalDefinition.ToDisplayString(); - if (ifaceName == "System.Collections.Generic.IList" || - ifaceName == "System.Collections.Generic.ICollection" || - ifaceName == "System.Collections.Generic.IReadOnlyList" || - ifaceName == "System.Collections.Generic.IReadOnlyCollection") - { - return true; - } - } - } - } + private static bool ImplementsGenericInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? genericInterfaceSymbol) + { + if (genericInterfaceSymbol == null) return false; - } - /// - /// Checks if a type is a value type that implements a collection interface (like NpgsqlPolygon) - /// These require special construction syntax - /// - private static bool IsValueTypeCollection(ITypeSymbol type) + return typeSymbol.AllInterfaces.Any(i => + i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, genericInterfaceSymbol)); + } + + private static CommandInfo? ExtractCommandInfo(INamedTypeSymbol commandType, INamedTypeSymbol? resultType) + { + var commandInfo = new CommandInfo { - // Unwrap nullable first to handle NpgsqlPolygon? - var unwrapped = UnwrapNullableType(type); + Name = commandType.Name, + FullyQualifiedName = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + Namespace = commandType.ContainingNamespace.ToDisplayString(), + Properties = new List() + }; - if (unwrapped is not INamedTypeSymbol namedType) - return false; - - // Must be a value type (struct) - if (!namedType.IsValueType) - return false; - - // Must implement a collection interface (pass unwrapped since IsListOrCollection also unwraps) - return IsListOrCollection(unwrapped); - } - - private static ITypeSymbol? GetListElementType(ITypeSymbol type) + // Set result type if provided + if (resultType != null) { - // Unwrap nullable first to handle NpgsqlPolygon? - var unwrapped = UnwrapNullableType(type); - - if (unwrapped is IArrayTypeSymbol arrayType) - return arrayType.ElementType; - - if (unwrapped is INamedTypeSymbol namedType) - { - // First try generic type arguments - if (namedType.IsGenericType && namedType.TypeArguments.Length > 0) - { - return namedType.TypeArguments[0]; - } - - // Fall back to checking interfaces (for types like NpgsqlPolygon that implement IList) - foreach (var iface in namedType.AllInterfaces) - { - if (iface.IsGenericType && iface.TypeArguments.Length == 1) - { - var ifaceName = iface.OriginalDefinition.ToDisplayString(); - if (ifaceName == "System.Collections.Generic.IList" || - ifaceName == "System.Collections.Generic.IReadOnlyList") - { - return iface.TypeArguments[0]; - } - } - } - - // Try ICollection or IEnumerable as fallback - foreach (var iface in namedType.AllInterfaces) - { - if (iface.IsGenericType && iface.TypeArguments.Length == 1) - { - var ifaceName = iface.OriginalDefinition.ToDisplayString(); - if (ifaceName == "System.Collections.Generic.ICollection" || - ifaceName == "System.Collections.Generic.IReadOnlyCollection" || - ifaceName == "System.Collections.Generic.IEnumerable") - { - return iface.TypeArguments[0]; - } - } - } - } - return null; - } - - /// - /// Generates the value expression for converting from proto type to C# type - /// - private static string GetProtoToCSharpConversion(PropertyInfo prop, string sourceExpr) - { - if (prop.IsGuid) - { - if (prop.IsNullable) - return $"string.IsNullOrEmpty({sourceExpr}) ? null : System.Guid.Parse({sourceExpr})"; - return $"System.Guid.Parse({sourceExpr})"; - } - if (prop.IsEnum) - { - // Enum is already handled correctly in proto - values match - return $"{sourceExpr}"; - } - // Default: direct assignment - return $"{sourceExpr}!"; - } - - /// - /// Generates the value expression for converting from C# type to proto type - /// - private static string GetCSharpToProtoConversion(PropertyInfo prop, string sourceExpr) - { - if (prop.IsGuid) - { - if (prop.IsNullable) - return $"{sourceExpr}?.ToString() ?? \"\""; - return $"{sourceExpr}.ToString()"; - } - // Default: direct assignment - return sourceExpr; - } - - private static void ExtractNestedProperties(INamedTypeSymbol type, List nestedProperties) - { - var properties = type.GetMembers().OfType() - .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) - .ToList(); - - foreach (var property in properties) - { - var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var propInfo = new PropertyInfo - { - Name = property.Name, - Type = propertyType, - FullyQualifiedType = propertyType, - ProtoType = string.Empty, - FieldNumber = 0, - IsComplexType = IsUserDefinedComplexType(property.Type), - IsList = IsListOrCollection(property.Type), - IsValueTypeCollection = IsValueTypeCollection(property.Type), - }; - - // Recursively extract nested properties for complex types - if (propInfo.IsComplexType && property.Type is INamedTypeSymbol namedType) - { - ExtractNestedProperties(namedType, propInfo.NestedProperties); - } - - nestedProperties.Add(propInfo); - } - } - - private static void ExtractNestedPropertiesWithTypeInfo(INamedTypeSymbol type, List nestedProperties) - { - var properties = type.GetMembers().OfType() - .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) - .ToList(); - - foreach (var property in properties) - { - // Skip read-only properties (no setter) - they are computed and can't be set - var isReadOnly = property.IsReadOnly || property.SetMethod == null; - - var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var propInfo = new PropertyInfo - { - Name = property.Name, - Type = propertyType, - FullyQualifiedType = propertyType, - ProtoType = string.Empty, - FieldNumber = 0, - IsComplexType = IsUserDefinedComplexType(property.Type), - IsReadOnly = isReadOnly, - // Type metadata - IsNullable = IsNullableType(property.Type), - IsEnum = IsEnumType(property.Type), - IsDecimal = IsDecimalType(property.Type), - IsDateTime = IsDateTimeType(property.Type), - IsDateTimeOffset = IsDateTimeOffsetType(property.Type), - IsGuid = IsGuidType(property.Type), - IsJsonElement = IsJsonElementType(property.Type), - IsList = IsListOrCollection(property.Type), - IsValueTypeCollection = IsValueTypeCollection(property.Type), - IsBinaryType = IsBinaryType(property.Type), - IsStream = IsStreamType(property.Type), - }; - - // If it's a list, extract element type info - if (propInfo.IsList) - { - var elementType = GetListElementType(property.Type); - if (elementType != null) - { - propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); - propInfo.IsElementGuid = IsGuidType(elementType); - - // Extract nested properties for complex element types - if (propInfo.IsElementComplexType && elementType is INamedTypeSymbol namedElementType) - { - propInfo.ElementNestedProperties = new List(); - ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); - } - } - } - // Recursively extract nested properties for complex types - else if (propInfo.IsComplexType) - { - var unwrapped = UnwrapNullableType(property.Type); - if (unwrapped is INamedTypeSymbol namedType) - { - ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties); - } - } - - nestedProperties.Add(propInfo); - } - } - - private static void GenerateNestedPropertyMapping(StringBuilder sb, List properties, string sourcePrefix, string indent) - { - foreach (var prop in properties) - { - var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); - if (prop.IsComplexType) - { - // Generate nested object mapping - sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName} != null ? new {prop.FullyQualifiedType}"); - sb.AppendLine($"{indent}{{"); - GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{sourcePropName}", indent + " "); - sb.AppendLine($"{indent}}} : null!,"); - } - else - { - sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName},"); - } - } - } - - private static string GeneratePropertyAssignment(PropertyInfo prop, string requestVar, string indent) - { - var requestPropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); - var source = $"{requestVar}.{requestPropName}"; - - // Handle lists - if (prop.IsList) - { - if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) - { - // Complex list: map each element - return GenerateComplexListMapping(prop, source, indent); - } - else if (prop.IsElementGuid) - { - // List from proto -> List in C# - if (prop.IsValueTypeCollection) - { - // Value type collection proto message has Items field - var constructorType = prop.FullyQualifiedType.TrimEnd('?'); - return $"{indent}{prop.Name} = new {constructorType}({source}?.Items?.Select(x => System.Guid.Parse(x)).ToArray() ?? System.Array.Empty()),"; - } - return $"{indent}{prop.Name} = {source}?.Select(x => System.Guid.Parse(x)).ToList(),"; - } - else if (prop.IsValueTypeCollection) - { - // Value type collection (like NpgsqlPolygon): proto message has Items field - var constructorType = prop.FullyQualifiedType.TrimEnd('?'); - return $"{indent}{prop.Name} = new {constructorType}({source}?.Items?.Select(x => new {prop.ElementType} {{ X = x.X, Y = x.Y }}).ToArray() ?? System.Array.Empty<{prop.ElementType ?? "object"}>()),"; - } - else - { - // Primitive list: just ToList() - return $"{indent}{prop.Name} = {source}?.ToList(),"; - } - } - - // Handle enums (proto int32 -> C# enum) - if (prop.IsEnum) - { - return $"{indent}{prop.Name} = ({prop.FullyQualifiedType}){source},"; - } - - // Handle decimals (proto string -> C# decimal) - if (prop.IsDecimal) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),"; - } - else - { - return $"{indent}{prop.Name} = decimal.Parse({source}),"; - } - } - - // Handle DateTime (proto Timestamp -> C# DateTime) - if (prop.IsDateTime) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} == null ? (System.DateTime?)null : {source}.ToDateTime(),"; - } - else - { - return $"{indent}{prop.Name} = {source}.ToDateTime(),"; - } - } - - // Handle DateTimeOffset (proto Timestamp -> C# DateTimeOffset) - if (prop.IsDateTimeOffset) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} == null ? (System.DateTimeOffset?)null : {source}.ToDateTimeOffset(),"; - } - else - { - return $"{indent}{prop.Name} = {source}.ToDateTimeOffset(),"; - } - } - - // Handle Guid (proto string -> C# Guid) - if (prop.IsGuid) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : System.Guid.Parse({source}),"; - } - else - { - return $"{indent}{prop.Name} = System.Guid.Parse({source}),"; - } - } - - // Handle binary types (proto ByteString -> C# byte[]/Stream) - if (prop.IsBinaryType) - { - if (prop.IsStream) - { - // ByteString -> MemoryStream - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}?.IsEmpty == false ? new System.IO.MemoryStream({source}.ToByteArray()) : null,"; - } - return $"{indent}{prop.Name} = new System.IO.MemoryStream({source}.ToByteArray()),"; - } - else - { - // ByteString -> byte[] - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}?.IsEmpty == false ? {source}.ToByteArray() : null,"; - } - return $"{indent}{prop.Name} = {source}.ToByteArray(),"; - } - } - - // Handle complex types (single objects) - if (prop.IsComplexType) - { - return GenerateComplexObjectMapping(prop, source, indent); - } - - // Default: direct assignment - return $"{indent}{prop.Name} = {source},"; - } - - private static string GenerateComplexListMapping(PropertyInfo prop, string source, string indent) - { - var sb = new StringBuilder(); - // For value type collections, the proto message has an Items field containing the repeated elements - var itemsSource = prop.IsValueTypeCollection ? $"{source}?.Items" : source; - sb.AppendLine($"{indent}{prop.Name} = {itemsSource}?.Select(x => new {prop.ElementType}"); - sb.AppendLine($"{indent}{{"); - - foreach (var nestedProp in prop.ElementNestedProperties!) - { - // Skip read-only properties - they can't be assigned - if (nestedProp.IsReadOnly) continue; - - var nestedSourcePropName = char.ToUpper(nestedProp.Name[0]) + nestedProp.Name.Substring(1); - var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " "); - sb.AppendLine(nestedAssignment); - } - - if (prop.IsValueTypeCollection) - { - // Value type collection: wrap in constructor with array - // Use non-nullable type for constructor (remove trailing ? if present) - var constructorType = prop.FullyQualifiedType.TrimEnd('?'); - sb.Append($"{indent}}}).ToArray() is {{ }} arr ? new {constructorType}(arr) : default,"); - } - else - { - sb.Append($"{indent}}}).ToList(),"); - } - return sb.ToString(); - } - - private static string GenerateComplexObjectMapping(PropertyInfo prop, string source, string indent) - { - var sb = new StringBuilder(); - sb.AppendLine($"{indent}{prop.Name} = {source} != null ? new {prop.FullyQualifiedType}"); - sb.AppendLine($"{indent}{{"); - - foreach (var nestedProp in prop.NestedProperties) - { - // Skip read-only properties - they can't be assigned - if (nestedProp.IsReadOnly) continue; - - var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, source, indent + " "); - sb.AppendLine(nestedAssignment); - } - - sb.Append($"{indent}}} : null!,"); - return sb.ToString(); - } - - private static string GenerateNestedPropertyAssignment(PropertyInfo prop, string sourceVar, string indent) - { - var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); - var source = $"{sourceVar}.{sourcePropName}"; - - // Handle enums - if (prop.IsEnum) - { - return $"{indent}{prop.Name} = ({prop.FullyQualifiedType}){source},"; - } - - // Handle decimals - if (prop.IsDecimal) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),"; - } - else - { - return $"{indent}{prop.Name} = decimal.Parse({source}),"; - } - } - - // Handle DateTime (proto Timestamp -> C# DateTime) - if (prop.IsDateTime) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} == null ? (System.DateTime?)null : {source}.ToDateTime(),"; - } - else - { - return $"{indent}{prop.Name} = {source}.ToDateTime(),"; - } - } - - // Handle Guid - if (prop.IsGuid) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : System.Guid.Parse({source}),"; - } - else - { - return $"{indent}{prop.Name} = System.Guid.Parse({source}),"; - } - } - - // Handle lists with complex element types - if (prop.IsList) - { - if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) - { - return GenerateComplexListMapping(prop, source, indent); - } - if (prop.IsValueTypeCollection) - { - // Value type collection (like NpgsqlPolygon): use constructor with array - var constructorType = prop.FullyQualifiedType.TrimEnd('?'); - return $"{indent}{prop.Name} = new {constructorType}({source}?.ToArray() ?? System.Array.Empty<{prop.ElementType ?? "object"}>()),"; - } - return $"{indent}{prop.Name} = {source}?.ToList(),"; - } - - // Handle complex types - if (prop.IsComplexType && prop.NestedProperties.Any()) - { - return GenerateComplexObjectMapping(prop, source, indent); - } - - // Default: direct assignment - return $"{indent}{prop.Name} = {source},"; - } - - /// - /// Generates C# to proto property mapping (reverse of GeneratePropertyAssignment) - /// - private static string GenerateResultPropertyMapping(PropertyInfo prop, string sourceVar, string indent) - { - var source = $"{sourceVar}.{prop.Name}"; - - // Handle lists - if (prop.IsList) - { - // Value type collections (like NpgsqlPolygon) map to a wrapper message with Items field - if (prop.IsValueTypeCollection) - { - var protoWrapperType = prop.Type.Split('.').Last().Replace("?", ""); - var protoElementType = prop.ElementType?.Split('.').Last() ?? "object"; - if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) - { - return GenerateResultValueTypeCollectionWithComplexElements(prop, source, indent, protoWrapperType, protoElementType); - } - else - { - // Simple element type - return $"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{ {source}.Select(x => new {protoElementType} {{ X = x.X, Y = x.Y }}) }} }},"; - } - } - else if (prop.IsElementComplexType) - { - // Complex list: map each element to proto type - return GenerateResultComplexListMapping(prop, source, indent); - } - else if (prop.IsElementGuid) - { - // List -> repeated string - return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty() }},"; - } - else - { - // Primitive list: just copy - return $"{indent}{prop.Name} = {{ {source} ?? Enumerable.Empty<{prop.Type.Replace("System.Collections.Generic.List<", "").Replace(">", "").Replace("?", "")}>() }},"; - } - } - - // Handle Guid (C# Guid -> proto string) - if (prop.IsGuid) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; - } - else - { - return $"{indent}{prop.Name} = {source}.ToString(),"; - } - } - - // Handle decimals (C# decimal -> proto string) - if (prop.IsDecimal) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; - } - else - { - return $"{indent}{prop.Name} = {source}.ToString(),"; - } - } - - // Handle DateTime (C# DateTime -> proto Timestamp) - if (prop.IsDateTime) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}.HasValue ? Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}.Value, System.DateTimeKind.Utc)) : null,"; - } - else - { - return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}, System.DateTimeKind.Utc)),"; - } - } - - // Handle TimeSpan (C# TimeSpan -> proto Duration) - if (prop.FullyQualifiedType.Contains("System.TimeSpan")) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}.HasValue ? Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan({source}.Value) : null,"; - } - else - { - return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan({source}),"; - } - } - - // Handle enums (C# enum -> proto int32) - if (prop.IsEnum) - { - return $"{indent}{prop.Name} = (int){source},"; - } - - // Handle binary types (byte[], Stream -> ByteString) - if (prop.IsBinaryType) - { - if (prop.IsStream) - { - // Stream -> ByteString: read stream to bytes first - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.FromStream({source}) : Google.Protobuf.ByteString.Empty,"; - } - return $"{indent}{prop.Name} = Google.Protobuf.ByteString.FromStream({source}),"; - } - else - { - // byte[] -> ByteString - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.CopyFrom({source}) : Google.Protobuf.ByteString.Empty,"; - } - return $"{indent}{prop.Name} = Google.Protobuf.ByteString.CopyFrom({source}),"; - } - } - - // Handle complex types (single objects) - if (prop.IsComplexType) - { - return GenerateResultComplexObjectMapping(prop, source, indent); - } - - // Default: direct assignment (strings, ints, bools, etc.) - if (prop.IsNullable) - { - if (prop.Type.Contains("string")) - { - return $"{indent}{prop.Name} = {source} ?? string.Empty,"; - } - // Handle nullable primitives (long?, int?, etc.) - use default value - return $"{indent}{prop.Name} = {source} ?? default,"; - } - return $"{indent}{prop.Name} = {source},"; - } - - private static string GenerateResultComplexListMapping(PropertyInfo prop, string source, string indent) - { - var sb = new StringBuilder(); - var protoElementType = prop.ElementType?.Split('.').Last() ?? prop.Type; - sb.AppendLine($"{indent}{prop.Name} = {{"); - sb.AppendLine($"{indent} {source}?.Select(x => new {protoElementType}"); - sb.AppendLine($"{indent} {{"); - - if (prop.ElementNestedProperties != null) - { - foreach (var nestedProp in prop.ElementNestedProperties) - { - var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, "x", indent + " "); - sb.AppendLine(nestedAssignment); - } - } - - sb.AppendLine($"{indent} }}) ?? Enumerable.Empty<{protoElementType}>()"); - sb.Append($"{indent}}},"); - return sb.ToString(); - } - - private static string GenerateResultValueTypeCollectionWithComplexElements(PropertyInfo prop, string source, string indent, string protoWrapperType, string protoElementType) - { - var sb = new StringBuilder(); - sb.AppendLine($"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{"); - sb.AppendLine($"{indent} {source}.Select(x => new {protoElementType}"); - sb.AppendLine($"{indent} {{"); - - if (prop.ElementNestedProperties != null) - { - foreach (var nestedProp in prop.ElementNestedProperties) - { - var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, "x", indent + " "); - sb.AppendLine(nestedAssignment); - } - } - - sb.AppendLine($"{indent} }})"); - sb.Append($"{indent}}} }},"); - return sb.ToString(); - } - - private static string GenerateResultComplexObjectMapping(PropertyInfo prop, string source, string indent) - { - var sb = new StringBuilder(); - var protoType = prop.Type.Split('.').Last().Replace("?", ""); - sb.AppendLine($"{indent}{prop.Name} = {source} != null ? new {protoType}"); - sb.AppendLine($"{indent}{{"); - - foreach (var nestedProp in prop.NestedProperties) - { - var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, source, indent + " "); - sb.AppendLine(nestedAssignment); - } - - sb.Append($"{indent}}} : null,"); - return sb.ToString(); - } - - private static string GenerateResultNestedPropertyMapping(PropertyInfo prop, string sourceVar, string indent) - { - var source = $"{sourceVar}.{prop.Name}"; - - // Handle DateTime (C# DateTime -> proto Timestamp) - if (prop.IsDateTime) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}.Value, System.DateTimeKind.Utc)) : null,"; - } - else - { - return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}, System.DateTimeKind.Utc)),"; - } - } - - // Handle DateOnly (C# DateOnly -> proto string) - if (prop.Type.Contains("DateOnly")) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}?.ToString(\"yyyy-MM-dd\") ?? string.Empty,"; - } - else - { - return $"{indent}{prop.Name} = {source}.ToString(\"yyyy-MM-dd\"),"; - } - } - - // Handle Guid - if (prop.IsGuid) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; - } - else - { - return $"{indent}{prop.Name} = {source}.ToString(),"; - } - } - - // Handle decimals - if (prop.IsDecimal) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; - } - else - { - return $"{indent}{prop.Name} = {source}.ToString(),"; - } - } - - // Handle enums - if (prop.IsEnum) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = (int)({source} ?? default),"; - } - return $"{indent}{prop.Name} = (int){source},"; - } - - // Handle binary types (C# byte[]/Stream -> proto ByteString) - if (prop.IsBinaryType) - { - if (prop.IsStream) - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.FromStream({source}) : Google.Protobuf.ByteString.Empty,"; - } - return $"{indent}{prop.Name} = Google.Protobuf.ByteString.FromStream({source}),"; - } - else - { - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.CopyFrom({source}) : Google.Protobuf.ByteString.Empty,"; - } - return $"{indent}{prop.Name} = Google.Protobuf.ByteString.CopyFrom({source}),"; - } - } - - // Handle lists - if (prop.IsList) - { - // Value type collections (like NpgsqlPolygon) map to a wrapper message with Items field - if (prop.IsValueTypeCollection) - { - var protoWrapperType = prop.Type.Split('.').Last().Replace("?", ""); - var protoElementType = prop.ElementType?.Split('.').Last() ?? "object"; - if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) - { - return GenerateResultValueTypeCollectionWithComplexElements(prop, source, indent, protoWrapperType, protoElementType); - } - else - { - // Simple element type (like NpgsqlPoint) - return $"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{ {source}.Select(x => new {protoElementType} {{ X = x.X, Y = x.Y }}) }} }},"; - } - } - else if (prop.IsElementGuid) - { - return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty() }},"; - } - else if (prop.IsElementComplexType) - { - // Complex list elements need mapping - if (prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) - { - // Use recursive mapping for nested properties - return GenerateResultComplexListMapping(prop, source, indent); - } - else - { - // Fall back to creating empty proto objects (the user needs to ensure types are compatible) - var elementTypeName = prop.ElementType?.Split('.').Last() ?? "object"; - return $"{indent}{prop.Name} = {{ {source}?.Select(x => new {elementTypeName}()) ?? Enumerable.Empty<{elementTypeName}>() }},"; - } - } - return $"{indent}{prop.Name} = {{ {source} }},"; - } - - // Handle complex types (non-list) - if (prop.IsComplexType) - { - var typeName = prop.Type.Split('.').Last().Replace("?", ""); - if (prop.NestedProperties != null && prop.NestedProperties.Any()) - { - // Use recursive mapping for nested properties - return GenerateResultComplexObjectMapping(prop, source, indent); - } - else if (prop.IsNullable || prop.Type.EndsWith("?")) - { - return $"{indent}{prop.Name} = {source} != null ? new {typeName}() : null,"; - } - else - { - return $"{indent}{prop.Name} = new {typeName}(),"; - } - } - - // Handle nullable strings - if (prop.IsNullable && prop.Type.Contains("string")) - { - return $"{indent}{prop.Name} = {source} ?? string.Empty,"; - } - - // Handle nullable value types (int?, long?, double?, etc.) - if (prop.IsNullable) - { - return $"{indent}{prop.Name} = {source} ?? default,"; - } - - // Default: direct assignment - return $"{indent}{prop.Name} = {source},"; - } - - private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType) - { - var queryInfo = new QueryInfo - { - Name = queryType.Name, - FullyQualifiedName = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - Namespace = queryType.ContainingNamespace.ToDisplayString(), - Properties = new List() - }; - - // Set result type - queryInfo.ResultType = resultType.Name; - queryInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + commandInfo.ResultType = resultType.Name; + commandInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Use fully qualified names to avoid ambiguity with proto-generated types - var queryTypeFullyQualified = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryTypeFullyQualified}, {resultTypeFullyQualified}>"; + commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}, {resultTypeFullyQualified}>"; // Check if result type is primitive var resultTypeString = resultType.ToDisplayString(); - queryInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString); + commandInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString); // Extract result type properties if it's a complex type - if (!queryInfo.IsResultPrimitiveType) + if (!commandInfo.IsResultPrimitiveType) { var resultProperties = resultType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) @@ -1486,30 +343,33 @@ namespace Svrnty.CQRS.Grpc.Generators foreach (var property in resultProperties) { + var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var isList = IsListOrCollection(property.Type); + var isComplexType = IsUserDefinedComplexType(property.Type); + var propInfo = new PropertyInfo { Name = property.Name, Type = property.Type.ToDisplayString(), - FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - ProtoType = string.Empty, - FieldNumber = 0, - IsNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated || - (property.Type is INamedTypeSymbol nt && nt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T), + FullyQualifiedType = propertyType, + ProtoType = string.Empty, // Not needed for result mapping + FieldNumber = 0, // Not needed for result mapping + IsList = isList, + IsComplexType = isComplexType, + IsNullable = IsNullableType(property.Type), IsEnum = IsEnumType(property.Type), IsDecimal = IsDecimalType(property.Type), IsDateTime = IsDateTimeType(property.Type), IsDateTimeOffset = IsDateTimeOffsetType(property.Type), IsGuid = IsGuidType(property.Type), IsJsonElement = IsJsonElementType(property.Type), - IsList = IsListOrCollection(property.Type), IsValueTypeCollection = IsValueTypeCollection(property.Type), - IsComplexType = IsUserDefinedComplexType(property.Type), IsBinaryType = IsBinaryType(property.Type), IsStream = IsStreamType(property.Type), }; // If it's a list, extract element type info - if (propInfo.IsList) + if (isList) { var elementType = GetListElementType(property.Type); if (elementType != null) @@ -1517,47 +377,1122 @@ namespace Svrnty.CQRS.Grpc.Generators propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); propInfo.IsElementGuid = IsGuidType(elementType); - - // Extract nested properties for complex element types - if (propInfo.IsElementComplexType && elementType is INamedTypeSymbol namedElementType) - { - propInfo.ElementNestedProperties = new List(); - ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); - } } } - // If it's a complex type (not list), extract nested properties - else if (propInfo.IsComplexType) + + commandInfo.ResultProperties.Add(propInfo); + } + } + } + else + { + var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}>"; + } + + // Extract properties + var properties = commandType.GetMembers().OfType() + .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) + .ToList(); + + int fieldNumber = 1; + foreach (var property in properties) + { + var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); + + var propInfo = new PropertyInfo + { + Name = property.Name, + Type = propertyType, + FullyQualifiedType = propertyType, + ProtoType = protoType, + FieldNumber = fieldNumber++, + IsComplexType = IsUserDefinedComplexType(property.Type), + // New type metadata fields + IsNullable = IsNullableType(property.Type), + IsEnum = IsEnumType(property.Type), + IsDecimal = IsDecimalType(property.Type), + IsDateTime = IsDateTimeType(property.Type), + IsDateTimeOffset = IsDateTimeOffsetType(property.Type), + IsGuid = IsGuidType(property.Type), + IsJsonElement = IsJsonElementType(property.Type), + IsList = IsListOrCollection(property.Type), + IsValueTypeCollection = IsValueTypeCollection(property.Type), + IsBinaryType = IsBinaryType(property.Type), + IsStream = IsStreamType(property.Type), + }; + + // If it's a list, extract element type info + if (propInfo.IsList) + { + var elementType = GetListElementType(property.Type); + if (elementType != null) + { + propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); + propInfo.IsElementGuid = IsGuidType(elementType); + + // If element is complex, extract nested properties + if (propInfo.IsElementComplexType) { - var unwrapped = UnwrapNullableType(property.Type); - if (unwrapped is INamedTypeSymbol namedType) + var unwrappedElement = UnwrapNullableType(elementType); + if (unwrappedElement is INamedTypeSymbol namedElementType) { - ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties); + propInfo.ElementNestedProperties = new List(); + ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); } } - - queryInfo.ResultProperties.Add(propInfo); + } + } + // If it's a complex type (not list), extract nested properties + else if (propInfo.IsComplexType) + { + var unwrapped = UnwrapNullableType(property.Type); + if (unwrapped is INamedTypeSymbol namedType) + { + ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties); } } - // Extract properties - var properties = queryType.GetMembers().OfType() + commandInfo.Properties.Add(propInfo); + } + + return commandInfo; + } + + private static bool IsUserDefinedComplexType(ITypeSymbol type) + { + if (type == null) + return false; + + // Unwrap nullable first + var unwrapped = UnwrapNullableType(type); + + if (unwrapped.TypeKind != TypeKind.Class && unwrapped.TypeKind != TypeKind.Struct) + return false; + + var fullName = unwrapped.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + // Exclude system types and primitives + if (fullName.StartsWith("global::System.")) + return false; + if (IsPrimitiveType(fullName)) + return false; + + return true; + } + + private static ITypeSymbol UnwrapNullableType(ITypeSymbol type) + { + // Handle Nullable (value type nullability) + if (type is INamedTypeSymbol namedType && + namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && + namedType.TypeArguments.Length == 1) + { + return namedType.TypeArguments[0]; + } + return type; + } + + private static bool IsNullableType(ITypeSymbol type) + { + // Check for Nullable (value type nullability) + if (type is INamedTypeSymbol namedType && + namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + return true; + } + // Check for reference type nullability (C# 8.0+) + if (type.NullableAnnotation == NullableAnnotation.Annotated) + { + return true; + } + return false; + } + + private static bool IsDecimalType(ITypeSymbol type) + { + var unwrapped = UnwrapNullableType(type); + return unwrapped.SpecialType == SpecialType.System_Decimal; + } + + private static bool IsDateTimeType(ITypeSymbol type) + { + var unwrapped = UnwrapNullableType(type); + return unwrapped.SpecialType == SpecialType.System_DateTime; + } + + private static bool IsDateTimeOffsetType(ITypeSymbol type) + { + var unwrapped = UnwrapNullableType(type); + return unwrapped.ToDisplayString() == "System.DateTimeOffset"; + } + + private static bool IsEnumType(ITypeSymbol type) + { + var unwrapped = UnwrapNullableType(type); + return unwrapped.TypeKind == TypeKind.Enum; + } + + private static bool IsGuidType(ITypeSymbol type) + { + var unwrapped = UnwrapNullableType(type); + return unwrapped.ToDisplayString() == "System.Guid"; + } + + private static bool IsJsonElementType(ITypeSymbol type) + { + var unwrapped = UnwrapNullableType(type); + return unwrapped.ToDisplayString() == "System.Text.Json.JsonElement"; + } + + private static bool IsBinaryType(ITypeSymbol type) + { + return ProtoFileTypeMapper.IsBinaryType(type); + } + + private static bool IsStreamType(ITypeSymbol type) + { + var fullTypeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + if (fullTypeName.Contains("System.IO.Stream") || + fullTypeName.Contains("System.IO.MemoryStream") || + fullTypeName.Contains("System.IO.FileStream")) + { + return true; + } + var typeName = type.Name; + return typeName == "Stream" || typeName == "MemoryStream" || typeName == "FileStream"; + } + + private static bool IsListOrCollection(ITypeSymbol type) + { + // Unwrap nullable first to handle NpgsqlPolygon? + var unwrapped = UnwrapNullableType(type); + + if (unwrapped is IArrayTypeSymbol arrayType) + { + // byte[] is binary data, not a collection + if (arrayType.ElementType.SpecialType == SpecialType.System_Byte) + return false; + return true; + } + + if (unwrapped is INamedTypeSymbol namedType) + { + // Check if it's a generic collection type + if (namedType.IsGenericType) + { + var typeName = namedType.OriginalDefinition.ToDisplayString(); + if (typeName.StartsWith("System.Collections.Generic.List<") || + typeName.StartsWith("System.Collections.Generic.IList<") || + typeName.StartsWith("System.Collections.Generic.ICollection<") || + typeName.StartsWith("System.Collections.Generic.IEnumerable<")) + { + return true; + } + } + + // Check if it implements IList, ICollection, or IEnumerable (handles types like NpgsqlPolygon) + // Skip string which implements IEnumerable + if (namedType.SpecialType == SpecialType.System_String) + return false; + + foreach (var iface in namedType.AllInterfaces) + { + if (iface.IsGenericType && iface.TypeArguments.Length == 1) + { + var ifaceName = iface.OriginalDefinition.ToDisplayString(); + if (ifaceName == "System.Collections.Generic.IList" || + ifaceName == "System.Collections.Generic.ICollection" || + ifaceName == "System.Collections.Generic.IReadOnlyList" || + ifaceName == "System.Collections.Generic.IReadOnlyCollection") + { + return true; + } + } + } + } + return false; + } + + /// + /// Checks if a type is a value type that implements a collection interface (like NpgsqlPolygon) + /// These require special construction syntax + /// + private static bool IsValueTypeCollection(ITypeSymbol type) + { + // Unwrap nullable first to handle NpgsqlPolygon? + var unwrapped = UnwrapNullableType(type); + + if (unwrapped is not INamedTypeSymbol namedType) + return false; + + // Must be a value type (struct) + if (!namedType.IsValueType) + return false; + + // Must implement a collection interface (pass unwrapped since IsListOrCollection also unwraps) + return IsListOrCollection(unwrapped); + } + + private static ITypeSymbol? GetListElementType(ITypeSymbol type) + { + // Unwrap nullable first to handle NpgsqlPolygon? + var unwrapped = UnwrapNullableType(type); + + if (unwrapped is IArrayTypeSymbol arrayType) + return arrayType.ElementType; + + if (unwrapped is INamedTypeSymbol namedType) + { + // First try generic type arguments + if (namedType.IsGenericType && namedType.TypeArguments.Length > 0) + { + return namedType.TypeArguments[0]; + } + + // Fall back to checking interfaces (for types like NpgsqlPolygon that implement IList) + foreach (var iface in namedType.AllInterfaces) + { + if (iface.IsGenericType && iface.TypeArguments.Length == 1) + { + var ifaceName = iface.OriginalDefinition.ToDisplayString(); + if (ifaceName == "System.Collections.Generic.IList" || + ifaceName == "System.Collections.Generic.IReadOnlyList") + { + return iface.TypeArguments[0]; + } + } + } + + // Try ICollection or IEnumerable as fallback + foreach (var iface in namedType.AllInterfaces) + { + if (iface.IsGenericType && iface.TypeArguments.Length == 1) + { + var ifaceName = iface.OriginalDefinition.ToDisplayString(); + if (ifaceName == "System.Collections.Generic.ICollection" || + ifaceName == "System.Collections.Generic.IReadOnlyCollection" || + ifaceName == "System.Collections.Generic.IEnumerable") + { + return iface.TypeArguments[0]; + } + } + } + } + return null; + } + + /// + /// Generates the value expression for converting from proto type to C# type + /// + private static string GetProtoToCSharpConversion(PropertyInfo prop, string sourceExpr) + { + if (prop.IsGuid) + { + if (prop.IsNullable) + return $"string.IsNullOrEmpty({sourceExpr}) ? null : System.Guid.Parse({sourceExpr})"; + return $"System.Guid.Parse({sourceExpr})"; + } + if (prop.IsEnum) + { + // Enum is already handled correctly in proto - values match + return $"{sourceExpr}"; + } + // Default: direct assignment + return $"{sourceExpr}!"; + } + + /// + /// Generates the value expression for converting from C# type to proto type + /// + private static string GetCSharpToProtoConversion(PropertyInfo prop, string sourceExpr) + { + if (prop.IsGuid) + { + if (prop.IsNullable) + return $"{sourceExpr}?.ToString() ?? \"\""; + return $"{sourceExpr}.ToString()"; + } + // Default: direct assignment + return sourceExpr; + } + + private static void ExtractNestedProperties(INamedTypeSymbol type, List nestedProperties) + { + var properties = type.GetMembers().OfType() + .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) + .ToList(); + + foreach (var property in properties) + { + var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var propInfo = new PropertyInfo + { + Name = property.Name, + Type = propertyType, + FullyQualifiedType = propertyType, + ProtoType = string.Empty, + FieldNumber = 0, + IsComplexType = IsUserDefinedComplexType(property.Type), + IsList = IsListOrCollection(property.Type), + IsValueTypeCollection = IsValueTypeCollection(property.Type), + }; + + // Recursively extract nested properties for complex types + if (propInfo.IsComplexType && property.Type is INamedTypeSymbol namedType) + { + ExtractNestedProperties(namedType, propInfo.NestedProperties); + } + + nestedProperties.Add(propInfo); + } + } + + private static void ExtractNestedPropertiesWithTypeInfo(INamedTypeSymbol type, List nestedProperties) + { + var properties = type.GetMembers().OfType() + .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) + .ToList(); + + foreach (var property in properties) + { + // Skip read-only properties (no setter) - they are computed and can't be set + var isReadOnly = property.IsReadOnly || property.SetMethod == null; + + var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var propInfo = new PropertyInfo + { + Name = property.Name, + Type = propertyType, + FullyQualifiedType = propertyType, + ProtoType = string.Empty, + FieldNumber = 0, + IsComplexType = IsUserDefinedComplexType(property.Type), + IsReadOnly = isReadOnly, + // Type metadata + IsNullable = IsNullableType(property.Type), + IsEnum = IsEnumType(property.Type), + IsDecimal = IsDecimalType(property.Type), + IsDateTime = IsDateTimeType(property.Type), + IsDateTimeOffset = IsDateTimeOffsetType(property.Type), + IsGuid = IsGuidType(property.Type), + IsJsonElement = IsJsonElementType(property.Type), + IsList = IsListOrCollection(property.Type), + IsValueTypeCollection = IsValueTypeCollection(property.Type), + IsBinaryType = IsBinaryType(property.Type), + IsStream = IsStreamType(property.Type), + }; + + // If it's a list, extract element type info + if (propInfo.IsList) + { + var elementType = GetListElementType(property.Type); + if (elementType != null) + { + propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); + propInfo.IsElementGuid = IsGuidType(elementType); + + // Extract nested properties for complex element types + if (propInfo.IsElementComplexType && elementType is INamedTypeSymbol namedElementType) + { + propInfo.ElementNestedProperties = new List(); + ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); + } + } + } + // Recursively extract nested properties for complex types + else if (propInfo.IsComplexType) + { + var unwrapped = UnwrapNullableType(property.Type); + if (unwrapped is INamedTypeSymbol namedType) + { + ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties); + } + } + + nestedProperties.Add(propInfo); + } + } + + private static void GenerateNestedPropertyMapping(StringBuilder sb, List properties, string sourcePrefix, string indent) + { + foreach (var prop in properties) + { + var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); + if (prop.IsComplexType) + { + // Generate nested object mapping + sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName} != null ? new {prop.FullyQualifiedType}"); + sb.AppendLine($"{indent}{{"); + GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{sourcePropName}", indent + " "); + sb.AppendLine($"{indent}}} : null!,"); + } + else + { + sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName},"); + } + } + } + + private static string GeneratePropertyAssignment(PropertyInfo prop, string requestVar, string indent) + { + var requestPropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); + var source = $"{requestVar}.{requestPropName}"; + + // Handle lists + if (prop.IsList) + { + if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) + { + // Complex list: map each element + return GenerateComplexListMapping(prop, source, indent); + } + else if (prop.IsElementGuid) + { + // List from proto -> List in C# + if (prop.IsValueTypeCollection) + { + // Value type collection proto message has Items field + var constructorType = prop.FullyQualifiedType.TrimEnd('?'); + return $"{indent}{prop.Name} = new {constructorType}({source}?.Items?.Select(x => System.Guid.Parse(x)).ToArray() ?? System.Array.Empty()),"; + } + return $"{indent}{prop.Name} = {source}?.Select(x => System.Guid.Parse(x)).ToList(),"; + } + else if (prop.IsValueTypeCollection) + { + // Value type collection (like NpgsqlPolygon): proto message has Items field + var constructorType = prop.FullyQualifiedType.TrimEnd('?'); + return $"{indent}{prop.Name} = new {constructorType}({source}?.Items?.Select(x => new {prop.ElementType} {{ X = x.X, Y = x.Y }}).ToArray() ?? System.Array.Empty<{prop.ElementType ?? "object"}>()),"; + } + else + { + // Primitive list: just ToList() + return $"{indent}{prop.Name} = {source}?.ToList(),"; + } + } + + // Handle enums (proto int32 -> C# enum) + if (prop.IsEnum) + { + return $"{indent}{prop.Name} = ({prop.FullyQualifiedType}){source},"; + } + + // Handle decimals (proto string -> C# decimal) + if (prop.IsDecimal) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),"; + } + else + { + return $"{indent}{prop.Name} = decimal.Parse({source}),"; + } + } + + // Handle DateTime (proto Timestamp -> C# DateTime) + if (prop.IsDateTime) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} == null ? (System.DateTime?)null : {source}.ToDateTime(),"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToDateTime(),"; + } + } + + // Handle DateTimeOffset (proto Timestamp -> C# DateTimeOffset) + if (prop.IsDateTimeOffset) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} == null ? (System.DateTimeOffset?)null : {source}.ToDateTimeOffset(),"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToDateTimeOffset(),"; + } + } + + // Handle Guid (proto string -> C# Guid) + if (prop.IsGuid) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : System.Guid.Parse({source}),"; + } + else + { + return $"{indent}{prop.Name} = System.Guid.Parse({source}),"; + } + } + + // Handle binary types (proto ByteString -> C# byte[]/Stream) + if (prop.IsBinaryType) + { + if (prop.IsStream) + { + // ByteString -> MemoryStream + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.IsEmpty == false ? new System.IO.MemoryStream({source}.ToByteArray()) : null,"; + } + return $"{indent}{prop.Name} = new System.IO.MemoryStream({source}.ToByteArray()),"; + } + else + { + // ByteString -> byte[] + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.IsEmpty == false ? {source}.ToByteArray() : null,"; + } + return $"{indent}{prop.Name} = {source}.ToByteArray(),"; + } + } + + // Handle complex types (single objects) + if (prop.IsComplexType) + { + return GenerateComplexObjectMapping(prop, source, indent); + } + + // Default: direct assignment + return $"{indent}{prop.Name} = {source},"; + } + + private static string GenerateComplexListMapping(PropertyInfo prop, string source, string indent) + { + var sb = new StringBuilder(); + // For value type collections, the proto message has an Items field containing the repeated elements + var itemsSource = prop.IsValueTypeCollection ? $"{source}?.Items" : source; + sb.AppendLine($"{indent}{prop.Name} = {itemsSource}?.Select(x => new {prop.ElementType}"); + sb.AppendLine($"{indent}{{"); + + foreach (var nestedProp in prop.ElementNestedProperties!) + { + // Skip read-only properties - they can't be assigned + if (nestedProp.IsReadOnly) continue; + + var nestedSourcePropName = char.ToUpper(nestedProp.Name[0]) + nestedProp.Name.Substring(1); + var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " "); + sb.AppendLine(nestedAssignment); + } + + if (prop.IsValueTypeCollection) + { + // Value type collection: wrap in constructor with array + // Use non-nullable type for constructor (remove trailing ? if present) + var constructorType = prop.FullyQualifiedType.TrimEnd('?'); + sb.Append($"{indent}}}).ToArray() is {{ }} arr ? new {constructorType}(arr) : default,"); + } + else + { + sb.Append($"{indent}}}).ToList(),"); + } + return sb.ToString(); + } + + private static string GenerateComplexObjectMapping(PropertyInfo prop, string source, string indent) + { + var sb = new StringBuilder(); + sb.AppendLine($"{indent}{prop.Name} = {source} != null ? new {prop.FullyQualifiedType}"); + sb.AppendLine($"{indent}{{"); + + foreach (var nestedProp in prop.NestedProperties) + { + // Skip read-only properties - they can't be assigned + if (nestedProp.IsReadOnly) continue; + + var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, source, indent + " "); + sb.AppendLine(nestedAssignment); + } + + sb.Append($"{indent}}} : null!,"); + return sb.ToString(); + } + + private static string GenerateNestedPropertyAssignment(PropertyInfo prop, string sourceVar, string indent) + { + var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); + var source = $"{sourceVar}.{sourcePropName}"; + + // Handle enums + if (prop.IsEnum) + { + return $"{indent}{prop.Name} = ({prop.FullyQualifiedType}){source},"; + } + + // Handle decimals + if (prop.IsDecimal) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),"; + } + else + { + return $"{indent}{prop.Name} = decimal.Parse({source}),"; + } + } + + // Handle DateTime (proto Timestamp -> C# DateTime) + if (prop.IsDateTime) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} == null ? (System.DateTime?)null : {source}.ToDateTime(),"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToDateTime(),"; + } + } + + // Handle Guid + if (prop.IsGuid) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : System.Guid.Parse({source}),"; + } + else + { + return $"{indent}{prop.Name} = System.Guid.Parse({source}),"; + } + } + + // Handle lists with complex element types + if (prop.IsList) + { + if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) + { + return GenerateComplexListMapping(prop, source, indent); + } + if (prop.IsValueTypeCollection) + { + // Value type collection (like NpgsqlPolygon): use constructor with array + var constructorType = prop.FullyQualifiedType.TrimEnd('?'); + return $"{indent}{prop.Name} = new {constructorType}({source}?.ToArray() ?? System.Array.Empty<{prop.ElementType ?? "object"}>()),"; + } + return $"{indent}{prop.Name} = {source}?.ToList(),"; + } + + // Handle complex types + if (prop.IsComplexType && prop.NestedProperties.Any()) + { + return GenerateComplexObjectMapping(prop, source, indent); + } + + // Default: direct assignment + return $"{indent}{prop.Name} = {source},"; + } + + /// + /// Generates C# to proto property mapping (reverse of GeneratePropertyAssignment) + /// + private static string GenerateResultPropertyMapping(PropertyInfo prop, string sourceVar, string indent) + { + var source = $"{sourceVar}.{prop.Name}"; + + // Handle lists + if (prop.IsList) + { + // Value type collections (like NpgsqlPolygon) map to a wrapper message with Items field + if (prop.IsValueTypeCollection) + { + var protoWrapperType = prop.Type.Split('.').Last().Replace("?", ""); + var protoElementType = prop.ElementType?.Split('.').Last() ?? "object"; + if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) + { + return GenerateResultValueTypeCollectionWithComplexElements(prop, source, indent, protoWrapperType, protoElementType); + } + else + { + // Simple element type + return $"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{ {source}.Select(x => new {protoElementType} {{ X = x.X, Y = x.Y }}) }} }},"; + } + } + else if (prop.IsElementComplexType) + { + // Complex list: map each element to proto type + return GenerateResultComplexListMapping(prop, source, indent); + } + else if (prop.IsElementGuid) + { + // List -> repeated string + return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty() }},"; + } + else + { + // Primitive list: just copy + return $"{indent}{prop.Name} = {{ {source} ?? Enumerable.Empty<{prop.Type.Replace("System.Collections.Generic.List<", "").Replace(">", "").Replace("?", "")}>() }},"; + } + } + + // Handle Guid (C# Guid -> proto string) + if (prop.IsGuid) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(),"; + } + } + + // Handle decimals (C# decimal -> proto string) + if (prop.IsDecimal) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(),"; + } + } + + // Handle DateTime (C# DateTime -> proto Timestamp) + if (prop.IsDateTime) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}.HasValue ? Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}.Value, System.DateTimeKind.Utc)) : null,"; + } + else + { + return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}, System.DateTimeKind.Utc)),"; + } + } + + // Handle TimeSpan (C# TimeSpan -> proto Duration) + if (prop.FullyQualifiedType.Contains("System.TimeSpan")) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}.HasValue ? Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan({source}.Value) : null,"; + } + else + { + return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan({source}),"; + } + } + + // Handle enums (C# enum -> proto int32) + if (prop.IsEnum) + { + return $"{indent}{prop.Name} = (int){source},"; + } + + // Handle binary types (byte[], Stream -> ByteString) + if (prop.IsBinaryType) + { + if (prop.IsStream) + { + // Stream -> ByteString: read stream to bytes first + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.FromStream({source}) : Google.Protobuf.ByteString.Empty,"; + } + return $"{indent}{prop.Name} = Google.Protobuf.ByteString.FromStream({source}),"; + } + else + { + // byte[] -> ByteString + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.CopyFrom({source}) : Google.Protobuf.ByteString.Empty,"; + } + return $"{indent}{prop.Name} = Google.Protobuf.ByteString.CopyFrom({source}),"; + } + } + + // Handle complex types (single objects) + if (prop.IsComplexType) + { + return GenerateResultComplexObjectMapping(prop, source, indent); + } + + // Default: direct assignment (strings, ints, bools, etc.) + if (prop.IsNullable) + { + if (prop.Type.Contains("string")) + { + return $"{indent}{prop.Name} = {source} ?? string.Empty,"; + } + // Handle nullable primitives (long?, int?, etc.) - use default value + return $"{indent}{prop.Name} = {source} ?? default,"; + } + return $"{indent}{prop.Name} = {source},"; + } + + private static string GenerateResultComplexListMapping(PropertyInfo prop, string source, string indent) + { + var sb = new StringBuilder(); + var protoElementType = prop.ElementType?.Split('.').Last() ?? prop.Type; + sb.AppendLine($"{indent}{prop.Name} = {{"); + sb.AppendLine($"{indent} {source}?.Select(x => new {protoElementType}"); + sb.AppendLine($"{indent} {{"); + + if (prop.ElementNestedProperties != null) + { + foreach (var nestedProp in prop.ElementNestedProperties) + { + var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, "x", indent + " "); + sb.AppendLine(nestedAssignment); + } + } + + sb.AppendLine($"{indent} }}) ?? Enumerable.Empty<{protoElementType}>()"); + sb.Append($"{indent}}},"); + return sb.ToString(); + } + + private static string GenerateResultValueTypeCollectionWithComplexElements(PropertyInfo prop, string source, string indent, string protoWrapperType, string protoElementType) + { + var sb = new StringBuilder(); + sb.AppendLine($"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{"); + sb.AppendLine($"{indent} {source}.Select(x => new {protoElementType}"); + sb.AppendLine($"{indent} {{"); + + if (prop.ElementNestedProperties != null) + { + foreach (var nestedProp in prop.ElementNestedProperties) + { + var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, "x", indent + " "); + sb.AppendLine(nestedAssignment); + } + } + + sb.AppendLine($"{indent} }})"); + sb.Append($"{indent}}} }},"); + return sb.ToString(); + } + + private static string GenerateResultComplexObjectMapping(PropertyInfo prop, string source, string indent) + { + var sb = new StringBuilder(); + var protoType = prop.Type.Split('.').Last().Replace("?", ""); + sb.AppendLine($"{indent}{prop.Name} = {source} != null ? new {protoType}"); + sb.AppendLine($"{indent}{{"); + + foreach (var nestedProp in prop.NestedProperties) + { + var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, source, indent + " "); + sb.AppendLine(nestedAssignment); + } + + sb.Append($"{indent}}} : null,"); + return sb.ToString(); + } + + private static string GenerateResultNestedPropertyMapping(PropertyInfo prop, string sourceVar, string indent) + { + var source = $"{sourceVar}.{prop.Name}"; + + // Handle DateTime (C# DateTime -> proto Timestamp) + if (prop.IsDateTime) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}.Value, System.DateTimeKind.Utc)) : null,"; + } + else + { + return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}, System.DateTimeKind.Utc)),"; + } + } + + // Handle DateOnly (C# DateOnly -> proto string) + if (prop.Type.Contains("DateOnly")) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString(\"yyyy-MM-dd\") ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(\"yyyy-MM-dd\"),"; + } + } + + // Handle Guid + if (prop.IsGuid) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(),"; + } + } + + // Handle decimals + if (prop.IsDecimal) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(),"; + } + } + + // Handle enums + if (prop.IsEnum) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = (int)({source} ?? default),"; + } + return $"{indent}{prop.Name} = (int){source},"; + } + + // Handle binary types (C# byte[]/Stream -> proto ByteString) + if (prop.IsBinaryType) + { + if (prop.IsStream) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.FromStream({source}) : Google.Protobuf.ByteString.Empty,"; + } + return $"{indent}{prop.Name} = Google.Protobuf.ByteString.FromStream({source}),"; + } + else + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.CopyFrom({source}) : Google.Protobuf.ByteString.Empty,"; + } + return $"{indent}{prop.Name} = Google.Protobuf.ByteString.CopyFrom({source}),"; + } + } + + // Handle lists + if (prop.IsList) + { + // Value type collections (like NpgsqlPolygon) map to a wrapper message with Items field + if (prop.IsValueTypeCollection) + { + var protoWrapperType = prop.Type.Split('.').Last().Replace("?", ""); + var protoElementType = prop.ElementType?.Split('.').Last() ?? "object"; + if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) + { + return GenerateResultValueTypeCollectionWithComplexElements(prop, source, indent, protoWrapperType, protoElementType); + } + else + { + // Simple element type (like NpgsqlPoint) + return $"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{ {source}.Select(x => new {protoElementType} {{ X = x.X, Y = x.Y }}) }} }},"; + } + } + else if (prop.IsElementGuid) + { + return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty() }},"; + } + else if (prop.IsElementComplexType) + { + // Complex list elements need mapping + if (prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) + { + // Use recursive mapping for nested properties + return GenerateResultComplexListMapping(prop, source, indent); + } + else + { + // Fall back to creating empty proto objects (the user needs to ensure types are compatible) + var elementTypeName = prop.ElementType?.Split('.').Last() ?? "object"; + return $"{indent}{prop.Name} = {{ {source}?.Select(x => new {elementTypeName}()) ?? Enumerable.Empty<{elementTypeName}>() }},"; + } + } + return $"{indent}{prop.Name} = {{ {source} }},"; + } + + // Handle complex types (non-list) + if (prop.IsComplexType) + { + var typeName = prop.Type.Split('.').Last().Replace("?", ""); + if (prop.NestedProperties != null && prop.NestedProperties.Any()) + { + // Use recursive mapping for nested properties + return GenerateResultComplexObjectMapping(prop, source, indent); + } + else if (prop.IsNullable || prop.Type.EndsWith("?")) + { + return $"{indent}{prop.Name} = {source} != null ? new {typeName}() : null,"; + } + else + { + return $"{indent}{prop.Name} = new {typeName}(),"; + } + } + + // Handle nullable strings + if (prop.IsNullable && prop.Type.Contains("string")) + { + return $"{indent}{prop.Name} = {source} ?? string.Empty,"; + } + + // Handle nullable value types (int?, long?, double?, etc.) + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} ?? default,"; + } + + // Default: direct assignment + return $"{indent}{prop.Name} = {source},"; + } + + private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType) + { + var queryInfo = new QueryInfo + { + Name = queryType.Name, + FullyQualifiedName = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + Namespace = queryType.ContainingNamespace.ToDisplayString(), + Properties = new List() + }; + + // Set result type + queryInfo.ResultType = resultType.Name; + queryInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + // Use fully qualified names to avoid ambiguity with proto-generated types + var queryTypeFullyQualified = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryTypeFullyQualified}, {resultTypeFullyQualified}>"; + + // Check if result type is primitive + var resultTypeString = resultType.ToDisplayString(); + queryInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString); + + // Extract result type properties if it's a complex type + if (!queryInfo.IsResultPrimitiveType) + { + var resultProperties = resultType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); - int fieldNumber = 1; - foreach (var property in properties) + foreach (var property in resultProperties) { - var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); - var propInfo = new PropertyInfo { Name = property.Name, - Type = propertyType, - FullyQualifiedType = propertyType, - ProtoType = protoType, - FieldNumber = fieldNumber++, + Type = property.Type.ToDisplayString(), + FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ProtoType = string.Empty, + FieldNumber = 0, IsNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated || (property.Type is INamedTypeSymbol nt && nt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T), IsEnum = IsEnumType(property.Type), @@ -1582,6 +1517,13 @@ namespace Svrnty.CQRS.Grpc.Generators propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); propInfo.IsElementGuid = IsGuidType(elementType); + + // Extract nested properties for complex element types + if (propInfo.IsElementComplexType && elementType is INamedTypeSymbol namedElementType) + { + propInfo.ElementNestedProperties = new List(); + ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); + } } } // If it's a complex type (not list), extract nested properties @@ -1594,907 +1536,852 @@ namespace Svrnty.CQRS.Grpc.Generators } } - queryInfo.Properties.Add(propInfo); + queryInfo.ResultProperties.Add(propInfo); } - - return queryInfo; } - private static DynamicQueryInfo? ExtractDynamicQueryInfo(INamedTypeSymbol sourceType, INamedTypeSymbol destinationType, INamedTypeSymbol? paramsType) + // Extract properties + var properties = queryType.GetMembers().OfType() + .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) + .ToList(); + + int fieldNumber = 1; + foreach (var property in properties) { - var dynamicQueryInfo = new DynamicQueryInfo + var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); + + var propInfo = new PropertyInfo { - SourceType = sourceType.Name, - SourceTypeFullyQualified = sourceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - DestinationType = destinationType.Name, - DestinationTypeFullyQualified = destinationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - HasParams = paramsType != null + Name = property.Name, + Type = propertyType, + FullyQualifiedType = propertyType, + ProtoType = protoType, + FieldNumber = fieldNumber++, + IsNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated || + (property.Type is INamedTypeSymbol nt && nt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T), + IsEnum = IsEnumType(property.Type), + IsDecimal = IsDecimalType(property.Type), + IsDateTime = IsDateTimeType(property.Type), + IsDateTimeOffset = IsDateTimeOffsetType(property.Type), + IsGuid = IsGuidType(property.Type), + IsJsonElement = IsJsonElementType(property.Type), + IsList = IsListOrCollection(property.Type), + IsValueTypeCollection = IsValueTypeCollection(property.Type), + IsComplexType = IsUserDefinedComplexType(property.Type), + IsBinaryType = IsBinaryType(property.Type), + IsStream = IsStreamType(property.Type), }; - if (paramsType != null) + // If it's a list, extract element type info + if (propInfo.IsList) { - dynamicQueryInfo.ParamsType = paramsType.Name; - dynamicQueryInfo.ParamsTypeFullyQualified = paramsType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - } - - // Pluralize destination type for naming (e.g., User -> Users) - dynamicQueryInfo.Name = Pluralize(destinationType.Name); - - // Build interface names - if (paramsType != null) - { - dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}, {dynamicQueryInfo.ParamsTypeFullyQualified}>"; - dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>"; - } - else - { - dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}>"; - dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>"; - } - - return dynamicQueryInfo; - } - - private static string Pluralize(string word) - { - // Simple pluralization logic - can be enhanced with Pluralize.NET if needed - if (word.EndsWith("y") && word.Length > 1 && !"aeiou".Contains(word[word.Length - 2])) - return word.Substring(0, word.Length - 1) + "ies"; - if (word.EndsWith("s") || word.EndsWith("x") || word.EndsWith("z") || word.EndsWith("ch") || word.EndsWith("sh")) - return word + "es"; - return word + "s"; - } - - private static void GenerateProtoAndServices(SourceProductionContext context, List commands, List queries, List dynamicQueries, List notifications, Compilation compilation) - { - // Get root namespace from compilation - var rootNamespace = compilation.AssemblyName ?? "Application"; - - // Generate service implementations for commands - if (commands.Any()) - { - var commandService = GenerateCommandServiceImpl(commands, rootNamespace); - context.AddSource("CommandServiceImpl.g.cs", commandService); - } - - // Generate service implementations for queries - if (queries.Any()) - { - var queryService = GenerateQueryServiceImpl(queries, rootNamespace); - context.AddSource("QueryServiceImpl.g.cs", queryService); - } - - // Generate service implementations for dynamic queries - if (dynamicQueries.Any()) - { - var dynamicQueryService = GenerateDynamicQueryServiceImpl(dynamicQueries, rootNamespace); - context.AddSource("DynamicQueryServiceImpl.g.cs", dynamicQueryService); - } - - // Generate service implementations for notifications (streaming) - if (notifications.Any()) - { - var notificationService = GenerateNotificationServiceImpl(notifications, rootNamespace); - context.AddSource("NotificationServiceImpl.g.cs", notificationService); - } - - // Generate registration extensions - var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), dynamicQueries.Any(), notifications.Any(), rootNamespace); - context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions); - } - - private static string GenerateCommandMessages(List commands, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using System.Runtime.Serialization;"); - sb.AppendLine("using ProtoBuf;"); - sb.AppendLine(); - sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages"); - sb.AppendLine("{"); - - foreach (var command in commands) - { - // Generate command DTO - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine($" public sealed class {command.Name}Dto"); - sb.AppendLine(" {"); - - foreach (var prop in command.Properties) + var elementType = GetListElementType(property.Type); + if (elementType != null) { - sb.AppendLine($" [ProtoMember({prop.FieldNumber})]"); - sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]"); - sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}"); - sb.AppendLine(); + propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); + propInfo.IsElementGuid = IsGuidType(elementType); } + } + // If it's a complex type (not list), extract nested properties + else if (propInfo.IsComplexType) + { + var unwrapped = UnwrapNullableType(property.Type); + if (unwrapped is INamedTypeSymbol namedType) + { + ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties); + } + } - sb.AppendLine(" }"); + queryInfo.Properties.Add(propInfo); + } + + return queryInfo; + } + + private static DynamicQueryInfo? ExtractDynamicQueryInfo(INamedTypeSymbol sourceType, INamedTypeSymbol destinationType, INamedTypeSymbol? paramsType) + { + var dynamicQueryInfo = new DynamicQueryInfo + { + SourceType = sourceType.Name, + SourceTypeFullyQualified = sourceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + DestinationType = destinationType.Name, + DestinationTypeFullyQualified = destinationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + HasParams = paramsType != null + }; + + if (paramsType != null) + { + dynamicQueryInfo.ParamsType = paramsType.Name; + dynamicQueryInfo.ParamsTypeFullyQualified = paramsType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + // Pluralize destination type for naming (e.g., User -> Users) + dynamicQueryInfo.Name = Pluralize(destinationType.Name); + + // Build interface names + if (paramsType != null) + { + dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}, {dynamicQueryInfo.ParamsTypeFullyQualified}>"; + dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>"; + } + else + { + dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}>"; + dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>"; + } + + return dynamicQueryInfo; + } + + private static string Pluralize(string word) + { + // Simple pluralization logic - can be enhanced with Pluralize.NET if needed + if (word.EndsWith("y") && word.Length > 1 && !"aeiou".Contains(word[word.Length - 2])) + return word.Substring(0, word.Length - 1) + "ies"; + if (word.EndsWith("s") || word.EndsWith("x") || word.EndsWith("z") || word.EndsWith("ch") || word.EndsWith("sh")) + return word + "es"; + return word + "s"; + } + + private static void GenerateProtoAndServices(SourceProductionContext context, List commands, List queries, List dynamicQueries, List notifications, Compilation compilation) + { + // Get root namespace from compilation + var rootNamespace = compilation.AssemblyName ?? "Application"; + + // Generate service implementations for commands + if (commands.Any()) + { + var commandService = GenerateCommandServiceImpl(commands, rootNamespace); + context.AddSource("CommandServiceImpl.g.cs", commandService); + } + + // Generate service implementations for queries + if (queries.Any()) + { + var queryService = GenerateQueryServiceImpl(queries, rootNamespace); + context.AddSource("QueryServiceImpl.g.cs", queryService); + } + + // Generate service implementations for dynamic queries + if (dynamicQueries.Any()) + { + var dynamicQueryService = GenerateDynamicQueryServiceImpl(dynamicQueries, rootNamespace); + context.AddSource("DynamicQueryServiceImpl.g.cs", dynamicQueryService); + } + + // Generate service implementations for notifications (streaming) + if (notifications.Any()) + { + var notificationService = GenerateNotificationServiceImpl(notifications, rootNamespace); + context.AddSource("NotificationServiceImpl.g.cs", notificationService); + } + + // Generate registration extensions + var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), dynamicQueries.Any(), notifications.Any(), rootNamespace); + context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions); + } + + private static string GenerateCommandMessages(List commands, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using System.Runtime.Serialization;"); + sb.AppendLine("using ProtoBuf;"); + sb.AppendLine(); + sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages"); + sb.AppendLine("{"); + + foreach (var command in commands) + { + // Generate command DTO + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine($" public sealed class {command.Name}Dto"); + sb.AppendLine(" {"); + + foreach (var prop in command.Properties) + { + sb.AppendLine($" [ProtoMember({prop.FieldNumber})]"); + sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]"); + sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}"); sb.AppendLine(); - - // Generate result DTO if command has a result - if (command.HasResult) - { - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine($" public sealed class {command.Name}ResultDto"); - sb.AppendLine(" {"); - sb.AppendLine(" [ProtoMember(1)]"); - sb.AppendLine(" [DataMember(Order = 1)]"); - sb.AppendLine($" public {command.ResultFullyQualifiedName} Result {{ get; set; }}"); - sb.AppendLine(" }"); - sb.AppendLine(); - } } - sb.AppendLine("}"); - return sb.ToString(); - } - - private static string GenerateQueryMessages(List queries, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using System.Runtime.Serialization;"); - sb.AppendLine("using ProtoBuf;"); + sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages"); - sb.AppendLine("{"); - foreach (var query in queries) + // Generate result DTO if command has a result + if (command.HasResult) { - // Generate query DTO sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); - sb.AppendLine($" public sealed class {query.Name}Dto"); - sb.AppendLine(" {"); - - foreach (var prop in query.Properties) - { - sb.AppendLine($" [ProtoMember({prop.FieldNumber})]"); - sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]"); - sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}"); - sb.AppendLine(); - } - - sb.AppendLine(" }"); - sb.AppendLine(); - - // Generate result DTO - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine($" public sealed class {query.Name}ResultDto"); + sb.AppendLine($" public sealed class {command.Name}ResultDto"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); - sb.AppendLine($" public {query.ResultFullyQualifiedName} Result {{ get; set; }}"); + sb.AppendLine($" public {command.ResultFullyQualifiedName} Result {{ get; set; }}"); sb.AppendLine(" }"); sb.AppendLine(); } - - sb.AppendLine("}"); - return sb.ToString(); } - private static string GenerateCommandService(List commands, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using System.ServiceModel;"); - sb.AppendLine("using System.Threading;"); - sb.AppendLine("using System.Threading.Tasks;"); - sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); - sb.AppendLine($"using {rootNamespace}.Grpc.Messages;"); - sb.AppendLine("using Svrnty.CQRS.Abstractions;"); - sb.AppendLine("using ProtoBuf.Grpc;"); - sb.AppendLine(); + sb.AppendLine("}"); + return sb.ToString(); + } - // Generate service interface - sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); - sb.AppendLine("{"); - sb.AppendLine(" [ServiceContract]"); - sb.AppendLine(" public interface ICommandService"); + private static string GenerateQueryMessages(List queries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using System.Runtime.Serialization;"); + sb.AppendLine("using ProtoBuf;"); + sb.AppendLine(); + sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages"); + sb.AppendLine("{"); + + foreach (var query in queries) + { + // Generate query DTO + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine($" public sealed class {query.Name}Dto"); sb.AppendLine(" {"); - foreach (var command in commands) + foreach (var prop in query.Properties) { - if (command.HasResult) - { - sb.AppendLine($" [OperationContract]"); - sb.AppendLine($" Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);"); - } - else - { - sb.AppendLine($" [OperationContract]"); - sb.AppendLine($" Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);"); - } + sb.AppendLine($" [ProtoMember({prop.FieldNumber})]"); + sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]"); + sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); - // Generate service implementation - sb.AppendLine(" public sealed class CommandService : ICommandService"); + // Generate result DTO + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine($" public sealed class {query.Name}ResultDto"); sb.AppendLine(" {"); - sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); - sb.AppendLine(); - sb.AppendLine(" public CommandService(IServiceProvider serviceProvider)"); - sb.AppendLine(" {"); - sb.AppendLine(" _serviceProvider = serviceProvider;"); - sb.AppendLine(" }"); - sb.AppendLine(); - - foreach (var command in commands) - { - if (command.HasResult) - { - sb.AppendLine($" public async Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)"); - sb.AppendLine(" {"); - sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); - sb.AppendLine($" var command = new {command.FullyQualifiedName}"); - sb.AppendLine(" {"); - foreach (var prop in command.Properties) - { - var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}"); - sb.AppendLine($" {prop.Name} = {conversion},"); - } - sb.AppendLine(" };"); - sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); - sb.AppendLine($" return new {command.Name}ResultDto {{ Result = result }};"); - sb.AppendLine(" }"); - } - else - { - sb.AppendLine($" public async Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)"); - sb.AppendLine(" {"); - sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); - sb.AppendLine($" var command = new {command.FullyQualifiedName}"); - sb.AppendLine(" {"); - foreach (var prop in command.Properties) - { - var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}"); - sb.AppendLine($" {prop.Name} = {conversion},"); - } - sb.AppendLine(" };"); - sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); - sb.AppendLine(" }"); - } - sb.AppendLine(); - } - + sb.AppendLine(" [ProtoMember(1)]"); + sb.AppendLine(" [DataMember(Order = 1)]"); + sb.AppendLine($" public {query.ResultFullyQualifiedName} Result {{ get; set; }}"); sb.AppendLine(" }"); - sb.AppendLine("}"); - - return sb.ToString(); + sb.AppendLine(); } - private static string GenerateQueryService(List queries, string rootNamespace) + sb.AppendLine("}"); + return sb.ToString(); + } + + private static string GenerateCommandService(List commands, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using System.ServiceModel;"); + sb.AppendLine("using System.Threading;"); + sb.AppendLine("using System.Threading.Tasks;"); + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + sb.AppendLine($"using {rootNamespace}.Grpc.Messages;"); + sb.AppendLine("using Svrnty.CQRS.Abstractions;"); + sb.AppendLine("using ProtoBuf.Grpc;"); + sb.AppendLine(); + + // Generate service interface + sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); + sb.AppendLine("{"); + sb.AppendLine(" [ServiceContract]"); + sb.AppendLine(" public interface ICommandService"); + sb.AppendLine(" {"); + + foreach (var command in commands) { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using System.ServiceModel;"); - sb.AppendLine("using System.Threading;"); - sb.AppendLine("using System.Threading.Tasks;"); - sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); - sb.AppendLine($"using {rootNamespace}.Grpc.Messages;"); - sb.AppendLine("using Svrnty.CQRS.Abstractions;"); - sb.AppendLine("using ProtoBuf.Grpc;"); - sb.AppendLine(); - - // Generate service interface - sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); - sb.AppendLine("{"); - sb.AppendLine(" [ServiceContract]"); - sb.AppendLine(" public interface IQueryService"); - sb.AppendLine(" {"); - - foreach (var query in queries) + if (command.HasResult) { sb.AppendLine($" [OperationContract]"); - sb.AppendLine($" Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default);"); - sb.AppendLine(); + sb.AppendLine($" Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);"); } - - sb.AppendLine(" }"); - sb.AppendLine(); - - // Generate service implementation - sb.AppendLine(" public sealed class QueryService : IQueryService"); - sb.AppendLine(" {"); - sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); - sb.AppendLine(); - sb.AppendLine(" public QueryService(IServiceProvider serviceProvider)"); - sb.AppendLine(" {"); - sb.AppendLine(" _serviceProvider = serviceProvider;"); - sb.AppendLine(" }"); - sb.AppendLine(); - - foreach (var query in queries) + else { - sb.AppendLine($" public async Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default)"); + sb.AppendLine($" [OperationContract]"); + sb.AppendLine($" Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);"); + } + sb.AppendLine(); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + + // Generate service implementation + sb.AppendLine(" public sealed class CommandService : ICommandService"); + sb.AppendLine(" {"); + sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); + sb.AppendLine(); + sb.AppendLine(" public CommandService(IServiceProvider serviceProvider)"); + sb.AppendLine(" {"); + sb.AppendLine(" _serviceProvider = serviceProvider;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + foreach (var command in commands) + { + if (command.HasResult) + { + sb.AppendLine($" public async Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); - sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();"); - sb.AppendLine($" var query = new {query.FullyQualifiedName}"); + sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); + sb.AppendLine($" var command = new {command.FullyQualifiedName}"); sb.AppendLine(" {"); - foreach (var prop in query.Properties) + foreach (var prop in command.Properties) { var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}"); sb.AppendLine($" {prop.Name} = {conversion},"); } sb.AppendLine(" };"); - sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); - sb.AppendLine($" return new {query.Name}ResultDto {{ Result = result }};"); - sb.AppendLine(" }"); - sb.AppendLine(); - } - - sb.AppendLine(" }"); - sb.AppendLine("}"); - - return sb.ToString(); - } - - private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, bool hasDynamicQueries, bool hasNotifications, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("using Microsoft.AspNetCore.Builder;"); - sb.AppendLine("using Microsoft.AspNetCore.Routing;"); - sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); - sb.AppendLine($"using {rootNamespace}.Grpc.Services;"); - if (hasNotifications) - { - sb.AppendLine("using Svrnty.CQRS.Notifications.Grpc;"); - } - sb.AppendLine(); - sb.AppendLine($"namespace {rootNamespace}.Grpc.Extensions"); - sb.AppendLine("{"); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static class GrpcServiceRegistrationExtensions"); - sb.AppendLine(" {"); - - if (hasCommands) - { - sb.AppendLine(" /// "); - sb.AppendLine(" /// Registers the auto-generated Command gRPC service"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)"); - sb.AppendLine(" {"); - sb.AppendLine(" services.AddGrpc();"); - sb.AppendLine(" services.AddSingleton();"); - sb.AppendLine(" return services;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)"); - sb.AppendLine(" {"); - sb.AppendLine(" endpoints.MapGrpcService();"); - sb.AppendLine(" return endpoints;"); - sb.AppendLine(" }"); - sb.AppendLine(); - } - - if (hasQueries) - { - sb.AppendLine(" /// "); - sb.AppendLine(" /// Registers the auto-generated Query gRPC service"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)"); - sb.AppendLine(" {"); - sb.AppendLine(" services.AddGrpc();"); - sb.AppendLine(" services.AddSingleton();"); - sb.AppendLine(" return services;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)"); - sb.AppendLine(" {"); - sb.AppendLine(" endpoints.MapGrpcService();"); - sb.AppendLine(" return endpoints;"); - sb.AppendLine(" }"); - sb.AppendLine(); - } - - if (hasDynamicQueries) - { - sb.AppendLine(" /// "); - sb.AppendLine(" /// Registers the auto-generated DynamicQuery gRPC service"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IServiceCollection AddGrpcDynamicQueryService(this IServiceCollection services)"); - sb.AppendLine(" {"); - sb.AppendLine(" services.AddGrpc();"); - sb.AppendLine(" services.AddSingleton();"); - sb.AppendLine(" return services;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Maps the auto-generated DynamicQuery gRPC service endpoints"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcDynamicQueries(this IEndpointRouteBuilder endpoints)"); - sb.AppendLine(" {"); - sb.AppendLine(" endpoints.MapGrpcService();"); - sb.AppendLine(" return endpoints;"); - sb.AppendLine(" }"); - sb.AppendLine(); - } - - if (hasNotifications) - { - sb.AppendLine(" /// "); - sb.AppendLine(" /// Registers the auto-generated Notification streaming gRPC service"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IServiceCollection AddGrpcNotificationService(this IServiceCollection services)"); - sb.AppendLine(" {"); - sb.AppendLine(" services.AddGrpc();"); - sb.AppendLine(" services.AddStreamingNotifications();"); - sb.AppendLine(" services.AddSingleton();"); - sb.AppendLine(" return services;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Maps the auto-generated Notification streaming gRPC service endpoints"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcNotifications(this IEndpointRouteBuilder endpoints)"); - sb.AppendLine(" {"); - sb.AppendLine(" endpoints.MapGrpcService();"); - sb.AppendLine(" return endpoints;"); - sb.AppendLine(" }"); - sb.AppendLine(); - } - - if (hasCommands || hasQueries || hasDynamicQueries || hasNotifications) - { - sb.AppendLine(" /// "); - sb.AppendLine(" /// Registers all auto-generated gRPC services (Commands, Queries, DynamicQueries, and Notifications)"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IServiceCollection AddGrpcCommandsAndQueries(this IServiceCollection services)"); - sb.AppendLine(" {"); - sb.AppendLine(" services.AddGrpc();"); - sb.AppendLine(" services.AddGrpcReflection();"); - if (hasCommands) - sb.AppendLine(" services.AddSingleton();"); - if (hasQueries) - sb.AppendLine(" services.AddSingleton();"); - if (hasDynamicQueries) - sb.AppendLine(" services.AddSingleton();"); - if (hasNotifications) - { - sb.AppendLine(" services.AddStreamingNotifications();"); - sb.AppendLine(" services.AddSingleton();"); - } - sb.AppendLine(" return services;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Maps all auto-generated gRPC service endpoints (Commands, Queries, DynamicQueries, and Notifications)"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)"); - sb.AppendLine(" {"); - if (hasCommands) - sb.AppendLine(" endpoints.MapGrpcService();"); - if (hasQueries) - sb.AppendLine(" endpoints.MapGrpcService();"); - if (hasDynamicQueries) - sb.AppendLine(" endpoints.MapGrpcService();"); - if (hasNotifications) - sb.AppendLine(" endpoints.MapGrpcService();"); - sb.AppendLine(" return endpoints;"); - sb.AppendLine(" }"); - sb.AppendLine(); - - // Add configuration-based methods - sb.AppendLine(" /// "); - sb.AppendLine(" /// Registers gRPC services based on configuration"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IServiceCollection AddGrpcFromConfiguration(this IServiceCollection services)"); - sb.AppendLine(" {"); - sb.AppendLine(" var config = services.BuildServiceProvider().GetService();"); - sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); - 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();"); - } - if (hasQueries) - { - sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); - sb.AppendLine(" services.AddSingleton();"); - } - if (hasDynamicQueries) - { - sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); - sb.AppendLine(" services.AddSingleton();"); - } - if (hasNotifications) - { - sb.AppendLine(" // Always register notification service if it exists"); - sb.AppendLine(" services.AddStreamingNotifications();"); - sb.AppendLine(" services.AddSingleton();"); - } - sb.AppendLine(" }"); - sb.AppendLine(" return services;"); - sb.AppendLine(" }"); - sb.AppendLine(); - - sb.AppendLine(" /// "); - sb.AppendLine(" /// Maps gRPC service endpoints based on configuration"); - sb.AppendLine(" /// "); - sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcFromConfiguration(this IEndpointRouteBuilder endpoints)"); - sb.AppendLine(" {"); - sb.AppendLine(" var config = endpoints.ServiceProvider.GetService();"); - sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); - sb.AppendLine(" if (grpcOptions != null)"); - sb.AppendLine(" {"); - if (hasCommands) - { - sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); - sb.AppendLine(" endpoints.MapGrpcService();"); - } - if (hasQueries) - { - sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); - sb.AppendLine(" endpoints.MapGrpcService();"); - } - if (hasDynamicQueries) - { - sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); - sb.AppendLine(" endpoints.MapGrpcService();"); - } - if (hasNotifications) - { - sb.AppendLine(" // Always map notification service if it exists"); - sb.AppendLine(" endpoints.MapGrpcService();"); - } - sb.AppendLine(); - sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); - sb.AppendLine(" endpoints.MapGrpcReflectionService();"); - sb.AppendLine(" }"); - sb.AppendLine(" return endpoints;"); + sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); + sb.AppendLine($" return new {command.Name}ResultDto {{ Result = result }};"); sb.AppendLine(" }"); } - - sb.AppendLine(" }"); - sb.AppendLine("}"); - - return sb.ToString(); - } - - private static string ToCamelCase(string str) - { - if (string.IsNullOrEmpty(str) || char.IsLower(str[0])) - return str; - - return char.ToLowerInvariant(str[0]) + str.Substring(1); - } - - // New methods for standard gRPC generation - - private static string GenerateCommandsProto(List commands, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("syntax = \"proto3\";"); - sb.AppendLine(); - sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";"); - sb.AppendLine(); - sb.AppendLine("package cqrs;"); - sb.AppendLine(); - sb.AppendLine("// Command service for CQRS operations"); - sb.AppendLine("service CommandService {"); - - foreach (var command in commands) + else { - var methodName = command.Name.Replace("Command", ""); - sb.AppendLine($" // {command.Name}"); - sb.AppendLine($" rpc {methodName} ({command.Name}Request) returns ({command.Name}Response);"); - } - - sb.AppendLine("}"); - sb.AppendLine(); - - // Generate message types - foreach (var command in commands) - { - // Request message - sb.AppendLine($"message {command.Name}Request {{"); - foreach (var prop in command.Properties) - { - sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};"); - } - sb.AppendLine("}"); - sb.AppendLine(); - - // Response message - sb.AppendLine($"message {command.Name}Response {{"); - if (command.HasResult) - { - sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(command.ResultFullyQualifiedName!, out _, out _)} result = 1;"); - } - sb.AppendLine("}"); - sb.AppendLine(); - } - - return sb.ToString(); - } - - private static string GenerateQueriesProto(List queries, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("syntax = \"proto3\";"); - sb.AppendLine(); - sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";"); - sb.AppendLine(); - sb.AppendLine("package cqrs;"); - sb.AppendLine(); - sb.AppendLine("// Query service for CQRS operations"); - sb.AppendLine("service QueryService {"); - - foreach (var query in queries) - { - var methodName = query.Name.Replace("Query", ""); - sb.AppendLine($" // {query.Name}"); - sb.AppendLine($" rpc {methodName} ({query.Name}Request) returns ({query.Name}Response);"); - } - - sb.AppendLine("}"); - sb.AppendLine(); - - // Generate message types - foreach (var query in queries) - { - // Request message - sb.AppendLine($"message {query.Name}Request {{"); - foreach (var prop in query.Properties) - { - sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};"); - } - sb.AppendLine("}"); - sb.AppendLine(); - - // Response message - sb.AppendLine($"message {query.Name}Response {{"); - sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(query.ResultFullyQualifiedName, out _, out _)} result = 1;"); - sb.AppendLine("}"); - sb.AppendLine(); - } - - return sb.ToString(); - } - - private static string GenerateCommandServiceImpl(List commands, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using Grpc.Core;"); - sb.AppendLine("using System.Threading.Tasks;"); - sb.AppendLine("using System.Linq;"); - sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); - sb.AppendLine("using FluentValidation;"); - sb.AppendLine("using Google.Rpc;"); - sb.AppendLine("using Google.Protobuf.WellKnownTypes;"); - sb.AppendLine($"using {rootNamespace}.Grpc;"); - sb.AppendLine("using Svrnty.CQRS.Abstractions;"); - sb.AppendLine("using Svrnty.CQRS.Abstractions.Security;"); - sb.AppendLine(); - - sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); - sb.AppendLine("{"); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands"); - sb.AppendLine(" /// "); - sb.AppendLine(" public sealed class CommandServiceImpl : CommandService.CommandServiceBase"); - sb.AppendLine(" {"); - sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); - sb.AppendLine(); - sb.AppendLine(" public CommandServiceImpl(IServiceScopeFactory scopeFactory)"); - sb.AppendLine(" {"); - sb.AppendLine(" _scopeFactory = scopeFactory;"); - sb.AppendLine(" }"); - sb.AppendLine(); - - foreach (var command in commands) - { - var methodName = command.Name.Replace("Command", ""); - var requestType = $"{command.Name}Request"; - var responseType = $"{command.Name}Response"; - - sb.AppendLine($" public override async Task<{responseType}> {methodName}("); - sb.AppendLine($" {requestType} request,"); - sb.AppendLine(" ServerCallContext context)"); + sb.AppendLine($" public async Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); - sb.AppendLine(" using var scope = _scopeFactory.CreateScope();"); - sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); - sb.AppendLine(); - sb.AppendLine(" // Authorization check"); - sb.AppendLine($" var authorizationService = serviceProvider.GetService();"); - sb.AppendLine(" if (authorizationService != null)"); - sb.AppendLine(" {"); - sb.AppendLine($" var authResult = await authorizationService.IsAllowedAsync(typeof({command.FullyQualifiedName}), context.CancellationToken);"); - sb.AppendLine(" if (authResult == AuthorizationResult.Unauthorized)"); - sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unauthenticated, \"Unauthorized\"));"); - sb.AppendLine(" if (authResult == AuthorizationResult.Forbidden)"); - sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.PermissionDenied, \"Forbidden\"));"); - sb.AppendLine(" }"); - sb.AppendLine(); + sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); sb.AppendLine($" var command = new {command.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { - var assignment = GeneratePropertyAssignment(prop, "request", " "); - sb.AppendLine(assignment); + var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}"); + sb.AppendLine($" {prop.Name} = {conversion},"); } sb.AppendLine(" };"); - sb.AppendLine(); - sb.AppendLine(" // Validate command if validator is registered"); - sb.AppendLine($" var validator = serviceProvider.GetService>();"); - sb.AppendLine(" if (validator != null)"); - sb.AppendLine(" {"); - sb.AppendLine(" var validationResult = await validator.ValidateAsync(command, context.CancellationToken);"); - sb.AppendLine(" if (!validationResult.IsValid)"); - sb.AppendLine(" {"); - sb.AppendLine(" // Create Rich Error Model with structured field violations"); - sb.AppendLine(" var badRequest = new BadRequest();"); - sb.AppendLine(" foreach (var error in validationResult.Errors)"); - sb.AppendLine(" {"); - sb.AppendLine(" badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation"); - sb.AppendLine(" {"); - sb.AppendLine(" Field = error.PropertyName,"); - sb.AppendLine(" Description = error.ErrorMessage"); - sb.AppendLine(" });"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" var status = new Google.Rpc.Status"); - sb.AppendLine(" {"); - sb.AppendLine(" Code = (int)Code.InvalidArgument,"); - sb.AppendLine(" Message = \"Validation failed\","); - sb.AppendLine(" Details = { Any.Pack(badRequest) }"); - sb.AppendLine(" };"); - sb.AppendLine(); - sb.AppendLine(" throw status.ToRpcException();"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine($" var handler = serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); - - if (command.HasResult) - { - sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); - - // Generate response with mapping if complex type - if (command.IsResultPrimitiveType) - { - // Handle primitive type result conversion (e.g., Guid.ToString()) - if (command.ResultFullyQualifiedName?.Contains("System.Guid") == true) - { - sb.AppendLine($" return new {responseType} {{ Result = result.ToString() }};"); - } - else - { - sb.AppendLine($" return new {responseType} {{ Result = result }};"); - } - } - else - { - // Complex type - need to map from C# type to proto type - sb.AppendLine($" if (result == null)"); - sb.AppendLine($" {{"); - sb.AppendLine($" return new {responseType}();"); - sb.AppendLine($" }}"); - sb.AppendLine($" return new {responseType}"); - sb.AppendLine(" {"); - sb.AppendLine($" Result = new {command.ResultType}"); - sb.AppendLine(" {"); - foreach (var prop in command.ResultProperties) - { - var assignment = GenerateResultPropertyMapping(prop, "result", " "); - sb.AppendLine(assignment); - } - sb.AppendLine(" }"); - sb.AppendLine(" };"); - } - } - else - { - sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); - sb.AppendLine($" return new {responseType}();"); - } - + sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine(" }"); - sb.AppendLine(); } - - sb.AppendLine(" }"); - sb.AppendLine("}"); - - return sb.ToString(); + sb.AppendLine(); } - private static string GenerateQueryServiceImpl(List queries, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using Grpc.Core;"); - sb.AppendLine("using System.Threading.Tasks;"); - sb.AppendLine("using System.Linq;"); - sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); - sb.AppendLine($"using {rootNamespace}.Grpc;"); - sb.AppendLine("using Svrnty.CQRS.Abstractions;"); - sb.AppendLine("using Svrnty.CQRS.Abstractions.Security;"); - sb.AppendLine(); + sb.AppendLine(" }"); + sb.AppendLine("}"); - sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); - sb.AppendLine("{"); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries"); - sb.AppendLine(" /// "); - sb.AppendLine(" public sealed class QueryServiceImpl : QueryService.QueryServiceBase"); - sb.AppendLine(" {"); - sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); + return sb.ToString(); + } + + private static string GenerateQueryService(List queries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using System.ServiceModel;"); + sb.AppendLine("using System.Threading;"); + sb.AppendLine("using System.Threading.Tasks;"); + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + sb.AppendLine($"using {rootNamespace}.Grpc.Messages;"); + sb.AppendLine("using Svrnty.CQRS.Abstractions;"); + sb.AppendLine("using ProtoBuf.Grpc;"); + sb.AppendLine(); + + // Generate service interface + sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); + sb.AppendLine("{"); + sb.AppendLine(" [ServiceContract]"); + sb.AppendLine(" public interface IQueryService"); + sb.AppendLine(" {"); + + foreach (var query in queries) + { + sb.AppendLine($" [OperationContract]"); + sb.AppendLine($" Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default);"); sb.AppendLine(); - sb.AppendLine(" public QueryServiceImpl(IServiceScopeFactory scopeFactory)"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + + // Generate service implementation + sb.AppendLine(" public sealed class QueryService : IQueryService"); + sb.AppendLine(" {"); + sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); + sb.AppendLine(); + sb.AppendLine(" public QueryService(IServiceProvider serviceProvider)"); + sb.AppendLine(" {"); + sb.AppendLine(" _serviceProvider = serviceProvider;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + foreach (var query in queries) + { + sb.AppendLine($" public async Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); - sb.AppendLine(" _scopeFactory = scopeFactory;"); + sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();"); + sb.AppendLine($" var query = new {query.FullyQualifiedName}"); + sb.AppendLine(" {"); + foreach (var prop in query.Properties) + { + var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}"); + sb.AppendLine($" {prop.Name} = {conversion},"); + } + sb.AppendLine(" };"); + sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); + sb.AppendLine($" return new {query.Name}ResultDto {{ Result = result }};"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, bool hasDynamicQueries, bool hasNotifications, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("using Microsoft.AspNetCore.Builder;"); + sb.AppendLine("using Microsoft.AspNetCore.Routing;"); + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + sb.AppendLine($"using {rootNamespace}.Grpc.Services;"); + if (hasNotifications) + { + sb.AppendLine("using Svrnty.CQRS.Notifications.Grpc;"); + } + sb.AppendLine(); + sb.AppendLine($"namespace {rootNamespace}.Grpc.Extensions"); + sb.AppendLine("{"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static class GrpcServiceRegistrationExtensions"); + sb.AppendLine(" {"); + + if (hasCommands) + { + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers the auto-generated Command gRPC service"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" services.AddSingleton();"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + sb.AppendLine(" endpoints.MapGrpcService();"); + sb.AppendLine(" return endpoints;"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + if (hasQueries) + { + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers the auto-generated Query gRPC service"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" services.AddSingleton();"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + sb.AppendLine(" endpoints.MapGrpcService();"); + sb.AppendLine(" return endpoints;"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + if (hasDynamicQueries) + { + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers the auto-generated DynamicQuery gRPC service"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcDynamicQueryService(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" services.AddSingleton();"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps the auto-generated DynamicQuery gRPC service endpoints"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcDynamicQueries(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + sb.AppendLine(" endpoints.MapGrpcService();"); + sb.AppendLine(" return endpoints;"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + if (hasNotifications) + { + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers the auto-generated Notification streaming gRPC service"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcNotificationService(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" services.AddStreamingNotifications();"); + sb.AppendLine(" services.AddSingleton();"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps the auto-generated Notification streaming gRPC service endpoints"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcNotifications(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + sb.AppendLine(" endpoints.MapGrpcService();"); + sb.AppendLine(" return endpoints;"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + if (hasCommands || hasQueries || hasDynamicQueries || hasNotifications) + { + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers all auto-generated gRPC services (Commands, Queries, DynamicQueries, and Notifications)"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcCommandsAndQueries(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" services.AddGrpcReflection();"); + if (hasCommands) + sb.AppendLine(" services.AddSingleton();"); + if (hasQueries) + sb.AppendLine(" services.AddSingleton();"); + if (hasDynamicQueries) + sb.AppendLine(" services.AddSingleton();"); + if (hasNotifications) + { + sb.AppendLine(" services.AddStreamingNotifications();"); + sb.AppendLine(" services.AddSingleton();"); + } + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps all auto-generated gRPC service endpoints (Commands, Queries, DynamicQueries, and Notifications)"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + if (hasCommands) + sb.AppendLine(" endpoints.MapGrpcService();"); + if (hasQueries) + sb.AppendLine(" endpoints.MapGrpcService();"); + if (hasDynamicQueries) + sb.AppendLine(" endpoints.MapGrpcService();"); + if (hasNotifications) + sb.AppendLine(" endpoints.MapGrpcService();"); + sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); - foreach (var query in queries) + // Add configuration-based methods + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers gRPC services based on configuration"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcFromConfiguration(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" var config = services.BuildServiceProvider().GetService();"); + sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); + sb.AppendLine(" if (grpcOptions != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); + sb.AppendLine(" services.AddGrpcReflection();"); + sb.AppendLine(); + if (hasCommands) { - var methodName = query.Name.Replace("Query", ""); - var requestType = $"{query.Name}Request"; - var responseType = $"{query.Name}Response"; + sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); + sb.AppendLine(" services.AddSingleton();"); + } + if (hasQueries) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); + sb.AppendLine(" services.AddSingleton();"); + } + if (hasDynamicQueries) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); + sb.AppendLine(" services.AddSingleton();"); + } + if (hasNotifications) + { + sb.AppendLine(" // Always register notification service if it exists"); + sb.AppendLine(" services.AddStreamingNotifications();"); + sb.AppendLine(" services.AddSingleton();"); + } + sb.AppendLine(" }"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); - sb.AppendLine($" public override async Task<{responseType}> {methodName}("); - sb.AppendLine($" {requestType} request,"); - sb.AppendLine(" ServerCallContext context)"); - sb.AppendLine(" {"); - sb.AppendLine(" using var scope = _scopeFactory.CreateScope();"); - sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); - sb.AppendLine(); - sb.AppendLine(" // Authorization check"); - sb.AppendLine($" var authorizationService = serviceProvider.GetService();"); - sb.AppendLine(" if (authorizationService != null)"); - sb.AppendLine(" {"); - sb.AppendLine($" var authResult = await authorizationService.IsAllowedAsync(typeof({query.FullyQualifiedName}), context.CancellationToken);"); - sb.AppendLine(" if (authResult == AuthorizationResult.Unauthorized)"); - sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unauthenticated, \"Unauthorized\"));"); - sb.AppendLine(" if (authResult == AuthorizationResult.Forbidden)"); - sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.PermissionDenied, \"Forbidden\"));"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine($" var handler = serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();"); - sb.AppendLine($" var query = new {query.FullyQualifiedName}"); - sb.AppendLine(" {"); - foreach (var prop in query.Properties) - { - var assignment = GeneratePropertyAssignment(prop, "request", " "); - sb.AppendLine(assignment); - } - sb.AppendLine(" };"); - sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps gRPC service endpoints based on configuration"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcFromConfiguration(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + sb.AppendLine(" var config = endpoints.ServiceProvider.GetService();"); + sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); + sb.AppendLine(" if (grpcOptions != null)"); + sb.AppendLine(" {"); + if (hasCommands) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); + sb.AppendLine(" endpoints.MapGrpcService();"); + } + if (hasQueries) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); + sb.AppendLine(" endpoints.MapGrpcService();"); + } + if (hasDynamicQueries) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); + sb.AppendLine(" endpoints.MapGrpcService();"); + } + if (hasNotifications) + { + sb.AppendLine(" // Always map notification service if it exists"); + sb.AppendLine(" endpoints.MapGrpcService();"); + } + sb.AppendLine(); + sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); + sb.AppendLine(" endpoints.MapGrpcReflectionService();"); + sb.AppendLine(" }"); + sb.AppendLine(" return endpoints;"); + sb.AppendLine(" }"); + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static string ToCamelCase(string str) + { + if (string.IsNullOrEmpty(str) || char.IsLower(str[0])) + return str; + + return char.ToLowerInvariant(str[0]) + str.Substring(1); + } + + // New methods for standard gRPC generation + + private static string GenerateCommandsProto(List commands, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("syntax = \"proto3\";"); + sb.AppendLine(); + sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";"); + sb.AppendLine(); + sb.AppendLine("package cqrs;"); + sb.AppendLine(); + sb.AppendLine("// Command service for CQRS operations"); + sb.AppendLine("service CommandService {"); + + foreach (var command in commands) + { + var methodName = command.Name.Replace("Command", ""); + sb.AppendLine($" // {command.Name}"); + sb.AppendLine($" rpc {methodName} ({command.Name}Request) returns ({command.Name}Response);"); + } + + sb.AppendLine("}"); + sb.AppendLine(); + + // Generate message types + foreach (var command in commands) + { + // Request message + sb.AppendLine($"message {command.Name}Request {{"); + foreach (var prop in command.Properties) + { + sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};"); + } + sb.AppendLine("}"); + sb.AppendLine(); + + // Response message + sb.AppendLine($"message {command.Name}Response {{"); + if (command.HasResult) + { + sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(command.ResultFullyQualifiedName!, out _, out _)} result = 1;"); + } + sb.AppendLine("}"); + sb.AppendLine(); + } + + return sb.ToString(); + } + + private static string GenerateQueriesProto(List queries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("syntax = \"proto3\";"); + sb.AppendLine(); + sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";"); + sb.AppendLine(); + sb.AppendLine("package cqrs;"); + sb.AppendLine(); + sb.AppendLine("// Query service for CQRS operations"); + sb.AppendLine("service QueryService {"); + + foreach (var query in queries) + { + var methodName = query.Name.Replace("Query", ""); + sb.AppendLine($" // {query.Name}"); + sb.AppendLine($" rpc {methodName} ({query.Name}Request) returns ({query.Name}Response);"); + } + + sb.AppendLine("}"); + sb.AppendLine(); + + // Generate message types + foreach (var query in queries) + { + // Request message + sb.AppendLine($"message {query.Name}Request {{"); + foreach (var prop in query.Properties) + { + sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};"); + } + sb.AppendLine("}"); + sb.AppendLine(); + + // Response message + sb.AppendLine($"message {query.Name}Response {{"); + sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(query.ResultFullyQualifiedName, out _, out _)} result = 1;"); + sb.AppendLine("}"); + sb.AppendLine(); + } + + return sb.ToString(); + } + + private static string GenerateCommandServiceImpl(List commands, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using Grpc.Core;"); + sb.AppendLine("using System.Threading.Tasks;"); + sb.AppendLine("using System.Linq;"); + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + sb.AppendLine("using FluentValidation;"); + sb.AppendLine("using Google.Rpc;"); + sb.AppendLine("using Google.Protobuf.WellKnownTypes;"); + sb.AppendLine($"using {rootNamespace}.Grpc;"); + sb.AppendLine("using Svrnty.CQRS.Abstractions;"); + sb.AppendLine("using Svrnty.CQRS.Abstractions.Security;"); + sb.AppendLine(); + + sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); + sb.AppendLine("{"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands"); + sb.AppendLine(" /// "); + sb.AppendLine(" public sealed class CommandServiceImpl : CommandService.CommandServiceBase"); + sb.AppendLine(" {"); + sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); + sb.AppendLine(); + sb.AppendLine(" public CommandServiceImpl(IServiceScopeFactory scopeFactory)"); + sb.AppendLine(" {"); + sb.AppendLine(" _scopeFactory = scopeFactory;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + foreach (var command in commands) + { + var methodName = command.Name.Replace("Command", ""); + var requestType = $"{command.Name}Request"; + var responseType = $"{command.Name}Response"; + + sb.AppendLine($" public override async Task<{responseType}> {methodName}("); + sb.AppendLine($" {requestType} request,"); + sb.AppendLine(" ServerCallContext context)"); + sb.AppendLine(" {"); + sb.AppendLine(" using var scope = _scopeFactory.CreateScope();"); + sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); + sb.AppendLine(); + sb.AppendLine(" // Authorization check"); + sb.AppendLine($" var authorizationService = serviceProvider.GetService();"); + sb.AppendLine(" if (authorizationService != null)"); + sb.AppendLine(" {"); + sb.AppendLine($" var authResult = await authorizationService.IsAllowedAsync(typeof({command.FullyQualifiedName}), context.CancellationToken);"); + sb.AppendLine(" if (authResult == AuthorizationResult.Unauthorized)"); + sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unauthenticated, \"Unauthorized\"));"); + sb.AppendLine(" if (authResult == AuthorizationResult.Forbidden)"); + sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.PermissionDenied, \"Forbidden\"));"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine($" var command = new {command.FullyQualifiedName}"); + sb.AppendLine(" {"); + foreach (var prop in command.Properties) + { + var assignment = GeneratePropertyAssignment(prop, "request", " "); + sb.AppendLine(assignment); + } + sb.AppendLine(" };"); + sb.AppendLine(); + sb.AppendLine(" // Validate command if validator is registered"); + sb.AppendLine($" var validator = serviceProvider.GetService>();"); + sb.AppendLine(" if (validator != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" var validationResult = await validator.ValidateAsync(command, context.CancellationToken);"); + sb.AppendLine(" if (!validationResult.IsValid)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Create Rich Error Model with structured field violations"); + sb.AppendLine(" var badRequest = new BadRequest();"); + sb.AppendLine(" foreach (var error in validationResult.Errors)"); + sb.AppendLine(" {"); + sb.AppendLine(" badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation"); + sb.AppendLine(" {"); + sb.AppendLine(" Field = error.PropertyName,"); + sb.AppendLine(" Description = error.ErrorMessage"); + sb.AppendLine(" });"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" var status = new Google.Rpc.Status"); + sb.AppendLine(" {"); + sb.AppendLine(" Code = (int)Code.InvalidArgument,"); + sb.AppendLine(" Message = \"Validation failed\","); + sb.AppendLine(" Details = { Any.Pack(badRequest) }"); + sb.AppendLine(" };"); + sb.AppendLine(); + sb.AppendLine(" throw status.ToRpcException();"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine($" var handler = serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); + + if (command.HasResult) + { + sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); // Generate response with mapping if complex type - if (query.IsResultPrimitiveType) + if (command.IsResultPrimitiveType) { // Handle primitive type result conversion (e.g., Guid.ToString()) - if (query.ResultFullyQualifiedName?.Contains("System.Guid") == true) + if (command.ResultFullyQualifiedName?.Contains("System.Guid") == true) { sb.AppendLine($" return new {responseType} {{ Result = result.ToString() }};"); } @@ -2512,9 +2399,9 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine($" }}"); sb.AppendLine($" return new {responseType}"); sb.AppendLine(" {"); - sb.AppendLine($" Result = new {query.ResultType}"); + sb.AppendLine($" Result = new {command.ResultType}"); sb.AppendLine(" {"); - foreach (var prop in query.ResultProperties) + foreach (var prop in command.ResultProperties) { var assignment = GenerateResultPropertyMapping(prop, "result", " "); sb.AppendLine(assignment); @@ -2522,841 +2409,953 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" }"); sb.AppendLine(" };"); } - - sb.AppendLine(" }"); - sb.AppendLine(); + } + else + { + sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); + sb.AppendLine($" return new {responseType}();"); } - sb.AppendLine(" }"); - sb.AppendLine("}"); - - return sb.ToString(); - } - - private static bool IsPrimitiveType(string typeName) - { - // Check for common primitive and built-in types - var primitiveTypes = new[] - { - "int", "System.Int32", - "long", "System.Int64", - "short", "System.Int16", - "byte", "System.Byte", - "bool", "System.Boolean", - "float", "System.Single", - "double", "System.Double", - "decimal", "System.Decimal", - "string", "System.String", - "System.DateTime", - "System.DateTimeOffset", - "System.TimeSpan", - "System.Guid" - }; - - if (primitiveTypes.Contains(typeName)) - return true; - - // Handle nullable types - check if the underlying type is primitive - if (typeName.EndsWith("?")) - { - var underlyingType = typeName.Substring(0, typeName.Length - 1); - return IsPrimitiveType(underlyingType); - } - - if (typeName.StartsWith("System.Nullable<") && typeName.EndsWith(">")) - { - var underlyingType = typeName.Substring("System.Nullable<".Length, typeName.Length - "System.Nullable<".Length - 1); - return IsPrimitiveType(underlyingType); - } - - return false; - } - - private static string GenerateDynamicQueryMessages(List dynamicQueries, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using System.Collections.Generic;"); - sb.AppendLine("using System.ServiceModel;"); - sb.AppendLine("using System.Runtime.Serialization;"); - sb.AppendLine("using ProtoBuf;"); - sb.AppendLine("using ProtoBuf.Grpc;"); - sb.AppendLine(); - sb.AppendLine($"namespace {rootNamespace}.Grpc.DynamicQuery"); - sb.AppendLine("{"); - - // Common message types - sb.AppendLine(" /// "); - sb.AppendLine(" /// Dynamic query filter with support for nested AND/OR logic"); - sb.AppendLine(" /// "); - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine(" public sealed class DynamicQueryFilter"); - sb.AppendLine(" {"); - sb.AppendLine(" [ProtoMember(1)]"); - sb.AppendLine(" [DataMember(Order = 1)]"); - sb.AppendLine(" public string Path { get; set; } = string.Empty;"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(2)]"); - sb.AppendLine(" [DataMember(Order = 2)]"); - sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.FilterType"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(3)]"); - sb.AppendLine(" [DataMember(Order = 3)]"); - sb.AppendLine(" public string Value { get; set; } = string.Empty;"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(4)]"); - sb.AppendLine(" [DataMember(Order = 4)]"); - sb.AppendLine(" public List? And { get; set; }"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(5)]"); - sb.AppendLine(" [DataMember(Order = 5)]"); - sb.AppendLine(" public List? Or { get; set; }"); - sb.AppendLine(" }"); - sb.AppendLine(); - - sb.AppendLine(" /// "); - sb.AppendLine(" /// Dynamic query sort"); - sb.AppendLine(" /// "); - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine(" public sealed class DynamicQuerySort"); - sb.AppendLine(" {"); - sb.AppendLine(" [ProtoMember(1)]"); - sb.AppendLine(" [DataMember(Order = 1)]"); - sb.AppendLine(" public string Path { get; set; } = string.Empty;"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(2)]"); - sb.AppendLine(" [DataMember(Order = 2)]"); - sb.AppendLine(" public bool Ascending { get; set; } = true;"); - sb.AppendLine(" }"); - sb.AppendLine(); - - sb.AppendLine(" /// "); - sb.AppendLine(" /// Dynamic query group"); - sb.AppendLine(" /// "); - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine(" public sealed class DynamicQueryGroup"); - sb.AppendLine(" {"); - sb.AppendLine(" [ProtoMember(1)]"); - sb.AppendLine(" [DataMember(Order = 1)]"); - sb.AppendLine(" public string Path { get; set; } = string.Empty;"); - sb.AppendLine(" }"); - sb.AppendLine(); - - sb.AppendLine(" /// "); - sb.AppendLine(" /// Dynamic query aggregate"); - sb.AppendLine(" /// "); - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine(" public sealed class DynamicQueryAggregate"); - sb.AppendLine(" {"); - sb.AppendLine(" [ProtoMember(1)]"); - sb.AppendLine(" [DataMember(Order = 1)]"); - sb.AppendLine(" public string Path { get; set; } = string.Empty;"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(2)]"); - sb.AppendLine(" [DataMember(Order = 2)]"); - sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.AggregateType"); - sb.AppendLine(" }"); - sb.AppendLine(); - - // Generate request/response messages for each dynamic query - foreach (var dynamicQuery in dynamicQueries) - { - // Request message - sb.AppendLine($" /// "); - sb.AppendLine($" /// Request message for dynamic query on {dynamicQuery.Name}"); - sb.AppendLine($" /// "); - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Request"); - sb.AppendLine(" {"); - sb.AppendLine(" [ProtoMember(1)]"); - sb.AppendLine(" [DataMember(Order = 1)]"); - sb.AppendLine(" public int Page { get; set; }"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(2)]"); - sb.AppendLine(" [DataMember(Order = 2)]"); - sb.AppendLine(" public int PageSize { get; set; }"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(3)]"); - sb.AppendLine(" [DataMember(Order = 3)]"); - sb.AppendLine(" public List Filters { get; set; } = new();"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(4)]"); - sb.AppendLine(" [DataMember(Order = 4)]"); - sb.AppendLine(" public List Sorts { get; set; } = new();"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(5)]"); - sb.AppendLine(" [DataMember(Order = 5)]"); - sb.AppendLine(" public List Groups { get; set; } = new();"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(6)]"); - sb.AppendLine(" [DataMember(Order = 6)]"); - sb.AppendLine(" public List Aggregates { get; set; } = new();"); - sb.AppendLine(" }"); - sb.AppendLine(); - - // Response message - sb.AppendLine($" /// "); - sb.AppendLine($" /// Response message for dynamic query on {dynamicQuery.Name}"); - sb.AppendLine($" /// "); - sb.AppendLine(" [ProtoContract]"); - sb.AppendLine(" [DataContract]"); - sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Response"); - sb.AppendLine(" {"); - sb.AppendLine(" [ProtoMember(1)]"); - sb.AppendLine(" [DataMember(Order = 1)]"); - sb.AppendLine($" public List<{dynamicQuery.DestinationTypeFullyQualified}> Data {{ get; set; }} = new();"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(2)]"); - sb.AppendLine(" [DataMember(Order = 2)]"); - sb.AppendLine(" public long TotalCount { get; set; }"); - sb.AppendLine(); - sb.AppendLine(" [ProtoMember(3)]"); - sb.AppendLine(" [DataMember(Order = 3)]"); - sb.AppendLine(" public int NumberOfPages { get; set; }"); - sb.AppendLine(" }"); - sb.AppendLine(); - } - - // Generate service interface - sb.AppendLine(" /// "); - sb.AppendLine(" /// gRPC service interface for DynamicQueries"); - sb.AppendLine(" /// "); - sb.AppendLine(" [ServiceContract]"); - sb.AppendLine(" public interface IDynamicQueryService"); - sb.AppendLine(" {"); - - foreach (var dynamicQuery in dynamicQueries) - { - var methodName = $"Query{dynamicQuery.Name}"; - sb.AppendLine($" /// "); - sb.AppendLine($" /// Execute dynamic query on {dynamicQuery.Name}"); - sb.AppendLine($" /// "); - sb.AppendLine(" [OperationContract]"); - sb.AppendLine($" System.Threading.Tasks.Task {methodName}Async(DynamicQuery{dynamicQuery.Name}Request request, CallContext context = default);"); - sb.AppendLine(); - } - - sb.AppendLine(" }"); - sb.AppendLine("}"); - - return sb.ToString(); - } - - private static string GenerateDynamicQueryServiceImpl(List dynamicQueries, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using Grpc.Core;"); - sb.AppendLine("using System.Threading.Tasks;"); - sb.AppendLine("using System.Collections.Generic;"); - sb.AppendLine("using System.Linq;"); - sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); - sb.AppendLine($"using {rootNamespace}.Grpc;"); - sb.AppendLine("using Svrnty.CQRS.Abstractions;"); - sb.AppendLine("using Svrnty.CQRS.Abstractions.Security;"); - sb.AppendLine("using Svrnty.CQRS.DynamicQuery.Abstractions;"); - sb.AppendLine("using PoweredSoft.DynamicQuery.Core;"); - sb.AppendLine(); - - sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); - sb.AppendLine("{"); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Auto-generated gRPC service implementation for DynamicQueries"); - sb.AppendLine(" /// "); - sb.AppendLine(" public sealed class DynamicQueryServiceImpl : DynamicQueryService.DynamicQueryServiceBase"); - sb.AppendLine(" {"); - sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); - sb.AppendLine(); - sb.AppendLine(" public DynamicQueryServiceImpl(IServiceScopeFactory scopeFactory)"); - sb.AppendLine(" {"); - sb.AppendLine(" _scopeFactory = scopeFactory;"); sb.AppendLine(" }"); sb.AppendLine(); + } - foreach (var dynamicQuery in dynamicQueries) + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static string GenerateQueryServiceImpl(List queries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using Grpc.Core;"); + sb.AppendLine("using System.Threading.Tasks;"); + sb.AppendLine("using System.Linq;"); + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + sb.AppendLine($"using {rootNamespace}.Grpc;"); + sb.AppendLine("using Svrnty.CQRS.Abstractions;"); + sb.AppendLine("using Svrnty.CQRS.Abstractions.Security;"); + sb.AppendLine(); + + sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); + sb.AppendLine("{"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries"); + sb.AppendLine(" /// "); + sb.AppendLine(" public sealed class QueryServiceImpl : QueryService.QueryServiceBase"); + sb.AppendLine(" {"); + sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); + sb.AppendLine(); + sb.AppendLine(" public QueryServiceImpl(IServiceScopeFactory scopeFactory)"); + sb.AppendLine(" {"); + sb.AppendLine(" _scopeFactory = scopeFactory;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + foreach (var query in queries) + { + var methodName = query.Name.Replace("Query", ""); + var requestType = $"{query.Name}Request"; + var responseType = $"{query.Name}Response"; + + sb.AppendLine($" public override async Task<{responseType}> {methodName}("); + sb.AppendLine($" {requestType} request,"); + sb.AppendLine(" ServerCallContext context)"); + sb.AppendLine(" {"); + sb.AppendLine(" using var scope = _scopeFactory.CreateScope();"); + sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); + sb.AppendLine(); + sb.AppendLine(" // Authorization check"); + sb.AppendLine($" var authorizationService = serviceProvider.GetService();"); + sb.AppendLine(" if (authorizationService != null)"); + sb.AppendLine(" {"); + sb.AppendLine($" var authResult = await authorizationService.IsAllowedAsync(typeof({query.FullyQualifiedName}), context.CancellationToken);"); + sb.AppendLine(" if (authResult == AuthorizationResult.Unauthorized)"); + sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unauthenticated, \"Unauthorized\"));"); + sb.AppendLine(" if (authResult == AuthorizationResult.Forbidden)"); + sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.PermissionDenied, \"Forbidden\"));"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine($" var handler = serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();"); + sb.AppendLine($" var query = new {query.FullyQualifiedName}"); + sb.AppendLine(" {"); + foreach (var prop in query.Properties) { - var methodName = $"Query{dynamicQuery.Name}"; - var requestType = $"DynamicQuery{dynamicQuery.Name}Request"; - var responseType = $"DynamicQuery{dynamicQuery.Name}Response"; + var assignment = GeneratePropertyAssignment(prop, "request", " "); + sb.AppendLine(assignment); + } + sb.AppendLine(" };"); + sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); - sb.AppendLine($" public override async Task<{responseType}> {methodName}("); - sb.AppendLine($" {requestType} request,"); - sb.AppendLine(" ServerCallContext context)"); - sb.AppendLine(" {"); - sb.AppendLine(" using var scope = _scopeFactory.CreateScope();"); - sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); - sb.AppendLine(); - sb.AppendLine(" // Authorization check"); - sb.AppendLine($" var authorizationService = serviceProvider.GetService();"); - sb.AppendLine(" if (authorizationService != null)"); - sb.AppendLine(" {"); - sb.AppendLine($" var authResult = await authorizationService.IsAllowedAsync(typeof({dynamicQuery.QueryInterfaceName}), context.CancellationToken);"); - sb.AppendLine(" if (authResult == AuthorizationResult.Unauthorized)"); - sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unauthenticated, \"Unauthorized\"));"); - sb.AppendLine(" if (authResult == AuthorizationResult.Forbidden)"); - sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.PermissionDenied, \"Forbidden\"));"); - sb.AppendLine(" }"); - sb.AppendLine(); - - // Build the dynamic query object - if (dynamicQuery.HasParams) + // Generate response with mapping if complex type + if (query.IsResultPrimitiveType) + { + // Handle primitive type result conversion (e.g., Guid.ToString()) + if (query.ResultFullyQualifiedName?.Contains("System.Guid") == true) { - sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>"); + sb.AppendLine($" return new {responseType} {{ Result = result.ToString() }};"); } else { - sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>"); + sb.AppendLine($" return new {responseType} {{ Result = result }};"); } - sb.AppendLine(" {"); - sb.AppendLine(" Page = request.Page > 0 ? request.Page : null,"); - sb.AppendLine(" PageSize = request.PageSize > 0 ? request.PageSize : null,"); - sb.AppendLine(" Filters = ConvertFilters(request.Filters) ?? new(),"); - sb.AppendLine(" Sorts = ConvertSorts(request.Sorts) ?? new(),"); - sb.AppendLine(" Groups = ConvertGroups(request.Groups) ?? new(),"); - sb.AppendLine(" Aggregates = ConvertAggregates(request.Aggregates) ?? new()"); - sb.AppendLine(" };"); - sb.AppendLine(); - - // Get the handler and execute - sb.AppendLine($" var handler = serviceProvider.GetRequiredService>>();"); - sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); - sb.AppendLine(); - - // Build response - sb.AppendLine($" var response = new {responseType}"); - sb.AppendLine(" {"); - sb.AppendLine(" TotalRecords = result.TotalRecords,"); - sb.AppendLine(" NumberOfPages = (int)(result.NumberOfPages ?? 0)"); - sb.AppendLine(" };"); - sb.AppendLine(); - sb.AppendLine(" if (result.Data != null)"); - sb.AppendLine(" {"); - sb.AppendLine(" foreach (var item in result.Data)"); - sb.AppendLine(" {"); - sb.AppendLine($" response.Data.Add(MapTo{dynamicQuery.Name}ProtoModel(item));"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" // TODO: Add aggregates and groups to response if needed"); - sb.AppendLine(); - sb.AppendLine(" return response;"); - sb.AppendLine(" }"); - sb.AppendLine(); } - - // Add helper methods for converting proto messages to AspNetCore types - sb.AppendLine(" private static List? ConvertFilters(Google.Protobuf.Collections.RepeatedField protoFilters)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (protoFilters == null || protoFilters.Count == 0)"); - sb.AppendLine(" return null;"); - sb.AppendLine(); - sb.AppendLine(" var filters = new List();"); - sb.AppendLine(" foreach (var protoFilter in protoFilters)"); - sb.AppendLine(" {"); - 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(),"); - sb.AppendLine(" Value = protoFilter.Value,"); - sb.AppendLine(" And = true"); - sb.AppendLine(" };"); - sb.AppendLine(); - sb.AppendLine(" // Handle nested AND filters"); - sb.AppendLine(" if (protoFilter.And != null && protoFilter.And.Count > 0)"); - sb.AppendLine(" {"); - sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.And);"); - sb.AppendLine(" filter.And = true;"); - sb.AppendLine(" }"); - sb.AppendLine(" // Handle nested OR filters"); - sb.AppendLine(" else if (protoFilter.Or != null && protoFilter.Or.Count > 0)"); - sb.AppendLine(" {"); - sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.Or);"); - sb.AppendLine(" filter.And = false;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" filters.Add(filter);"); - sb.AppendLine(" }"); - sb.AppendLine(" return filters;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" private static List ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField protoFilters)"); - sb.AppendLine(" {"); - sb.AppendLine(" var result = new List();"); - sb.AppendLine(" foreach (var pf in protoFilters)"); - sb.AppendLine(" {"); - 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(),"); - sb.AppendLine(" Value = pf.Value,"); - sb.AppendLine(" And = true"); - sb.AppendLine(" };"); - sb.AppendLine(" if (pf.And != null && pf.And.Count > 0)"); - sb.AppendLine(" {"); - sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.And);"); - sb.AppendLine(" filter.And = true;"); - sb.AppendLine(" }"); - sb.AppendLine(" else if (pf.Or != null && pf.Or.Count > 0)"); - sb.AppendLine(" {"); - sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.Or);"); - sb.AppendLine(" filter.And = false;"); - sb.AppendLine(" }"); - sb.AppendLine(" result.Add(filter);"); - sb.AppendLine(" }"); - sb.AppendLine(" return result;"); - sb.AppendLine(" }"); - sb.AppendLine(); - - sb.AppendLine(" private static List? ConvertSorts(Google.Protobuf.Collections.RepeatedField protoSorts)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (protoSorts == null || protoSorts.Count == 0)"); - sb.AppendLine(" return null;"); - sb.AppendLine(); - sb.AppendLine(" return protoSorts.Select(s => new PoweredSoft.DynamicQuery.Sort"); - sb.AppendLine(" {"); - sb.AppendLine(" Path = s.Path,"); - sb.AppendLine(" Ascending = s.Ascending"); - sb.AppendLine(" }).ToList();"); - sb.AppendLine(" }"); - sb.AppendLine(); - - sb.AppendLine(" private static List? ConvertGroups(Google.Protobuf.Collections.RepeatedField protoGroups)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (protoGroups == null || protoGroups.Count == 0)"); - sb.AppendLine(" return null;"); - sb.AppendLine(); - sb.AppendLine(" return protoGroups.Select(g => new PoweredSoft.DynamicQuery.Group"); - sb.AppendLine(" {"); - sb.AppendLine(" Path = g.Path"); - sb.AppendLine(" }).ToList();"); - sb.AppendLine(" }"); - sb.AppendLine(); - - sb.AppendLine(" private static List? ConvertAggregates(Google.Protobuf.Collections.RepeatedField 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.DynamicQueryAggregate"); - sb.AppendLine(" {"); - sb.AppendLine(" Path = a.Path,"); - sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.AggregateType)a.Type).ToString()"); - sb.AppendLine(" }).ToList();"); - sb.AppendLine(" }"); - sb.AppendLine(); - - // Add generic reflection-based mapper helper - sb.AppendLine(" private static TProto MapToProtoModel(TDomain domainModel) where TProto : Google.Protobuf.IMessage, new()"); - sb.AppendLine(" {"); - sb.AppendLine(" if (domainModel == null) return new TProto();"); - sb.AppendLine(" var proto = new TProto();"); - sb.AppendLine(" var domainProps = typeof(TDomain).GetProperties();"); - sb.AppendLine(" var protoDesc = proto.Descriptor;"); - sb.AppendLine(); - sb.AppendLine(" foreach (var domainProp in domainProps)"); - sb.AppendLine(" {"); - sb.AppendLine(" // Convert property name to proto field name (PascalCase to snake_case)"); - sb.AppendLine(" var protoFieldName = ToSnakeCase(domainProp.Name);"); - sb.AppendLine(" var protoField = protoDesc.FindFieldByName(protoFieldName);"); - sb.AppendLine(" if (protoField == null) continue;"); - sb.AppendLine(); - sb.AppendLine(" var domainValue = domainProp.GetValue(domainModel);"); - sb.AppendLine(" if (domainValue == null) continue;"); - sb.AppendLine(); - sb.AppendLine(" var protoAccessor = protoField.Accessor;"); - sb.AppendLine(); - sb.AppendLine(" // Handle DateTime -> Timestamp conversion"); - sb.AppendLine(" if (domainProp.PropertyType == typeof(DateTime) || domainProp.PropertyType == typeof(DateTime?))"); - sb.AppendLine(" {"); - sb.AppendLine(" var dateTime = (DateTime)domainValue;"); - sb.AppendLine(" // Ensure UTC for Timestamp conversion"); - sb.AppendLine(" if (dateTime.Kind != DateTimeKind.Utc)"); - sb.AppendLine(" dateTime = dateTime.ToUniversalTime();"); - sb.AppendLine(" protoAccessor.SetValue(proto, Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime));"); - sb.AppendLine(" }"); - sb.AppendLine(" else if (domainProp.PropertyType == typeof(DateTimeOffset) || domainProp.PropertyType == typeof(DateTimeOffset?))"); - sb.AppendLine(" {"); - sb.AppendLine(" var dateTimeOffset = (DateTimeOffset)domainValue;"); - sb.AppendLine(" protoAccessor.SetValue(proto, Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(dateTimeOffset));"); - sb.AppendLine(" }"); - sb.AppendLine(" // Handle collections (List, IList, etc.) - must check before complex types"); - sb.AppendLine(" else if (protoField.IsRepeated && domainValue is System.Collections.IEnumerable enumerable && domainProp.PropertyType != typeof(string))"); - sb.AppendLine(" {"); - sb.AppendLine(" var repeatedField = protoAccessor.GetValue(proto);"); - sb.AppendLine(" if (repeatedField == null) continue;"); - sb.AppendLine(); - sb.AppendLine(" // Get the element type of the RepeatedField"); - sb.AppendLine(" var repeatedFieldType = repeatedField.GetType();"); - sb.AppendLine(" var repeatedElementType = repeatedFieldType.IsGenericType ? repeatedFieldType.GetGenericArguments()[0] : null;"); - sb.AppendLine(" if (repeatedElementType == null) continue;"); - sb.AppendLine(); - sb.AppendLine(" // Get Add(T) method with specific parameter type to avoid ambiguity"); - sb.AppendLine(" var addMethod = repeatedFieldType.GetMethod(\"Add\", new[] { repeatedElementType });"); - sb.AppendLine(" if (addMethod == null) continue;"); - sb.AppendLine(); - sb.AppendLine(" // Get element types"); - sb.AppendLine(" var domainElementType = domainProp.PropertyType.IsArray"); - sb.AppendLine(" ? domainProp.PropertyType.GetElementType()"); - sb.AppendLine(" : domainProp.PropertyType.IsGenericType ? domainProp.PropertyType.GetGenericArguments()[0] : null;"); - sb.AppendLine(" // Only access MessageType for message fields (throws for primitives)"); - sb.AppendLine(" var protoElementType = protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message ? protoField.MessageType?.ClrType : null;"); - sb.AppendLine(); - sb.AppendLine(" foreach (var item in enumerable)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (item == null) continue;"); - sb.AppendLine(); - sb.AppendLine(" // Check if elements need mapping (complex types)"); - sb.AppendLine(" if (protoElementType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoElementType) && domainElementType != null)"); - sb.AppendLine(" {"); - sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\","); - sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!"); - sb.AppendLine(" .MakeGenericMethod(domainElementType, protoElementType);"); - sb.AppendLine(" var mappedItem = mapMethod.Invoke(null, new[] { item });"); - sb.AppendLine(" if (mappedItem != null)"); - sb.AppendLine(" addMethod.Invoke(repeatedField, new[] { mappedItem });"); - sb.AppendLine(" }"); - sb.AppendLine(" else"); - sb.AppendLine(" {"); - sb.AppendLine(" // Primitive types, enums, strings - add directly"); - sb.AppendLine(" try { addMethod.Invoke(repeatedField, new[] { item }); }"); - sb.AppendLine(" catch { /* Type mismatch, skip */ }"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" // Handle enumerable value types that map to proto messages with repeated fields (e.g., NpgsqlPolygon -> proto message with items)"); - sb.AppendLine(" else if (domainProp.PropertyType.IsValueType && "); - sb.AppendLine(" domainValue is System.Collections.IEnumerable valueTypeEnumerable &&"); - sb.AppendLine(" protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message)"); - sb.AppendLine(" {"); - sb.AppendLine(" // Create the proto message and look for its 'items' repeated field"); - sb.AppendLine(" var protoFieldType = protoField.MessageType?.ClrType;"); - sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))"); - sb.AppendLine(" {"); - sb.AppendLine(" var nestedProto = System.Activator.CreateInstance(protoFieldType) as Google.Protobuf.IMessage;"); - sb.AppendLine(" if (nestedProto != null)"); - sb.AppendLine(" {"); - sb.AppendLine(" // Find the 'items' field in the proto message"); - sb.AppendLine(" var itemsField = nestedProto.Descriptor.FindFieldByName(\"items\");"); - sb.AppendLine(" if (itemsField != null && itemsField.IsRepeated)"); - sb.AppendLine(" {"); - sb.AppendLine(" var repeatedField = itemsField.Accessor.GetValue(nestedProto);"); - sb.AppendLine(" var repeatedFieldType = repeatedField?.GetType();"); - sb.AppendLine(" var protoElementType = itemsField.MessageType?.ClrType;"); - sb.AppendLine(" "); - sb.AppendLine(" if (repeatedFieldType != null && protoElementType != null)"); - sb.AppendLine(" {"); - sb.AppendLine(" var addMethod = repeatedFieldType.GetMethod(\"Add\", new[] { protoElementType });"); - sb.AppendLine(" if (addMethod != null)"); - sb.AppendLine(" {"); - sb.AppendLine(" // Get element type from the enumerable"); - sb.AppendLine(" var enumerableInterface = domainProp.PropertyType.GetInterfaces()"); - sb.AppendLine(" .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(System.Collections.Generic.IEnumerable<>));"); - sb.AppendLine(" var domainElementType = enumerableInterface?.GetGenericArguments()[0];"); - sb.AppendLine(" "); - sb.AppendLine(" foreach (var item in valueTypeEnumerable)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (item == null) continue;"); - sb.AppendLine(" "); - sb.AppendLine(" // Map each item to the proto element type"); - sb.AppendLine(" if (domainElementType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoElementType))"); - sb.AppendLine(" {"); - sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\","); - sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!"); - sb.AppendLine(" .MakeGenericMethod(domainElementType, protoElementType);"); - sb.AppendLine(" var mappedItem = mapMethod.Invoke(null, new[] { item });"); - sb.AppendLine(" if (mappedItem != null)"); - sb.AppendLine(" addMethod.Invoke(repeatedField, new[] { mappedItem });"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" protoAccessor.SetValue(proto, nestedProto);"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" // Handle nested complex types (non-primitive, non-enum, non-string, non-collection)"); - sb.AppendLine(" else if (!domainProp.PropertyType.IsPrimitive && "); - sb.AppendLine(" domainProp.PropertyType != typeof(string) && "); - sb.AppendLine(" !domainProp.PropertyType.IsEnum &&"); - sb.AppendLine(" !domainProp.PropertyType.IsValueType)"); - sb.AppendLine(" {"); - sb.AppendLine(" // Get the proto field type and recursively map (only access MessageType for message fields)"); - sb.AppendLine(" var protoFieldType = protoAccessor.GetValue(proto)?.GetType() ?? (protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message ? protoField.MessageType?.ClrType : null);"); - sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))"); - sb.AppendLine(" {"); - sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\", "); - sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!"); - sb.AppendLine(" .MakeGenericMethod(domainProp.PropertyType, protoFieldType);"); - sb.AppendLine(" var nestedProto = mapMethod.Invoke(null, new[] { domainValue });"); - sb.AppendLine(" if (nestedProto != null)"); - sb.AppendLine(" protoAccessor.SetValue(proto, nestedProto);"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" // Handle decimal -> string conversion"); - sb.AppendLine(" else if (domainProp.PropertyType == typeof(decimal) || domainProp.PropertyType == typeof(decimal?))"); - sb.AppendLine(" {"); - sb.AppendLine(" protoAccessor.SetValue(proto, ((decimal)domainValue).ToString(System.Globalization.CultureInfo.InvariantCulture));"); - sb.AppendLine(" }"); - sb.AppendLine(" // Handle Guid -> string conversion"); - sb.AppendLine(" else if (domainProp.PropertyType == typeof(Guid) || domainProp.PropertyType == typeof(Guid?))"); - sb.AppendLine(" {"); - sb.AppendLine(" protoAccessor.SetValue(proto, ((Guid)domainValue).ToString());"); - sb.AppendLine(" }"); - sb.AppendLine(" else"); - sb.AppendLine(" {"); - sb.AppendLine(" // Direct assignment for primitives, strings, enums"); - sb.AppendLine(" try { protoAccessor.SetValue(proto, domainValue); }"); - sb.AppendLine(" catch { /* Type mismatch, skip */ }"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - sb.AppendLine(" return proto;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" private static string ToSnakeCase(string str)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (string.IsNullOrEmpty(str)) return str;"); - sb.AppendLine(" var result = new System.Text.StringBuilder();"); - sb.AppendLine(" for (int i = 0; i < str.Length; i++)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (i > 0 && char.IsUpper(str[i]))"); - sb.AppendLine(" result.Append('_');"); - sb.AppendLine(" result.Append(char.ToLowerInvariant(str[i]));"); - sb.AppendLine(" }"); - sb.AppendLine(" return result.ToString();"); - sb.AppendLine(" }"); - sb.AppendLine(); - - // Add mapper methods for each entity type - foreach (var dynamicQuery in dynamicQueries) + else { - var entityName = dynamicQuery.Name; - var protoTypeName = $"{entityName.TrimEnd('s')}"; // User from Users - - sb.AppendLine($" private static {protoTypeName} MapTo{entityName}ProtoModel({dynamicQuery.DestinationTypeFullyQualified} domainModel)"); - sb.AppendLine(" {"); - sb.AppendLine($" return MapToProtoModel<{dynamicQuery.DestinationTypeFullyQualified}, {protoTypeName}>(domainModel);"); - sb.AppendLine(" }"); - sb.AppendLine(); + // Complex type - need to map from C# type to proto type + sb.AppendLine($" if (result == null)"); + sb.AppendLine($" {{"); + sb.AppendLine($" return new {responseType}();"); + sb.AppendLine($" }}"); + sb.AppendLine($" return new {responseType}"); + sb.AppendLine(" {"); + sb.AppendLine($" Result = new {query.ResultType}"); + sb.AppendLine(" {"); + foreach (var prop in query.ResultProperties) + { + var assignment = GenerateResultPropertyMapping(prop, "result", " "); + sb.AppendLine(assignment); + } + sb.AppendLine(" }"); + sb.AppendLine(" };"); } - sb.AppendLine(" }"); - sb.AppendLine("}"); - - return sb.ToString(); + sb.AppendLine(" }"); + sb.AppendLine(); } - /// - /// Discovers types marked with [StreamingNotification] attribute - /// - private static List DiscoverNotifications(IEnumerable allTypes, Compilation compilation) + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static bool IsPrimitiveType(string typeName) + { + // Check for common primitive and built-in types + var primitiveTypes = new[] { - var streamingNotificationAttribute = compilation.GetTypeByMetadataName( - "Svrnty.CQRS.Notifications.Abstractions.StreamingNotificationAttribute"); + "int", "System.Int32", + "long", "System.Int64", + "short", "System.Int16", + "byte", "System.Byte", + "bool", "System.Boolean", + "float", "System.Single", + "double", "System.Double", + "decimal", "System.Decimal", + "string", "System.String", + "System.DateTime", + "System.DateTimeOffset", + "System.TimeSpan", + "System.Guid" + }; - if (streamingNotificationAttribute == null) - return new List(); + if (primitiveTypes.Contains(typeName)) + return true; - var notifications = new List(); + // Handle nullable types - check if the underlying type is primitive + if (typeName.EndsWith("?")) + { + var underlyingType = typeName.Substring(0, typeName.Length - 1); + return IsPrimitiveType(underlyingType); + } - foreach (var type in allTypes) + if (typeName.StartsWith("System.Nullable<") && typeName.EndsWith(">")) + { + var underlyingType = typeName.Substring("System.Nullable<".Length, typeName.Length - "System.Nullable<".Length - 1); + return IsPrimitiveType(underlyingType); + } + + return false; + } + + private static string GenerateDynamicQueryMessages(List dynamicQueries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using System.Collections.Generic;"); + sb.AppendLine("using System.ServiceModel;"); + sb.AppendLine("using System.Runtime.Serialization;"); + sb.AppendLine("using ProtoBuf;"); + sb.AppendLine("using ProtoBuf.Grpc;"); + sb.AppendLine(); + sb.AppendLine($"namespace {rootNamespace}.Grpc.DynamicQuery"); + sb.AppendLine("{"); + + // Common message types + sb.AppendLine(" /// "); + sb.AppendLine(" /// Dynamic query filter with support for nested AND/OR logic"); + sb.AppendLine(" /// "); + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine(" public sealed class DynamicQueryFilter"); + sb.AppendLine(" {"); + sb.AppendLine(" [ProtoMember(1)]"); + sb.AppendLine(" [DataMember(Order = 1)]"); + sb.AppendLine(" public string Path { get; set; } = string.Empty;"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(2)]"); + sb.AppendLine(" [DataMember(Order = 2)]"); + sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.FilterType"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(3)]"); + sb.AppendLine(" [DataMember(Order = 3)]"); + sb.AppendLine(" public string Value { get; set; } = string.Empty;"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(4)]"); + sb.AppendLine(" [DataMember(Order = 4)]"); + sb.AppendLine(" public List? And { get; set; }"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(5)]"); + sb.AppendLine(" [DataMember(Order = 5)]"); + sb.AppendLine(" public List? Or { get; set; }"); + sb.AppendLine(" }"); + sb.AppendLine(); + + sb.AppendLine(" /// "); + sb.AppendLine(" /// Dynamic query sort"); + sb.AppendLine(" /// "); + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine(" public sealed class DynamicQuerySort"); + sb.AppendLine(" {"); + sb.AppendLine(" [ProtoMember(1)]"); + sb.AppendLine(" [DataMember(Order = 1)]"); + sb.AppendLine(" public string Path { get; set; } = string.Empty;"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(2)]"); + sb.AppendLine(" [DataMember(Order = 2)]"); + sb.AppendLine(" public bool Ascending { get; set; } = true;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + sb.AppendLine(" /// "); + sb.AppendLine(" /// Dynamic query group"); + sb.AppendLine(" /// "); + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine(" public sealed class DynamicQueryGroup"); + sb.AppendLine(" {"); + sb.AppendLine(" [ProtoMember(1)]"); + sb.AppendLine(" [DataMember(Order = 1)]"); + sb.AppendLine(" public string Path { get; set; } = string.Empty;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + sb.AppendLine(" /// "); + sb.AppendLine(" /// Dynamic query aggregate"); + sb.AppendLine(" /// "); + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine(" public sealed class DynamicQueryAggregate"); + sb.AppendLine(" {"); + sb.AppendLine(" [ProtoMember(1)]"); + sb.AppendLine(" [DataMember(Order = 1)]"); + sb.AppendLine(" public string Path { get; set; } = string.Empty;"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(2)]"); + sb.AppendLine(" [DataMember(Order = 2)]"); + sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.AggregateType"); + sb.AppendLine(" }"); + sb.AppendLine(); + + // Generate request/response messages for each dynamic query + foreach (var dynamicQuery in dynamicQueries) + { + // Request message + sb.AppendLine($" /// "); + sb.AppendLine($" /// Request message for dynamic query on {dynamicQuery.Name}"); + sb.AppendLine($" /// "); + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Request"); + sb.AppendLine(" {"); + sb.AppendLine(" [ProtoMember(1)]"); + sb.AppendLine(" [DataMember(Order = 1)]"); + sb.AppendLine(" public int Page { get; set; }"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(2)]"); + sb.AppendLine(" [DataMember(Order = 2)]"); + sb.AppendLine(" public int PageSize { get; set; }"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(3)]"); + sb.AppendLine(" [DataMember(Order = 3)]"); + sb.AppendLine(" public List Filters { get; set; } = new();"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(4)]"); + sb.AppendLine(" [DataMember(Order = 4)]"); + sb.AppendLine(" public List Sorts { get; set; } = new();"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(5)]"); + sb.AppendLine(" [DataMember(Order = 5)]"); + sb.AppendLine(" public List Groups { get; set; } = new();"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(6)]"); + sb.AppendLine(" [DataMember(Order = 6)]"); + sb.AppendLine(" public List Aggregates { get; set; } = new();"); + sb.AppendLine(" }"); + sb.AppendLine(); + + // Response message + sb.AppendLine($" /// "); + sb.AppendLine($" /// Response message for dynamic query on {dynamicQuery.Name}"); + sb.AppendLine($" /// "); + sb.AppendLine(" [ProtoContract]"); + sb.AppendLine(" [DataContract]"); + sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Response"); + sb.AppendLine(" {"); + sb.AppendLine(" [ProtoMember(1)]"); + sb.AppendLine(" [DataMember(Order = 1)]"); + sb.AppendLine($" public List<{dynamicQuery.DestinationTypeFullyQualified}> Data {{ get; set; }} = new();"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(2)]"); + sb.AppendLine(" [DataMember(Order = 2)]"); + sb.AppendLine(" public long TotalCount { get; set; }"); + sb.AppendLine(); + sb.AppendLine(" [ProtoMember(3)]"); + sb.AppendLine(" [DataMember(Order = 3)]"); + sb.AppendLine(" public int NumberOfPages { get; set; }"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + // Generate service interface + sb.AppendLine(" /// "); + sb.AppendLine(" /// gRPC service interface for DynamicQueries"); + sb.AppendLine(" /// "); + sb.AppendLine(" [ServiceContract]"); + sb.AppendLine(" public interface IDynamicQueryService"); + sb.AppendLine(" {"); + + foreach (var dynamicQuery in dynamicQueries) + { + var methodName = $"Query{dynamicQuery.Name}"; + sb.AppendLine($" /// "); + sb.AppendLine($" /// Execute dynamic query on {dynamicQuery.Name}"); + sb.AppendLine($" /// "); + sb.AppendLine(" [OperationContract]"); + sb.AppendLine($" System.Threading.Tasks.Task {methodName}Async(DynamicQuery{dynamicQuery.Name}Request request, CallContext context = default);"); + sb.AppendLine(); + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static string GenerateDynamicQueryServiceImpl(List dynamicQueries, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using Grpc.Core;"); + sb.AppendLine("using System.Threading.Tasks;"); + sb.AppendLine("using System.Collections.Generic;"); + sb.AppendLine("using System.Linq;"); + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + sb.AppendLine($"using {rootNamespace}.Grpc;"); + sb.AppendLine("using Svrnty.CQRS.Abstractions;"); + sb.AppendLine("using Svrnty.CQRS.Abstractions.Security;"); + sb.AppendLine("using Svrnty.CQRS.DynamicQuery.Abstractions;"); + sb.AppendLine("using PoweredSoft.DynamicQuery.Core;"); + sb.AppendLine(); + + sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); + sb.AppendLine("{"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Auto-generated gRPC service implementation for DynamicQueries"); + sb.AppendLine(" /// "); + sb.AppendLine(" public sealed class DynamicQueryServiceImpl : DynamicQueryService.DynamicQueryServiceBase"); + sb.AppendLine(" {"); + sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); + sb.AppendLine(); + sb.AppendLine(" public DynamicQueryServiceImpl(IServiceScopeFactory scopeFactory)"); + sb.AppendLine(" {"); + sb.AppendLine(" _scopeFactory = scopeFactory;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + foreach (var dynamicQuery in dynamicQueries) + { + var methodName = $"Query{dynamicQuery.Name}"; + var requestType = $"DynamicQuery{dynamicQuery.Name}Request"; + var responseType = $"DynamicQuery{dynamicQuery.Name}Response"; + + sb.AppendLine($" public override async Task<{responseType}> {methodName}("); + sb.AppendLine($" {requestType} request,"); + sb.AppendLine(" ServerCallContext context)"); + sb.AppendLine(" {"); + sb.AppendLine(" using var scope = _scopeFactory.CreateScope();"); + sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); + sb.AppendLine(); + sb.AppendLine(" // Authorization check"); + sb.AppendLine($" var authorizationService = serviceProvider.GetService();"); + sb.AppendLine(" if (authorizationService != null)"); + sb.AppendLine(" {"); + sb.AppendLine($" var authResult = await authorizationService.IsAllowedAsync(typeof({dynamicQuery.QueryInterfaceName}), context.CancellationToken);"); + sb.AppendLine(" if (authResult == AuthorizationResult.Unauthorized)"); + sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unauthenticated, \"Unauthorized\"));"); + sb.AppendLine(" if (authResult == AuthorizationResult.Forbidden)"); + sb.AppendLine(" throw new RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.PermissionDenied, \"Forbidden\"));"); + sb.AppendLine(" }"); + sb.AppendLine(); + + // Build the dynamic query object + if (dynamicQuery.HasParams) { - if (type.IsAbstract || type.IsStatic) - continue; + sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>"); + } + else + { + sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>"); + } + sb.AppendLine(" {"); + sb.AppendLine(" Page = request.Page > 0 ? request.Page : null,"); + sb.AppendLine(" PageSize = request.PageSize > 0 ? request.PageSize : null,"); + sb.AppendLine(" Filters = ConvertFilters(request.Filters) ?? new(),"); + sb.AppendLine(" Sorts = ConvertSorts(request.Sorts) ?? new(),"); + sb.AppendLine(" Groups = ConvertGroups(request.Groups) ?? new(),"); + sb.AppendLine(" Aggregates = ConvertAggregates(request.Aggregates) ?? new()"); + sb.AppendLine(" };"); + sb.AppendLine(); - var attr = type.GetAttributes() - .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals( - a.AttributeClass, streamingNotificationAttribute)); + // Get the handler and execute + sb.AppendLine($" var handler = serviceProvider.GetRequiredService>>();"); + sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); + sb.AppendLine(); - if (attr == null) - continue; + // Build response + sb.AppendLine($" var response = new {responseType}"); + sb.AppendLine(" {"); + sb.AppendLine(" TotalRecords = result.TotalRecords,"); + sb.AppendLine(" NumberOfPages = (int)(result.NumberOfPages ?? 0)"); + sb.AppendLine(" };"); + sb.AppendLine(); + sb.AppendLine(" if (result.Data != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" foreach (var item in result.Data)"); + sb.AppendLine(" {"); + sb.AppendLine($" response.Data.Add(MapTo{dynamicQuery.Name}ProtoModel(item));"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" // TODO: Add aggregates and groups to response if needed"); + sb.AppendLine(); + sb.AppendLine(" return response;"); + sb.AppendLine(" }"); + sb.AppendLine(); + } - // Extract SubscriptionKey from attribute - var subscriptionKeyArg = attr.NamedArguments - .FirstOrDefault(a => a.Key == "SubscriptionKey"); - var subscriptionKeyProp = subscriptionKeyArg.Value.Value as string; + // Add helper methods for converting proto messages to AspNetCore types + sb.AppendLine(" private static List? ConvertFilters(Google.Protobuf.Collections.RepeatedField protoFilters)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (protoFilters == null || protoFilters.Count == 0)"); + sb.AppendLine(" return null;"); + sb.AppendLine(); + sb.AppendLine(" var filters = new List();"); + sb.AppendLine(" foreach (var protoFilter in protoFilters)"); + sb.AppendLine(" {"); + 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(),"); + sb.AppendLine(" Value = protoFilter.Value,"); + sb.AppendLine(" And = true"); + sb.AppendLine(" };"); + sb.AppendLine(); + sb.AppendLine(" // Handle nested AND filters"); + sb.AppendLine(" if (protoFilter.And != null && protoFilter.And.Count > 0)"); + sb.AppendLine(" {"); + sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.And);"); + sb.AppendLine(" filter.And = true;"); + sb.AppendLine(" }"); + sb.AppendLine(" // Handle nested OR filters"); + sb.AppendLine(" else if (protoFilter.Or != null && protoFilter.Or.Count > 0)"); + sb.AppendLine(" {"); + sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.Or);"); + sb.AppendLine(" filter.And = false;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" filters.Add(filter);"); + sb.AppendLine(" }"); + sb.AppendLine(" return filters;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" private static List ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField protoFilters)"); + sb.AppendLine(" {"); + sb.AppendLine(" var result = new List();"); + sb.AppendLine(" foreach (var pf in protoFilters)"); + sb.AppendLine(" {"); + 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(),"); + sb.AppendLine(" Value = pf.Value,"); + sb.AppendLine(" And = true"); + sb.AppendLine(" };"); + sb.AppendLine(" if (pf.And != null && pf.And.Count > 0)"); + sb.AppendLine(" {"); + sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.And);"); + sb.AppendLine(" filter.And = true;"); + sb.AppendLine(" }"); + sb.AppendLine(" else if (pf.Or != null && pf.Or.Count > 0)"); + sb.AppendLine(" {"); + sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.Or);"); + sb.AppendLine(" filter.And = false;"); + sb.AppendLine(" }"); + sb.AppendLine(" result.Add(filter);"); + sb.AppendLine(" }"); + sb.AppendLine(" return result;"); + sb.AppendLine(" }"); + sb.AppendLine(); - if (string.IsNullOrEmpty(subscriptionKeyProp)) - continue; + sb.AppendLine(" private static List? ConvertSorts(Google.Protobuf.Collections.RepeatedField protoSorts)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (protoSorts == null || protoSorts.Count == 0)"); + sb.AppendLine(" return null;"); + sb.AppendLine(); + sb.AppendLine(" return protoSorts.Select(s => new PoweredSoft.DynamicQuery.Sort"); + sb.AppendLine(" {"); + sb.AppendLine(" Path = s.Path,"); + sb.AppendLine(" Ascending = s.Ascending"); + sb.AppendLine(" }).ToList();"); + sb.AppendLine(" }"); + sb.AppendLine(); - // Get all properties of the notification type - var properties = new List(); - int fieldNumber = 1; + sb.AppendLine(" private static List? ConvertGroups(Google.Protobuf.Collections.RepeatedField protoGroups)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (protoGroups == null || protoGroups.Count == 0)"); + sb.AppendLine(" return null;"); + sb.AppendLine(); + sb.AppendLine(" return protoGroups.Select(g => new PoweredSoft.DynamicQuery.Group"); + sb.AppendLine(" {"); + sb.AppendLine(" Path = g.Path"); + sb.AppendLine(" }).ToList();"); + sb.AppendLine(" }"); + sb.AppendLine(); - foreach (var prop in type.GetMembers().OfType() - .Where(p => p.DeclaredAccessibility == Accessibility.Public)) + sb.AppendLine(" private static List? ConvertAggregates(Google.Protobuf.Collections.RepeatedField 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.DynamicQueryAggregate"); + sb.AppendLine(" {"); + sb.AppendLine(" Path = a.Path,"); + sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.AggregateType)a.Type).ToString()"); + sb.AppendLine(" }).ToList();"); + sb.AppendLine(" }"); + sb.AppendLine(); + + // Add generic reflection-based mapper helper + sb.AppendLine(" private static TProto MapToProtoModel(TDomain domainModel) where TProto : Google.Protobuf.IMessage, new()"); + sb.AppendLine(" {"); + sb.AppendLine(" if (domainModel == null) return new TProto();"); + sb.AppendLine(" var proto = new TProto();"); + sb.AppendLine(" var domainProps = typeof(TDomain).GetProperties();"); + sb.AppendLine(" var protoDesc = proto.Descriptor;"); + sb.AppendLine(); + sb.AppendLine(" foreach (var domainProp in domainProps)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Convert property name to proto field name (PascalCase to snake_case)"); + sb.AppendLine(" var protoFieldName = ToSnakeCase(domainProp.Name);"); + sb.AppendLine(" var protoField = protoDesc.FindFieldByName(protoFieldName);"); + sb.AppendLine(" if (protoField == null) continue;"); + sb.AppendLine(); + sb.AppendLine(" var domainValue = domainProp.GetValue(domainModel);"); + sb.AppendLine(" if (domainValue == null) continue;"); + sb.AppendLine(); + sb.AppendLine(" var protoAccessor = protoField.Accessor;"); + sb.AppendLine(); + sb.AppendLine(" // Handle DateTime -> Timestamp conversion"); + sb.AppendLine(" if (domainProp.PropertyType == typeof(DateTime) || domainProp.PropertyType == typeof(DateTime?))"); + sb.AppendLine(" {"); + sb.AppendLine(" var dateTime = (DateTime)domainValue;"); + sb.AppendLine(" // Ensure UTC for Timestamp conversion"); + sb.AppendLine(" if (dateTime.Kind != DateTimeKind.Utc)"); + sb.AppendLine(" dateTime = dateTime.ToUniversalTime();"); + sb.AppendLine(" protoAccessor.SetValue(proto, Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime));"); + sb.AppendLine(" }"); + sb.AppendLine(" else if (domainProp.PropertyType == typeof(DateTimeOffset) || domainProp.PropertyType == typeof(DateTimeOffset?))"); + sb.AppendLine(" {"); + sb.AppendLine(" var dateTimeOffset = (DateTimeOffset)domainValue;"); + sb.AppendLine(" protoAccessor.SetValue(proto, Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(dateTimeOffset));"); + sb.AppendLine(" }"); + sb.AppendLine(" // Handle collections (List, IList, etc.) - must check before complex types"); + sb.AppendLine(" else if (protoField.IsRepeated && domainValue is System.Collections.IEnumerable enumerable && domainProp.PropertyType != typeof(string))"); + sb.AppendLine(" {"); + sb.AppendLine(" var repeatedField = protoAccessor.GetValue(proto);"); + sb.AppendLine(" if (repeatedField == null) continue;"); + sb.AppendLine(); + sb.AppendLine(" // Get the element type of the RepeatedField"); + sb.AppendLine(" var repeatedFieldType = repeatedField.GetType();"); + sb.AppendLine(" var repeatedElementType = repeatedFieldType.IsGenericType ? repeatedFieldType.GetGenericArguments()[0] : null;"); + sb.AppendLine(" if (repeatedElementType == null) continue;"); + sb.AppendLine(); + sb.AppendLine(" // Get Add(T) method with specific parameter type to avoid ambiguity"); + sb.AppendLine(" var addMethod = repeatedFieldType.GetMethod(\"Add\", new[] { repeatedElementType });"); + sb.AppendLine(" if (addMethod == null) continue;"); + sb.AppendLine(); + sb.AppendLine(" // Get element types"); + sb.AppendLine(" var domainElementType = domainProp.PropertyType.IsArray"); + sb.AppendLine(" ? domainProp.PropertyType.GetElementType()"); + sb.AppendLine(" : domainProp.PropertyType.IsGenericType ? domainProp.PropertyType.GetGenericArguments()[0] : null;"); + sb.AppendLine(" // Only access MessageType for message fields (throws for primitives)"); + sb.AppendLine(" var protoElementType = protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message ? protoField.MessageType?.ClrType : null;"); + sb.AppendLine(); + sb.AppendLine(" foreach (var item in enumerable)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (item == null) continue;"); + sb.AppendLine(); + sb.AppendLine(" // Check if elements need mapping (complex types)"); + sb.AppendLine(" if (protoElementType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoElementType) && domainElementType != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\","); + sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!"); + sb.AppendLine(" .MakeGenericMethod(domainElementType, protoElementType);"); + sb.AppendLine(" var mappedItem = mapMethod.Invoke(null, new[] { item });"); + sb.AppendLine(" if (mappedItem != null)"); + sb.AppendLine(" addMethod.Invoke(repeatedField, new[] { mappedItem });"); + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine(" // Primitive types, enums, strings - add directly"); + sb.AppendLine(" try { addMethod.Invoke(repeatedField, new[] { item }); }"); + sb.AppendLine(" catch { /* Type mismatch, skip */ }"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" // Handle enumerable value types that map to proto messages with repeated fields (e.g., NpgsqlPolygon -> proto message with items)"); + sb.AppendLine(" else if (domainProp.PropertyType.IsValueType && "); + sb.AppendLine(" domainValue is System.Collections.IEnumerable valueTypeEnumerable &&"); + sb.AppendLine(" protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Create the proto message and look for its 'items' repeated field"); + sb.AppendLine(" var protoFieldType = protoField.MessageType?.ClrType;"); + sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))"); + sb.AppendLine(" {"); + sb.AppendLine(" var nestedProto = System.Activator.CreateInstance(protoFieldType) as Google.Protobuf.IMessage;"); + sb.AppendLine(" if (nestedProto != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Find the 'items' field in the proto message"); + sb.AppendLine(" var itemsField = nestedProto.Descriptor.FindFieldByName(\"items\");"); + sb.AppendLine(" if (itemsField != null && itemsField.IsRepeated)"); + sb.AppendLine(" {"); + sb.AppendLine(" var repeatedField = itemsField.Accessor.GetValue(nestedProto);"); + sb.AppendLine(" var repeatedFieldType = repeatedField?.GetType();"); + sb.AppendLine(" var protoElementType = itemsField.MessageType?.ClrType;"); + sb.AppendLine(" "); + sb.AppendLine(" if (repeatedFieldType != null && protoElementType != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" var addMethod = repeatedFieldType.GetMethod(\"Add\", new[] { protoElementType });"); + sb.AppendLine(" if (addMethod != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Get element type from the enumerable"); + sb.AppendLine(" var enumerableInterface = domainProp.PropertyType.GetInterfaces()"); + sb.AppendLine(" .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(System.Collections.Generic.IEnumerable<>));"); + sb.AppendLine(" var domainElementType = enumerableInterface?.GetGenericArguments()[0];"); + sb.AppendLine(" "); + sb.AppendLine(" foreach (var item in valueTypeEnumerable)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (item == null) continue;"); + sb.AppendLine(" "); + sb.AppendLine(" // Map each item to the proto element type"); + sb.AppendLine(" if (domainElementType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoElementType))"); + sb.AppendLine(" {"); + sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\","); + sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!"); + sb.AppendLine(" .MakeGenericMethod(domainElementType, protoElementType);"); + sb.AppendLine(" var mappedItem = mapMethod.Invoke(null, new[] { item });"); + sb.AppendLine(" if (mappedItem != null)"); + sb.AppendLine(" addMethod.Invoke(repeatedField, new[] { mappedItem });"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" protoAccessor.SetValue(proto, nestedProto);"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" // Handle nested complex types (non-primitive, non-enum, non-string, non-collection)"); + sb.AppendLine(" else if (!domainProp.PropertyType.IsPrimitive && "); + sb.AppendLine(" domainProp.PropertyType != typeof(string) && "); + sb.AppendLine(" !domainProp.PropertyType.IsEnum &&"); + sb.AppendLine(" !domainProp.PropertyType.IsValueType)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Get the proto field type and recursively map (only access MessageType for message fields)"); + sb.AppendLine(" var protoFieldType = protoAccessor.GetValue(proto)?.GetType() ?? (protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message ? protoField.MessageType?.ClrType : null);"); + sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))"); + sb.AppendLine(" {"); + sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\", "); + sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!"); + sb.AppendLine(" .MakeGenericMethod(domainProp.PropertyType, protoFieldType);"); + sb.AppendLine(" var nestedProto = mapMethod.Invoke(null, new[] { domainValue });"); + sb.AppendLine(" if (nestedProto != null)"); + sb.AppendLine(" protoAccessor.SetValue(proto, nestedProto);"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" // Handle decimal -> string conversion"); + sb.AppendLine(" else if (domainProp.PropertyType == typeof(decimal) || domainProp.PropertyType == typeof(decimal?))"); + sb.AppendLine(" {"); + sb.AppendLine(" protoAccessor.SetValue(proto, ((decimal)domainValue).ToString(System.Globalization.CultureInfo.InvariantCulture));"); + sb.AppendLine(" }"); + sb.AppendLine(" // Handle Guid -> string conversion"); + sb.AppendLine(" else if (domainProp.PropertyType == typeof(Guid) || domainProp.PropertyType == typeof(Guid?))"); + sb.AppendLine(" {"); + sb.AppendLine(" protoAccessor.SetValue(proto, ((Guid)domainValue).ToString());"); + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine(" // Direct assignment for primitives, strings, enums"); + sb.AppendLine(" try { protoAccessor.SetValue(proto, domainValue); }"); + sb.AppendLine(" catch { /* Type mismatch, skip */ }"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" return proto;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" private static string ToSnakeCase(string str)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (string.IsNullOrEmpty(str)) return str;"); + sb.AppendLine(" var result = new System.Text.StringBuilder();"); + sb.AppendLine(" for (int i = 0; i < str.Length; i++)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (i > 0 && char.IsUpper(str[i]))"); + sb.AppendLine(" result.Append('_');"); + sb.AppendLine(" result.Append(char.ToLowerInvariant(str[i]));"); + sb.AppendLine(" }"); + sb.AppendLine(" return result.ToString();"); + sb.AppendLine(" }"); + sb.AppendLine(); + + // Add mapper methods for each entity type + foreach (var dynamicQuery in dynamicQueries) + { + var entityName = dynamicQuery.Name; + var protoTypeName = $"{entityName.TrimEnd('s')}"; // User from Users + + sb.AppendLine($" private static {protoTypeName} MapTo{entityName}ProtoModel({dynamicQuery.DestinationTypeFullyQualified} domainModel)"); + sb.AppendLine(" {"); + sb.AppendLine($" return MapToProtoModel<{dynamicQuery.DestinationTypeFullyQualified}, {protoTypeName}>(domainModel);"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + /// + /// Discovers types marked with [StreamingNotification] attribute + /// + private static List DiscoverNotifications(IEnumerable allTypes, Compilation compilation) + { + var streamingNotificationAttribute = compilation.GetTypeByMetadataName( + "Svrnty.CQRS.Notifications.Abstractions.StreamingNotificationAttribute"); + + if (streamingNotificationAttribute == null) + return new List(); + + var notifications = new List(); + + foreach (var type in allTypes) + { + if (type.IsAbstract || type.IsStatic) + continue; + + var attr = type.GetAttributes() + .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals( + a.AttributeClass, streamingNotificationAttribute)); + + if (attr == null) + continue; + + // Extract SubscriptionKey from attribute + var subscriptionKeyArg = attr.NamedArguments + .FirstOrDefault(a => a.Key == "SubscriptionKey"); + var subscriptionKeyProp = subscriptionKeyArg.Value.Value as string; + + if (string.IsNullOrEmpty(subscriptionKeyProp)) + continue; + + // Get all properties of the notification type + var properties = new List(); + int fieldNumber = 1; + + foreach (var prop in type.GetMembers().OfType() + .Where(p => p.DeclaredAccessibility == Accessibility.Public)) + { + var propType = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var protoType = ProtoTypeMapper.MapToProtoType(propType, out _, out _); + + properties.Add(new PropertyInfo { - var propType = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var protoType = ProtoTypeMapper.MapToProtoType(propType, out _, out _); - - properties.Add(new PropertyInfo - { - Name = prop.Name, - Type = propType, - FullyQualifiedType = propType, - ProtoType = protoType, - FieldNumber = fieldNumber++, - IsEnum = prop.Type.TypeKind == TypeKind.Enum, - IsDecimal = propType.Contains("decimal") || propType.Contains("Decimal"), - IsDateTime = propType.Contains("DateTime"), - IsList = IsListOrCollection(prop.Type), - IsValueTypeCollection = IsValueTypeCollection(prop.Type), - }); - } - - // Find the subscription key property info - var keyPropInfo = properties.FirstOrDefault(p => p.Name == subscriptionKeyProp); - if (keyPropInfo == null) - continue; - - notifications.Add(new NotificationInfo - { - Name = type.Name, - FullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - Namespace = type.ContainingNamespace?.ToDisplayString() ?? "", - SubscriptionKeyProperty = subscriptionKeyProp!, // Already validated as non-null above - SubscriptionKeyInfo = keyPropInfo, - Properties = properties + Name = prop.Name, + Type = propType, + FullyQualifiedType = propType, + ProtoType = protoType, + FieldNumber = fieldNumber++, + IsEnum = prop.Type.TypeKind == TypeKind.Enum, + IsDecimal = propType.Contains("decimal") || propType.Contains("Decimal"), + IsDateTime = propType.Contains("DateTime"), + IsList = IsListOrCollection(prop.Type), + IsValueTypeCollection = IsValueTypeCollection(prop.Type), }); } - return notifications; + // Find the subscription key property info + var keyPropInfo = properties.FirstOrDefault(p => p.Name == subscriptionKeyProp); + if (keyPropInfo == null) + continue; + + notifications.Add(new NotificationInfo + { + Name = type.Name, + FullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + Namespace = type.ContainingNamespace?.ToDisplayString() ?? "", + SubscriptionKeyProperty = subscriptionKeyProp!, // Already validated as non-null above + SubscriptionKeyInfo = keyPropInfo, + Properties = properties + }); } - /// - /// Generates the NotificationServiceImpl class for streaming notifications - /// - private static string GenerateNotificationServiceImpl(List notifications, string rootNamespace) - { - var sb = new StringBuilder(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using Grpc.Core;"); - sb.AppendLine("using System.Threading.Tasks;"); - sb.AppendLine("using System.Threading;"); - sb.AppendLine("using Google.Protobuf.WellKnownTypes;"); - sb.AppendLine($"using {rootNamespace}.Grpc;"); - sb.AppendLine("using Svrnty.CQRS.Notifications.Grpc;"); - sb.AppendLine(); + return notifications; + } + + /// + /// Generates the NotificationServiceImpl class for streaming notifications + /// + private static string GenerateNotificationServiceImpl(List notifications, string rootNamespace) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using Grpc.Core;"); + sb.AppendLine("using System.Threading.Tasks;"); + sb.AppendLine("using System.Threading;"); + sb.AppendLine("using Google.Protobuf.WellKnownTypes;"); + sb.AppendLine($"using {rootNamespace}.Grpc;"); + sb.AppendLine("using Svrnty.CQRS.Notifications.Grpc;"); + sb.AppendLine(); + + sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); + sb.AppendLine("{"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Auto-generated gRPC service implementation for streaming Notifications"); + sb.AppendLine(" /// "); + sb.AppendLine(" public sealed class NotificationServiceImpl : NotificationService.NotificationServiceBase"); + sb.AppendLine(" {"); + sb.AppendLine(" private readonly NotificationSubscriptionManager _subscriptionManager;"); + sb.AppendLine(); + sb.AppendLine(" public NotificationServiceImpl(NotificationSubscriptionManager subscriptionManager)"); + sb.AppendLine(" {"); + sb.AppendLine(" _subscriptionManager = subscriptionManager;"); + sb.AppendLine(" }"); + + foreach (var notification in notifications) + { + var methodName = $"SubscribeTo{notification.Name}"; + var requestType = $"SubscribeTo{notification.Name}Request"; + var keyPropName = notification.SubscriptionKeyProperty; + // Proto uses PascalCase for C# properties + var keyPropPascal = ToPascalCaseHelper(ToSnakeCaseHelper(keyPropName)); - sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); - sb.AppendLine("{"); - sb.AppendLine(" /// "); - sb.AppendLine(" /// Auto-generated gRPC service implementation for streaming Notifications"); - sb.AppendLine(" /// "); - sb.AppendLine(" public sealed class NotificationServiceImpl : NotificationService.NotificationServiceBase"); - sb.AppendLine(" {"); - sb.AppendLine(" private readonly NotificationSubscriptionManager _subscriptionManager;"); sb.AppendLine(); - sb.AppendLine(" public NotificationServiceImpl(NotificationSubscriptionManager subscriptionManager)"); + sb.AppendLine($" public override async Task {methodName}("); + sb.AppendLine($" {requestType} request,"); + sb.AppendLine($" IServerStreamWriter<{notification.Name}> responseStream,"); + sb.AppendLine(" ServerCallContext context)"); sb.AppendLine(" {"); - sb.AppendLine(" _subscriptionManager = subscriptionManager;"); + sb.AppendLine($" // Subscribe with mapper from domain notification to proto message"); + sb.AppendLine($" using var subscription = _subscriptionManager.Subscribe<{notification.FullyQualifiedName}, {notification.Name}>("); + sb.AppendLine($" request.{keyPropPascal},"); + sb.AppendLine($" responseStream,"); + sb.AppendLine($" domainNotification => Map{notification.Name}(domainNotification));"); + sb.AppendLine(); + sb.AppendLine(" // Keep the stream alive until client disconnects"); + sb.AppendLine(" try"); + sb.AppendLine(" {"); + sb.AppendLine(" await Task.Delay(Timeout.Infinite, context.CancellationToken);"); + sb.AppendLine(" }"); + sb.AppendLine(" catch (OperationCanceledException)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Client disconnected - normal behavior"); + sb.AppendLine(" }"); sb.AppendLine(" }"); + } - foreach (var notification in notifications) + // Generate mapper methods + foreach (var notification in notifications) + { + sb.AppendLine(); + sb.AppendLine($" private static {notification.Name} Map{notification.Name}({notification.FullyQualifiedName} domain)"); + sb.AppendLine(" {"); + sb.AppendLine($" return new {notification.Name}"); + sb.AppendLine(" {"); + + foreach (var prop in notification.Properties) { - var methodName = $"SubscribeTo{notification.Name}"; - var requestType = $"SubscribeTo{notification.Name}Request"; - var keyPropName = notification.SubscriptionKeyProperty; - // Proto uses PascalCase for C# properties - var keyPropPascal = ToPascalCaseHelper(ToSnakeCaseHelper(keyPropName)); - - sb.AppendLine(); - sb.AppendLine($" public override async Task {methodName}("); - sb.AppendLine($" {requestType} request,"); - sb.AppendLine($" IServerStreamWriter<{notification.Name}> responseStream,"); - sb.AppendLine(" ServerCallContext context)"); - sb.AppendLine(" {"); - sb.AppendLine($" // Subscribe with mapper from domain notification to proto message"); - sb.AppendLine($" using var subscription = _subscriptionManager.Subscribe<{notification.FullyQualifiedName}, {notification.Name}>("); - sb.AppendLine($" request.{keyPropPascal},"); - sb.AppendLine($" responseStream,"); - sb.AppendLine($" domainNotification => Map{notification.Name}(domainNotification));"); - sb.AppendLine(); - sb.AppendLine(" // Keep the stream alive until client disconnects"); - sb.AppendLine(" try"); - sb.AppendLine(" {"); - sb.AppendLine(" await Task.Delay(Timeout.Infinite, context.CancellationToken);"); - sb.AppendLine(" }"); - sb.AppendLine(" catch (OperationCanceledException)"); - sb.AppendLine(" {"); - sb.AppendLine(" // Client disconnected - normal behavior"); - sb.AppendLine(" }"); - sb.AppendLine(" }"); - } - - // Generate mapper methods - foreach (var notification in notifications) - { - sb.AppendLine(); - sb.AppendLine($" private static {notification.Name} Map{notification.Name}({notification.FullyQualifiedName} domain)"); - sb.AppendLine(" {"); - sb.AppendLine($" return new {notification.Name}"); - sb.AppendLine(" {"); - - foreach (var prop in notification.Properties) + var protoFieldName = ToPascalCaseHelper(ToSnakeCaseHelper(prop.Name)); + if (prop.IsDateTime) { - var protoFieldName = ToPascalCaseHelper(ToSnakeCaseHelper(prop.Name)); - if (prop.IsDateTime) - { - sb.AppendLine($" {protoFieldName} = Timestamp.FromDateTime(domain.{prop.Name}.ToUniversalTime()),"); - } - else if (prop.IsDecimal) - { - sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),"); - } - else if (prop.IsGuid) - { - sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),"); - } - else if (prop.IsEnum) - { - // Map domain enum to proto enum - get simple type name - var simpleTypeName = prop.Type.Replace("global::", "").Split('.').Last(); - sb.AppendLine($" {protoFieldName} = ({simpleTypeName})((int)domain.{prop.Name}),"); - } - else - { - sb.AppendLine($" {protoFieldName} = domain.{prop.Name},"); - } + sb.AppendLine($" {protoFieldName} = Timestamp.FromDateTime(domain.{prop.Name}.ToUniversalTime()),"); + } + else if (prop.IsDecimal) + { + sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),"); + } + else if (prop.IsGuid) + { + sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),"); + } + else if (prop.IsEnum) + { + // Map domain enum to proto enum - get simple type name + var simpleTypeName = prop.Type.Replace("global::", "").Split('.').Last(); + sb.AppendLine($" {protoFieldName} = ({simpleTypeName})((int)domain.{prop.Name}),"); + } + else + { + sb.AppendLine($" {protoFieldName} = domain.{prop.Name},"); } - - sb.AppendLine(" };"); - sb.AppendLine(" }"); } - sb.AppendLine(" }"); - sb.AppendLine("}"); - - return sb.ToString(); + sb.AppendLine(" };"); + sb.AppendLine(" }"); } - private static string ToSnakeCaseHelper(string str) + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static string ToSnakeCaseHelper(string str) + { + if (string.IsNullOrEmpty(str)) return str; + var result = new StringBuilder(); + for (int i = 0; i < str.Length; i++) { - if (string.IsNullOrEmpty(str)) return str; - var result = new StringBuilder(); - for (int i = 0; i < str.Length; i++) - { - if (i > 0 && char.IsUpper(str[i])) - result.Append('_'); - result.Append(char.ToLowerInvariant(str[i])); - } - return result.ToString(); + if (i > 0 && char.IsUpper(str[i])) + result.Append('_'); + result.Append(char.ToLowerInvariant(str[i])); } + return result.ToString(); + } - private static string ToPascalCaseHelper(string snakeCase) - { - if (string.IsNullOrEmpty(snakeCase)) return snakeCase; - var parts = snakeCase.Split('_'); - return string.Join("", parts.Select(p => - p.Length > 0 ? char.ToUpperInvariant(p[0]) + p.Substring(1).ToLowerInvariant() : "")); - } + private static string ToPascalCaseHelper(string snakeCase) + { + if (string.IsNullOrEmpty(snakeCase)) return snakeCase; + var parts = snakeCase.Split('_'); + return string.Join("", parts.Select(p => + p.Length > 0 ? char.ToUpperInvariant(p[0]) + p.Substring(1).ToLowerInvariant() : "")); } } diff --git a/Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs b/Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs index f31eb42..d70d367 100644 --- a/Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs +++ b/Svrnty.CQRS.Grpc.Generators/Helpers/ProtoTypeMapper.cs @@ -1,102 +1,101 @@ 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 TypeMap = new Dictionary { - private static readonly Dictionary TypeMap = new Dictionary + // 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 - { "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("?", ""); + 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("?", ""); } } diff --git a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs index b4d21aa..6711f45 100644 --- a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs +++ b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs @@ -1,83 +1,82 @@ using System.Collections.Generic; 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 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 ResultProperties { get; set; } + public bool IsResultPrimitiveType { get; set; } + + public CommandInfo() { - public string Name { get; set; } - public string FullyQualifiedName { get; set; } - public string Namespace { get; set; } - public List 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 ResultProperties { get; set; } - public bool IsResultPrimitiveType { get; set; } - - public CommandInfo() - { - Name = string.Empty; - FullyQualifiedName = string.Empty; - Namespace = string.Empty; - Properties = new List(); - HandlerInterfaceName = string.Empty; - ResultProperties = new List(); - IsResultPrimitiveType = false; - } - } - - public class PropertyInfo - { - public string Name { get; set; } - public string Type { get; set; } - public string FullyQualifiedType { get; set; } - public string ProtoType { get; set; } - public int FieldNumber { get; set; } - public bool IsComplexType { get; set; } - public List NestedProperties { get; set; } - - // Type conversion metadata - public bool IsEnum { get; set; } - public bool IsList { get; set; } - public bool IsNullable { get; set; } - public bool IsDecimal { get; set; } - public bool IsDateTime { get; set; } - public bool IsDateTimeOffset { get; set; } - public bool IsGuid { get; set; } - public bool IsJsonElement { get; set; } - public bool IsBinaryType { get; set; } // Stream, byte[], MemoryStream - public bool IsStream { get; set; } // Specifically Stream types (not byte[]) - public bool IsReadOnly { get; set; } // Read-only/computed properties should be skipped - public bool IsValueTypeCollection { get; set; } // Value types that implement IList (like NpgsqlPolygon) - public string? ElementType { get; set; } - public bool IsElementComplexType { get; set; } - public bool IsElementGuid { get; set; } - public List? ElementNestedProperties { get; set; } - - public PropertyInfo() - { - Name = string.Empty; - Type = string.Empty; - FullyQualifiedType = string.Empty; - ProtoType = string.Empty; - IsComplexType = false; - NestedProperties = new List(); - IsEnum = 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; - } + Name = string.Empty; + FullyQualifiedName = string.Empty; + Namespace = string.Empty; + Properties = new List(); + HandlerInterfaceName = string.Empty; + ResultProperties = new List(); + IsResultPrimitiveType = false; + } +} + +public class PropertyInfo +{ + public string Name { get; set; } + public string Type { get; set; } + public string FullyQualifiedType { get; set; } + public string ProtoType { get; set; } + public int FieldNumber { get; set; } + public bool IsComplexType { get; set; } + public List NestedProperties { get; set; } + + // Type conversion metadata + public bool IsEnum { get; set; } + public bool IsList { get; set; } + public bool IsNullable { get; set; } + public bool IsDecimal { get; set; } + public bool IsDateTime { get; set; } + public bool IsDateTimeOffset { get; set; } + public bool IsGuid { get; set; } + public bool IsJsonElement { get; set; } + public bool IsBinaryType { get; set; } // Stream, byte[], MemoryStream + public bool IsStream { get; set; } // Specifically Stream types (not byte[]) + public bool IsReadOnly { get; set; } // Read-only/computed properties should be skipped + public bool IsValueTypeCollection { get; set; } // Value types that implement IList (like NpgsqlPolygon) + public string? ElementType { get; set; } + public bool IsElementComplexType { get; set; } + public bool IsElementGuid { get; set; } + public List? ElementNestedProperties { get; set; } + + public PropertyInfo() + { + Name = string.Empty; + Type = string.Empty; + FullyQualifiedType = string.Empty; + ProtoType = string.Empty; + IsComplexType = false; + NestedProperties = new List(); + IsEnum = 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; } } diff --git a/Svrnty.CQRS.Grpc.Generators/Models/DynamicQueryInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/DynamicQueryInfo.cs index 4c2ac69..03985eb 100644 --- a/Svrnty.CQRS.Grpc.Generators/Models/DynamicQueryInfo.cs +++ b/Svrnty.CQRS.Grpc.Generators/Models/DynamicQueryInfo.cs @@ -1,28 +1,27 @@ -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; } +namespace Svrnty.CQRS.Grpc.Generators.Models; - 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; - } +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() + { + Name = string.Empty; + SourceType = string.Empty; + SourceTypeFullyQualified = string.Empty; + DestinationType = string.Empty; + DestinationTypeFullyQualified = string.Empty; + HandlerInterfaceName = string.Empty; + QueryInterfaceName = string.Empty; + HasParams = false; } } diff --git a/Svrnty.CQRS.Grpc.Generators/Models/NotificationInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/NotificationInfo.cs index 3285265..b63f583 100644 --- a/Svrnty.CQRS.Grpc.Generators/Models/NotificationInfo.cs +++ b/Svrnty.CQRS.Grpc.Generators/Models/NotificationInfo.cs @@ -1,50 +1,49 @@ using System.Collections.Generic; -namespace Svrnty.CQRS.Grpc.Generators.Models +namespace Svrnty.CQRS.Grpc.Generators.Models; + +/// +/// Represents a discovered streaming notification type for proto/gRPC generation. +/// +public class NotificationInfo { /// - /// Represents a discovered streaming notification type for proto/gRPC generation. + /// The notification type name (e.g., "InventoryChangeNotification"). /// - public class NotificationInfo + public string Name { get; set; } + + /// + /// The fully qualified type name including namespace. + /// + public string FullyQualifiedName { get; set; } + + /// + /// The namespace of the notification type. + /// + public string Namespace { get; set; } + + /// + /// The property name used as the subscription key (from [StreamingNotification] attribute). + /// + public string SubscriptionKeyProperty { get; set; } + + /// + /// The subscription key property info. + /// + public PropertyInfo SubscriptionKeyInfo { get; set; } + + /// + /// All properties of the notification type. + /// + public List Properties { get; set; } + + public NotificationInfo() { - /// - /// The notification type name (e.g., "InventoryChangeNotification"). - /// - public string Name { get; set; } - - /// - /// The fully qualified type name including namespace. - /// - public string FullyQualifiedName { get; set; } - - /// - /// The namespace of the notification type. - /// - public string Namespace { get; set; } - - /// - /// The property name used as the subscription key (from [StreamingNotification] attribute). - /// - public string SubscriptionKeyProperty { get; set; } - - /// - /// The subscription key property info. - /// - public PropertyInfo SubscriptionKeyInfo { get; set; } - - /// - /// All properties of the notification type. - /// - public List Properties { get; set; } - - public NotificationInfo() - { - Name = string.Empty; - FullyQualifiedName = string.Empty; - Namespace = string.Empty; - SubscriptionKeyProperty = string.Empty; - SubscriptionKeyInfo = new PropertyInfo(); - Properties = new List(); - } + Name = string.Empty; + FullyQualifiedName = string.Empty; + Namespace = string.Empty; + SubscriptionKeyProperty = string.Empty; + SubscriptionKeyInfo = new PropertyInfo(); + Properties = new List(); } } diff --git a/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs index 93924f4..3ae01b5 100644 --- a/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs +++ b/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs @@ -1,30 +1,29 @@ using System.Collections.Generic; -namespace Svrnty.CQRS.Grpc.Generators.Models -{ - public class QueryInfo - { - public string Name { get; set; } - public string FullyQualifiedName { get; set; } - public string Namespace { get; set; } - public List Properties { get; set; } - public string ResultType { get; set; } - public string ResultFullyQualifiedName { get; set; } - public string HandlerInterfaceName { get; set; } - public List ResultProperties { get; set; } - public bool IsResultPrimitiveType { get; set; } +namespace Svrnty.CQRS.Grpc.Generators.Models; - public QueryInfo() - { - Name = string.Empty; - FullyQualifiedName = string.Empty; - Namespace = string.Empty; - Properties = new List(); - ResultType = string.Empty; - ResultFullyQualifiedName = string.Empty; - HandlerInterfaceName = string.Empty; - ResultProperties = new List(); - IsResultPrimitiveType = false; - } +public class QueryInfo +{ + public string Name { get; set; } + public string FullyQualifiedName { get; set; } + public string Namespace { get; set; } + public List Properties { get; set; } + public string ResultType { get; set; } + public string ResultFullyQualifiedName { get; set; } + public string HandlerInterfaceName { get; set; } + public List ResultProperties { get; set; } + public bool IsResultPrimitiveType { get; set; } + + public QueryInfo() + { + Name = string.Empty; + FullyQualifiedName = string.Empty; + Namespace = string.Empty; + Properties = new List(); + ResultType = string.Empty; + ResultFullyQualifiedName = string.Empty; + HandlerInterfaceName = string.Empty; + ResultProperties = new List(); + IsResultPrimitiveType = false; } } diff --git a/Svrnty.CQRS/Configuration/CqrsBuilder.cs b/Svrnty.CQRS/Configuration/CqrsBuilder.cs index 9fe3b3f..98900b9 100644 --- a/Svrnty.CQRS/Configuration/CqrsBuilder.cs +++ b/Svrnty.CQRS/Configuration/CqrsBuilder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.Discovery; @@ -43,7 +44,7 @@ public class CqrsBuilder /// /// Adds a command handler to the CQRS pipeline /// - public CqrsBuilder AddCommand() + public CqrsBuilder AddCommand() where TCommand : class where TCommandHandler : class, ICommandHandler { @@ -54,7 +55,7 @@ public class CqrsBuilder /// /// Adds a command handler with result to the CQRS pipeline /// - public CqrsBuilder AddCommand() + public CqrsBuilder AddCommand() where TCommand : class where TCommandHandler : class, ICommandHandler { diff --git a/Svrnty.CQRS/Discovery/CommandDiscovery.cs b/Svrnty.CQRS/Discovery/CommandDiscovery.cs index 570605f..ea0af86 100644 --- a/Svrnty.CQRS/Discovery/CommandDiscovery.cs +++ b/Svrnty.CQRS/Discovery/CommandDiscovery.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Svrnty.CQRS.Abstractions.Discovery; diff --git a/Svrnty.CQRS/Discovery/QueryDiscovery.cs b/Svrnty.CQRS/Discovery/QueryDiscovery.cs index 8098e40..33a85dd 100644 --- a/Svrnty.CQRS/Discovery/QueryDiscovery.cs +++ b/Svrnty.CQRS/Discovery/QueryDiscovery.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Svrnty.CQRS.Abstractions.Discovery; diff --git a/Svrnty.Sample/Program.cs b/Svrnty.Sample/Program.cs index dda0ff4..ecd5bd3 100644 --- a/Svrnty.Sample/Program.cs +++ b/Svrnty.Sample/Program.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Svrnty.CQRS; +using Svrnty.CQRS.Abstractions; +using Svrnty.CQRS.DynamicQuery; using Svrnty.CQRS.FluentValidation; using Svrnty.CQRS.Grpc; -using Svrnty.Sample; using Svrnty.CQRS.MinimalApi; -using Svrnty.CQRS.DynamicQuery; -using Svrnty.CQRS.Abstractions; +using Svrnty.Sample; var builder = WebApplication.CreateBuilder(args); diff --git a/Svrnty.Sample/SimpleAsyncQueryableService.cs b/Svrnty.Sample/SimpleAsyncQueryableService.cs index 7828789..6528e2f 100644 --- a/Svrnty.Sample/SimpleAsyncQueryableService.cs +++ b/Svrnty.Sample/SimpleAsyncQueryableService.cs @@ -1,5 +1,5 @@ -using PoweredSoft.Data.Core; using System.Linq.Expressions; +using PoweredSoft.Data.Core; namespace Svrnty.Sample;