Compare commits

..

6 Commits

Author SHA1 Message Date
david.nguyen 03041721ca Preserve existing proto files instead of overwriting
Publish NuGets / build (release) Successful in 39s
If a proto file already exists (committed to repo), don't overwrite it
with a placeholder. This allows first-time builds to work correctly
when the proto file is already in the repository.

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 02:19:50 -05:00
david.nguyen 377977b080 Make GeneratedProtoFile class public for cross-assembly discovery
Publish NuGets / build (release) Successful in 34s
The generated proto file holder class was internal, preventing
reflection-based service registration from discovering it across
assembly boundaries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 00:31:34 -05:00
david.nguyen 20147bfec7 Add diagnostic logging when gRPC generated code not found
Publish NuGets / build (release) Successful in 37s
When AddGrpcFromConfiguration method is not found via reflection,
logs detailed diagnostics to help identify the root cause:
- Entry assembly name and total type count
- All Grpc-related types with their IsClass/IsSealed/IsPublic flags
- Whether the target method exists on each type
- ReflectionTypeLoadException details if type loading fails

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 16:10:18 -05:00
david.nguyen 18f81a28e8 Add authorization checks to gRPC service implementations
Publish NuGets / build (release) Successful in 44s
- Add ICommandAuthorizationService check to CommandServiceImpl
- Add IQueryAuthorizationService check to QueryServiceImpl
- Add IQueryAuthorizationService check to DynamicQueryServiceImpl
- Return Unauthenticated/PermissionDenied gRPC status codes
- Use global:: prefix for Grpc.Core namespace to avoid conflicts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 14:18:07 -05:00
5 changed files with 134 additions and 5 deletions
@@ -2291,6 +2291,7 @@ namespace Svrnty.CQRS.Grpc.Generators
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");
@@ -2321,6 +2322,17 @@ namespace Svrnty.CQRS.Grpc.Generators
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<ICommandAuthorizationService>();");
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)
@@ -2425,6 +2437,7 @@ namespace Svrnty.CQRS.Grpc.Generators
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");
@@ -2455,6 +2468,17 @@ namespace Svrnty.CQRS.Grpc.Generators
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<IQueryAuthorizationService>();");
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(" {");
@@ -2736,6 +2760,7 @@ namespace Svrnty.CQRS.Grpc.Generators
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();
@@ -2768,6 +2793,17 @@ namespace Svrnty.CQRS.Grpc.Generators
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<IQueryAuthorizationService>();");
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)
@@ -65,7 +65,7 @@ public class ProtoFileSourceGenerator : IIncrementalGenerator
/// <summary>
/// Contains the auto-generated Protocol Buffer definition
/// </summary>
internal static class GeneratedProtoFile
public static class GeneratedProtoFile
{
public const string FileName = "{{protoFileName}}";
@@ -39,6 +39,16 @@ public class WriteProtoFileTask : Task
[Required]
public string ProtoFileName { get; set; } = string.Empty;
/// <summary>
/// The root namespace of the project (optional, falls back to AssemblyName)
/// </summary>
public string RootNamespace { get; set; } = string.Empty;
/// <summary>
/// The assembly name of the project (used for proto namespace if RootNamespace not set)
/// </summary>
public string AssemblyName { get; set; } = string.Empty;
public override bool Execute()
{
try
@@ -57,6 +67,16 @@ public class WriteProtoFileTask : Task
"GeneratedProtoFile.g.cs"
);
// Check if proto file already exists (committed to repo or from previous build)
var existingProtoPath = Path.Combine(ProjectDirectory, OutputDirectory, ProtoFileName);
if (File.Exists(existingProtoPath) && !File.Exists(generatedFilePath))
{
Log.LogMessage(MessageImportance.High,
$"Svrnty.CQRS.Grpc: Using existing proto file at {existingProtoPath}. " +
"To regenerate, delete the file and build twice.");
return true;
}
if (!File.Exists(generatedFilePath))
{
Log.LogWarning(
@@ -65,13 +85,30 @@ public class WriteProtoFileTask : Task
// Write a minimal placeholder proto file so Grpc.Tools doesn't fail
// The real content will be generated on the next build
var placeholderProto = @"syntax = ""proto3"";
// Use project's namespace so GrpcGenerator can find the service base types
var projectNamespace = !string.IsNullOrEmpty(RootNamespace) ? RootNamespace
: !string.IsNullOrEmpty(AssemblyName) ? AssemblyName
: "Generated";
var grpcNamespace = $"{projectNamespace}.Grpc";
option csharp_namespace = ""Generated.Grpc"";
var placeholderProto = $@"syntax = ""proto3"";
option csharp_namespace = ""{grpcNamespace}"";
package cqrs;
// Placeholder proto file - will be regenerated on next build
// Placeholder proto file - will be regenerated on next build with actual services
// Using namespace: {grpcNamespace}
// Empty service definitions so Grpc.Tools generates base classes
service CommandService {{
}}
service QueryService {{
}}
service DynamicQueryService {{
}}
";
var placeholderOutputPath = Path.Combine(ProjectDirectory, OutputDirectory);
Directory.CreateDirectory(placeholderOutputPath);
@@ -32,6 +32,8 @@
ProjectDirectory="$(MSBuildProjectDirectory)"
IntermediateOutputPath="$(IntermediateOutputPath)"
OutputDirectory="$(ProtoOutputDirectory)"
ProtoFileName="$(GeneratedProtoFileName)" />
ProtoFileName="$(GeneratedProtoFileName)"
RootNamespace="$(RootNamespace)"
AssemblyName="$(AssemblyName)" />
</Target>
</Project>
+54
View File
@@ -33,6 +33,7 @@ public static class CqrsBuilderExtensions
{
Console.WriteLine("Warning: AddGrpcFromConfiguration not found. gRPC services were not registered.");
Console.WriteLine("Make sure your project has source generators enabled and references Svrnty.CQRS.Grpc.Generators.");
DiagnoseGeneratedCode();
}
// Register mapping callback for automatic endpoint mapping
@@ -49,6 +50,59 @@ public static class CqrsBuilderExtensions
return builder;
}
private static void DiagnoseGeneratedCode()
{
var entryAsm = Assembly.GetEntryAssembly();
if (entryAsm == null)
{
Console.WriteLine("Diagnostic: Entry assembly is null");
return;
}
Console.WriteLine($"Diagnostic: Entry assembly = {entryAsm.GetName().Name}");
try
{
var allTypes = entryAsm.GetTypes();
Console.WriteLine($"Diagnostic: Total types in entry assembly = {allTypes.Length}");
var grpcTypes = allTypes
.Where(t => t.FullName?.Contains("Grpc") == true)
.ToList();
if (grpcTypes.Any())
{
Console.WriteLine("Diagnostic: Found Grpc-related types:");
foreach (var t in grpcTypes)
{
Console.WriteLine($" - {t.FullName} (IsClass={t.IsClass}, IsSealed={t.IsSealed}, IsPublic={t.IsPublic})");
// Check for our target method
var method = t.GetMethod("AddGrpcFromConfiguration", BindingFlags.Static | BindingFlags.Public);
if (method != null)
Console.WriteLine($" -> HAS AddGrpcFromConfiguration method!");
}
}
else
{
Console.WriteLine("Diagnostic: No Grpc-related types found. Source generator did NOT run.");
}
}
catch (ReflectionTypeLoadException ex)
{
Console.WriteLine($"Diagnostic: ReflectionTypeLoadException - {ex.Message}");
foreach (var le in ex.LoaderExceptions)
{
if (le != null)
Console.WriteLine($" LoaderException: {le.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Diagnostic: Exception - {ex.GetType().Name}: {ex.Message}");
}
}
private static MethodInfo? FindExtensionMethod(string methodName, Type parameterType)
{
// Search through all loaded assemblies for the extension method