Home > Enterprise >  Entity Framework Core - Relationship between owned child and another chained owned child
Entity Framework Core - Relationship between owned child and another chained owned child

Time:05-09

I am stuck with Problem, that I am trying to solve the right way.

I have multiple one-to-many 'owning' relationships. But I have a duplicate relationship between two children Story should have a List of Variables and chained child Change should point to one Variable from this List. I cannot set one class to be owned by 2 entities and with just a 'having' relationship I am not able to save the whole structure at once. Example of how my code structure looks.

Edit: Another Problem comes with TransitionOption that has Step to which it belongs and next Step to which it points.

public class Story //Main structure example class - A before Edit
{
   [Key]
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public int Id { get; set; }

   ...name, creator...
   
   [ForeignKey("FK_Story_ID")]
   public List<Step> Steps { get; set; } //Set as OwnsMany

   [ForeignKey("FK_Story_ID")]
   public List<Variable> Variables { get; set; } //Set as OwnsMany , Full list of Variables, that will be shown, which can but does not need to be changed during Step transitions
}

//example class with some main text about changes like part of the story
public class Step 
{
   [Key]
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public int Id { get; set; }

   ...some text...

   [ForeignKey("FK_Step_ID")]
   public List<TransitionOption> TransitionOptions { get; set; } //Set as OwnsMany

   public int FK_Story_ID { get; set; }
}

//example class for some option pointing to another Step
public class TransitionOption
{
   [Key]
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public int Id { get; set; }       

   [ForeignKey("FK_TransitionOption_ID")]
   public List<Change> Changes { get; set; } //Set as OwnsMany

   public int FK_Step_ID { get; set; }

   public int FK_NextStep_ID { get; set; } //this will also cause problem and I forgot to mention it before Edit
}

//referencing to one Variable from List Variables in Story
public class Change
{
   [Key]
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public int Id { get; set; }

   [ForeignKey("FK_Change_ID")]
   public Variable ChangingVariable { get; set; } //Set as OwnsOne/HasOne - or another way around with List of Changes in Variable. But this is more suitable for my case

public int FK_TransitionOption_ID { get; set; }
}

//Child that I need to have in both one Story (Story OwnsMany Variables) and multiple Changes (Change has/owns one Variable respectively Variable has/owns multiple Changes)
public class Variable 
{
   [Key]
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public int Id { get; set; }

   ...Starting value, conditions, type (text/number)...

   public int FK_Change_ID { get; set; }
}

States will be held during the final process, so I don't need to track them in the Database. This is more like a Template and Holder for all the transitions and variables. This example shows something like an interactive story. It kind of matches my relationships.

Problem is, that I do not know how to best specify this relationship for EF Core right. Edit: Another Problem comes with TransitionOption that has Step to which it belongs and the next Step to which it points. This Problem I have found out during editation to more readable example format.

I was trying Owning the relationship Story-Variable and having the Owning relationship Change-Variable or Variable-Change which throws an error on migration. With having relationship Story-Variable, saving throws foreign key violation, as FK is 0, because Story was not saved yet and has no ID, and 0 is at this point somehow taken as a valid ID. I would like to save it all as one (if possible) and load it all as one, as I have Angular front end expecting it and sending it back edited.

If something is missing or not easy to understand, I will try to better answer and edit this. Thanks for all comments.

I hope someone has already solved a scenario like this, but I can not find anything for a case like this in the documentation. Please help.

EDIT: I have changed the structure to be more like an Example that I gave to @grek40 (Thanks). I cannot publish real classes, but this example should be almost 1:1 to Problems, that I have.

CodePudding user response:

Basically, OwnsMany is a special kind of relationship that's not suitable as soon as you have multiple entities with navigation properties to the same target. In such cases, you should use HasMany relationship.

When you have multiple references to the same entity (like TransitionOptions that belongs to some CurrentStep and points to some NextStep), use FluentApi or annotate with InversePropertyAnnotation, not with ForeignKeyAnnotation.

Generally, I'd advise you to use more fluent api configuration and less annotation based configuration - it's easier to maintain.

Here is some sample code based on your question.

Model classes

public class Story
{
    public int Id { get; set; }

    public List<Step>? Steps { get; set; }

    public List<Variable>? Variables { get; set; }
}

public class Variable
{
    public int Id { get; set; }

    public int StoryId { get; set; }

    public string? Value { get; set; }

    public Story? Story { get; set; }

    public List<Change>? Changes { get; set; }
}

public class Step
{
    public int Id { get; set; }

    public int StoryId { get; set; }

    public Story? Story { get; set; }

    public List<TransitionOption>? TransitionOptions { get; set; }
}

public class TransitionOption
{
    public int Id { get; set; }

    public int StepId { get; set; }

    public int? NextStepId { get; set; }

    public Step? CurrentStep { get; set; }

    public Step? NextStep { get; set; }
    
    public List<Change>? Changes { get; set; }
}

public class Change
{
    // If combination of TransitionOptionId and VariableId is unique,
    // you can use them as composite key instead
    public int Id { get; set; }

    public int TransitionOptionId { get; set; }

    public int VariableId { get; set; }

    public Variable? Variable { get; set; }

    public TransitionOption? TransitionOption { get; set; }
}

Entity Configuration classes

internal class StoryConfiguration : IEntityTypeConfiguration<Story>
{
    public void Configure(EntityTypeBuilder<Story> builder)
    {
        builder.HasMany(x => x.Variables)
            .WithOne(x => x.Story)
            .HasForeignKey(x => x.StoryId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.HasMany(x => x.Steps)
            .WithOne(x => x.Story)
            .HasForeignKey(x => x.StoryId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}
internal class VariableConfiguration : IEntityTypeConfiguration<Variable>
{
    public void Configure(EntityTypeBuilder<Variable> builder)
    {
        builder.HasMany(x => x.Changes)
            .WithOne(x => x.Variable)
            .HasForeignKey(x => x.VariableId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}
internal class StepConfiguration : IEntityTypeConfiguration<Step>
{
    public void Configure(EntityTypeBuilder<Step> builder)
    {
        builder.HasMany(x => x.TransitionOptions)
            .WithOne(x => x.CurrentStep)
            .HasForeignKey(x => x.StepId)
            .OnDelete(DeleteBehavior.Cascade);

        // No navigation property in Step for TransitionOptions pointing
        // to this as NextStep.
        // It would also be possible to define such a property and mention it
        // in HasMany
        builder.HasMany<TransitionOption>()
            .WithOne(x => x.NextStep)
            .HasForeignKey(x => x.NextStepId)
            // If next step is deleted, only disconnect referencing TransitionOptions
            .OnDelete(DeleteBehavior.SetNull);
    }
}
internal class TransitionOptionConfiguration : IEntityTypeConfiguration<TransitionOption>
{
    public void Configure(EntityTypeBuilder<TransitionOption> builder)
    {
        builder.HasMany(x => x.Changes)
            .WithOne(x => x.TransitionOption)
            .HasForeignKey(x => x.TransitionOptionId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

Apply the configurations within the OnModelCreating method.

Sample method to create a whole story structure with 1 SaveChanges

public async Task InitDb(MyDatabase db)
{
    var variable1 = new Variable
    {
        Value = "1st Value"
    };
    db.Stories?.Add(new Story
    {
        Variables = new List<Variable>
        {
            variable1
        },
        Steps = new List<Step>
        {
            new Step
            {
                TransitionOptions = new List<TransitionOption>
                {
                    new TransitionOption
                    {
                        Changes = new List<Change>
                        {
                            new Change
                            {
                                Variable=variable1
                            }
                        }
                    }
                }
            }
        }
    });
    await db.SaveChangesAsync();
}

Hope this example shows everything you need for your described structure.

  • Related