From c4d8bfa35c2fcc1d234d92826e2901e4d3ce0e2e Mon Sep 17 00:00:00 2001 From: Rhodon Date: Wed, 25 Jun 2025 12:08:13 +0200 Subject: [PATCH 1/2] Support explicitly implemented interface members --- .../ProjectionExpressionClassNameGenerator.cs | 2 +- ...ExplicitlyImplementedProperty.verified.txt | 2 ++ .../InheritedModelTests.cs | 25 ++++++++++++++++ ...Tests.ExplicitInterfaceMember.verified.txt | 16 ++++++++++ .../ProjectionExpressionGeneratorTests.cs | 29 +++++++++++++++++++ 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverExplicitlyImplementedProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExplicitInterfaceMember.verified.txt diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs index 1c63e77..fd253df 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs @@ -55,7 +55,7 @@ static string GenerateNameImpl(StringBuilder stringBuilder, string? namespaceNam } } - stringBuilder.Append(memberName); + stringBuilder.Append(memberName.Replace(".", "__")); // Support explicit interface implementations if (arity > 0) { diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverExplicitlyImplementedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverExplicitlyImplementedProperty.verified.txt new file mode 100644 index 0000000..5c98c81 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverExplicitlyImplementedProperty.verified.txt @@ -0,0 +1,2 @@ +SELECT [o].[Id] / 2 +FROM [OtherConcrete] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs index d2a7f34..7944ee7 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs @@ -53,6 +53,11 @@ public abstract class Base : IBase public virtual int SampleMethod() => 0; } + public interface IDefaultBase + { + int Explicit { get; } + } + public class Concrete : Base { [Projectable] @@ -62,6 +67,12 @@ public class Concrete : Base public override int SampleMethod() => 1; } + public class OtherConcrete : Base, IDefaultBase + { + [Projectable] + int IDefaultBase.Explicit => Id / 2; + } + public class MoreConcrete : Concrete { } @@ -130,6 +141,16 @@ public Task ProjectOverImplementedMethod() return Verifier.Verify(query.ToQueryString()); } + [Fact] + public Task ProjectOverExplicitlyImplementedProperty() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set().SelectExplicitProperty(); + + return Verifier.Verify(query.ToQueryString()); + } + [Fact] public Task ProjectOverProvider() { @@ -157,6 +178,10 @@ public static IQueryable SelectComputedProperty(this IQueryable< where TConcrete : InheritedModelTests.IBase => concretes.Select(x => x.ComputedProperty); + public static IQueryable SelectExplicitProperty(this IQueryable concretes) + where TConcrete : InheritedModelTests.IDefaultBase + => concretes.Select(x => x.Explicit); + public static IQueryable SelectComputedMethod(this IQueryable concretes) where TConcrete : InheritedModelTests.IBase => concretes.Select(x => x.ComputedMethod()); diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExplicitInterfaceMember.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExplicitInterfaceMember.verified.txt new file mode 100644 index 0000000..d405def --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExplicitInterfaceMember.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class _Concrete_IBase__ComputedProperty + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Concrete @this) => @this.Id + 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index a982c79..d8d18bd 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -1856,6 +1856,35 @@ public Task GenericTypesWithConstraints() return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task ExplicitInterfaceMember() + { + var compilation = CreateCompilation( + """ + using System; + using EntityFrameworkCore.Projectables; + + public interface IBase + { + int ComputedProperty { get; } + } + + public class Concrete : IBase + { + int Id { get; } + [Projectable] + int IBase.ComputedProperty => Id + 1; + } + """); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + #region Helpers Compilation CreateCompilation(string source, bool expectedToCompile = true) From a142c85065c02eb1cca11410c1e3a7221f0d4f96 Mon Sep 17 00:00:00 2001 From: Rhodon Date: Wed, 25 Jun 2025 12:08:52 +0200 Subject: [PATCH 2/2] Support default interface members --- .../Extensions/TypeExtensions.cs | 35 ++++++---- ...ExplicitlyImplementedProperty.verified.txt | 2 + ...verDefaultImplementedProperty.verified.txt | 2 + .../InheritedModelTests.cs | 37 ++++++++++- ...efaultExplicitInterfaceMember.verified.txt | 16 +++++ ...rTests.DefaultInterfaceMember.verified.txt | 16 +++++ .../ProjectionExpressionGeneratorTests.cs | 65 +++++++++++++++++++ 7 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverDefaultExplicitlyImplementedProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverDefaultImplementedProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DefaultExplicitInterfaceMember.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DefaultInterfaceMember.verified.txt diff --git a/src/EntityFrameworkCore.Projectables/Extensions/TypeExtensions.cs b/src/EntityFrameworkCore.Projectables/Extensions/TypeExtensions.cs index bc86a95..5f974c2 100644 --- a/src/EntityFrameworkCore.Projectables/Extensions/TypeExtensions.cs +++ b/src/EntityFrameworkCore.Projectables/Extensions/TypeExtensions.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Metadata; -using System.Runtime.CompilerServices; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading.Tasks; +using System.Reflection; namespace EntityFrameworkCore.Projectables.Extensions { @@ -137,13 +129,32 @@ public static PropertyInfo GetImplementingProperty(this Type derivedType, Proper return propertyInfo; } - var derivedProperties = derivedType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var implementingType = implementingAccessor.DeclaringType + // This should only be null if it is a property accessor on the global module, + // which should never happen since we found it from derivedType + ?? throw new ApplicationException("The property accessor has no declaring type!"); + + var derivedProperties = implementingType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); return derivedProperties.First(propertyInfo.GetMethod == accessor - ? p => p.GetMethod == implementingAccessor - : p => p.SetMethod == implementingAccessor); + ? p => MethodInfosEqual(p.GetMethod, implementingAccessor) + : p => MethodInfosEqual(p.SetMethod, implementingAccessor)); } + /// + /// The built-in + /// does not work if the s don't agree. + /// + private static bool MethodInfosEqual(MethodInfo? first, MethodInfo second) + => first?.ReflectedType == second.ReflectedType + ? first == second + : first is not null + && first.DeclaringType == second.DeclaringType + && first.Name == second.Name + && first.GetParameters().Select(p => p.ParameterType) + .SequenceEqual(second.GetParameters().Select(p => p.ParameterType)) + && first.GetGenericArguments().SequenceEqual(second.GetGenericArguments()); + public static MethodInfo GetConcreteMethod(this Type derivedType, MethodInfo methodInfo) => methodInfo.DeclaringType?.IsInterface == true ? derivedType.GetImplementingMethod(methodInfo) diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverDefaultExplicitlyImplementedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverDefaultExplicitlyImplementedProperty.verified.txt new file mode 100644 index 0000000..a473c5f --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverDefaultExplicitlyImplementedProperty.verified.txt @@ -0,0 +1,2 @@ +SELECT [c].[Id] * 2 +FROM [Concrete] AS [c] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverDefaultImplementedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverDefaultImplementedProperty.verified.txt new file mode 100644 index 0000000..03753d3 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.ProjectOverDefaultImplementedProperty.verified.txt @@ -0,0 +1,2 @@ +SELECT 49 +FROM [Concrete] AS [c] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs index 7944ee7..524c17d 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs @@ -56,9 +56,18 @@ public abstract class Base : IBase public interface IDefaultBase { int Explicit { get; } + + [Projectable] + int Default => 49; + } + + public interface IDefaultBaseImplementation : IDefaultBase, IBase + { + [Projectable] + int IDefaultBase.Explicit => Id * 2; } - public class Concrete : Base + public class Concrete : Base, IDefaultBaseImplementation { [Projectable] public override int SampleProperty => 1; @@ -141,6 +150,16 @@ public Task ProjectOverImplementedMethod() return Verifier.Verify(query.ToQueryString()); } + [Fact] + public Task ProjectOverDefaultImplementedProperty() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set().SelectDefaultProperty(); + + return Verifier.Verify(query.ToQueryString()); + } + [Fact] public Task ProjectOverExplicitlyImplementedProperty() { @@ -151,6 +170,16 @@ public Task ProjectOverExplicitlyImplementedProperty() return Verifier.Verify(query.ToQueryString()); } + [Fact] + public Task ProjectOverDefaultExplicitlyImplementedProperty() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set().SelectExplicitProperty(); + + return Verifier.Verify(query.ToQueryString()); + } + [Fact] public Task ProjectOverProvider() { @@ -177,7 +206,11 @@ public static class ModelExtensions public static IQueryable SelectComputedProperty(this IQueryable concretes) where TConcrete : InheritedModelTests.IBase => concretes.Select(x => x.ComputedProperty); - + + public static IQueryable SelectDefaultProperty(this IQueryable concretes) + where TConcrete : InheritedModelTests.IDefaultBase + => concretes.Select(x => x.Default); + public static IQueryable SelectExplicitProperty(this IQueryable concretes) where TConcrete : InheritedModelTests.IDefaultBase => concretes.Select(x => x.Explicit); diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DefaultExplicitInterfaceMember.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DefaultExplicitInterfaceMember.verified.txt new file mode 100644 index 0000000..f4fc80e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DefaultExplicitInterfaceMember.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class _IDefaultBaseImplementation_IDefaultBase__Default + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::IDefaultBaseImplementation @this) => @this.ComputedProperty * 2; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DefaultInterfaceMember.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DefaultInterfaceMember.verified.txt new file mode 100644 index 0000000..8bf6f22 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DefaultInterfaceMember.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class _IDefaultBase_Default + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::IDefaultBase @this) => @this.ComputedProperty * 2; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index d8d18bd..1f574f6 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -1885,6 +1885,71 @@ public class Concrete : IBase return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task DefaultInterfaceMember() + { + var compilation = CreateCompilation( + """ + using System; + using EntityFrameworkCore.Projectables; + + public interface IBase + { + int Id { get; } + int ComputedProperty { get; } + int ComputedMethod(); + } + + public interface IDefaultBase : IBase + { + [Projectable] + int Default => ComputedProperty * 2; + } + """); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task DefaultExplicitInterfaceMember() + { + var compilation = CreateCompilation( + """ + using System; + using EntityFrameworkCore.Projectables; + + public interface IBase + { + int Id { get; } + int ComputedProperty { get; } + int ComputedMethod(); + } + + public interface IDefaultBase + { + int Default { get; } + } + + public interface IDefaultBaseImplementation : IDefaultBase, IBase + { + [Projectable] + int IDefaultBase.Default => ComputedProperty * 2; + } + """); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + #region Helpers Compilation CreateCompilation(string source, bool expectedToCompile = true)