diff --git a/src/BlueWest.MapTo/EfAddGeneratorContext.cs b/src/BlueWest.MapTo/EfAddGeneratorContext.cs index 2f333fc..b8ffceb 100644 --- a/src/BlueWest.MapTo/EfAddGeneratorContext.cs +++ b/src/BlueWest.MapTo/EfAddGeneratorContext.cs @@ -19,14 +19,12 @@ namespace MapTo protected EfAddGeneratorContext( Compilation compilation, SourceGenerationOptions sourceGenerationOptions, - MemberDeclarationSyntax memberSyntax, - string addSourceTemplate) + MemberDeclarationSyntax memberSyntax) { Compilation = compilation; _ignoredNamespaces = new(); Diagnostics = ImmutableArray.Empty; Usings = ImmutableArray.Create("System", Constants.RootNamespace); - SourceTemplate = addSourceTemplate; SourceGenerationOptions = sourceGenerationOptions; MemberSyntax = memberSyntax; @@ -34,8 +32,7 @@ namespace MapTo AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis"); } - - public string SourceTemplate { get; } + public ImmutableArray Diagnostics { get; private set; } @@ -54,12 +51,11 @@ namespace MapTo public static EfAddGeneratorContext Create( Compilation compilation, SourceGenerationOptions sourceGenerationOptions, - MemberDeclarationSyntax typeSyntax, - string addSourceTemplate) + MemberDeclarationSyntax typeSyntax) { EfAddGeneratorContext context = typeSyntax switch { - PropertyDeclarationSyntax => new EfAddGeneratorContext(compilation, sourceGenerationOptions, typeSyntax, addSourceTemplate), + PropertyDeclarationSyntax => new EfAddGeneratorContext(compilation, sourceGenerationOptions, typeSyntax), _ => throw new ArgumentOutOfRangeException() }; @@ -94,6 +90,13 @@ namespace MapTo return sourceSymbol; } + protected ImmutableArray GetEntityStringLiteralSymbol(MemberDeclarationSyntax memberDeclarationSyntax, string attributeName, SemanticModel? semanticModel = null) + { + var attributeData = memberDeclarationSyntax.GetAttribute(attributeName); + var sourceSymbol = GetEntityStringLiteralSymbol(attributeData, semanticModel); + return sourceSymbol; + } + protected ImmutableArray GetEntityTypeSymbols(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null) { if (attributeSyntax is null) @@ -123,6 +126,25 @@ namespace MapTo return resultList.ToImmutableArray(); } + protected ImmutableArray GetEntityStringLiteralSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null) + { + if (attributeSyntax is null) + { + return new ImmutableArray(){}; + } + + semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree); + var descendentNodes = attributeSyntax + .DescendantNodes(); + + var sourceTypeExpressionSyntax = descendentNodes + .OfType() + .ToImmutableArray(); + + + return sourceTypeExpressionSyntax; + } + protected bool IsEnumerable(ISymbol property, out INamedTypeSymbol? namedTypeSymbolResult) { @@ -194,6 +216,16 @@ namespace MapTo return null; } + var updateConfig = ExtractEfUpdateMethodsModel(semanticModel, entityTypeName, entityTypeFullName); + + if (updateConfig == null) + { + AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(MemberSyntax.GetLocation())); + return null; + } + + + return new EfMethodsModel( SourceGenerationOptions, contextNamespace, @@ -203,6 +235,7 @@ namespace MapTo entityTypeFullName, entityTypeName, addConfig, + updateConfig, Usings); } @@ -233,6 +266,48 @@ namespace MapTo return new EfAddMethodsModel(createTypeFullName, createTypeIdentifierName, returnTypeFullName, returnTypeIdentifierName); } + + private EfUpdateMethodsModel ExtractEfUpdateMethodsModel(SemanticModel semanticModel, string entityTypeName, string entityTypeFullName) + { + var efAddAttributeTypeSymbols = + GetEntityTypeSymbol(MemberSyntax, EfUpdateMethodsAttributeSource.AttributeName, semanticModel); + + var keyPropertyName = GetEntityStringLiteralSymbol(MemberSyntax, EfUpdateMethodsAttributeSource.AttributeName, semanticModel) + .FirstOrDefault()? + .Token.ValueText ?? "Id"; + + if (efAddAttributeTypeSymbols == null) return null; + + string updateTypeIdentifierName = entityTypeName; + string updateTypeFullName = entityTypeFullName; + string returnTypeIdentifierName = entityTypeName; + string returnTypeFullName = entityTypeFullName; + string keyPropertyTypeFullName = "int"; + + if (efAddAttributeTypeSymbols.Length > 0) + { + updateTypeIdentifierName = efAddAttributeTypeSymbols[0].Name; + updateTypeFullName = efAddAttributeTypeSymbols[0].ToDisplayString(); + } + if (efAddAttributeTypeSymbols.Length > 0) + { + returnTypeIdentifierName = efAddAttributeTypeSymbols[1].Name; + returnTypeFullName = efAddAttributeTypeSymbols[1].ToDisplayString(); + } + + + + if (efAddAttributeTypeSymbols.Length > 2) + { + keyPropertyTypeFullName = efAddAttributeTypeSymbols[2].ToDisplayString(); + } + + + return new EfUpdateMethodsModel(updateTypeFullName, updateTypeIdentifierName, returnTypeFullName, + returnTypeIdentifierName, keyPropertyName, keyPropertyTypeFullName); + } + + private static ITypeSymbol GetEntityTypeData(MemberDeclarationSyntax memberDeclarationSyntax, SemanticModel? semanticModel = null) { diff --git a/src/BlueWest.MapTo/EfAddMethodsGenerator.cs b/src/BlueWest.MapTo/EfAddMethodsGenerator.cs index f73093a..96ad530 100644 --- a/src/BlueWest.MapTo/EfAddMethodsGenerator.cs +++ b/src/BlueWest.MapTo/EfAddMethodsGenerator.cs @@ -39,7 +39,9 @@ namespace MapTo var options = SourceGenerationOptions.From(context); var compilation = context.Compilation - .AddSource(ref context, EfAddMethodsAttributeSource.Generate(options)); + .AddSource(ref context, EfAddMethodsAttributeSource.Generate(options)) + .AddSource(ref context, EfUpdateMethodsAttributeSource.Generate(options)); + if (context.SyntaxReceiver is EfMethodsSyntaxReceiver receiver && receiver.CandidateMembers.Any()) { @@ -59,17 +61,18 @@ namespace MapTo foreach (var candidateMember in candidateMembers) { - string addSourceTemplate = string.Empty; - - if (context.AdditionalFiles.Length > 0) - { - addSourceTemplate = context.AdditionalFiles - .FirstOrDefault()? + string addSourceTemplate = context.AdditionalFiles + .FirstOrDefault(x => x.Path.Contains("AddToEntityTemplate"))? .GetText()? .ToString() ?? string.Empty; - } - - var mappingContext = EfAddGeneratorContext.Create(compilation, options, candidateMember.MemberDeclarationSyntax, addSourceTemplate); + + string updateSourceTemplate = context.AdditionalFiles + .FirstOrDefault(x => x.Path.Contains("UpdateEntityTemplate"))? + .GetText()? + .ToString() ?? string.Empty; + + + var mappingContext = EfAddGeneratorContext.Create(compilation, options, candidateMember.MemberDeclarationSyntax); mappingContext.Diagnostics.ForEach(context.ReportDiagnostic); @@ -80,7 +83,7 @@ namespace MapTo var (source, hintName) = candidateMember.MemberDeclarationSyntax switch { - PropertyDeclarationSyntax => EfMethodsSource.Generate(mappingContext.Model, addSourceTemplate), + PropertyDeclarationSyntax => EfMethodsSource.Generate(mappingContext.Model, addSourceTemplate, updateSourceTemplate), _ => throw new ArgumentOutOfRangeException() }; diff --git a/src/BlueWest.MapTo/Extensions/RoslynExtensions.cs b/src/BlueWest.MapTo/Extensions/RoslynExtensions.cs index 036e330..86f7cbe 100644 --- a/src/BlueWest.MapTo/Extensions/RoslynExtensions.cs +++ b/src/BlueWest.MapTo/Extensions/RoslynExtensions.cs @@ -35,11 +35,10 @@ namespace MapTo.Extensions { var attributeLists = typeDeclarationSyntax.AttributeLists; var selection = attributeLists - .SelectMany(al => al.Attributes); - var result = selection - .FirstOrDefault(); + .SelectMany(al => al.Attributes) + .FirstOrDefault(x => x.Name.ToString().Contains(attributeName)); - return result; + return selection; } public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => diff --git a/src/BlueWest.MapTo/Models.cs b/src/BlueWest.MapTo/Models.cs index 5ed7002..d59fa79 100644 --- a/src/BlueWest.MapTo/Models.cs +++ b/src/BlueWest.MapTo/Models.cs @@ -79,10 +79,12 @@ namespace MapTo ) : IEfMethodsConfiguration; internal record EfUpdateMethodsModel( - string CreateTypeFullName, - string CreateTypeIdentifierName, + string UpdateTypeFullName, + string UpdateTypeIdentifierName, string ReturnTypeFullName, - string ReturnTypeIdentifierName + string ReturnTypeIdentifierName, + string keyPropertyName, + string keyFullTypeName ): IEfMethodsConfiguration; @@ -95,6 +97,7 @@ namespace MapTo string EntityTypeFullName, string EntityTypeIdentifierName, EfAddMethodsModel AddMethodsModel, + EfUpdateMethodsModel UpdateMethodsModel, ImmutableArray Usings ); diff --git a/src/BlueWest.MapTo/Sources/EfAddMethodsAttributeSource.cs b/src/BlueWest.MapTo/Sources/EfAddMethodsAttributeSource.cs index 9b66085..c75c46f 100644 --- a/src/BlueWest.MapTo/Sources/EfAddMethodsAttributeSource.cs +++ b/src/BlueWest.MapTo/Sources/EfAddMethodsAttributeSource.cs @@ -32,7 +32,7 @@ namespace MapTo.Sources .WriteOpeningBracket(); builder - .WriteLine($"public {AttributeName}Attribute(Type createDto = null, Type returnType = null)") + .WriteLine($"public {AttributeName}Attribute(Type createType = null, Type returnType = null)") .WriteOpeningBracket() .WriteClosingBracket() .WriteLine(); diff --git a/src/BlueWest.MapTo/Sources/EfMethodsSource.cs b/src/BlueWest.MapTo/Sources/EfMethodsSource.cs index a0dece0..c2a567c 100644 --- a/src/BlueWest.MapTo/Sources/EfMethodsSource.cs +++ b/src/BlueWest.MapTo/Sources/EfMethodsSource.cs @@ -9,7 +9,7 @@ namespace MapTo.Sources { internal static class EfMethodsSource { - internal static SourceCode Generate(EfMethodsModel model, string addSourceTemplate) + internal static SourceCode Generate(EfMethodsModel model, string addSourceTemplate, string updateSourceTemplate) { using var builder = new SourceBuilder(); @@ -21,6 +21,10 @@ namespace MapTo.Sources builder .WriteUsings(model.Usings) .WriteLine("using Microsoft.EntityFrameworkCore;") + .WriteLine("using System.Linq;") + .WriteLine("using System.Linq.Expressions;") + .WriteLine("using System.Threading.Tasks;") + .WriteLine() // Namespace declaration .WriteLine($"namespace {model.Namespace}") @@ -36,6 +40,12 @@ namespace MapTo.Sources contextFullName, entityTypeFullName, propertyName) + .EfAddUpdateEntityMethod(model, updateSourceTemplate, + entityTypeName, + contextFullName, + entityTypeFullName, + propertyName) + // End class declaration .WriteClosingBracket() .WriteLine() @@ -93,45 +103,53 @@ namespace MapTo.Sources return builder; } - private static SourceBuilder EfAddUpdateEntityMethod(this SourceBuilder builder, EfMethodsModel model, string addSourceTemplate, string entityTypeName, + private static SourceBuilder EfAddUpdateEntityMethod(this SourceBuilder builder, EfMethodsModel model, string updateSourceTemplate, string entityTypeName, string contextFullName, string entityTypeFullName, string propertyName) { - var returnTypeFullName = model.AddMethodsModel.ReturnTypeFullName; - var updateTypeFullName = model.AddMethodsModel.CreateTypeFullName; - var newEntityVarName = $"new{model.EntityTypeIdentifierName}"; - var toCreateVarName = $"{model.EntityTypeIdentifierName.ToCamelCase()}ToCreate"; - - if (!addSourceTemplate.IsEmpty()) + var returnTypeFullName = model.UpdateMethodsModel.ReturnTypeFullName; + var updateTypeFullName = model.UpdateMethodsModel.UpdateTypeFullName; + var updateVarName = $"{model.EntityTypeIdentifierName.ToCamelCase()}ToUpdate"; + var keyPropertyName = model.UpdateMethodsModel.keyPropertyName; + var keyTypeFullName = model.UpdateMethodsModel.keyFullTypeName; + var existingVarName = entityTypeName.ToCamelCase(); + var keyVarName = entityTypeName.ToCamelCase() + model.UpdateMethodsModel.keyPropertyName; + if (!updateSourceTemplate.IsEmpty()) { - var templateToSourceBuilder = new StringBuilder(addSourceTemplate); + var templateToSourceBuilder = new StringBuilder(updateSourceTemplate); templateToSourceBuilder .Replace("{entityTypeName}", entityTypeName) .Replace("{returnTypeFullName}", returnTypeFullName) .Replace("{updateTypeFullName}", updateTypeFullName) + .Replace("{updateVarName}", updateVarName) .Replace("{contextFullName}", contextFullName) - .Replace("{propertyName}", propertyName); - + .Replace("{propertyName}", propertyName) + .Replace("{keyTypeFullName}", keyTypeFullName) + .Replace("{keyPropertyName}", keyPropertyName) + .Replace("{keyVarName}", keyVarName) + .Replace("{existingEntityVarName}", existingVarName); builder .Write(templateToSourceBuilder.ToString()); } - if (addSourceTemplate.IsEmpty()) + if (updateSourceTemplate.IsEmpty()) { builder .WriteLine(GeneratedFilesHeader) - .WriteLine($"public static (bool, {model.AddMethodsModel.ReturnTypeFullName}) Add{entityTypeName}(") + .WriteLine($"public static (bool, {returnTypeFullName}) Update{entityTypeName}(") .WriteLine($"this {contextFullName} dbContext,") - .WriteLine($"{updateTypeFullName} {toCreateVarName})") + .WriteLine($"{updateTypeFullName} {updateVarName},") + .WriteLine($"{keyTypeFullName} {keyVarName})") .WriteOpeningBracket() - .WriteLine($"var {newEntityVarName} = new {model.EntityTypeFullName}({toCreateVarName});") - .WriteLine($"dbContext.{propertyName}.Add({newEntityVarName});") + .WriteLine($"var {existingVarName} = dbContext.{propertyName}.FirstOrDefault(x => x.{keyPropertyName} == {keyVarName});") + .WriteLine($"if ({existingVarName} == null) return (false, null);") .WriteLine($"var success = dbContext.SaveChanges() >= 0;") - .WriteLine($"return (success, new {returnTypeFullName}({newEntityVarName}));") + .WriteLine($"return (success, new {returnTypeFullName}({existingVarName}));") .WriteClosingBracket(); builder .WriteLine(); } + builder.Indent(); return builder; } diff --git a/src/BlueWest.MapTo/Sources/EfUpdateMethodsAttributeSource.cs b/src/BlueWest.MapTo/Sources/EfUpdateMethodsAttributeSource.cs index 36457ff..1468c7f 100644 --- a/src/BlueWest.MapTo/Sources/EfUpdateMethodsAttributeSource.cs +++ b/src/BlueWest.MapTo/Sources/EfUpdateMethodsAttributeSource.cs @@ -31,7 +31,7 @@ namespace MapTo.Sources .WriteOpeningBracket(); builder - .WriteLine($"public {AttributeName}Attribute(Type updateType = null, Type returnType = null, string keyPropertyMemberName, Type keyPropertyMemberType)") + .WriteLine($"public {AttributeName}Attribute(Type updateType, Type returnType, string keyPropertyMemberName, Type keyPropertyMemberType)") .WriteOpeningBracket() .WriteClosingBracket() .WriteLine(); diff --git a/src/BlueWest.MapTo/Sources/UpdateDataAttributeSource.cs b/src/BlueWest.MapTo/Sources/UpdateDataAttributeSource.cs deleted file mode 100644 index 9b04d4c..0000000 --- a/src/BlueWest.MapTo/Sources/UpdateDataAttributeSource.cs +++ /dev/null @@ -1,69 +0,0 @@ -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - /// - /// UpdateData Attribute Source - /// - public class UpdateDataAttributeSource - { - internal const string AttributeName = "UpdateData"; - 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("/// Generates CRUD functions to be used with the specified database context and entity.") - .WriteLine("/// "); - } - - builder - .WriteLine("[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]") - .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine($"/// Initializes a new instance of the class with the specified .") - .WriteLine("/// ") - .WriteLine("/// The type of to map from."); - } - - builder - .WriteLine($"public {AttributeName}Attribute(Type databaseContextType)") - .WriteOpeningBracket() - .WriteLine("DatabaseContextType = databaseContextType;") - .WriteClosingBracket() - .WriteLine(); - - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Gets the type to map from.") - .WriteLine("/// "); - } - - builder - .WriteLine("public Type DatabaseContextType { get; }") - .WriteClosingBracket() // class - .WriteClosingBracket(); // namespace - - return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); - } - } -} \ No newline at end of file