dotnet / dotnet/efcore

Add an API to retrieve owned entries for a given EntityEntry.

Open
#33,428 0 comments 7 reactions 0 assignees View on GitHub
area-change-tracking area-owned-entities customer-reported
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

Hi All,

In the last couple of versions owned entities were extended and improved, and some really great features have been added. We got JSON support, then JSON arrays, and so on. And we're thankful for that :)
On the other hand, working with the tracker and retrieving the correct information is getting increasingly difficult. It would be great if there is an exposed API to retrieve owned entries for a given EntityEntry. Something like `entry.GetOwnedEntries()`.

To describe the motivation behind this request, let's examine a soft-delete implementation (and its evolution) as a sample.
- Option 1. In the beginning, this was simple.
```csharp
public override Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries())
{
if (entry.State != EntityState.Deleted) continue;

entry.State = EntityState.Modified;
entry.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true;
}

return base.SaveChangesAsync(cancellationToken);
}
```

- Option 2. With the introduction of owned entity types, the above implementation no longer works. In the case of `OwnsOne`, the type might be mapped to the same parent table. Considering that owned types internally are designed as fully blown entities (with generated PK/FK shadow properties), then for each parent, we must find the owned types and change their state as well. Failing to do so will result in an exception.
```csharp
public override Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries())
{
if (entry.State != EntityState.Deleted) continue;

entry.State = EntityState.Modified;
entry.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true;

var ownedEntries = entry.References
.Where(x => x.TargetEntry is not null && x.TargetEntry.Metadata.IsOwned());

foreach (var ownedEntry in ownedEntries)
{
if (ownedEntry.TargetEntry is not null)
{
ownedEntry.TargetEntry.State = EntityState.Modified;
}
}
}

return base.SaveChangesAsync(cancellationToken);
}
```

- Option 3. In the implementation above, we didn't have to account for `OwnsMany` since they won't be mapped to the same table anyway. But, with the introduction of JSON columns that is possible now. The `entry.References` doesn't include collections, and searching through `entry.Collections` or `entry.Navigations` is not straightforward either. So, we end up with the following.

```csharp
public override Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
List? ownedEntries = null;

foreach (var entry in ChangeTracker.Entries())
{
if (entry.State != EntityState.Deleted) continue;

entry.State = EntityState.Modified;
entry.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true;

ownedEntries ??= ChangeTracker.Entries()
.Where(x => x.State == EntityState.Deleted && x.Metadata.IsOwned())
.ToList();

foreach (var ownedEntry in ownedEntries)
{
if (ownedEntry.Metadata.IsInOwnershipPath(entry.Metadata))
{
ownedEntry.State = EntityState.Modified;
}
}
}

return base.SaveChangesAsync(cancellationToken);
}
```

I do believe this infrastructure will keep evolving, and having an API to deal with it will abstract the consumers from the internal design changes.
In addition to the above, retrieving entries by state is also cumbersome at the moment. Usually, we end up with the following extensions.

```csharp
public static class ChangeTrackerExtensions
{
public static bool IsAdded(this EntityEntry entry) =>
entry.State == EntityState.Added;

public static bool IsModified(this EntityEntry entry) =>
entry.State != EntityState.Added &&
(entry.State == EntityState.Modified ||
entry.References.Any(r => r.TargetEntry is not null &&
r.TargetEntry.Metadata.IsOwned() &&
(r.TargetEntry.State == EntityState.Added ||
r.TargetEntry.State == EntityState.Modified)));
}
```

Thank you.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.