diff --git a/src/BlueWest.MapTo/EfMethodsSyntaxReceiver.cs b/src/BlueWest.MapTo/EfMethodsSyntaxReceiver.cs index 30b048e..b3f253c 100644 --- a/src/BlueWest.MapTo/EfMethodsSyntaxReceiver.cs +++ b/src/BlueWest.MapTo/EfMethodsSyntaxReceiver.cs @@ -21,53 +21,33 @@ namespace MapTo internal class EfMethodsSyntaxReceiver : ISyntaxReceiver { - public List CandidateMembers { get; } = new(); + public List CandidateTypes { get; } = new(); + /// public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { - if (syntaxNode is not MemberDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax) + if (syntaxNode is not TypeDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax) { return; } - var syntaxAttributeState = EfMethodsAttributeType.Invalid; - var attributeSyntax = attributes .SelectMany(a => a.Attributes) - .FirstOrDefault(a => - { - syntaxAttributeState = IsValidAttributeType(a); - return syntaxAttributeState != EfMethodsAttributeType.Invalid; - }); - + .FirstOrDefault(a => a.Name is + IdentifierNameSyntax { Identifier: { ValueText: EfGeneratorAttributeSource.AttributeName } } + or + QualifiedNameSyntax + { + Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } }, + Right: IdentifierNameSyntax { Identifier: { ValueText: EfGeneratorAttributeSource.AttributeName } } + } + ); if (attributeSyntax is not null) { - CandidateMembers.Add(new CandidateMember(typeDeclarationSyntax, syntaxAttributeState)); + CandidateTypes.Add(typeDeclarationSyntax); } } - - private static EfMethodsAttributeType IsValidAttributeType(AttributeSyntax a) - { - return a.Name switch - { - IdentifierNameSyntax { Identifier: { ValueText: EfAddMethodsAttributeSource.AttributeName } } // For: [EfAddMethods] - or QualifiedNameSyntax // For: [MapTo.EfAddMethods] - { - Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } }, - Right: IdentifierNameSyntax { Identifier: { ValueText: EfAddMethodsAttributeSource.AttributeName } } - } => EfMethodsAttributeType.Add, - - IdentifierNameSyntax { Identifier: { ValueText: EfUpdateMethodsAttributeSource.AttributeName } } // For: [EfAddMethods] - or QualifiedNameSyntax // For: [MapTo.EfUpdateMethods] - { - Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } }, - Right: IdentifierNameSyntax { Identifier: { ValueText: EfUpdateMethodsAttributeSource.AttributeName } } - } => EfMethodsAttributeType.Update, - - _ => EfMethodsAttributeType.Invalid - }; - } } } \ No newline at end of file diff --git a/src/BlueWest.MapTo/Sources/EfMethods/EfGeneratorContext.cs b/src/BlueWest.MapTo/Sources/EfMethods/EfGeneratorContext.cs index 1ec556f..81ea626 100644 --- a/src/BlueWest.MapTo/Sources/EfMethods/EfGeneratorContext.cs +++ b/src/BlueWest.MapTo/Sources/EfMethods/EfGeneratorContext.cs @@ -30,7 +30,7 @@ namespace MapTo protected SourceGenerationOptions SourceGenerationOptions { get; } - protected MemberDeclarationSyntax MemberSyntax { get; } + protected TypeDeclarationSyntax TypeSyntax { get; } protected ImmutableArray Usings { get; private set; } @@ -44,14 +44,14 @@ namespace MapTo protected EfGeneratorContext( Compilation compilation, SourceGenerationOptions sourceGenerationOptions, - MemberDeclarationSyntax memberSyntax) + TypeDeclarationSyntax typeSyntax) { Compilation = compilation; _ignoredNamespaces = new(); Diagnostics = ImmutableArray.Empty; Usings = ImmutableArray.Create("System", Constants.RootNamespace); SourceGenerationOptions = sourceGenerationOptions; - MemberSyntax = memberSyntax; + TypeSyntax = typeSyntax; EfUpdateMethodsTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(EfUpdateMethodsAttributeSource.FullyQualifiedName); EfAddMethodsTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(EfAddMethodsAttributeSource.FullyQualifiedName); AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis"); @@ -60,11 +60,11 @@ namespace MapTo public static EfGeneratorContext Create( Compilation compilation, SourceGenerationOptions sourceGenerationOptions, - MemberDeclarationSyntax typeSyntax) + TypeDeclarationSyntax typeSyntax) { EfGeneratorContext context = typeSyntax switch { - PropertyDeclarationSyntax => new EfGeneratorContext(compilation, sourceGenerationOptions, typeSyntax), + ClassDeclarationSyntax => new EfGeneratorContext(compilation, sourceGenerationOptions, typeSyntax), _ => throw new ArgumentOutOfRangeException() }; @@ -86,7 +86,7 @@ namespace MapTo protected void AddUsingIfRequired(bool condition, string? ns) { - if (ns is not null && condition && ns != MemberSyntax.GetNamespace() && !Usings.Contains(ns)) + if (ns is not null && condition && ns != TypeSyntax.GetNamespace() && !Usings.Contains(ns)) { Usings = Usings.Add(ns); } @@ -238,13 +238,28 @@ namespace MapTo namedTypeSymbolResult = null; return false; } + + private ImmutableArray GetAllowedMemberSyntaxes(string attributeName) + { + ClassDeclarationSyntax classDeclarationSyntax = TypeSyntax as ClassDeclarationSyntax; + + var syntaxes = classDeclarationSyntax.DescendantNodes() + .OfType() + .Where(x => x + .DescendantNodes() + .OfType().Any(syntax => + syntax.Name.ToString() == attributeName)) + .ToImmutableArray(); + + return syntaxes; + } private EfMethodsModel? CreateMappingModel() { - var semanticModel = Compilation.GetSemanticModel(MemberSyntax.SyntaxTree); + var semanticModel = Compilation.GetSemanticModel(TypeSyntax.SyntaxTree); // get containing class type information - ClassDeclarationSyntax classDeclarationSyntax = MemberSyntax.Parent as ClassDeclarationSyntax; + ClassDeclarationSyntax classDeclarationSyntax = TypeSyntax as ClassDeclarationSyntax; // context name var dbContextName = classDeclarationSyntax.Identifier.ValueText; @@ -252,22 +267,11 @@ namespace MapTo var contextNamespace = namespaceDeclaration.Name.ToString(); var contextTypeFullName = $"{contextNamespace}.{dbContextName}"; - - var addAttributeSymbols = classDeclarationSyntax.DescendantNodes() - .OfType() - .Where(x => x.DescendantNodes().OfType().Any( - syntax => syntax.Name.ToString() == EfUpdateMethodsAttributeSource.AttributeName - )) - .ToImmutableArray(); - - var updateAttributesMembers = classDeclarationSyntax.DescendantNodes() - .OfType() - .Where(x => x.DescendantNodes().OfType().Any( - syntax => syntax.Name.ToString() == EfUpdateMethodsAttributeSource.AttributeName - )) - .ToImmutableArray(); + var addAttributeSymbols = GetAllowedMemberSyntaxes(EfAddMethodsAttributeSource.AttributeName); + + var updateAttributesMembers = GetAllowedMemberSyntaxes(EfUpdateMethodsAttributeSource.AttributeName); @@ -275,12 +279,8 @@ namespace MapTo foreach (var uProperty in updateAttributesMembers) { - var entityTypeData = GetEntityTypeData(uProperty, semanticModel); - string entityIdentifierName = entityTypeData.Name; - string entityFullName = entityTypeData.ToDisplayString(); - var propertyNamex = (uProperty as PropertyDeclarationSyntax ).Identifier.ValueText; - var entityDataModel = new EfEntityDataModel(propertyNamex, entityFullName, entityIdentifierName); - var newUpdateModel = ExtractEfUpdateMethodsModel(semanticModel, uProperty, entityDataModel); + + var newUpdateModel = ExtractEfUpdateMethodsModel(semanticModel, uProperty); methodsModels.Add(newUpdateModel); } @@ -297,13 +297,7 @@ namespace MapTo } - SpinWait.SpinUntil(() => Debugger.IsAttached); - - var classType = semanticModel.GetTypeInfo(classDeclarationSyntax); - if (classType.Type != null) - { - var addAttributes = GetSymbolsFromAttribute(classType.Type, EfAddMethodsTypeSymbol); - } + //SpinWait.SpinUntil(() => Debugger.IsAttached); return new EfMethodsModel( SourceGenerationOptions, @@ -341,16 +335,22 @@ namespace MapTo returnTypeIdentifierName); } - private EfUpdateMethodsModel ExtractEfUpdateMethodsModel(SemanticModel semanticModel, MemberDeclarationSyntax memberDeclarationSyntax, EfEntityDataModel efEntityDataModel) + private EfUpdateMethodsModel ExtractEfUpdateMethodsModel(SemanticModel semanticModel, MemberDeclarationSyntax uProperty) { - var efAddAttributeTypeSymbols = - GetEntityTypeSymbol(MemberSyntax, EfUpdateMethodsAttributeSource.AttributeName, semanticModel); - - var keyPropertyName = ExtractNameOfMemberName(memberDeclarationSyntax, EfUpdateMethodsAttributeSource.AttributeName, semanticModel); + var entityTypeData = GetEntityTypeData(uProperty, semanticModel); + string entityIdentifierName = entityTypeData.Name; + string entityFullName = entityTypeData.ToDisplayString(); + var propertyNamex = (uProperty as PropertyDeclarationSyntax ).Identifier.ValueText; + var entityDataModel = new EfEntityDataModel(propertyNamex, entityFullName, entityIdentifierName); - var keyMemberType = FindFirstTypeInfoOfMemberExpressionSyntax(semanticModel, MemberSyntax); + var efAddAttributeTypeSymbols = + GetEntityTypeSymbol(uProperty, EfUpdateMethodsAttributeSource.AttributeName, semanticModel); - var secondTypeInfo = FindSecondTypeInfoOfMemberExpressionSyntax(semanticModel, MemberSyntax); + var keyPropertyName = ExtractNameOfMemberName(uProperty, EfUpdateMethodsAttributeSource.AttributeName, semanticModel); + + var keyMemberType = FindFirstTypeInfoOfMemberExpressionSyntax(semanticModel, TypeSyntax); + + var secondTypeInfo = FindSecondTypeInfoOfMemberExpressionSyntax(semanticModel, TypeSyntax); @@ -361,17 +361,17 @@ namespace MapTo // Try grabbing string literal if there's no nameof in it if (keyPropertyName == string.Empty) { - keyPropertyName = GetEntityStringLiteralSymbol(MemberSyntax, EfUpdateMethodsAttributeSource.AttributeName, semanticModel) + keyPropertyName = GetEntityStringLiteralSymbol(TypeSyntax, EfUpdateMethodsAttributeSource.AttributeName, semanticModel) .FirstOrDefault()? .Token.ValueText ?? "Id"; } if (efAddAttributeTypeSymbols == null) return null; - string updateTypeIdentifierName = efEntityDataModel.EntityTypeIdentifierName; - string updateTypeFullName = efEntityDataModel.EntityTypeFullName; - string returnTypeIdentifierName = efEntityDataModel.EntityTypeIdentifierName; - string returnTypeFullName = efEntityDataModel.EntityTypeFullName; + string updateTypeIdentifierName = entityDataModel.EntityTypeIdentifierName; + string updateTypeFullName = entityDataModel.EntityTypeFullName; + string returnTypeIdentifierName = entityDataModel.EntityTypeIdentifierName; + string returnTypeFullName = entityDataModel.EntityTypeFullName; if (efAddAttributeTypeSymbols.Length > 0) { @@ -387,7 +387,7 @@ namespace MapTo } - return new EfUpdateMethodsModel(efEntityDataModel, updateTypeFullName, updateTypeIdentifierName, returnTypeFullName, + return new EfUpdateMethodsModel(entityDataModel, updateTypeFullName, updateTypeIdentifierName, returnTypeFullName, returnTypeIdentifierName, keyPropertyName, keyMemberFullName); } @@ -419,7 +419,7 @@ namespace MapTo return null; } - var containingNamespace = MemberSyntax.GetNamespace(); + var containingNamespace = TypeSyntax.GetNamespace(); var symbolNamespace = symbol.ContainingNamespace.ToDisplayString(); return containingNamespace != symbolNamespace && _ignoredNamespaces.Contains(symbol.ContainingNamespace.ToDisplayParts().First()) ? symbol.ToDisplayString() diff --git a/src/BlueWest.MapTo/Sources/EfMethods/EfGeneratorExtensionMethods.cs b/src/BlueWest.MapTo/Sources/EfMethods/EfGeneratorExtensionMethods.cs deleted file mode 100644 index 32f25ff..0000000 --- a/src/BlueWest.MapTo/Sources/EfMethods/EfGeneratorExtensionMethods.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MapTo.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace MapTo.Sources.EfMethods -{ - public static class EfGeneratorExtensionMethods - { - public static void InitializeEfGenerator(this ISourceGenerator sourceGenerator, GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new EfMethodsSyntaxReceiver()); - } - - /// - public static void ExecuteEfGenerator(this ISourceGenerator sourceGenerator, GeneratorExecutionContext context) - { - try - { - var options = SourceGenerationOptions.From(context); - - var compilation = context.Compilation - .AddSource(ref context, EfAddMethodsAttributeSource.Generate(options)) - .AddSource(ref context, EfUpdateMethodsAttributeSource.Generate(options)); - - - if (context.SyntaxReceiver is EfMethodsSyntaxReceiver receiver && receiver.CandidateMembers.Any()) - { - AddGeneratedExtensions(context, compilation, receiver.CandidateMembers, options); - } - } - catch (Exception ex) - { - Console.WriteLine(ex); - throw; - } - } - - private static void AddGeneratedExtensions( GeneratorExecutionContext context, Compilation compilation, IEnumerable candidateMembers, SourceGenerationOptions options) - { - //SpinWait.SpinUntil(() => Debugger.IsAttached); - - foreach (var candidateMember in candidateMembers) - { - string addSourceTemplate = context.AdditionalFiles - .FirstOrDefault(x => x.Path.Contains("AddToEntityTemplate"))? - .GetText()? - .ToString() ?? string.Empty; - - string updateSourceTemplate = context.AdditionalFiles - .FirstOrDefault(x => x.Path.Contains("UpdateEntityTemplate"))? - .GetText()? - .ToString() ?? string.Empty; - - - var mappingContext = EfGeneratorContext.Create(compilation, options, candidateMember.MemberDeclarationSyntax); - - mappingContext.Diagnostics.ForEach(context.ReportDiagnostic); - - if (mappingContext.Model is null) - { - continue; - } - - var (source, hintName) = candidateMember.MemberDeclarationSyntax switch - { - PropertyDeclarationSyntax => EfMethodsSource.Generate(mappingContext.Model, addSourceTemplate, updateSourceTemplate), - _ => throw new ArgumentOutOfRangeException() - }; - - context.AddSource(hintName, source); - } - } - } -} \ No newline at end of file diff --git a/src/BlueWest.MapTo/Sources/EfMethods/EfGetOneByAttributeSource.cs b/src/BlueWest.MapTo/Sources/EfMethods/EfGetOneByAttributeSource.cs new file mode 100644 index 0000000..aa14c77 --- /dev/null +++ b/src/BlueWest.MapTo/Sources/EfMethods/EfGetOneByAttributeSource.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Text; +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal class EfGetOneByAttributeSource + { + internal const string AttributeName = "EfGetOneBy"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Generate Add methods for interacting with the entity") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]") + .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") + .WriteOpeningBracket(); + + builder + .WriteLine($"public {AttributeName}Attribute(string bynameof, Type returnType)") + .WriteOpeningBracket() + .WriteClosingBracket() + .WriteLine(); + + builder + .WriteClosingBracket() // class + .WriteClosingBracket(); // namespace + + return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); + } + } +} diff --git a/src/BlueWest.MapTo/Sources/EfMethods/EfMethodsGenerator.cs b/src/BlueWest.MapTo/Sources/EfMethods/EfMethodsGenerator.cs index 09a0a12..ebc005d 100644 --- a/src/BlueWest.MapTo/Sources/EfMethods/EfMethodsGenerator.cs +++ b/src/BlueWest.MapTo/Sources/EfMethods/EfMethodsGenerator.cs @@ -32,12 +32,13 @@ namespace MapTo var compilation = context.Compilation .AddSource(ref context, EfGeneratorAttributeSource.Generate(options)) .AddSource(ref context, EfAddMethodsAttributeSource.Generate(options)) - .AddSource(ref context, EfUpdateMethodsAttributeSource.Generate(options)); - + .AddSource(ref context, EfUpdateMethodsAttributeSource.Generate(options)) + .AddSource(ref context, EfGetOneByAttributeSource.Generate(options)); - if (context.SyntaxReceiver is EfMethodsSyntaxReceiver receiver && receiver.CandidateMembers.Any()) + + if (context.SyntaxReceiver is EfMethodsSyntaxReceiver receiver && receiver.CandidateTypes.Any()) { - AddGeneratedExtensions(context, compilation, receiver.CandidateMembers, options); + AddGeneratedExtensions(context, compilation, receiver.CandidateTypes, options); } } catch (Exception ex) @@ -47,7 +48,7 @@ namespace MapTo } } - private static void AddGeneratedExtensions(GeneratorExecutionContext context, Compilation compilation, IEnumerable candidateMembers, SourceGenerationOptions options) + private static void AddGeneratedExtensions(GeneratorExecutionContext context, Compilation compilation, IEnumerable candidateMembers, SourceGenerationOptions options) { foreach (var candidateMember in candidateMembers) @@ -63,7 +64,7 @@ namespace MapTo .ToString() ?? string.Empty; - var mappingContext = EfGeneratorContext.Create(compilation, options, candidateMember.MemberDeclarationSyntax); + var mappingContext = EfGeneratorContext.Create(compilation, options, candidateMember); mappingContext.Diagnostics.ForEach(context.ReportDiagnostic); @@ -72,9 +73,9 @@ namespace MapTo continue; } - var (source, hintName) = candidateMember.MemberDeclarationSyntax switch + var (source, hintName) = candidateMember switch { - PropertyDeclarationSyntax => EfMethodsSource.Generate(mappingContext.Model, addSourceTemplate, updateSourceTemplate), + ClassDeclarationSyntax => EfMethodsSource.Generate(mappingContext.Model, addSourceTemplate, updateSourceTemplate), _ => throw new ArgumentOutOfRangeException() }; diff --git a/src/BlueWest.MapTo/Sources/EfMethods/EfMethodsSource.cs b/src/BlueWest.MapTo/Sources/EfMethods/EfMethodsSource.cs index 365b085..54f4930 100644 --- a/src/BlueWest.MapTo/Sources/EfMethods/EfMethodsSource.cs +++ b/src/BlueWest.MapTo/Sources/EfMethods/EfMethodsSource.cs @@ -66,8 +66,10 @@ namespace MapTo.Sources entityTypeFullName, propertyName); } - - + + builder.WriteLine(); + + } builder @@ -78,13 +80,14 @@ namespace MapTo.Sources .WriteClosingBracket(); var generatedCode = builder.ToString(); - var hintName = $"{entityTypeName}Extensions.g.cs"; + var hintName = $"{model.ContextTypeName}Extensions.g.cs"; return new(generatedCode, hintName); } private static SourceBuilder ParseTemplate(this SourceBuilder builder, string finalTemplate) { string[] array = finalTemplate.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + foreach (var line in array) { @@ -123,6 +126,7 @@ namespace MapTo.Sources if (addSourceTemplate.IsEmpty()) { builder + .WriteComment("Generated body") .WriteLine(GeneratedFilesHeader) .WriteLine($"public static (bool, {model.ReturnTypeFullName}) Add{entityTypeName}(") .WriteLine($"this {contextFullName} dbContext,") @@ -172,6 +176,7 @@ namespace MapTo.Sources if (updateSourceTemplate.IsEmpty()) { builder + .WriteComment("Generated body") .WriteLine($"public static (bool, {returnTypeFullName}) Update{entityTypeName}(") .WriteLine($"this {contextFullName} dbContext,") .WriteLine($"{updateTypeFullName} {updateVarName},")