0

I have a new Problem, that I don't know how it came up.

public static int SaveNewProjectGraphic(string svgString, int pageId, ApplicationDbContext db, string user = "System")
    {
        db = new ApplicationDbContext();   //tried with new and with db-context given from controller-action
        LayoutGraphic layoutGraphic = new LayoutGraphic()
        {
            byUser = user,
            EditorSettingId = pageId,
            SvgString = svgString
        };

        db.LayoutGraphics.Add(layoutGraphic);
        db.SaveChanges();
        layoutGraphic.EditorSetting.Project.LastChange = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        layoutGraphic.EditorSetting.Project.byUser = user;
        db.Entry(layoutGraphic.EditorSetting.Project).State = EntityState.Modified;
        string newId = layoutGraphic.Id.ToString();
        string newSvgString = svgString.Replace("newGraphicId", "g" + newId);
        layoutGraphic.SvgString = newSvgString;
        db.Entry(layoutGraphic).State = EntityState.Modified;
        db.SaveChanges();
        return layoutGraphic.Id;
    }

In the static function above, I create a new record.
After db.SaveChanges() I normally got the virtual record (EditorSetting), that is associated with the new record.
Now I could use this association to Change some properties (fields) of that record.

I am 100% sure that this worked before but somehow this is broken (not only on this model), so I only get a null-reference-exception on EditorSetting (And of Course Project, which is associated with Editorsetting).
I have no idea, what I have done :-(

Here the LayouGraphic-model (Detail):

public class LayoutGraphic : BaseEntity
{
    [Display(Name = "SVG")]
    public string SvgString { get; set; }

    public int EditorSettingId { get; set; }
    public virtual EditorSetting EditorSetting{ get; set; }
}

And the associated master-model:

public class EditorSetting
{
    //defaults setzen
    public EditorSetting()
    {
        ViewBox = "0 0 600 500";
        Created = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        LastChange = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
    }

    public int Id { get; set; }
    public int ProjectId { get; set; }
    public virtual Project Project { get; set; }

    public string ViewBox { get; set; }
    public string ViewName { get; set; }
    public string Pagenumber { get; set; }

    public string Created { get; set; }
    public string LastChange { get; set; }
    public string byUser { get; set; }

    //1:n Beziehung zu Unterfunktionen
    public virtual ICollection<SubFunction> SubFunctions { get; set; }
}

Any ideas? Thanks Carsten

Telefisch
  • 305
  • 1
  • 19
  • I don't see where you ever fetched EditorSetting or passed in its values. SaveChanges() won't do that - see [this post](https://stackoverflow.com/questions/26610337/get-entity-navigation-properties-after-insert). – Steve Greene Jan 23 '20 at 15:05

2 Answers2

1

This behavior difference is a consequence of long-living DbContexts. By declaring a new DbContext in the method, you will never see the EditorSetting reference updated by setting the EditorSettingId FK on the entity and saving it.

When you use a longer-lived DbContext then you may see it updated, provided the DBContext knows about it. (The referenced EditorSetting is a tracked entity in the DbContext)

For example:

Assuming we have a Parent and Child entity. Parent contains a collection of Children, and Child contains a ParentId FK and a virtual Parent navigation property. Parent ID#1 exists in the database:

using (var context = new TestDbContext())
{
    var newChild = new Child { Name = "Freddy", BirthDate = new DateTime(1999, 3, 3), ParentId = 1 };
    context.Children.Add(newChild);
    context.SaveChanges();
}

Now at this point after the SaveChanges, if you look at the Parent reference for newChild, it will be #null. Instead if you do this:

using (var context = new TestDbContext())
{
    var neverUsedParent = context.Parents.Single(x => x.ParentId == 1);

    var newChild = new Child { Name = "Freddy", BirthDate = new DateTime(1999, 3, 3), ParentId = 1 };
    context.Children.Add(newChild);
    context.SaveChanges();
}

In this example, after the save changes you will see that the Parent reference is present, even though we didn't associate it, we had still only set the ID.

When you have a long-lived DbContext, a difference in behavior when setting FK properties is bound to happen, and it can have really weird consequences that are revealed at runtime. If one of the EditorSettings had been previously loaded from the DB by that DbContext, creating a new record with that EditorSettingId would fill in the association, while another EditorSettingId would not. (Because the DbContext didn't know about it) That EditorSetting may have been loaded anywhere in the code prior to this call provided the same DbContext instance was used.

Even doing this:

using (var context = new TestDbContext())
{
    var newChild = new Child { Name = "Freddy", BirthDate = new DateTime(1999, 3, 3), ParentId = 1 };
    context.Children.Add(newChild);
    context.SaveChanges();

    // newChild.Parent == #null here....

    var neverUsedParent = context.Parents.Single(x => x.ParentId == 1);

    // newChild.Parent is available here!
}

In general it is better to avoid having both navigation properties and FKs mapped. I advise entities should use one or the other. When using navigation properties you can map FKs without properties using Shadow Properties in EF Core or Map.MapKey() in EF 6. For cases where I want to load/manipulate large amounts of data and don't need the related details I will use FKs with no navigation properties in those entities.

Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • Thank you for this great answer, Steve. This all sounds plausible. As you can see in the Parameters, I also tried to use existing dbContext, which is used in the controller-class but had the same results. My thoughts were. to save time without explicit loading the master-recordsetes but I think this will be the safest way? So in future I would prefer to work with the context of the Controller (using it as Parameter) and load the needed data explicitly. Does it make any difference to load the exisiting FK before creating the new child, instead of loading the data after creating? – Telefisch Jan 24 '20 at 08:01
  • 1
    As long as the navigation property is a proxy (virtual) then it doesn't matter if the entity is loaded before or after, which can make the behaviour even more unpredictable. The safest way is to either use just FKs or just navigation properties rather than both. With both, even if you are careful and always use the nav property, the FK will not be updated automatically until SaveChanges gets called. Changing a Child from Parent 1 to 3: Before that you will have a Parent with ID = 3 associated, but the Child's ParentId still reports ID 1. – Steve Py Jan 24 '20 at 23:33
0

Couple of things come to mind why this might happen:

  1. There was a change from lazy loading to eager loading in the context (see here), so after the first SaveChanges, accessing the EditorSettings property used to load it from the database.
  2. An EditoSetting object with that specific id already existed in the database. This is not relevant if you create a new context in the method. Howeveryou mentioned it used to be in the controller, so maybe the something in the flow was changed.
Shahafo
  • 235
  • 1
  • 7