Croberts
Croberts

Reputation: 393

GraphDiff and EF6.1 – Recursive Many-to-Many Relationship

I am having an issue with GraphDiff attaching an object which has a recursive many-to-many relationship. I am using GraphDiff 2.0.1 and Entity Framework 6.1.1.

The progam manages Recipes which can have 0-n RecipeLines which contains a link to another Recipe which is the ComponentRecipe and this has RecipeLines and so on.

I pass a Recipe Object into the repository which has 1 recipe line with the Recipe object on the line pointing back to the parent and the ComponentRecipe object pointing to a different recipe.

When GraphDiff attaches the item then the attachedItem it returns has 2 recipe lines and both of these are the same and have the ComponentRecipe point to the same object as the Recipe which creates a loop on itself.

I am not sure if this is an issue with GraphDiff or more likely I have something wrong in my EF mappings or my GraphDiff mappings. Any help appreciated, let me know if you need more information and I can provide a simple program that demonstrates this issue.

Domain Models

public class Recipe : EntityBase
{
        public override object Key
        {
            get { return Id; }
        }

        public int Id { get; set; }

        public string Name { get; set; }

        public virtual IList<RecipeLine> RecipeLines { get; set; }

 }

public class RecipeLine : EntityBase
{
        public override object Key
        {
            get { return Id; }
        }

        public int Id { get; set; }

        public int RecipeId { get; set; }

        public int ComponentRecipeId { get; set; }

        public decimal Quantity { get; set; }

        public virtual Recipe Recipe { get; set; }

        public virtual Recipe ComponentRecipe { get; set; }
}

Data Context

class DataContext : DbContext
    {
        public DataContext()
            : base("Connection")
        {
            Configuration.ProxyCreationEnabled = false;
            Configuration.LazyLoadingEnabled = false;
        }

        public DbSet<Recipe> Recipes { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            Database.SetInitializer<DataContext>(null);

            modelBuilder.Entity<Recipe>().ToTable("Recipes");
            modelBuilder.Entity<Recipe>().HasKey(x => x.Id);
            modelBuilder.Entity<Recipe>().Property(x => x.Id).HasColumnName("Id");
            modelBuilder.Entity<Recipe>().Property(x => x.Name).HasColumnName("Name");

            modelBuilder.Entity<RecipeLine>().ToTable("RecipeLines");
            modelBuilder.Entity<RecipeLine>().HasKey(x => x.Id);
            modelBuilder.Entity<RecipeLine>().Property(x => x.Id).HasColumnName("Id");
            modelBuilder.Entity<RecipeLine>().Property(x => x.RecipeId).HasColumnName("RecipeId");
            modelBuilder.Entity<RecipeLine>().Property(x => x.ComponentRecipeId).HasColumnName("ComponentRecipeId");
            modelBuilder.Entity<RecipeLine>().Property(x => x.Quantity).HasColumnName("Quantity").HasPrecision(18, 5);

            modelBuilder.Entity<Recipe>().HasMany(x => x.RecipeLines).WithRequired(x => x.Recipe).HasForeignKey(x => x.RecipeId);

            modelBuilder.Entity<RecipeLine>().HasRequired(x => x.ComponentRecipe).WithMany().HasForeignKey(x => x.ComponentRecipeId);

        }
    }

Repository

public override void UpdateEntity(IEntity entity)
{
            using (var context = new DataContext())
            {
                context.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
                var item = (Recipe)entity;
                var attachedItem = context.UpdateGraph(item,
                    a => a.OwnedCollection(b => b.RecipeLines, c => c.AssociatedEntity(d => d.ComponentRecipe)));
                context.SaveChanges();
            }
}

Data Context Log on Attach

Opened connection at 14/09/2014 16:57:06 +01:00

SELECT 
    [Project1].[Id] AS [Id], 
    [Project1].[Name] AS [Name], 
    [Project1].[C1] AS [C1], 
    [Project1].[Id1] AS [Id1], 
    [Project1].[RecipeId] AS [RecipeId], 
    [Project1].[ComponentRecipeId] AS [ComponentRecipeId], 
    [Project1].[Quantity] AS [Quantity], 
    [Project1].[Id2] AS [Id2], 
    [Project1].[Name1] AS [Name1]
    FROM ( SELECT 
        [Limit1].[Id] AS [Id], 
        [Limit1].[Name] AS [Name], 
        [Join1].[Id1] AS [Id1], 
        [Join1].[RecipeId] AS [RecipeId], 
        [Join1].[ComponentRecipeId] AS [ComponentRecipeId], 
        [Join1].[Quantity] AS [Quantity], 
        [Join1].[Id2] AS [Id2], 
        [Join1].[Name] AS [Name1], 
        CASE WHEN ([Join1].[Id1] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM   (SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name]
            FROM [dbo].[Recipes] AS [Extent1]
            WHERE 11 = [Extent1].[Id] ) AS [Limit1]
        LEFT OUTER JOIN  (SELECT [Extent2].[Id] AS [Id1], [Extent2].[RecipeId] AS [RecipeId], [Extent2].[ComponentRecipeId] AS [ComponentRecipeId], [Extent2].[Quantity] AS [Quantity], [Extent3].[Id] AS [Id2], [Extent3].[Name] AS [Name]
            FROM  [dbo].[RecipeLines] AS [Extent2]
            INNER JOIN [dbo].[Recipes] AS [Extent3] ON [Extent2].[ComponentRecipeId] = [Extent3].[Id] ) AS [Join1] ON [Limit1].[Id] = [Join1].[RecipeId]
    )  AS [Project1]
    ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC


-- Executing at 14/09/2014 16:57:06 +01:00

-- Completed in 1 ms with result: SqlDataReader



Closed connection at 14/09/2014 16:57:06 +01:00

Opened connection at 14/09/2014 16:57:07 +01:00

SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Recipes] AS [Extent1]
    WHERE [Extent1].[Id] = 10


-- Executing at 14/09/2014 16:57:07 +01:00

-- Completed in 0 ms with result: SqlDataReader



Closed connection at 14/09/2014 16:57:07 +01:00

Data Context Log on SaveChanges

Opened connection at 14/09/2014 16:58:45 +01:00

INSERT [dbo].[RecipeLines]([RecipeId], [ComponentRecipeId], [Quantity])
VALUES (@0, @1, @2)
SELECT [Id]
FROM [dbo].[RecipeLines]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()


-- @0: '11' (Type = Int32)

-- @1: '11' (Type = Int32)

-- @2: '1' (Type = Decimal, Precision = 18, Scale = 5)

-- Executing at 14/09/2014 16:58:46 +01:00

-- Completed in 1 ms with result: SqlDataReader



Closed connection at 14/09/2014 16:58:46 +01:00

Upvotes: 2

Views: 367

Answers (1)

andyp
andyp

Reputation: 6269

This was a bug in GraphDiff. The code to attach parent navigation properties wasn't ready to handle multiple parents of the same type. I've just fixed that on the develop branch with pull request #102.

Upvotes: 2

Related Questions