Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make EntityEntryGraphIterator publicly usable #26461

Closed
Tracked by #22954
voroninp opened this issue Oct 25, 2021 · 10 comments · Fixed by #28459
Closed
Tracked by #22954

Make EntityEntryGraphIterator publicly usable #26461

voroninp opened this issue Oct 25, 2021 · 10 comments · Fixed by #28459
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-enhancement
Milestone

Comments

@voroninp
Copy link

voroninp commented Oct 25, 2021

Currently EntityEntryGraphNode constructor is internal and depends on internal types.


I want to automatically change concurrency token of an aggregate root when any of its entities changes.
It's is possible that AR entity has no changes, but child entities do. So I'd like to traverse the graph and check whether there are modified, added, or deleted entities. And if yes, Version property should be incremented.

There's is a TrackGraph method to adjust tracking for disconnected entities, but I want to traverse already attached graph. Do I have to write this functionality myself using EntityEntry api for accessing navigations?

I am also curious how I detect additions and removals of child entities? Is NavigationEntry.IsModified enough for this?

@voroninp
Copy link
Author

voroninp commented Oct 25, 2021

Ended up having such implementation (not tested yet):

private bool GraphHasChanges(TAggregateRoot aggregateRoot) => GraphHasChangesRecursive(aggregateRoot, new HashSet<object>());

private bool GraphHasChangesRecursive(object rootEntity, HashSet<object> checkedEntities)
{
    // prevents cycles.
    if (checkedEntities.Contains(rootEntity))
    {
        return false;
    }

    var rootEntityEntry = _dbContext.Entry(rootEntity);
    if (rootEntityEntry.State == EntityState.Detached)
    {
        throw new InvalidOperationException($"Root entity of type '{rootEntity.GetType()}' is not tracked.");
    }

    checkedEntities.Add(rootEntityEntry);

    if (rootEntityEntry.State != EntityState.Unchanged)
    {
        return true;
    }

    var changesInChildEntities = rootEntityEntry.Navigations.Any(_ => _.IsModified || _.CurrentValue switch
    {
        IEnumerable seq => seq.OfType<object>().Any(entity => GraphHasChangesRecursive(entity, checkedEntities)),
        object entity => GraphHasChangesRecursive(entity, checkedEntities)
    });

    if (changesInChildEntities)
    {
        return true;
    }

    return false;
}

@ajcvickers
Copy link
Contributor

@voroninp Have you considered using the IEntityEntryGraphIterator service?

@voroninp
Copy link
Author

@ajcvickers I was not aware of it, thanks. Should I resolve it with service provider, or is it accessible through DbContext?

@voroninp
Copy link
Author

voroninp commented Oct 28, 2021

@ajcvickers

handleNode:

Func<EntityEntryGraphNode<TState>,Boolean>

Is returned bool for signaling whether to continue traversal?

@ajcvickers
Copy link
Contributor

context.GetService. Yes.

@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label Oct 28, 2021
@voroninp
Copy link
Author

you won't get rid of me that easy =)

Constructor of EntityEntryGraphNode has this documentation:

This is an internal API that supports the Entity Framework Core infrastructure and not subject to the same compatibility standards as public APIs. It may be changed or removed without notice in any release. You should only use it directly in your code with extreme caution and knowing that doing so can result in application failures when updating to a new Entity Framework Core release.

And not much about its arguments. =(
Could you elaborate on them, please?

@ajcvickers
Copy link
Contributor

@voroninp Don't call the constructor. Use CreateNode.

@voroninp
Copy link
Author

voroninp commented Nov 3, 2021

Where can CreateNode method be found? It's virtual on EntityEntryGraphNode, so I need to have a node firts.

@ajcvickers
Copy link
Contributor

@voroninp You are right. We should look into making this available publicly. For now, you can use TrackGraph, without actually tracking anything. Here's an example, based on this test:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog { get; set; }
}

public class SomeDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(Your.ConnectionString)
            //.LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    public virtual DbSet<Blog> Blogs { get; set; }
}

public class Program
{
    public static void Main()
    {
        using (var context = new SomeDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.Add(new Blog {Posts = {new(), new()}});

            context.SaveChanges();
        }

        using(var context = new SomeDbContext())
        {
            var blog = context.Blogs.Include(e => e.Posts).Single();

            var visited = new HashSet<object>();
            var traversal = new List<string>();

            context.ChangeTracker.TrackGraph(
                blog,
                visited,
                node =>
                {
                    if (node.NodeState.Contains(node.Entry.Entity))
                    {
                        return false;
                    }

                    node.NodeState.Add(node.Entry.Entity);

                    traversal.Add(NodeString(node));

                    return true;
                });

            foreach (var visit in traversal)
            {
                Console.WriteLine(visit);
            }
        }
    }

    private static string NodeString(EntityEntryGraphNode node)
        => EntryString(node.SourceEntry)
           + " ---"
           + node.InboundNavigation?.Name
           + "--> "
           + EntryString(node.Entry);

    private static string EntryString(EntityEntry entry)
        => entry == null
            ? "<None>"
            : entry.Metadata.DisplayName()
              + ":"
              + entry.Property(entry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue;
}

Output:

<None> -----> Blog:1
Blog:1 ---Posts--> Post:1
Blog:1 ---Posts--> Post:2

@ajcvickers ajcvickers reopened this Nov 8, 2021
@ajcvickers ajcvickers changed the title Is there an out of the box functionality to traverse a graph of tracked entities? Make EntityEntryGraphIterator publicly usable Nov 8, 2021
@ajcvickers ajcvickers removed the closed-no-further-action The issue is closed and no further action is planned. label Nov 8, 2021
@ajcvickers ajcvickers added this to the 7.0.0 milestone Nov 10, 2021
@ajcvickers ajcvickers self-assigned this Nov 10, 2021
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Jul 15, 2022
@voroninp
Copy link
Author

Thanks, guys!

@ajcvickers ajcvickers modified the milestones: 7.0.0, 7.0.0-rc1 Jul 24, 2022
@ajcvickers ajcvickers modified the milestones: 7.0.0-rc1, 7.0.0 Nov 5, 2022
@ajcvickers ajcvickers removed their assignment Aug 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants