From 8a82bf8e85240106a50e6ff507881aa654b9150e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 09:52:07 +0000 Subject: [PATCH 1/3] Initial plan From e686b2308abae8ad324c0cfdc5e86fd22361d205 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:01:27 +0000 Subject: [PATCH 2/3] Fix GetImplementingProperty to handle explicit interface implementations Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- .../Extensions/TypeExtensions.cs | 4 +- .../Extensions/TypeExtensionTests.cs | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/EntityFrameworkCore.Projectables/Extensions/TypeExtensions.cs b/src/EntityFrameworkCore.Projectables/Extensions/TypeExtensions.cs index bc86a95..095173a 100644 --- a/src/EntityFrameworkCore.Projectables/Extensions/TypeExtensions.cs +++ b/src/EntityFrameworkCore.Projectables/Extensions/TypeExtensions.cs @@ -139,9 +139,9 @@ public static PropertyInfo GetImplementingProperty(this Type derivedType, Proper var derivedProperties = derivedType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - return derivedProperties.First(propertyInfo.GetMethod == accessor + return derivedProperties.FirstOrDefault(propertyInfo.GetMethod == accessor ? p => p.GetMethod == implementingAccessor - : p => p.SetMethod == implementingAccessor); + : p => p.SetMethod == implementingAccessor) ?? propertyInfo; } public static MethodInfo GetConcreteMethod(this Type derivedType, MethodInfo methodInfo) diff --git a/tests/EntityFrameworkCore.Projectables.Tests/Extensions/TypeExtensionTests.cs b/tests/EntityFrameworkCore.Projectables.Tests/Extensions/TypeExtensionTests.cs index a565381..3a22e0c 100644 --- a/tests/EntityFrameworkCore.Projectables.Tests/Extensions/TypeExtensionTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Tests/Extensions/TypeExtensionTests.cs @@ -32,6 +32,17 @@ public override void VirtualMethod(int arg1) { } public override void GenericVirtualMethod(TArg arg1) { } } + interface IStringId + { + string Id { get; } + } + + class ItemWithExplicitInterfaceImplementation : IStringId + { + public int Id { get; set; } + string IStringId.Id => Id.ToString(); + } + [Fact] public void GetNestedTypePath_OuterType_Returns1Entry() { @@ -120,5 +131,41 @@ public void GetOverridingMethod_DerivedTypeGenericVirtualMethod_FindsOverridingM Assert.Equal(derivedMethod, resolvedMethod); } + + [Fact] + public void GetImplementingProperty_ExplicitInterfaceImplementation_DoesNotThrow() + { + // This test verifies that when a class explicitly implements an interface property + // (e.g., string IStringId.Id => Id.ToString();), GetImplementingProperty doesn't throw + // an InvalidOperationException + var interfaceType = typeof(IStringId); + var interfaceProperty = interfaceType.GetProperty("Id")!; + var concreteType = typeof(ItemWithExplicitInterfaceImplementation); + + // This should not throw InvalidOperationException + var result = concreteType.GetImplementingProperty(interfaceProperty); + + // The result should be the explicit interface implementation property + Assert.NotNull(result); + Assert.NotEqual(interfaceProperty, result); + Assert.Contains("IStringId.Id", result.Name); + } + + [Fact] + public void GetConcreteProperty_ExplicitInterfaceImplementation_DoesNotThrow() + { + // This test verifies the same scenario but through GetConcreteProperty which is the entry point + var interfaceType = typeof(IStringId); + var interfaceProperty = interfaceType.GetProperty("Id")!; + var concreteType = typeof(ItemWithExplicitInterfaceImplementation); + + // This should not throw InvalidOperationException + var result = concreteType.GetConcreteProperty(interfaceProperty); + + // The result should be the explicit interface implementation property + Assert.NotNull(result); + Assert.NotEqual(interfaceProperty, result); + Assert.Contains("IStringId.Id", result.Name); + } } } From 201cabe5c1a3db3def42f1310cc388dfb4f1d274 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:14:16 +0000 Subject: [PATCH 3/3] Add functional and generator tests for explicit interface implementations Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- ...plicitInterfaceImplementation.verified.txt | 3 + ...plicitInterfaceImplementation.verified.txt | 2 + .../ExplicitInterfaceImplementationTests.cs | 55 +++++++++++++++++++ ...plicitInterfaceImplementation.verified.txt | 17 ++++++ .../ProjectionExpressionGeneratorTests.cs | 34 ++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.FilterOnExplicitInterfaceImplementation.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.ProjectOverExplicitInterfaceImplementation.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.cs create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExplicitInterfaceImplementation.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.FilterOnExplicitInterfaceImplementation.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.FilterOnExplicitInterfaceImplementation.verified.txt new file mode 100644 index 0000000..5afc7a1 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.FilterOnExplicitInterfaceImplementation.verified.txt @@ -0,0 +1,3 @@ +SELECT [i].[Id] +FROM [Item] AS [i] +WHERE CONVERT(varchar(11), [i].[Id]) = N'123' \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.ProjectOverExplicitInterfaceImplementation.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.ProjectOverExplicitInterfaceImplementation.verified.txt new file mode 100644 index 0000000..4383f0e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.ProjectOverExplicitInterfaceImplementation.verified.txt @@ -0,0 +1,2 @@ +SELECT CONVERT(varchar(11), [i].[Id]) +FROM [Item] AS [i] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.cs new file mode 100644 index 0000000..b72fb50 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExplicitInterfaceImplementationTests.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using VerifyXunit; +using Xunit; + +#nullable disable + +namespace EntityFrameworkCore.Projectables.FunctionalTests +{ + [UsesVerify] + public class ExplicitInterfaceImplementationTests + { + public interface IStringId + { + string Id { get; } + } + + public class Item : IStringId + { + public int Id { get; set; } + + // Explicit interface implementation without [Projectable] + // This tests that GetImplementingProperty handles this scenario + string IStringId.Id => Id.ToString(); + + [Projectable] + public string FormattedId => Id.ToString(); + } + + [Fact] + public Task ProjectOverExplicitInterfaceImplementation() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.FormattedId); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task FilterOnExplicitInterfaceImplementation() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Where(x => x.FormattedId == "123"); + + return Verifier.Verify(query.ToQueryString()); + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExplicitInterfaceImplementation.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExplicitInterfaceImplementation.verified.txt new file mode 100644 index 0000000..815c681 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExplicitInterfaceImplementation.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_Item_FormattedId + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Item @this) => ((global::Foo.IStringId)@this).Id; + } + } +} \ 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 2a74263..9b0f5ef 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -1902,6 +1902,40 @@ public Task GenericTypesWithConstraints() return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task ExplicitInterfaceImplementation() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public interface IStringId + { + string Id { get; } + } + + public class Item : IStringId + { + public int Id { get; set; } + + // Explicit interface implementation without [Projectable] + string IStringId.Id => Id.ToString(); + + [Projectable] + public string FormattedId => ((IStringId)this).Id; + } +} +"); + + 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)