Skip to content
Original file line number Diff line number Diff line change
@@ -1,22 +1,76 @@
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace EntityFrameworkCore.Projectables.Generator;

public class MemberDeclarationSyntaxAndCompilationEqualityComparer : IEqualityComparer<(MemberDeclarationSyntax, Compilation)>
public class MemberDeclarationSyntaxAndCompilationEqualityComparer
: IEqualityComparer<((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation)>
{
public bool Equals((MemberDeclarationSyntax, Compilation) x, (MemberDeclarationSyntax, Compilation) y)
{
return GetMemberDeclarationSyntaxAndCompilationName(x.Item1, x.Item2) == GetMemberDeclarationSyntaxAndCompilationName(y.Item1, y.Item2);
}
private readonly static MemberDeclarationSyntaxEqualityComparer _memberComparer = new();

public int GetHashCode((MemberDeclarationSyntax, Compilation) obj)
public bool Equals(
((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) x,
((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) y)
{
return GetMemberDeclarationSyntaxAndCompilationName(obj.Item1, obj.Item2).GetHashCode();
var (xLeft, xCompilation) = x;
var (yLeft, yCompilation) = y;

// 1. Fast reference equality short-circuit
if (ReferenceEquals(xLeft.Member, yLeft.Member) &&
ReferenceEquals(xCompilation, yCompilation))
{
return true;
}

// 2. The syntax tree of the member's own file must be the same object
// (Roslyn reuses SyntaxTree instances for unchanged files, even when
// the Compilation object itself is new due to edits elsewhere)
// Single pointer comparison — very cheap.
if (!ReferenceEquals(xLeft.Member.SyntaxTree, yLeft.Member.SyntaxTree))
{
return false;
}

// 3. Attribute arguments (primitive record struct) — cheap value comparison
if (xLeft.Attribute != yLeft.Attribute)
{
return false;
}

// 4. Member text — string allocation, only reached when the SyntaxTree is shared
if (!_memberComparer.Equals(xLeft.Member, yLeft.Member))
{
return false;
}

// 5. Assembly-level references — most expensive (ImmutableArray enumeration)
return xCompilation.ExternalReferences.SequenceEqual(yCompilation.ExternalReferences);
}

public static string GetMemberDeclarationSyntaxAndCompilationName(MemberDeclarationSyntax memberDeclarationSyntax, Compilation compilation)
public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) obj)
{
return $"{compilation.AssemblyName}:{MemberDeclarationSyntaxEqualityComparer.GetMemberDeclarationSyntaxName(memberDeclarationSyntax)}";
var (left, compilation) = obj;
unchecked
{
var hash = 17;
hash = hash * 31 + _memberComparer.GetHashCode(left.Member);
hash = hash * 31 + RuntimeHelpers.GetHashCode(left.Member.SyntaxTree);
hash = hash * 31 + left.Attribute.GetHashCode();

// Incorporate compilation external references to align with Equals
var references = compilation.ExternalReferences;
var referencesHash = 17;
referencesHash = referencesHash * 31 + references.Length;
foreach (var reference in references)
{
referencesHash = referencesHash * 31 + RuntimeHelpers.GetHashCode(reference);
}
hash = hash * 31 + referencesHash;

return hash;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace EntityFrameworkCore.Projectables.Generator;
Expand All @@ -7,57 +7,43 @@ public class MemberDeclarationSyntaxEqualityComparer : IEqualityComparer<MemberD
{
public bool Equals(MemberDeclarationSyntax x, MemberDeclarationSyntax y)
{
return GetMemberDeclarationSyntaxName(x) == GetMemberDeclarationSyntaxName(y);
}

public int GetHashCode(MemberDeclarationSyntax obj)
{
return GetMemberDeclarationSyntaxName(obj).GetHashCode();
}

public static string GetMemberDeclarationSyntaxName(MemberDeclarationSyntax memberDeclaration)
{
var sb = new StringBuilder();

// Get the member name
if (memberDeclaration is MethodDeclarationSyntax methodDeclaration)
if (ReferenceEquals(x, y))
{
sb.Append(methodDeclaration.Identifier.Text);
return true;
}
else if (memberDeclaration is PropertyDeclarationSyntax propertyDeclaration)

// Must be in the same file — if the syntax tree changed, treat as different
// (Roslyn reuses SyntaxTree objects for unchanged files, so a new SyntaxTree
// means the file was edited, even if this specific node text looks the same)
if (!ReferenceEquals(x.SyntaxTree, y.SyntaxTree))
{
sb.Append(propertyDeclaration.Identifier.Text);
return false;
}
else if (memberDeclaration is FieldDeclarationSyntax fieldDeclaration)

// Pré-filtres O(1) avant IsEquivalentTo
if (x.RawKind != y.RawKind)
{
sb.Append(string.Join(", ", fieldDeclaration.Declaration.Variables.Select(v => v.Identifier.Text)));
return false;
}

// Traverse up the tree to get containing type names
var parent = memberDeclaration.Parent;
while (parent != null)
if (x.FullSpan.Length != y.FullSpan.Length)
{
switch (parent)
{
case NamespaceDeclarationSyntax namespaceDeclaration:
sb.Insert(0, namespaceDeclaration.Name + ".");
break;
case ClassDeclarationSyntax classDeclaration:
sb.Insert(0, classDeclaration.Identifier.Text + ".");
break;
case StructDeclarationSyntax structDeclaration:
sb.Insert(0, structDeclaration.Identifier.Text + ".");
break;
case InterfaceDeclarationSyntax interfaceDeclaration:
sb.Insert(0, interfaceDeclaration.Identifier.Text + ".");
break;
case EnumDeclarationSyntax enumDeclaration:
sb.Insert(0, enumDeclaration.Identifier.Text + ".");
break;
}
parent = parent.Parent;
return false;
}

return sb.ToString();
// Comparaison structurelle Roslyn — pas d'allocation de string
return x.IsEquivalentTo(y);
}

public int GetHashCode(MemberDeclarationSyntax obj)
{
unchecked
{
var hash = 17;
hash = hash * 31 + RuntimeHelpers.GetHashCode(obj.SyntaxTree);
hash = hash * 31 + obj.RawKind;
hash = hash * 31 + obj.FullSpan.Length;
return hash;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Microsoft.CodeAnalysis;

namespace EntityFrameworkCore.Projectables.Generator;

/// <summary>
/// Plain-data snapshot of the [Projectable] attribute arguments.
/// </summary>
public readonly record struct ProjectableAttributeData
{
public NullConditionalRewriteSupport NullConditionalRewriteSupport { get; }
public string? UseMemberBody { get; }
public bool ExpandEnumMethods { get; }
public bool AllowBlockBody { get; }

public ProjectableAttributeData(AttributeData attribute)
{
var nullConditionalRewriteSupport = default(NullConditionalRewriteSupport);
string? useMemberBody = null;
var expandEnumMethods = false;
var allowBlockBody = false;

foreach (var namedArgument in attribute.NamedArguments)
{
var key = namedArgument.Key;
var value = namedArgument.Value;
switch (key)
{
case "NullConditionalRewriteSupport":
if (value.Kind == TypedConstantKind.Enum &&
value.Value is not null &&
Enum.IsDefined(typeof(NullConditionalRewriteSupport), value.Value))
{
nullConditionalRewriteSupport = (NullConditionalRewriteSupport)value.Value;
}
break;
case "UseMemberBody":
if (value.Value is string s)
{
useMemberBody = s;
}
break;
case "ExpandEnumMethods":
if (value.Value is bool expand && expand)
{
expandEnumMethods = true;
}
break;
case "AllowBlockBody":
if (value.Value is bool allow && allow)
{
allowBlockBody = true;
}
break;
}
}

NullConditionalRewriteSupport = nullConditionalRewriteSupport;
UseMemberBody = useMemberBody;
ExpandEnumMethods = expandEnumMethods;
AllowBlockBody = allowBlockBody;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,50 +40,19 @@ static IEnumerable<string> GetNestedInClassPathForExtensionMember(ITypeSymbol ex
return [];
}

public static ProjectableDescriptor? GetDescriptor(Compilation compilation, MemberDeclarationSyntax member, SourceProductionContext context)
public static ProjectableDescriptor? GetDescriptor(
SemanticModel semanticModel,
MemberDeclarationSyntax member,
ISymbol memberSymbol,
ProjectableAttributeData projectableAttribute,
SourceProductionContext context,
Compilation? compilation = null)
{
var semanticModel = compilation.GetSemanticModel(member.SyntaxTree);
var memberSymbol = semanticModel.GetDeclaredSymbol(member);

if (memberSymbol is null)
{
return null;
}

var projectableAttributeTypeSymbol = compilation.GetTypeByMetadataName("EntityFrameworkCore.Projectables.ProjectableAttribute");

var projectableAttributeClass = memberSymbol.GetAttributes()
.Where(x => x.AttributeClass?.Name == "ProjectableAttribute")
.FirstOrDefault();

if (projectableAttributeClass is null || !SymbolEqualityComparer.Default.Equals(projectableAttributeClass.AttributeClass, projectableAttributeTypeSymbol))
{
return null;
}

var nullConditionalRewriteSupport = projectableAttributeClass.NamedArguments
.Where(x => x.Key == "NullConditionalRewriteSupport")
.Where(x => x.Value.Kind == TypedConstantKind.Enum)
.Select(x => x.Value.Value)
.Where(x => Enum.IsDefined(typeof(NullConditionalRewriteSupport), x))
.Cast<NullConditionalRewriteSupport>()
.FirstOrDefault();

var useMemberBody = projectableAttributeClass.NamedArguments
.Where(x => x.Key == "UseMemberBody")
.Select(x => x.Value.Value)
.OfType<string?>()
.FirstOrDefault();

var expandEnumMethods = projectableAttributeClass.NamedArguments
.Where(x => x.Key == "ExpandEnumMethods")
.Select(x => x.Value.Value is bool b && b)
.FirstOrDefault();

var allowBlockBody = projectableAttributeClass.NamedArguments
.Where(x => x.Key == "AllowBlockBody")
.Select(x => x.Value.Value is bool b && b)
.FirstOrDefault();
// Read directly from the struct fields — no more LINQ over NamedArguments
var nullConditionalRewriteSupport = projectableAttribute.NullConditionalRewriteSupport;
var useMemberBody = projectableAttribute.UseMemberBody;
var expandEnumMethods = projectableAttribute.ExpandEnumMethods;
var allowBlockBody = projectableAttribute.AllowBlockBody;

var memberBody = member;

Expand Down Expand Up @@ -460,6 +429,14 @@ x is IPropertySymbol xProperty &&
// Projectable constructors
else if (memberBody is ConstructorDeclarationSyntax constructorDeclarationSyntax)
{
// Constructor delegation requires a Compilation to get semantic models
// for other syntax trees (base/this ctor may be in a different file).
if (compilation is null)
{
// Should not happen in practice: the pipeline passes compilation for constructors.
return null;
}

var containingType = memberSymbol.ContainingType;
var fullTypeName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

Expand Down
Loading