diff --git a/src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs b/src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs index b0ba38f3a5..711ad8517c 100644 --- a/src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs +++ b/src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs @@ -38,12 +38,12 @@ public void Initialize(DbContext dbContext) Initialize(); } - private void ScanForeignKeys(IModel entityModel) + private void ScanForeignKeys(IReadOnlyModel entityModel) { foreach (RelationshipAttribute relationship in ResourceGraph.GetResourceTypes().SelectMany(resourceType => resourceType.Relationships)) { - IEntityType? leftEntityType = entityModel.FindEntityType(relationship.LeftType.ClrType); - INavigation? navigation = leftEntityType?.FindNavigation(relationship.Property.Name); + IReadOnlyEntityType? leftEntityType = entityModel.FindEntityType(relationship.LeftType.ClrType); + IReadOnlyNavigation? navigation = leftEntityType?.FindNavigation(relationship.Property.Name); if (navigation != null) { @@ -57,7 +57,7 @@ private void ScanForeignKeys(IModel entityModel) } } - private void ScanColumnNullability(IModel entityModel) + private void ScanColumnNullability(IReadOnlyModel entityModel) { foreach (ResourceType resourceType in ResourceGraph.GetResourceTypes()) { @@ -65,15 +65,15 @@ private void ScanColumnNullability(IModel entityModel) } } - private void ScanColumnNullability(ResourceType resourceType, IModel entityModel) + private void ScanColumnNullability(ResourceType resourceType, IReadOnlyModel entityModel) { - IEntityType? entityType = entityModel.FindEntityType(resourceType.ClrType); + IReadOnlyEntityType? entityType = entityModel.FindEntityType(resourceType.ClrType); if (entityType != null) { foreach (AttrAttribute attribute in resourceType.Attributes) { - IProperty? property = entityType.FindProperty(attribute.Property.Name); + IReadOnlyProperty? property = entityType.FindProperty(attribute.Property.Name); if (property != null) { diff --git a/src/Examples/NoEntityFrameworkExample/Data/InMemoryModel.cs b/src/Examples/NoEntityFrameworkExample/Data/InMemoryModel.cs deleted file mode 100644 index c81aa07b8f..0000000000 --- a/src/Examples/NoEntityFrameworkExample/Data/InMemoryModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Reflection; -using JsonApiDotNetCore.Configuration; -using Microsoft.EntityFrameworkCore.Metadata; - -namespace NoEntityFrameworkExample.Data; - -internal sealed class InMemoryModel : RuntimeModel -{ - public InMemoryModel(IResourceGraph resourceGraph) - { - foreach (ResourceType resourceType in resourceGraph.GetResourceTypes()) - { - RuntimeEntityType entityType = AddEntityType(resourceType.ClrType.FullName!, resourceType.ClrType); - SetEntityProperties(entityType, resourceType); - } - } - - private static void SetEntityProperties(RuntimeEntityType entityType, ResourceType resourceType) - { - foreach (PropertyInfo property in resourceType.ClrType.GetProperties()) - { - entityType.AddProperty(property.Name, property.PropertyType, property); - } - } -} diff --git a/src/Examples/NoEntityFrameworkExample/Data/ResourceGraphExtensions.cs b/src/Examples/NoEntityFrameworkExample/Data/ResourceGraphExtensions.cs new file mode 100644 index 0000000000..ff35f0ab0d --- /dev/null +++ b/src/Examples/NoEntityFrameworkExample/Data/ResourceGraphExtensions.cs @@ -0,0 +1,32 @@ +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace NoEntityFrameworkExample.Data; + +internal static class ResourceGraphExtensions +{ + public static IReadOnlyModel ToEntityModel(this IResourceGraph resourceGraph) + { + var modelBuilder = new ModelBuilder(); + + foreach (ResourceType resourceType in resourceGraph.GetResourceTypes()) + { + IncludeResourceType(resourceType, modelBuilder); + } + + return modelBuilder.Model; + } + + private static void IncludeResourceType(ResourceType resourceType, ModelBuilder builder) + { + EntityTypeBuilder entityTypeBuilder = builder.Entity(resourceType.ClrType); + + foreach (PropertyInfo property in resourceType.ClrType.GetProperties()) + { + entityTypeBuilder.Property(property.PropertyType, property.Name); + } + } +} diff --git a/src/Examples/NoEntityFrameworkExample/Program.cs b/src/Examples/NoEntityFrameworkExample/Program.cs index 8546e939e8..f21d116e5f 100755 --- a/src/Examples/NoEntityFrameworkExample/Program.cs +++ b/src/Examples/NoEntityFrameworkExample/Program.cs @@ -1,5 +1,6 @@ using JsonApiDotNetCore.Configuration; using NoEntityFrameworkExample; +using NoEntityFrameworkExample.Data; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -20,6 +21,12 @@ #endif }, discovery => discovery.AddCurrentAssembly()); +builder.Services.AddSingleton(serviceProvider => +{ + var resourceGraph = serviceProvider.GetRequiredService(); + return resourceGraph.ToEntityModel(); +}); + WebApplication app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/src/Examples/NoEntityFrameworkExample/QueryLayerToLinqConverter.cs b/src/Examples/NoEntityFrameworkExample/QueryLayerToLinqConverter.cs index 29d5f999e9..a67a694aef 100644 --- a/src/Examples/NoEntityFrameworkExample/QueryLayerToLinqConverter.cs +++ b/src/Examples/NoEntityFrameworkExample/QueryLayerToLinqConverter.cs @@ -7,9 +7,9 @@ namespace NoEntityFrameworkExample; -internal sealed class QueryLayerToLinqConverter(IModel model, IQueryableBuilder queryableBuilder) +internal sealed class QueryLayerToLinqConverter(IReadOnlyModel entityModel, IQueryableBuilder queryableBuilder) { - private readonly IModel _model = model; + private readonly IReadOnlyModel _entityModel = entityModel; private readonly IQueryableBuilder _queryableBuilder = queryableBuilder; public IEnumerable ApplyQueryLayer(QueryLayer queryLayer, IEnumerable resources) @@ -21,7 +21,7 @@ public IEnumerable ApplyQueryLayer(QueryLayer queryLayer, // Convert QueryLayer into LINQ expression. IQueryable source = ((IEnumerable)resources).AsQueryable(); - var context = QueryableBuilderContext.CreateRoot(source, typeof(Enumerable), _model, null); + var context = QueryableBuilderContext.CreateRoot(source, typeof(Enumerable), _entityModel, null); Expression expression = _queryableBuilder.ApplyQuery(queryLayer, context); // Insert null checks to prevent a NullReferenceException during execution of expressions such as: diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs index 243b484a9b..9d0852ad7f 100644 --- a/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs +++ b/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs @@ -4,7 +4,7 @@ using JsonApiDotNetCore.Queries.QueryableBuilding; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; -using NoEntityFrameworkExample.Data; +using Microsoft.EntityFrameworkCore.Metadata; namespace NoEntityFrameworkExample.Repositories; @@ -19,19 +19,12 @@ namespace NoEntityFrameworkExample.Repositories; /// /// The resource identifier type. /// -public abstract class InMemoryResourceRepository : IResourceReadRepository +public abstract class InMemoryResourceRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel) + : IResourceReadRepository where TResource : class, IIdentifiable { - private readonly ResourceType _resourceType; - private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter; - - protected InMemoryResourceRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder) - { - _resourceType = resourceGraph.GetResourceType(); - - var model = new InMemoryModel(resourceGraph); - _queryLayerToLinqConverter = new QueryLayerToLinqConverter(model, queryableBuilder); - } + private readonly ResourceType _resourceType = resourceGraph.GetResourceType(); + private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter = new(entityModel, queryableBuilder); /// public Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken) diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/PersonRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/PersonRepository.cs index 897af592b7..8e2725379c 100644 --- a/src/Examples/NoEntityFrameworkExample/Repositories/PersonRepository.cs +++ b/src/Examples/NoEntityFrameworkExample/Repositories/PersonRepository.cs @@ -1,14 +1,15 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.QueryableBuilding; +using Microsoft.EntityFrameworkCore.Metadata; using NoEntityFrameworkExample.Data; using NoEntityFrameworkExample.Models; namespace NoEntityFrameworkExample.Repositories; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class PersonRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder) - : InMemoryResourceRepository(resourceGraph, queryableBuilder) +public sealed class PersonRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel) + : InMemoryResourceRepository(resourceGraph, queryableBuilder, entityModel) { protected override IEnumerable GetDataSource() { diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/TagRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/TagRepository.cs index 30658fb68d..81a28ed6bc 100644 --- a/src/Examples/NoEntityFrameworkExample/Repositories/TagRepository.cs +++ b/src/Examples/NoEntityFrameworkExample/Repositories/TagRepository.cs @@ -1,14 +1,15 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.QueryableBuilding; +using Microsoft.EntityFrameworkCore.Metadata; using NoEntityFrameworkExample.Data; using NoEntityFrameworkExample.Models; namespace NoEntityFrameworkExample.Repositories; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class TagRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder) - : InMemoryResourceRepository(resourceGraph, queryableBuilder) +public sealed class TagRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel) + : InMemoryResourceRepository(resourceGraph, queryableBuilder, entityModel) { protected override IEnumerable GetDataSource() { diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/TodoItemRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/TodoItemRepository.cs index 41774b0c8f..335d7c5c5a 100644 --- a/src/Examples/NoEntityFrameworkExample/Repositories/TodoItemRepository.cs +++ b/src/Examples/NoEntityFrameworkExample/Repositories/TodoItemRepository.cs @@ -1,14 +1,15 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.QueryableBuilding; +using Microsoft.EntityFrameworkCore.Metadata; using NoEntityFrameworkExample.Data; using NoEntityFrameworkExample.Models; namespace NoEntityFrameworkExample.Repositories; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class TodoItemRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder) - : InMemoryResourceRepository(resourceGraph, queryableBuilder) +public sealed class TodoItemRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel) + : InMemoryResourceRepository(resourceGraph, queryableBuilder, entityModel) { protected override IEnumerable GetDataSource() { diff --git a/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs b/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs index 67c7a4138c..e9b37560fc 100644 --- a/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs +++ b/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs @@ -7,7 +7,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Services; -using NoEntityFrameworkExample.Data; +using Microsoft.EntityFrameworkCore.Metadata; namespace NoEntityFrameworkExample.Services; @@ -30,32 +30,19 @@ namespace NoEntityFrameworkExample.Services; /// /// The resource identifier type. /// -public abstract class InMemoryResourceService : IResourceQueryService +public abstract class InMemoryResourceService( + IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, + IEnumerable constraintProviders, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel, + ILoggerFactory loggerFactory) : IResourceQueryService where TResource : class, IIdentifiable { - private readonly IJsonApiOptions _options; - private readonly IQueryLayerComposer _queryLayerComposer; - private readonly IPaginationContext _paginationContext; - private readonly IEnumerable _constraintProviders; - private readonly ILogger> _logger; - private readonly ResourceType _resourceType; - private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter; - - protected InMemoryResourceService(IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer, - IPaginationContext paginationContext, IEnumerable constraintProviders, IQueryableBuilder queryableBuilder, - ILoggerFactory loggerFactory) - { - _options = options; - _queryLayerComposer = queryLayerComposer; - _paginationContext = paginationContext; - _constraintProviders = constraintProviders; - - _logger = loggerFactory.CreateLogger>(); - _resourceType = resourceGraph.GetResourceType(); - - var model = new InMemoryModel(resourceGraph); - _queryLayerToLinqConverter = new QueryLayerToLinqConverter(model, queryableBuilder); - } + private readonly IJsonApiOptions _options = options; + private readonly IQueryLayerComposer _queryLayerComposer = queryLayerComposer; + private readonly IPaginationContext _paginationContext = paginationContext; + private readonly IEnumerable _constraintProviders = constraintProviders; + private readonly ILogger> _logger = loggerFactory.CreateLogger>(); + private readonly ResourceType _resourceType = resourceGraph.GetResourceType(); + private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter = new(entityModel, queryableBuilder); /// public Task> GetAsync(CancellationToken cancellationToken) diff --git a/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs b/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs index 294d23978c..d38cca9c94 100644 --- a/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs +++ b/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.QueryableBuilding; using JsonApiDotNetCore.Resources; +using Microsoft.EntityFrameworkCore.Metadata; using NoEntityFrameworkExample.Data; using NoEntityFrameworkExample.Models; @@ -11,8 +12,8 @@ namespace NoEntityFrameworkExample.Services; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TodoItemService( IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, - IEnumerable constraintProviders, IQueryableBuilder queryableBuilder, ILoggerFactory loggerFactory) - : InMemoryResourceService(options, resourceGraph, queryLayerComposer, paginationContext, constraintProviders, queryableBuilder, + IEnumerable constraintProviders, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel, ILoggerFactory loggerFactory) + : InMemoryResourceService(options, resourceGraph, queryLayerComposer, paginationContext, constraintProviders, queryableBuilder, entityModel, loggerFactory) { protected override IEnumerable GetDataSource(ResourceType resourceType) diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs index 42dcf80428..05cccf7943 100644 --- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs +++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs @@ -29,7 +29,7 @@ public sealed class QueryClauseBuilderContext /// /// The Entity Framework Core entity model. /// - public IModel EntityModel { get; } + public IReadOnlyModel EntityModel { get; } /// /// Used to produce unique names for lambda parameters. @@ -51,7 +51,7 @@ public sealed class QueryClauseBuilderContext /// public object? State { get; } - public QueryClauseBuilderContext(Expression source, ResourceType resourceType, Type extensionType, IModel entityModel, + public QueryClauseBuilderContext(Expression source, ResourceType resourceType, Type extensionType, IReadOnlyModel entityModel, LambdaScopeFactory lambdaScopeFactory, LambdaScope lambdaScope, IQueryableBuilder queryableBuilder, object? state) { ArgumentGuard.NotNull(source); diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilderContext.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilderContext.cs index 4659cca875..358990fdc7 100644 --- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilderContext.cs +++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilderContext.cs @@ -29,7 +29,7 @@ public sealed class QueryableBuilderContext /// /// The Entity Framework Core entity model. /// - public IModel EntityModel { get; } + public IReadOnlyModel EntityModel { get; } /// /// Used to produce unique names for lambda parameters. @@ -41,7 +41,7 @@ public sealed class QueryableBuilderContext /// public object? State { get; } - public QueryableBuilderContext(Expression source, Type elementType, Type extensionType, IModel entityModel, LambdaScopeFactory lambdaScopeFactory, + public QueryableBuilderContext(Expression source, Type elementType, Type extensionType, IReadOnlyModel entityModel, LambdaScopeFactory lambdaScopeFactory, object? state) { ArgumentGuard.NotNull(source); @@ -58,15 +58,15 @@ public QueryableBuilderContext(Expression source, Type elementType, Type extensi State = state; } - public static QueryableBuilderContext CreateRoot(IQueryable source, Type extensionType, IModel model, object? state) + public static QueryableBuilderContext CreateRoot(IQueryable source, Type extensionType, IReadOnlyModel entityModel, object? state) { ArgumentGuard.NotNull(source); ArgumentGuard.NotNull(extensionType); - ArgumentGuard.NotNull(model); + ArgumentGuard.NotNull(entityModel); var lambdaScopeFactory = new LambdaScopeFactory(); - return new QueryableBuilderContext(source.Expression, source.ElementType, extensionType, model, lambdaScopeFactory, state); + return new QueryableBuilderContext(source.Expression, source.ElementType, extensionType, entityModel, lambdaScopeFactory, state); } public QueryClauseBuilderContext CreateClauseContext(IQueryableBuilder queryableBuilder, Expression source, ResourceType resourceType, diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs index d1113946ad..c67b785d89 100644 --- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs @@ -40,8 +40,8 @@ public virtual Expression ApplySelect(FieldSelection selection, QueryClauseBuild private Expression CreateLambdaBodyInitializer(FieldSelection selection, ResourceType resourceType, LambdaScope lambdaScope, bool lambdaAccessorRequiresTestForNull, QueryClauseBuilderContext context) { - IEntityType entityType = context.EntityModel.FindEntityType(resourceType.ClrType)!; - IEntityType[] concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToArray(); + IReadOnlyEntityType entityType = context.EntityModel.FindEntityType(resourceType.ClrType)!; + IReadOnlyEntityType[] concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToArray(); Expression bodyInitializer = concreteEntityTypes.Length > 1 ? CreateLambdaBodyInitializerForTypeHierarchy(selection, resourceType, concreteEntityTypes, lambdaScope, context) @@ -56,12 +56,12 @@ private Expression CreateLambdaBodyInitializer(FieldSelection selection, Resourc } private Expression CreateLambdaBodyInitializerForTypeHierarchy(FieldSelection selection, ResourceType baseResourceType, - IEnumerable concreteEntityTypes, LambdaScope lambdaScope, QueryClauseBuilderContext context) + IEnumerable concreteEntityTypes, LambdaScope lambdaScope, QueryClauseBuilderContext context) { IReadOnlySet resourceTypes = selection.GetResourceTypes(); Expression rootCondition = lambdaScope.Accessor; - foreach (IEntityType entityType in concreteEntityTypes) + foreach (IReadOnlyEntityType entityType in concreteEntityTypes) { ResourceType? resourceType = resourceTypes.SingleOrDefault(type => type.ClrType == entityType.ClrType); @@ -115,7 +115,7 @@ private Expression CreateLambdaBodyInitializerForSingleType(FieldSelection selec } private static ICollection ToPropertySelectors(FieldSelectors fieldSelectors, ResourceType resourceType, Type elementType, - IModel entityModel) + IReadOnlyModel entityModel) { var propertySelectors = new Dictionary(); @@ -134,17 +134,18 @@ private static ICollection ToPropertySelectors(FieldSelectors return propertySelectors.Values; } - private static void IncludeAllScalarProperties(Type elementType, Dictionary propertySelectors, IModel entityModel) + private static void IncludeAllScalarProperties(Type elementType, Dictionary propertySelectors, IReadOnlyModel entityModel) { - IEntityType entityType = entityModel.GetEntityTypes().Single(type => type.ClrType == elementType); + IReadOnlyEntityType entityType = entityModel.GetEntityTypes().Single(type => type.ClrType == elementType); - foreach (IProperty property in entityType.GetProperties().Where(property => !property.IsShadowProperty())) + foreach (IReadOnlyProperty property in entityType.GetProperties().Where(property => !property.IsShadowProperty())) { var propertySelector = new PropertySelector(property.PropertyInfo!); IncludeWritableProperty(propertySelector, propertySelectors); } - foreach (INavigation navigation in entityType.GetNavigations().Where(navigation => navigation.ForeignKey.IsOwnership && !navigation.IsShadowProperty())) + foreach (IReadOnlyNavigation navigation in entityType.GetNavigations() + .Where(navigation => navigation.ForeignKey.IsOwnership && !navigation.IsShadowProperty())) { var propertySelector = new PropertySelector(navigation.PropertyInfo!); IncludeWritableProperty(propertySelector, propertySelectors);