From 3836283c30cf747353d7fb123586d34341be7e42 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Fri, 27 Mar 2026 10:55:40 +0100 Subject: [PATCH 1/2] Support close generics on extension methods in the resolver --- .../ProjectionExpressionClassNameGenerator.cs | 28 +++++++++++++++---- .../ExtensionMembers/EntityExtensions.cs | 14 ++++++++++ ...enericReceiverType.DotNet10_0.verified.txt | 2 ++ .../ExtensionMembers/ExtensionMemberTests.cs | 12 ++++++++ .../ExtensionMembers/GenericWrapper.cs | 10 +++++++ ...onMemberOnGenericReceiverType.verified.txt | 17 +++++++++++ .../ExtensionMemberTests.cs | 28 +++++++++++++++++++ ...ectionExpressionClassNameGeneratorTests.cs | 27 ++++++++++++++++++ 8 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnGenericReceiverType.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/GenericWrapper.cs create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnGenericReceiverType.verified.txt diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs index ee30ce6..bfaab69 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs @@ -112,19 +112,37 @@ static string GenerateNameImpl(StringBuilder stringBuilder, string? namespaceNam } /// - /// Appends to , stripping the - /// global:: prefix and replacing every character that is invalid in a C# identifier - /// with '_' — all in a single pass with no intermediate string allocations. + /// Appends to , stripping every + /// global:: occurrence (leading and those inside generic type argument lists) + /// and replacing every character that is invalid in a C# identifier with '_'. + /// + /// The multi-occurrence stripping is necessary so that fully-qualified generic types + /// such as global::Foo.Wrapper<global::Foo.Entity> — produced by Roslyn's + /// FullyQualifiedFormat — yield the same sanitised name as the runtime resolver, + /// which never includes global::. + /// /// private static void AppendSanitizedTypeName(StringBuilder sb, string typeName) { const string GlobalPrefix = "global::"; - var start = typeName.StartsWith(GlobalPrefix, StringComparison.Ordinal) ? GlobalPrefix.Length : 0; + const int PrefixLength = 8; // "global::".Length - for (var i = start; i < typeName.Length; i++) + var i = 0; + while (i < typeName.Length) { + // Skip every "global::" occurrence — both the leading prefix and any that + // appear inside generic type argument lists (e.g. "Wrapper"). + if (typeName[i] == 'g' + && i + PrefixLength <= typeName.Length + && string.CompareOrdinal(typeName, i, GlobalPrefix, 0, PrefixLength) == 0) + { + i += PrefixLength; + continue; + } + var c = typeName[i]; sb.Append(IsInvalidIdentifierChar(c) ? '_' : c); + i++; } } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/EntityExtensions.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/EntityExtensions.cs index f0c8c21..8532369 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/EntityExtensions.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/EntityExtensions.cs @@ -25,6 +25,20 @@ public static class EntityExtensions } } + public static class GenericWrapperExtensions + { + extension(GenericWrapper w) + { + /// + /// Extension member on a closed generic receiver type — exercises the + /// code path where global:: appears inside generic type arguments + /// in the fully-qualified receiver type name. + /// + [Projectable] + public int DoubleId() => w.Id * 2; + } + } + public static class IntExtensions { extension(int i) diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnGenericReceiverType.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnGenericReceiverType.DotNet10_0.verified.txt new file mode 100644 index 0000000..a3cb54d --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnGenericReceiverType.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id] * 2 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.cs index cf776d1..3ceb119 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.cs @@ -37,6 +37,18 @@ public Task ExtensionMemberMethodWithParameterOnEntity() return Verifier.Verify(query.ToQueryString()); } + + [Fact] + public Task ExtensionMemberMethodOnGenericReceiverType() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => new GenericWrapper { Id = x.Id }) + .Select(x => x.DoubleId()); + + return Verifier.Verify(query.ToQueryString()); + } } } #endif diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/GenericWrapper.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/GenericWrapper.cs new file mode 100644 index 0000000..123bc3e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/GenericWrapper.cs @@ -0,0 +1,10 @@ +#if NET10_0_OR_GREATER +namespace EntityFrameworkCore.Projectables.FunctionalTests.ExtensionMembers +{ + public class GenericWrapper + { + public int Id { get; set; } + } +} +#endif + diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnGenericReceiverType.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnGenericReceiverType.verified.txt new file mode 100644 index 0000000..5945ac4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnGenericReceiverType.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_WrapperExtensions_DoubleId_P0_Foo_Wrapper_Foo_Entity_ + { + static global::System.Linq.Expressions.Expression, int>> Expression() + { + return (global::Foo.Wrapper @this) => @this.Value.Id * 2; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.cs index f5bdbae..7972f70 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.cs @@ -279,5 +279,33 @@ static class EntityExtensions { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + + [Fact] + public Task ExtensionMemberOnGenericReceiverType() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { public int Id { get; set; } } + class Wrapper { public T Value { get; set; } } + + static class WrapperExtensions { + extension(Wrapper w) { + [Projectable] + public int DoubleId() => w.Value.Id * 2; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } #endif } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs index 5eb59b5..cf0df91 100644 --- a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs @@ -23,6 +23,33 @@ public void GenerateName(string? namespaceName, string[] nestedTypeNames, string Assert.Equal(expected, result); } + /// + /// Verifies that global:: inside generic type argument lists is stripped, not + /// preserved as global__. This is critical for C# 14 extension members whose + /// receiver type is a closed generic (e.g. extension(Wrapper<Entity> w)): + /// the generator uses SymbolDisplayFormat.FullyQualifiedFormat + /// which emits global:: on every nested type, but the runtime resolver never + /// includes global:: — so both sides must agree on the sanitised name. + /// + [Theory] + [InlineData( + "global::Foo.Wrapper", + "ns_a_m_P0_Foo_Wrapper_Foo_Entity_")] + [InlineData( + "global::System.Collections.Generic.List", + "ns_a_m_P0_System_Collections_Generic_List_System_Int32_")] + [InlineData( + "global::Foo.Entity", + "ns_a_m_P0_Foo_Entity")] + public void GenerateName_WithGlobalPrefixInGenericArgs_StripsAllGlobalPrefixes( + string paramTypeName, string expected) + { + var result = ProjectionExpressionClassNameGenerator.GenerateName( + "ns", new[] { "a" }, "m", new[] { paramTypeName }); + + Assert.Equal(expected, result); + } + [Fact] public void GeneratedFullName() { From 894ff2e726c04ea5c194edb73d2c7539c978ccea Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Fri, 27 Mar 2026 11:19:25 +0100 Subject: [PATCH 2/2] Add support for open generics on extension methods --- .../ProjectableInterpreter.BodyProcessors.cs | 9 ++ .../ProjectableInterpreter.Helpers.cs | 84 +++++++++++++++++++ .../Interpretation/ProjectableInterpreter.cs | 31 +------ .../ExtensionMembers/EntityExtensions.cs | 27 ++++-- ...nericReceiverType.DotNet10_0.verified.txt} | 0 ...enericReceiverType.DotNet10_0.verified.txt | 2 + .../ExtensionMembers/ExtensionMemberTests.cs | 27 +++++- ...mberOnOpenGenericReceiverType.verified.txt | 17 ++++ ...ricReceiverTypeWithConstraint.verified.txt | 18 ++++ .../ExtensionMemberTests.cs | 54 ++++++++++++ 10 files changed, 232 insertions(+), 37 deletions(-) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/{ExtensionMemberTests.ExtensionMemberMethodOnGenericReceiverType.DotNet10_0.verified.txt => ExtensionMemberTests.ExtensionMemberMethodOnClosedGenericReceiverType.DotNet10_0.verified.txt} (100%) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnOpenGenericReceiverType.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnOpenGenericReceiverType.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnOpenGenericReceiverTypeWithConstraint.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs index c0367fc..407d22a 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs @@ -64,6 +64,15 @@ private static bool TryApplyMethodBody( ApplyParameterList(methodDeclarationSyntax.ParameterList, declarationSyntaxRewriter, descriptor); ApplyTypeParameters(methodDeclarationSyntax, declarationSyntaxRewriter, descriptor); + // For C# 14 generic extension blocks (e.g. extension(Wrapper w)), the block-level + // type parameter T is on the extension type, not on the method declaration syntax. + // ApplyTypeParameters() therefore finds nothing; promote the extension-block type + // parameters to method-level type parameters when no syntax-level ones were found. + if (descriptor.TypeParameterList is null) + { + ApplyExtensionBlockTypeParameters(memberSymbol, descriptor); + } + return true; } diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs index 5d7a207..d18c9a3 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs @@ -50,6 +50,90 @@ private static void ApplyTypeParameters( } } + /// + /// For C# 14 generic extension blocks (e.g. extension<T>(Wrapper<T> w)), + /// the block-level type parameter T is owned by the extension type, not by the + /// method declaration syntax. therefore finds no + /// TypeParameterList on the method and produces nothing. + /// + /// This helper promotes those extension-block type parameters to method-level type + /// parameters on so the generated + /// Expression<T>() factory method is correctly generic. + /// It is a no-op when the containing type is not a generic extension block. + /// + /// + private static void ApplyExtensionBlockTypeParameters( + ISymbol memberSymbol, + ProjectableDescriptor descriptor) + { + if (memberSymbol.ContainingType is not { IsExtension: true } extensionType + || extensionType.TypeParameters.IsDefaultOrEmpty) + { + return; + } + + descriptor.TypeParameterList = SyntaxFactory.TypeParameterList(); + + foreach (var tp in extensionType.TypeParameters) + { + descriptor.TypeParameterList = descriptor.TypeParameterList.AddParameters( + SyntaxFactory.TypeParameter(tp.Name)); + + // Build the constraint clause when any constraint is present. + var hasAnyConstraint = + tp.HasReferenceTypeConstraint + || tp.HasValueTypeConstraint + || tp.HasNotNullConstraint + || !tp.ConstraintTypes.IsDefaultOrEmpty + || tp.HasConstructorConstraint; + + if (!hasAnyConstraint) + { + continue; + } + + descriptor.ConstraintClauses ??= SyntaxFactory.List(); + descriptor.ConstraintClauses = descriptor.ConstraintClauses.Value.Add(BuildConstraintClause(tp)); + } + } + + /// + /// Builds a for + /// by collecting all of its constraints in canonical order: + /// class / struct / notnull, explicit type constraints, then new(). + /// + private static TypeParameterConstraintClauseSyntax BuildConstraintClause(ITypeParameterSymbol tp) + { + var constraints = new List(); + + if (tp.HasReferenceTypeConstraint) + { + constraints.Add(MakeTypeConstraint("class")); + } + + if (tp.HasValueTypeConstraint) + { + constraints.Add(MakeTypeConstraint("struct")); + } + + if (tp.HasNotNullConstraint) + { + constraints.Add(MakeTypeConstraint("notnull")); + } + + constraints.AddRange(tp.ConstraintTypes + .Select(c => MakeTypeConstraint(c.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))); + + if (tp.HasConstructorConstraint) + { + constraints.Add(MakeTypeConstraint("new()")); + } + + return SyntaxFactory.TypeParameterConstraintClause( + SyntaxFactory.IdentifierName(tp.Name), + SyntaxFactory.SeparatedList(constraints)); + } + /// /// Returns the readable getter expression from a property declaration, trying in order: /// the property-level expression-body, the getter's expression-body, then the first diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs index d450d70..01c2d61 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs @@ -261,36 +261,7 @@ private static void SetupGenericTypeParameters(ProjectableDescriptor descriptor, } descriptor.ClassConstraintClauses ??= SyntaxFactory.List(); - - var constraints = new List(); - - if (tp.HasReferenceTypeConstraint) - { - constraints.Add(MakeTypeConstraint("class")); - } - - if (tp.HasValueTypeConstraint) - { - constraints.Add(MakeTypeConstraint("struct")); - } - - if (tp.HasNotNullConstraint) - { - constraints.Add(MakeTypeConstraint("notnull")); - } - - constraints.AddRange(tp.ConstraintTypes - .Select(c => MakeTypeConstraint(c.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))); - - if (tp.HasConstructorConstraint) - { - constraints.Add(MakeTypeConstraint("new()")); - } - - descriptor.ClassConstraintClauses = descriptor.ClassConstraintClauses.Value.Add( - SyntaxFactory.TypeParameterConstraintClause( - SyntaxFactory.IdentifierName(tp.Name), - SyntaxFactory.SeparatedList(constraints))); + descriptor.ClassConstraintClauses = descriptor.ClassConstraintClauses.Value.Add(BuildConstraintClause(tp)); } } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/EntityExtensions.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/EntityExtensions.cs index 8532369..89cedb1 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/EntityExtensions.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/EntityExtensions.cs @@ -25,20 +25,35 @@ public static class EntityExtensions } } - public static class GenericWrapperExtensions + /// + /// Extension on a closed generic receiver type: extension(GenericWrapper<Entity> w). + /// Tests the fix for the bug where global:: inside generic type arguments caused a + /// name mismatch between the generated class and the runtime resolver. + /// + public static class ClosedGenericWrapperExtensions { extension(GenericWrapper w) { - /// - /// Extension member on a closed generic receiver type — exercises the - /// code path where global:: appears inside generic type arguments - /// in the fully-qualified receiver type name. - /// [Projectable] public int DoubleId() => w.Id * 2; } } + /// + /// Extension on an open generic receiver type: extension<T>(GenericWrapper<T> w). + /// The block-level type parameter T becomes a method-level type parameter on the + /// generated Expression<T>() factory, resolved at runtime via generic method + /// reflection. + /// + public static class OpenGenericWrapperExtensions + { + extension(GenericWrapper w) where T : class + { + [Projectable] + public int TripleId() => w.Id * 3; + } + } + public static class IntExtensions { extension(int i) diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnGenericReceiverType.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnClosedGenericReceiverType.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnGenericReceiverType.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnClosedGenericReceiverType.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnOpenGenericReceiverType.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnOpenGenericReceiverType.DotNet10_0.verified.txt new file mode 100644 index 0000000..a7fa9cd --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.ExtensionMemberMethodOnOpenGenericReceiverType.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id] * 3 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.cs index 3ceb119..1b183b5 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionMembers/ExtensionMemberTests.cs @@ -38,8 +38,14 @@ public Task ExtensionMemberMethodWithParameterOnEntity() return Verifier.Verify(query.ToQueryString()); } + /// + /// Regression test: extension member on a closed generic receiver type + /// (e.g. extension(GenericWrapper<Entity> w)) previously threw + /// "Unable to resolve generated expression" because global:: inside generic + /// type arguments caused a naming mismatch between the generator and the resolver. + /// [Fact] - public Task ExtensionMemberMethodOnGenericReceiverType() + public Task ExtensionMemberMethodOnClosedGenericReceiverType() { using var dbContext = new SampleDbContext(); @@ -49,6 +55,25 @@ public Task ExtensionMemberMethodOnGenericReceiverType() return Verifier.Verify(query.ToQueryString()); } + + /// + /// Exercises support for extension members on an open generic receiver type + /// (e.g. extension<T>(GenericWrapper<T> w)). + /// The block-level type parameter T must be promoted to a method-level type + /// parameter on the generated Expression<T>() factory so the runtime + /// resolver can construct the correct closed-generic expression. + /// + [Fact] + public Task ExtensionMemberMethodOnOpenGenericReceiverType() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => new GenericWrapper { Id = x.Id }) + .Select(x => x.TripleId()); + + return Verifier.Verify(query.ToQueryString()); + } } } #endif diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnOpenGenericReceiverType.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnOpenGenericReceiverType.verified.txt new file mode 100644 index 0000000..2fd2cc0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnOpenGenericReceiverType.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_WrapperExtensions_DoubleId_P0_Foo_Wrapper_T_ + { + static global::System.Linq.Expressions.Expression, int>> Expression() + { + return (global::Foo.Wrapper @this) => @this.Id * 2; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnOpenGenericReceiverTypeWithConstraint.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnOpenGenericReceiverTypeWithConstraint.verified.txt new file mode 100644 index 0000000..d8c71a4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.ExtensionMemberOnOpenGenericReceiverTypeWithConstraint.verified.txt @@ -0,0 +1,18 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_WrapperExtensions_TripleId_P0_Foo_Wrapper_T_ + { + static global::System.Linq.Expressions.Expression, int>> Expression() + where T : class + { + return (global::Foo.Wrapper @this) => @this.Id * 3; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.cs index 7972f70..63656c9 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMemberTests.cs @@ -307,5 +307,59 @@ static class WrapperExtensions { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + + [Fact] + public Task ExtensionMemberOnOpenGenericReceiverType() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Wrapper { public int Id { get; set; } } + + static class WrapperExtensions { + extension(Wrapper w) { + [Projectable] + public int DoubleId() => w.Id * 2; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExtensionMemberOnOpenGenericReceiverTypeWithConstraint() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Wrapper { public int Id { get; set; } } + + static class WrapperExtensions { + extension(Wrapper w) where T : class { + [Projectable] + public int TripleId() => w.Id * 3; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } #endif } \ No newline at end of file