Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 779daea

Browse files
committed
Fixes two problems when using owned entities:
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.
1 parent 627bae9 commit 779daea

File tree

11 files changed

+111
-10
lines changed

11 files changed

+111
-10
lines changed

‎benchmarks/Tools/NeverResourceDefinitionAccessor.cs‎

Lines changed: 2 additions & 0 deletions
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;

‎src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs‎

Lines changed: 9 additions & 4 deletions
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
}

‎src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs‎

Lines changed: 18 additions & 1 deletion
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 />

‎src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs‎

Lines changed: 9 additions & 0 deletions
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>

‎src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs‎

Lines changed: 10 additions & 0 deletions
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);
Lines changed: 12 additions & 0 deletions
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+
}

‎test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingAttendee.cs‎

Lines changed: 3 additions & 0 deletions
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
}

‎test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationDbContext.cs‎

Lines changed: 10 additions & 0 deletions
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
}

‎test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationFakers.cs‎

Lines changed: 8 additions & 1 deletion
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;

‎test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs‎

Lines changed: 28 additions & 4 deletions
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"": {

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /