diff --git a/EntityFrameworkCore.Projectables.sln b/EntityFrameworkCore.Projectables.sln index 687280f..b49e6ed 100644 --- a/EntityFrameworkCore.Projectables.sln +++ b/EntityFrameworkCore.Projectables.sln @@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Projectables", "src\EntityFrameworkCore.Projectables\EntityFrameworkCore.Projectables.csproj", "{EE4D6CC1-78DE-4279-A567-C3D360C479F8}" diff --git a/global.json b/global.json new file mode 100644 index 0000000..bfca90d --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "8.0.400" + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablePropertiesNotMappedConvention.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablePropertiesNotMappedConvention.cs new file mode 100644 index 0000000..efa84b6 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablePropertiesNotMappedConvention.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; + +namespace EntityFrameworkCore.Projectables.Infrastructure.Internal; + +public class ProjectablePropertiesNotMappedConvention : IEntityTypeAddedConvention +{ + public void ProcessEntityTypeAdded(IConventionEntityTypeBuilder entityTypeBuilder, IConventionContext context) + { + if (entityTypeBuilder.Metadata.ClrType is null) + { + return; + } + + foreach (var property in entityTypeBuilder.Metadata.ClrType.GetRuntimeProperties()) + { + if (property.GetCustomAttribute() is not null) + { + entityTypeBuilder.Ignore(property.Name); + } + } + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablePropertiesNotMappedConventionPlugin.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablePropertiesNotMappedConventionPlugin.cs new file mode 100644 index 0000000..06e7c3d --- /dev/null +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablePropertiesNotMappedConventionPlugin.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace EntityFrameworkCore.Projectables.Infrastructure.Internal; + +public class ProjectablePropertiesNotMappedConventionPlugin : IConventionSetPlugin +{ + public ConventionSet ModifyConventions(ConventionSet conventionSet) + { + conventionSet.EntityTypeAddedConventions.Add(new ProjectablePropertiesNotMappedConvention()); + return conventionSet; + } +} diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs index 3cb2af7..ac593a9 100644 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs @@ -1,6 +1,8 @@ using EntityFrameworkCore.Projectables.Infrastructure; using EntityFrameworkCore.Projectables.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.Extensions.DependencyInjection; @@ -36,6 +38,9 @@ public ProjectionOptionsExtension(ProjectionOptionsExtension copyFrom) [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Needed")] public void ApplyServices(IServiceCollection services) { + // Register a convention that will ignore properties marked with the ProjectableAttribute + services.AddScoped(); + static object CreateTargetInstance(IServiceProvider services, ServiceDescriptor descriptor) { if (descriptor.ImplementationInstance is not null) diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/EnumerableProjectableTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/EnumerableProjectableTests.cs new file mode 100644 index 0000000..dc66219 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/EnumerableProjectableTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; + +namespace EntityFrameworkCore.Projectables.FunctionalTests +{ + public class EnumerableProjectableTests + { + public class Product + { + public int Id { get; set; } + + public List Prices { get; } = []; + + [Projectable] + public IEnumerable CheapPrices => Prices.Where(x => x.Price < 10D); + } + + public class ProductPrice + { + public int Id { get; set; } + + public double Price { get; set; } + } + + [Fact] + public void ProjectableProperty_IsIgnoredFromMapping() + { + var dbContext = new SampleDbContext(); + var productPriceType = dbContext.Model.GetEntityTypes().Single(x => x.ClrType == typeof(ProductPrice)); + + // Assert 3 properties: Id, Price, ProductId (synthetic) + Assert.Equal(3, productPriceType.GetProperties().Count()); + } + } +}