Skip to content

Commit

Permalink
Fixes two problems when using owned entities:
Browse files Browse the repository at this point in the history
1. Owned entity properties are not retrieved when using sparse fieldsets (because they are modeled as navigations in EF Core, instead of scalar properties)
2. When producing a LINQ query that includes an owned entity, EF Core produces an error, indicating that the query must be marked as non-tracked. Due to potential performance impact, a virtual method is provided that enables tweaking the behavior.
  • Loading branch information
bkoelman committed May 12, 2023
1 parent 627bae9 commit 779daea
Show file tree
Hide file tree
Showing 11 changed files with 111 additions and 10 deletions.
2 changes: 2 additions & 0 deletions benchmarks/Tools/NeverResourceDefinitionAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace Benchmarks.Tools;
/// </summary>
internal sealed class NeverResourceDefinitionAccessor : IResourceDefinitionAccessor
{
bool IResourceDefinitionAccessor.IsReadOnlyRequest => throw new NotImplementedException();

public IImmutableSet<IncludeElementExpression> OnApplyIncludes(ResourceType resourceType, IImmutableSet<IncludeElementExpression> existingIncludes)
{
return existingIncludes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,17 @@ private ICollection<PropertySelector> ToPropertySelectors(FieldSelectors fieldSe

private void IncludeAllScalarProperties(Type elementType, Dictionary<PropertyInfo, PropertySelector> propertySelectors)
{
IEntityType entityModel = _entityModel.GetEntityTypes().Single(type => type.ClrType == elementType);
IEnumerable<IProperty> entityProperties = entityModel.GetProperties().Where(property => !property.IsShadowProperty()).ToArray();
IEntityType entityType = _entityModel.GetEntityTypes().Single(type => type.ClrType == elementType);

foreach (IProperty entityProperty in entityProperties)
foreach (IProperty property in entityType.GetProperties().Where(property => !property.IsShadowProperty()))
{
var propertySelector = new PropertySelector(entityProperty.PropertyInfo!);
var propertySelector = new PropertySelector(property.PropertyInfo!);
IncludeWritableProperty(propertySelector, propertySelectors);
}

foreach (INavigation navigation in entityType.GetNavigations().Where(navigation => navigation.ForeignKey.IsOwnership && !navigation.IsShadowProperty()))
{
var propertySelector = new PropertySelector(navigation.PropertyInfo!);
IncludeWritableProperty(propertySelector, propertySelectors);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,24 @@ protected virtual IQueryable<TResource> ApplyQueryLayer(QueryLayer queryLayer)

protected virtual IQueryable<TResource> GetAll()
{
return _dbContext.Set<TResource>();
IQueryable<TResource> source = _dbContext.Set<TResource>();

return GetTrackingBehavior() switch
{
QueryTrackingBehavior.NoTrackingWithIdentityResolution => source.AsNoTrackingWithIdentityResolution(),
QueryTrackingBehavior.NoTracking => source.AsNoTracking(),
QueryTrackingBehavior.TrackAll => source.AsTracking(),
_ => source
};
}

protected virtual QueryTrackingBehavior? GetTrackingBehavior()
{
// EF Core rejects the way we project sparse fieldsets when owned entities are involved, unless the query is explicitly
// marked as non-tracked (see https://github.com/dotnet/EntityFramework.Docs/issues/2205#issuecomment-1542914439).
#pragma warning disable CS0618
return _resourceDefinitionAccessor.IsReadOnlyRequest ? QueryTrackingBehavior.NoTrackingWithIdentityResolution : null;
#pragma warning restore CS0618
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ namespace JsonApiDotNetCore.Resources;
/// </summary>
public interface IResourceDefinitionAccessor
{
/// <summary>
/// Indicates whether this request targets only fetching of data (resources and relationships), as opposed to applying changes.
/// </summary>
/// <remarks>
/// This property was added to reduce the impact of taking a breaking change. It will likely be removed in the next major version.
/// </remarks>
[Obsolete("Use IJsonApiRequest.IsReadOnly.")]
bool IsReadOnlyRequest { get; }

/// <summary>
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnApplyIncludes" /> for the specified resource type.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ public class ResourceDefinitionAccessor : IResourceDefinitionAccessor
private readonly IResourceGraph _resourceGraph;
private readonly IServiceProvider _serviceProvider;

/// <inheritdoc />
public bool IsReadOnlyRequest
{
get
{
var request = _serviceProvider.GetRequiredService<IJsonApiRequest>();
return request.IsReadOnly;
}
}

public ResourceDefinitionAccessor(IResourceGraph resourceGraph, IServiceProvider serviceProvider)
{
ArgumentGuard.NotNull(resourceGraph);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using JetBrains.Annotations;

namespace JsonApiDotNetCoreTests.IntegrationTests.Serialization;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public sealed class Address
{
public string Street { get; set; } = null!;
public string? ZipCode { get; set; }
public string City { get; set; } = null!;
public string Country { get; set; } = null!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public sealed class MeetingAttendee : Identifiable<Guid>
[Attr]
public string DisplayName { get; set; } = null!;

[Attr]
public Address HomeAddress { get; set; } = null!;

[HasOne]
public Meeting? Meeting { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Microsoft.EntityFrameworkCore;
using TestBuildingBlocks;

// @formatter:wrap_chained_method_calls chop_always

namespace JsonApiDotNetCoreTests.IntegrationTests.Serialization;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
Expand All @@ -14,4 +16,12 @@ public SerializationDbContext(DbContextOptions<SerializationDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<MeetingAttendee>()
.OwnsOne(meetingAttendee => meetingAttendee.HomeAddress);

base.OnModelCreating(builder);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ internal sealed class SerializationFakers : FakerContainer
private readonly Lazy<Faker<MeetingAttendee>> _lazyMeetingAttendeeFaker = new(() =>
new Faker<MeetingAttendee>()
.UseSeed(GetFakerSeed())
.RuleFor(attendee => attendee.DisplayName, faker => faker.Random.Utf16String()));
.RuleFor(attendee => attendee.DisplayName, faker => faker.Random.Utf16String())
.RuleFor(attendee => attendee.HomeAddress, faker => new Address
{
Street = faker.Address.StreetAddress(),
ZipCode = faker.Address.ZipCode(),
City = faker.Address.City(),
Country = faker.Address.Country()
}));

public Faker<Meeting> Meeting => _lazyMeetingFaker.Value;
public Faker<MeetingAttendee> MeetingAttendee => _lazyMeetingAttendeeFaker.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
""type"": ""meetingAttendees"",
""id"": """ + meeting.Attendees[0].StringId + @""",
""attributes"": {
""displayName"": """ + meeting.Attendees[0].DisplayName + @"""
""displayName"": """ + meeting.Attendees[0].DisplayName + @""",
""homeAddress"": {
""street"": """ + meeting.Attendees[0].HomeAddress.Street + @""",
""zipCode"": """ + meeting.Attendees[0].HomeAddress.ZipCode + @""",
""city"": """ + meeting.Attendees[0].HomeAddress.City + @""",
""country"": """ + meeting.Attendees[0].HomeAddress.Country + @"""
}
},
""relationships"": {
""meeting"": {
Expand Down Expand Up @@ -191,7 +197,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
""type"": ""meetingAttendees"",
""id"": """ + attendee.StringId + @""",
""attributes"": {
""displayName"": """ + attendee.DisplayName + @"""
""displayName"": """ + attendee.DisplayName + @""",
""homeAddress"": {
""street"": """ + attendee.HomeAddress.Street + @""",
""zipCode"": """ + attendee.HomeAddress.ZipCode + @""",
""city"": """ + attendee.HomeAddress.City + @""",
""country"": """ + attendee.HomeAddress.Country + @"""
}
},
""relationships"": {
""meeting"": {
Expand Down Expand Up @@ -465,7 +477,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
""type"": ""meetingAttendees"",
""id"": """ + meeting.Attendees[0].StringId + @""",
""attributes"": {
""displayName"": """ + meeting.Attendees[0].DisplayName + @"""
""displayName"": """ + meeting.Attendees[0].DisplayName + @""",
""homeAddress"": {
""street"": """ + meeting.Attendees[0].HomeAddress.Street + @""",
""zipCode"": """ + meeting.Attendees[0].HomeAddress.ZipCode + @""",
""city"": """ + meeting.Attendees[0].HomeAddress.City + @""",
""country"": """ + meeting.Attendees[0].HomeAddress.Country + @"""
}
},
""relationships"": {
""meeting"": {
Expand Down Expand Up @@ -704,7 +722,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
""type"": ""meetingAttendees"",
""id"": """ + existingAttendee.StringId + @""",
""attributes"": {
""displayName"": """ + existingAttendee.DisplayName + @"""
""displayName"": """ + existingAttendee.DisplayName + @""",
""homeAddress"": {
""street"": """ + existingAttendee.HomeAddress.Street + @""",
""zipCode"": """ + existingAttendee.HomeAddress.ZipCode + @""",
""city"": """ + existingAttendee.HomeAddress.City + @""",
""country"": """ + existingAttendee.HomeAddress.Country + @"""
}
},
""relationships"": {
""meeting"": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response;

internal sealed class FakeResourceDefinitionAccessor : IResourceDefinitionAccessor
{
bool IResourceDefinitionAccessor.IsReadOnlyRequest => throw new NotImplementedException();

public IImmutableSet<IncludeElementExpression> OnApplyIncludes(ResourceType resourceType, IImmutableSet<IncludeElementExpression> existingIncludes)
{
return existingIncludes;
Expand Down

0 comments on commit 779daea

Please sign in to comment.