I have a Modular Monolith along with Clean Architecture as follows:
Application projects depends on Domain projects, and endpoints are in Application projects while Entities are in Domain projects. Delete endpoints are almost identical, so I wanted to create delete endpoints with source generation. Here is the problem:
Entity definition/abstraction is in Common.Domain
project:
public abstract class AuditableEntity : IAuditableEntity
{
....
}
And all modules’ Domain projects are depends on this Common.Domain
and all Entities are derived from Common.Domain.Entities.AuditableEntity
. An example entity inside Inventory
module is:
public class Product : AggregateRoot<ProductId> // AggregateRoot is derived from AuditableEntity so Product is an AuditableEntity
{
....
}
So my DeleteEndpointSourceGenerator
is able to find entities derived from AuditableEntity
if it runs on Domain projects, but can not find any of them if it runs on Application projects(as I need it). How can I solve this?
Here is the DeleteEndpointSourceGenerator
:
[Generator]
public class DeleteEndpointSourceGenerator : ISourceGenerator
{
private const string AuditableEntityFullName = "Common.Domain.Entities.AuditableEntity";
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
{
context.ReportDiagnostic(Diagnostic.Create(_noSyntaxReceiverDescriptor, Location.None));
return;
}
var compilation = context.Compilation;
var auditableEntitySymbol = compilation.GetTypeByMetadataName(AuditableEntityFullName);
if (auditableEntitySymbol == null)
{
context.ReportDiagnostic(Diagnostic.Create(_auditableEntityNotFoundDescriptor, Location.None));
return;
}
foreach (var classDeclaration in receiver.CandidateClasses)
{
var model = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
if (model.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol)
{
continue;
}
if (!IsDerivedFrom(classSymbol, auditableEntitySymbol))
{
continue;
}
context.ReportDiagnostic(Diagnostic.Create(_classInheritsDescriptor, Location.None, classSymbol.Name, auditableEntitySymbol.Name));
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;
var source = GenerateDeleteEndpointCode(namespaceName, className);
context.AddSource($"{className}_DeleteEndpoint.g.cs", SourceText.From(source, Encoding.UTF8));
context.ReportDiagnostic(Diagnostic.Create(_endpointGeneratedDescriptor, Location.None, className));
}
}
private static bool IsDerivedFrom(INamedTypeSymbol? classSymbol, INamedTypeSymbol baseTypeSymbol)
{
var currentType = classSymbol;
while (currentType != null)
{
if (SymbolEqualityComparer.Default.Equals(currentType, baseTypeSymbol))
{
return true;
}
currentType = currentType.BaseType;
}
return false;
}
private static string GenerateDeleteEndpointCode(string namespaceName, string className)
{
var pluralClassName = Pluralize(className);
return $@"
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Mvc;
using Common.Domain.ResultMonad;
using Common.Application.Auth;
using Common.Application.Extensions;
using Common.Application.Persistence;
using {namespaceName};
using Microsoft.Extensions.DependencyInjection;
using Common.Application.ModelBinders;
using Ardalis.Specification;
namespace SourceGenerated.Application.{pluralClassName}.v1.Delete;
internal static class {className}DeleteEndpoint
{{
internal static void MapEndpoint(RouteGroupBuilder apiGroup)
{{
apiGroup
.MapDelete(""{{id}}"", Delete{className}Async)
.WithDescription(""Delete a {className}."")
.MustHavePermission(CustomActions.Delete, CustomResources.{pluralClassName})
.Produces(StatusCodes.Status204NoContent)
.TransformResultToNoContentResponse();
}}
private sealed class {className}ByIdSpec : SingleResultSpecification<{className}>
{{
public {className}ByIdSpec({className}Id id)
=> Query
.Where(p => p.Id == id);
}}
private static async Task<Result> Delete{className}Async(
[FromRoute, ModelBinder(typeof(StronglyTypedIdBinder<{className}Id>))] {className}Id id,
[FromServices] IRepository<{className}> repository,
[FromKeyedServices(nameof(Inventory))] IUnitOfWork unitOfWork,
CancellationToken cancellationToken)
=> await repository
.SingleOrDefaultAsResultAsync(new {className}ByIdSpec(id), cancellationToken)
.TapAsync(entity => repository.Delete(entity))
.TapAsync(async _ => await unitOfWork.SaveChangesAsync(cancellationToken));
}}
";
}
private static string Pluralize(string word)
{
if (word.EndsWith("y", StringComparison.OrdinalIgnoreCase))
{
return $"{word.Substring(0, word.Length - 1)}ies";
}
if (word.EndsWith("s", StringComparison.OrdinalIgnoreCase))
{
return $"{word}es";
}
return $"{word}s";
}
private sealed class SyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = [];
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax)
{
CandidateClasses.Add(classDeclarationSyntax);
}
}
}
private static readonly DiagnosticDescriptor _noSyntaxReceiverDescriptor = new(
id: "GEN001",
title: "Syntax Receiver Not Found",
messageFormat: "Syntax receiver not found",
category: "SourceGenerator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor _auditableEntityNotFoundDescriptor = new(
id: "GEN002",
title: "AuditableEntity Not Found",
messageFormat: "AuditableEntity not found",
category: "SourceGenerator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor _endpointGeneratedDescriptor = new(
id: "GEN003",
title: "Endpoint Generated",
messageFormat: "Generated delete endpoint for {0}",
category: "SourceGenerator",
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor _classInheritsDescriptor = new(
id: "GEN004",
title: "Class Inherits AuditableEntity",
messageFormat: "{0} is derived from {1}",
category: "SourceGenerator",
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true);
}
Expected behaviour is:
- Let’s say we run this source generator on
Inventory.Application
project, it should find theInventory.Domain
entities (Inventory.Application
has a reference toInventory.Domain
) then generate this endpoint. But it can’t. It only findsInventory.Domain
entities if it runs onInventory.Domain
.