Skip to content

Commit 2596635

Browse files
authored
Merge pull request #1272 from json-api-dotnet/fix-owned-entities
Fixes for EF Core owned entities
2 parents 627bae9 + 779daea commit 2596635

File tree

11 files changed

+111
-10
lines changed

11 files changed

+111
-10
lines changed

Diff for: benchmarks/Tools/NeverResourceDefinitionAccessor.cs

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ namespace Benchmarks.Tools;
1212
/// </summary>
1313
internal sealed class NeverResourceDefinitionAccessor : IResourceDefinitionAccessor
1414
{
15+
bool IResourceDefinitionAccessor.IsReadOnlyRequest => throw new NotImplementedException();
16+
1517
public IImmutableSet<IncludeElementExpression> OnApplyIncludes(ResourceType resourceType, IImmutableSet<IncludeElementExpression> existingIncludes)
1618
{
1719
return existingIncludes;

Diff for: src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,17 @@ private ICollection<PropertySelector> ToPropertySelectors(FieldSelectors fieldSe
150150

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

156-
foreach (IProperty entityProperty in entityProperties)
155+
foreach (IProperty property in entityType.GetProperties().Where(property => !property.IsShadowProperty()))
157156
{
158-
var propertySelector = new PropertySelector(entityProperty.PropertyInfo!);
157+
var propertySelector = new PropertySelector(property.PropertyInfo!);
158+
IncludeWritableProperty(propertySelector, propertySelectors);
159+
}
160+
161+
foreach (INavigation navigation in entityType.GetNavigations().Where(navigation => navigation.ForeignKey.IsOwnership && !navigation.IsShadowProperty()))
162+
{
163+
var propertySelector = new PropertySelector(navigation.PropertyInfo!);
159164
IncludeWritableProperty(propertySelector, propertySelectors);
160165
}
161166
}

Diff for: src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

+18-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,24 @@ protected virtual IQueryable<TResource> ApplyQueryLayer(QueryLayer queryLayer)
151151

152152
protected virtual IQueryable<TResource> GetAll()
153153
{
154-
return _dbContext.Set<TResource>();
154+
IQueryable<TResource> source = _dbContext.Set<TResource>();
155+
156+
return GetTrackingBehavior() switch
157+
{
158+
QueryTrackingBehavior.NoTrackingWithIdentityResolution => source.AsNoTrackingWithIdentityResolution(),
159+
QueryTrackingBehavior.NoTracking => source.AsNoTracking(),
160+
QueryTrackingBehavior.TrackAll => source.AsTracking(),
161+
_ => source
162+
};
163+
}
164+
165+
protected virtual QueryTrackingBehavior? GetTrackingBehavior()
166+
{
167+
// EF Core rejects the way we project sparse fieldsets when owned entities are involved, unless the query is explicitly
168+
// marked as non-tracked (see https://github.com/dotnet/EntityFramework.Docs/issues/2205#issuecomment-1542914439).
169+
#pragma warning disable CS0618
170+
return _resourceDefinitionAccessor.IsReadOnlyRequest ? QueryTrackingBehavior.NoTrackingWithIdentityResolution : null;
171+
#pragma warning restore CS0618
155172
}
156173

157174
/// <inheritdoc />

Diff for: src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ namespace JsonApiDotNetCore.Resources;
1111
/// </summary>
1212
public interface IResourceDefinitionAccessor
1313
{
14+
/// <summary>
15+
/// Indicates whether this request targets only fetching of data (resources and relationships), as opposed to applying changes.
16+
/// </summary>
17+
/// <remarks>
18+
/// This property was added to reduce the impact of taking a breaking change. It will likely be removed in the next major version.
19+
/// </remarks>
20+
[Obsolete("Use IJsonApiRequest.IsReadOnly.")]
21+
bool IsReadOnlyRequest { get; }
22+
1423
/// <summary>
1524
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnApplyIncludes" /> for the specified resource type.
1625
/// </summary>

Diff for: src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ public class ResourceDefinitionAccessor : IResourceDefinitionAccessor
1515
private readonly IResourceGraph _resourceGraph;
1616
private readonly IServiceProvider _serviceProvider;
1717

18+
/// <inheritdoc />
19+
public bool IsReadOnlyRequest
20+
{
21+
get
22+
{
23+
var request = _serviceProvider.GetRequiredService<IJsonApiRequest>();
24+
return request.IsReadOnly;
25+
}
26+
}
27+
1828
public ResourceDefinitionAccessor(IResourceGraph resourceGraph, IServiceProvider serviceProvider)
1929
{
2030
ArgumentGuard.NotNull(resourceGraph);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using JetBrains.Annotations;
2+
3+
namespace JsonApiDotNetCoreTests.IntegrationTests.Serialization;
4+
5+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
6+
public sealed class Address
7+
{
8+
public string Street { get; set; } = null!;
9+
public string? ZipCode { get; set; }
10+
public string City { get; set; } = null!;
11+
public string Country { get; set; } = null!;
12+
}

Diff for: test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingAttendee.cs

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ public sealed class MeetingAttendee : Identifiable<Guid>
1111
[Attr]
1212
public string DisplayName { get; set; } = null!;
1313

14+
[Attr]
15+
public Address HomeAddress { get; set; } = null!;
16+
1417
[HasOne]
1518
public Meeting? Meeting { get; set; }
1619
}

Diff for: test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationDbContext.cs

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using Microsoft.EntityFrameworkCore;
33
using TestBuildingBlocks;
44

5+
// @formatter:wrap_chained_method_calls chop_always
6+
57
namespace JsonApiDotNetCoreTests.IntegrationTests.Serialization;
68

79
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
@@ -14,4 +16,12 @@ public SerializationDbContext(DbContextOptions<SerializationDbContext> options)
1416
: base(options)
1517
{
1618
}
19+
20+
protected override void OnModelCreating(ModelBuilder builder)
21+
{
22+
builder.Entity<MeetingAttendee>()
23+
.OwnsOne(meetingAttendee => meetingAttendee.HomeAddress);
24+
25+
base.OnModelCreating(builder);
26+
}
1727
}

Diff for: test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationFakers.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ internal sealed class SerializationFakers : FakerContainer
2929
private readonly Lazy<Faker<MeetingAttendee>> _lazyMeetingAttendeeFaker = new(() =>
3030
new Faker<MeetingAttendee>()
3131
.UseSeed(GetFakerSeed())
32-
.RuleFor(attendee => attendee.DisplayName, faker => faker.Random.Utf16String()));
32+
.RuleFor(attendee => attendee.DisplayName, faker => faker.Random.Utf16String())
33+
.RuleFor(attendee => attendee.HomeAddress, faker => new Address
34+
{
35+
Street = faker.Address.StreetAddress(),
36+
ZipCode = faker.Address.ZipCode(),
37+
City = faker.Address.City(),
38+
Country = faker.Address.Country()
39+
}));
3340

3441
public Faker<Meeting> Meeting => _lazyMeetingFaker.Value;
3542
public Faker<MeetingAttendee> MeetingAttendee => _lazyMeetingAttendeeFaker.Value;

Diff for: test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs

+28-4
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
142142
""type"": ""meetingAttendees"",
143143
""id"": """ + meeting.Attendees[0].StringId + @""",
144144
""attributes"": {
145-
""displayName"": """ + meeting.Attendees[0].DisplayName + @"""
145+
""displayName"": """ + meeting.Attendees[0].DisplayName + @""",
146+
""homeAddress"": {
147+
""street"": """ + meeting.Attendees[0].HomeAddress.Street + @""",
148+
""zipCode"": """ + meeting.Attendees[0].HomeAddress.ZipCode + @""",
149+
""city"": """ + meeting.Attendees[0].HomeAddress.City + @""",
150+
""country"": """ + meeting.Attendees[0].HomeAddress.Country + @"""
151+
}
146152
},
147153
""relationships"": {
148154
""meeting"": {
@@ -191,7 +197,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
191197
""type"": ""meetingAttendees"",
192198
""id"": """ + attendee.StringId + @""",
193199
""attributes"": {
194-
""displayName"": """ + attendee.DisplayName + @"""
200+
""displayName"": """ + attendee.DisplayName + @""",
201+
""homeAddress"": {
202+
""street"": """ + attendee.HomeAddress.Street + @""",
203+
""zipCode"": """ + attendee.HomeAddress.ZipCode + @""",
204+
""city"": """ + attendee.HomeAddress.City + @""",
205+
""country"": """ + attendee.HomeAddress.Country + @"""
206+
}
195207
},
196208
""relationships"": {
197209
""meeting"": {
@@ -465,7 +477,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
465477
""type"": ""meetingAttendees"",
466478
""id"": """ + meeting.Attendees[0].StringId + @""",
467479
""attributes"": {
468-
""displayName"": """ + meeting.Attendees[0].DisplayName + @"""
480+
""displayName"": """ + meeting.Attendees[0].DisplayName + @""",
481+
""homeAddress"": {
482+
""street"": """ + meeting.Attendees[0].HomeAddress.Street + @""",
483+
""zipCode"": """ + meeting.Attendees[0].HomeAddress.ZipCode + @""",
484+
""city"": """ + meeting.Attendees[0].HomeAddress.City + @""",
485+
""country"": """ + meeting.Attendees[0].HomeAddress.Country + @"""
486+
}
469487
},
470488
""relationships"": {
471489
""meeting"": {
@@ -704,7 +722,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
704722
""type"": ""meetingAttendees"",
705723
""id"": """ + existingAttendee.StringId + @""",
706724
""attributes"": {
707-
""displayName"": """ + existingAttendee.DisplayName + @"""
725+
""displayName"": """ + existingAttendee.DisplayName + @""",
726+
""homeAddress"": {
727+
""street"": """ + existingAttendee.HomeAddress.Street + @""",
728+
""zipCode"": """ + existingAttendee.HomeAddress.ZipCode + @""",
729+
""city"": """ + existingAttendee.HomeAddress.City + @""",
730+
""country"": """ + existingAttendee.HomeAddress.Country + @"""
731+
}
708732
},
709733
""relationships"": {
710734
""meeting"": {

Diff for: test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/FakeResourceDefinitionAccessor.cs

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response;
99

1010
internal sealed class FakeResourceDefinitionAccessor : IResourceDefinitionAccessor
1111
{
12+
bool IResourceDefinitionAccessor.IsReadOnlyRequest => throw new NotImplementedException();
13+
1214
public IImmutableSet<IncludeElementExpression> OnApplyIncludes(ResourceType resourceType, IImmutableSet<IncludeElementExpression> existingIncludes)
1315
{
1416
return existingIncludes;

0 commit comments

Comments
 (0)