Support Ef Add method

This commit is contained in:
Wvader 2022-08-24 17:56:44 +01:00
parent 1400bd08e0
commit 340a89bbd2
11 changed files with 509 additions and 5 deletions

View File

@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
#pragma warning disable CS8602
namespace MapTo
{
internal class EfGeneratorContext
{
private readonly List<SymbolDisplayPart> _ignoredNamespaces;
public EfGeneratorContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, MemberDeclarationSyntax memberSyntax)
{
Compilation = compilation;
_ignoredNamespaces = new();
Diagnostics = ImmutableArray<Diagnostic>.Empty;
Usings = ImmutableArray.Create("System", Constants.RootNamespace);
SourceGenerationOptions = sourceGenerationOptions;
MemberSyntax = memberSyntax;
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
}
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
public EfMethodsModel? Model { get; private set; }
protected Compilation Compilation { get; }
protected INamedTypeSymbol MappingContextTypeSymbol { get; }
protected INamedTypeSymbol EfAddMethodsAttributeTypeSymbol { get; }
protected SourceGenerationOptions SourceGenerationOptions { get; }
protected MemberDeclarationSyntax MemberSyntax { get; }
protected ImmutableArray<string> Usings { get; private set; }
public static EfGeneratorContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, MemberDeclarationSyntax typeSyntax)
{
EfGeneratorContext context = typeSyntax switch
{
PropertyDeclarationSyntax => new EfGeneratorContext(compilation, sourceGenerationOptions, typeSyntax),
_ => throw new ArgumentOutOfRangeException()
};
context.Model = context.CreateMappingModel();
return context;
}
protected void AddDiagnostic(Diagnostic diagnostic)
{
Diagnostics = Diagnostics.Add(diagnostic);
}
protected void AddUsingIfRequired(ISymbol? namedTypeSymbol) =>
AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace);
protected void AddUsingIfRequired(bool condition, INamespaceSymbol? ns) =>
AddUsingIfRequired(condition && ns is not null && !_ignoredNamespaces.Contains(ns.ToDisplayParts().First()), ns?.ToDisplayString());
protected void AddUsingIfRequired(bool condition, string? ns)
{
if (ns is not null && condition && ns != MemberSyntax.GetNamespace() && !Usings.Contains(ns))
{
Usings = Usings.Add(ns);
}
}
protected ImmutableArray<INamedTypeSymbol> GetEntityTypeSymbol(MemberDeclarationSyntax memberDeclarationSyntax, SemanticModel? semanticModel = null)
{
var attributeData = memberDeclarationSyntax.GetAttribute(EfAddMethodsAttributeSource.AttributeName);
var sourceSymbol = GetEntityTypeSymbols(attributeData, semanticModel);
return sourceSymbol;
}
// we need two possible InamedTypeSymbol
protected ImmutableArray<INamedTypeSymbol> GetEntityTypeSymbols(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null)
{
if (attributeSyntax is null)
{
return new ImmutableArray<INamedTypeSymbol>(){};
}
semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
var descendentNodes = attributeSyntax
.DescendantNodes();
var sourceTypeExpressionSyntax = descendentNodes
.OfType<TypeOfExpressionSyntax>()
.ToImmutableArray();
// cast
var resultList = new List<INamedTypeSymbol>();
for (int i = 0; i < sourceTypeExpressionSyntax.Length; i++)
{
var sourceTypeExpression = sourceTypeExpressionSyntax[i];
if (semanticModel.GetTypeInfo(sourceTypeExpression.Type).Type is INamedTypeSymbol namedTypeSymbol)
{
resultList.Add(namedTypeSymbol);
}
}
return resultList.ToImmutableArray();
}
protected bool IsEnumerable(ISymbol property, out INamedTypeSymbol? namedTypeSymbolResult)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
namedTypeSymbolResult = null;
return false;
}
if (
propertyType is INamedTypeSymbol namedTypeSymbol &&
!propertyType.IsPrimitiveType() &&
(Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i))))
{
namedTypeSymbolResult = namedTypeSymbol;
return true;
}
namedTypeSymbolResult = null;
return false;
}
private EfMethodsModel? CreateMappingModel()
{
var semanticModel = Compilation.GetSemanticModel(MemberSyntax.SyntaxTree);
/*if (semanticModel.GetDeclaredSymbol(MemberSyntax) is not INamedTypeSymbol typeSymbol)
{
AddDiagnostic(DiagnosticsFactory.TypeNotFoundError(MemberSyntax.GetLocation(), MemberSyntax.Identifier.ValueText));
return null;
}*/
var attributeTypeSymbols = GetEntityTypeSymbol(MemberSyntax, semanticModel);
// Main Entity
var entityData = GetEntityTypeData(MemberSyntax, semanticModel);
string entityTypeName = entityData.Name, createTypeIdentifierName = entityData.Name, readTypeIdentifierName = entityData.Name;
string entityTypeFullName = entityData.ToDisplayString(), createTypeFullName = entityData.ToDisplayString(), readTypeFullName = entityData.ToDisplayString();
if (attributeTypeSymbols.Length > 0)
{
// Create DTO
createTypeIdentifierName = attributeTypeSymbols[0].Name;
createTypeFullName = attributeTypeSymbols[0].ToDisplayString();
}
if (attributeTypeSymbols.Length > 1)
{
// Read DTO
readTypeIdentifierName = attributeTypeSymbols[1].Name;
readTypeFullName = attributeTypeSymbols[1].ToDisplayString();
}
// get containing class type information
ClassDeclarationSyntax classDeclarationSyntax = MemberSyntax.Parent as ClassDeclarationSyntax;
// context name
var dbContextName = classDeclarationSyntax.Identifier.ValueText;
var namespaceDeclaration = classDeclarationSyntax.Parent as NamespaceDeclarationSyntax;
var namespaceFullName = namespaceDeclaration.Name.ToString();
var contextTypeFullName = $"{namespaceFullName}.{dbContextName}";
var propertyName = entityTypeName;
if (MemberSyntax is PropertyDeclarationSyntax propertyDeclarationSyntax)
{
propertyName = propertyDeclarationSyntax.Identifier.ValueText;
}
if (MemberSyntax is FieldDeclarationSyntax fieldDeclaration)
{
// TODO: Test
propertyName = fieldDeclaration.ToString();
}
if (attributeTypeSymbols.IsDefaultOrEmpty)
{
AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(MemberSyntax.GetLocation()));
return null;
}
//var typeIdentifierName = MemberSyntax.GetIdentifierName();
//var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
return new EfMethodsModel(
SourceGenerationOptions,
namespaceFullName,
propertyName,
dbContextName,
contextTypeFullName,
entityTypeFullName,
entityTypeName,
createTypeFullName,
createTypeIdentifierName,
readTypeFullName,
readTypeIdentifierName,
Usings);
}
private static ITypeSymbol GetEntityTypeData(MemberDeclarationSyntax memberDeclarationSyntax, SemanticModel? semanticModel = null)
{
var member = (memberDeclarationSyntax as PropertyDeclarationSyntax).Type;
var genericSyntax = member as GenericNameSyntax;
var typeInfo = semanticModel.GetTypeInfo(genericSyntax);
var firstTypeArgument = (typeInfo.ConvertedType as INamedTypeSymbol).TypeArguments[0];
return firstTypeArgument;
}
private string? ToQualifiedDisplayName(ISymbol? symbol)
{
if (symbol is null)
{
return null;
}
var containingNamespace = MemberSyntax.GetNamespace();
var symbolNamespace = symbol.ContainingNamespace.ToDisplayString();
return containingNamespace != symbolNamespace && _ignoredNamespaces.Contains(symbol.ContainingNamespace.ToDisplayParts().First())
? symbol.ToDisplayString()
: symbol.Name;
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
/*/// <summary>
/// Base source code generator typed class.
/// </summary>
public class SourceCodeGenerator : ISourceGenerator
{
public virtual void Initialize(GeneratorInitializationContext context) {}
public virtual void Execute(GeneratorExecutionContext context) {}
}*/
/// <summary>
/// Ef methods source generator
/// </summary>
[Generator]
public class EfMethodsGenerator: ISourceGenerator
{
/// <inheritdoc />
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new EfMethodsSyntaxReceiver());
}
/// <inheritdoc />
public void Execute(GeneratorExecutionContext context)
{
try
{
var options = SourceGenerationOptions.From(context);
var compilation = context.Compilation
.AddSource(ref context, EfAddMethodsAttributeSource.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<MemberDeclarationSyntax> candidateTypes, SourceGenerationOptions options)
{
//SpinWait.SpinUntil(() => Debugger.IsAttached);
foreach (var memberDeclarationSyntax in candidateTypes)
{
var mappingContext = EfGeneratorContext.Create(compilation, options, memberDeclarationSyntax);
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic);
if (mappingContext.Model is null)
{
continue;
}
var (source, hintName) = memberDeclarationSyntax switch
{
PropertyDeclarationSyntax => EfMethodsSource.Generate(mappingContext.Model),
_ => throw new ArgumentOutOfRangeException()
};
context.AddSource(hintName, source);
}
}
}
}

View File

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class EfMethodsSyntaxReceiver : ISyntaxReceiver
{
public List<MemberDeclarationSyntax> CandidateMembers { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is not MemberDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax)
{
return;
}
var attributeSyntax = attributes
.SelectMany(a => a.Attributes)
.FirstOrDefault(a => a.Name is
IdentifierNameSyntax { Identifier: { ValueText: EfAddMethodsAttributeSource.AttributeName } } // For: [EfAddMethods]
or
QualifiedNameSyntax // For: [MapTo.EfAddMethods]
{
Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } },
Right: IdentifierNameSyntax { Identifier: { ValueText: EfAddMethodsAttributeSource.AttributeName } }
}
);
if (attributeSyntax is not null)
{
CandidateMembers.Add(typeDeclarationSyntax);
}
}
}
}

View File

@ -0,0 +1,14 @@
using MapTo.Sources;
namespace MapTo.Extensions
{
internal static class EfGeneratorExtensions
{
internal static SourceCode GenerateEfMethods(this EfMethodsModel model)
{
var builder = new SourceBuilder();
return new SourceCode(builder.ToString(), $"{model.Namespace}.{model.EntityTypeIdentifierName}.g.cs");
}
}
}

View File

@ -31,7 +31,7 @@ namespace MapTo.Extensions
public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text;
public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax typeDeclarationSyntax, string attributeName)
public static AttributeSyntax? GetAttribute(this MemberDeclarationSyntax typeDeclarationSyntax, string attributeName)
{
var attributeLists = typeDeclarationSyntax.AttributeLists;
var selection = attributeLists
@ -51,7 +51,7 @@ namespace MapTo.Extensions
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes(attributeSymbol).FirstOrDefault();
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax
public static string? GetNamespace(this MemberDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax
.Ancestors()
.OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault()

View File

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
@ -17,7 +19,9 @@ namespace MapTo
/// <inheritdoc />
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver());
}
/// <inheritdoc />
@ -61,6 +65,7 @@ namespace MapTo
{
continue;
}
var (source, hintName) = typeDeclarationSyntax switch
{

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
@ -71,7 +73,6 @@ namespace MapTo
public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
//SpinWait.SpinUntil(() => Debugger.IsAttached);
MappingContext context = typeSyntax switch
{

View File

@ -68,6 +68,26 @@ namespace MapTo
ImmutableArray<string> Usings
);
internal record EfMethodsModel(
SourceGenerationOptions Options,
string Namespace,
string PropertyName,
string ContextTypeName,
string ContestFullType,
string EntityTypeFullName,
string EntityTypeIdentifierName,
string CreateTypeFullName,
string CreateTypeIdentifierName,
string ReadTypeFullName,
string ReadTypeIdentifierName,
ImmutableArray<string> Usings
);
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,

View File

@ -0,0 +1,47 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class EfAddMethodsAttributeSource
{
internal const string AttributeName = "EfAddMethods";
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("/// <summary>")
.WriteLine("/// Generate Add methods for interacting with the entity")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket();
builder
.WriteLine($"public {AttributeName}Attribute(Type createDto = null, Type returnType = null)")
.WriteOpeningBracket()
.WriteClosingBracket()
.WriteLine();
builder
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class EfMethodsSource
{
internal static SourceCode Generate(EfMethodsModel model)
{
const bool writeDebugInfo = false;
List<string> constructorHeaders = new List<string>();
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteUsings(model.Usings)
.WriteLine("using Microsoft.EntityFrameworkCore;")
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"public static partial class {model.EntityTypeIdentifierName}Extensions")
.WriteOpeningBracket()
.WriteLine()
.WriteLine($"public static (bool, {model.ReadTypeFullName}) Add{model.EntityTypeIdentifierName}(")
.WriteLine($"this {model.ContestFullType} dbContext,")
.WriteLine($"{model.CreateTypeFullName} {model.EntityTypeIdentifierName.ToLower()}toCreate)")
.WriteOpeningBracket()
.WriteLine($"var new{model.EntityTypeIdentifierName} = new {model.EntityTypeFullName}({model.EntityTypeIdentifierName.ToLower()}toCreate);")
.WriteLine($"dbContext.{model.PropertyName}.Add(new{model.EntityTypeIdentifierName});")
.WriteLine($"var success = dbContext.SaveChanges() >= 0;")
.WriteLine($"return (success, new {model.ReadTypeFullName}(new{model.EntityTypeIdentifierName}));")
.WriteClosingBracket();
builder
.WriteLine()
// End class declaration
.WriteClosingBracket()
.WriteLine()
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.EntityTypeIdentifierName}Extensions.g.cs");
}
}
}

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Tests.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@ -46,9 +48,9 @@ namespace MapTo.Tests.Infrastructure
// NB: fail tests when the injected program isn't valid _before_ running generators
compilation.GetDiagnostics().ShouldBeSuccessful();
}
var driver = CSharpGeneratorDriver.Create(
new[] { new MapToGenerator() },
new List<ISourceGenerator>() { new MapToGenerator(), new EfMethodsGenerator() },
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions),
parseOptions: new CSharpParseOptions(languageVersion)
);