Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework automatic foreign key population

Is there any way to force Entity Framework to populate the foreign keys immediately when an entity is added to the context, rather than delaying it until something else happens with the context? This default behavior is not very helpful when using data binding to display referenced entities.

Just referencing any DbSet from the context is enough to force EF to populate the Parent and Parent_Name of the added Children. But nothing short of SaveChanges seems to force EF to populate the Reference or Reference_Name.

I'd really like to mark Reference_Name with the [Required] ttribute so it will be Not Null in the database, but if I do that, I get validation errors when I try to call SaveChanges unless I've explicitly set the Reference_Name, even though SaveChanges itself will correctly populate Reference_Name if Reference is set.

I'd really like to be able to set either Reference or Reference_Name and be able to immediately use the other. Likewise, I'd like to be able to immediately use Parent or Parent_Name after adding the Child object, without having to use the kludge of accessing some other element from the context first.

Can anyone help me understand why EF delays these things, and how I can force it to populate the foreign key properties or the foreign key columns, preferably immediately but at least without having to call SaveChanges? I'd really prefer not to have to fully populate all the properties explicitly when EF is going to correctly populate them anyway.

public class OracleContext : DbContext
{
    public virtual DbSet<Parent> Parents { get; set; }
    public virtual DbSet<Child> Children { get; set; }
    public virtual DbSet<Reference> References { get; set; }
    public virtual DbSet<SomethingElse> SomethingElses { get; set; }
}

public class Parent
{
    [Key, MaxLength(30)]
    public string Name { get; set; }

    [InverseProperty("Parent")]
    public virtual List<Child> Children { get; set; } = new List<Child>();
}

public class Child
{
    [Key, Column(Order = 1), MaxLength(30)]
    public string Parent_Name { get; set; }

    [Key, Column(Order = 2), MaxLength(30)]
    public string Name { get; set; }

    public string Reference_Name { get; set; }

    [ForeignKey("Parent_Name")]
    public virtual Parent Parent { get; set; }

    [ForeignKey("Reference_Name")]
    public virtual Reference Reference { get; set; }

    public Child Clone()
    {
        return new Child
        {
            Parent_Name = this.Parent_Name,
            Name = this.Name,
            Reference_Name = this.Reference_Name,
            Parent = this.Parent,
            Reference = this.Reference
        };
    }
}

public class Reference
{
    [Key, MaxLength(30)]
    public string Name { get; set; }
}

public class SomethingElse
{
    [Key, MaxLength(30)]
    public string Name { get; set; }
}

private void button1_Click(object sender, EventArgs e)
{
    OracleContext context = new OracleContext();

    Reference reference = context.References.Add(new Reference { Name = "Reference" });

    Parent alpha = context.Parents.Add(new Parent { Name = "Alpha" });

    Child alphaOne = new Child { Name = "AlphaOne" };
    Child alphatwo = new Child { Name = "AlphaTwo", Reference_Name = "Reference" };
    alpha.Children.AddRange(new List<Child> { alphaOne, alphatwo });
    alphaOne.Reference = reference;

    var list = (
            from child in alpha.Children
            select new
            {
                Time = "Before referencing SomethingElses.Local",
                Child = child.Clone()
            }
        ).ToList();

    var x = context.SomethingElses.Local;

    list.AddRange(
            from child in alpha.Children
            select new
            {
                Time = "After referencing SomethingElses.Local",
                Child = child.Clone()
            }
        );

    list.AddRange(
            from parent in context.Parents.Local
            from child in parent.Children
            select new
            {
                Time = "Before SaveChanges",
                Child = child.Clone()
            }
        );

    context.SaveChanges();

    list.AddRange(
            from parent in context.Parents.Local
            from child in parent.Children
            select new
            {
                Time = "After SaveChanges",
                Child = child.Clone()
            }
        );

    foreach (var item in list)
    {
        Console.WriteLine("{0}:\r\n\tName = '{1}'\r\n\tParent = '{2}' ({3})\r\n\tReference = '{4}' ({5})",
            item.Time, item.Child.Name, item.Child.Parent_Name, item.Child.Parent, item.Child.Reference_Name, item.Child.Reference);
    }
}

Before referencing SomethingElses.Local:
    Name = 'AlphaOne'
    Parent = '' ()
    Reference = '' (WindowsFormsApplication2.Reference)
Before referencing SomethingElses.Local:
    Name = 'AlphaTwo'
    Parent = '' ()
    Reference = 'Reference' ()
After referencing SomethingElses.Local:
    Name = 'AlphaOne'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = '' (WindowsFormsApplication2.Reference)
After referencing SomethingElses.Local:
    Name = 'AlphaTwo'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = 'Reference' ()
Before SaveChanges:
    Name = 'AlphaOne'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = '' (WindowsFormsApplication2.Reference)
Before SaveChanges:
    Name = 'AlphaTwo'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = 'Reference' ()
After SaveChanges:
    Name = 'AlphaOne'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = 'Reference' (WindowsFormsApplication2.Reference)
After SaveChanges:
    Name = 'AlphaTwo'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = 'Reference' (WindowsFormsApplication2.Reference)
like image 233
Matt Knowles Avatar asked Oct 19 '22 21:10

Matt Knowles


1 Answers

Is there any way to force Entity Framework to populate the foreign keys immediately when an entity is added to the context, rather than delaying it until something else happens with the context?

Option 1:

If you just want to fix the relationalship between entities without saving them to the database hence calling DbContext.SaveChanges() then just call DbContext.ChangeTracker.DetectChanges().

Option 2:

EF can automatically fixup relationalship between entites without caling DbContext.SaveChanges() or DbContext.ChangeTracker.DetectChanges(). Those entites are called Proxy classes. A proxy class is a dynamically generated derived type that acts as a proxy for the entity. This proxy overrides some virtual properties of the entity to insert hooks for performing actions automatically when the property is accessed. Proxy creation is enabled by default for your DbContext unless you disabled it by calling DbContext.Configuration.ProxyEnabled = false;. You don't need to add that line of code because you need proxy creation to be enabled.

Anyway you need to modify some things on your classes before taking advantage of this feature:

  • all properties (scalar, navigational, collection) must be marked as virtual
  • all navigational collections must be declared as ICollection<T>
  • all entity instatiation must be done by using DbContext.DbSet<T>.Create() method.
  • all collection instantiation must not be initialized into a constructor. Proxy class will take care of the instantiation and will throw an exception if you don't follow this point.

Following this steps your entites classes must look like this:

public class Parent
{
    [Key, MaxLength(30)]
    public virtual string Name { get; set; }

    [InverseProperty("Parent")]
    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    [Key, Column(Order = 1), MaxLength(30)]
    public virtual string Parent_Name { get; set; }

    [Key, Column(Order = 2), MaxLength(30)]
    public virtual string Name { get; set; }

    public virtual string Reference_Name { get; set; }

    [ForeignKey("Parent_Name")]
    public virtual Parent Parent { get; set; }

    [ForeignKey("Reference_Name")]
    public virtual Reference Reference { get; set; }

    public Child Clone()
    {
        return new Child
        {
            Parent_Name = this.Parent_Name,
            Name = this.Name,
            Reference_Name = this.Reference_Name,
            Parent = this.Parent,
            Reference = this.Reference
        };
    }
}

public class Reference
{
    [Key, MaxLength(30)]
    public virtual string Name { get; set; }
}

public class SomethingElse
{
    [Key, MaxLength(30)]
    public virtual string Name { get; set; }
}

Your click event handler impelmentation will look like this:

Reference reference = context.References.Create();
reference.Name = "Reference";
context.References.Add(reference);

Parent alpha = context.Parents.Create();
alpha.Name = "Alpha"; 
context.Parents.Add(alpha);

Child alphaOne = context.Children.Create();
alphaOne.Name = "AlphaOne";

Child alphatwo = context.Children.Create();
alphatwo.Name = "AlphaTwo";
alphatwo.Reference = reference; // Notice we use the navigational property.

alpha.Children.Add(alphaOne);
alpha.Children.Add(alphatwo);
alphaOne.Reference = reference;

var list = (
        from child in alpha.Children
        select new
        {
            Time = "Before referencing SomethingElses.Local",
            Child = child.Clone()
        }
    ).ToList();

var x = context.SomethingElses.Local;

list.AddRange(
        from child in alpha.Children
        select new
        {
            Time = "After referencing SomethingElses.Local",
            Child = child.Clone()
        }
    );

list.AddRange(
        from parent in context.Parents.Local
        from child in parent.Children
        select new
        {
            Time = "Before SaveChanges",
            Child = child.Clone()
        }
    );

context.SaveChanges();

list.AddRange(
        from parent in context.Parents.Local
        from child in parent.Children
        select new
        {
            Time = "After SaveChanges",
            Child = child.Clone()
        }
    );

foreach (var item in list)
{
    Console.WriteLine("{0}:\r\n\tName = '{1}'\r\n\tParent = '{2}' ({3})\r\n\tReference = '{4}' ({5})",
        item.Time, item.Child.Name, item.Child.Parent_Name, item.Child.Parent, item.Child.Reference_Name, item.Child.Reference);
}

There are two noticeable changes in this implementation:

  • as said you must use DbContext.DbSet.Create to get an instance of the generated proxy of T instead of using the default constructor which by pass the proxy.
  • one thing to know about Lazy Loading is that it check if the navigational property is loaded. If not then it check the database to load the entity. In your case all your entities are on Added state then doing Reference_Name = "Reference" will not help your context to lazy load the navigational property Refererence. This is why instead of doing alphatwo.Reference_Name = "Reference"; I did alphatwo.Reference = reference; because reference is on Added state, Lazy Load will find nothing on database.
like image 173
CodeNotFound Avatar answered Oct 30 '22 21:10

CodeNotFound