22

I have three tables. Word -> WordForm -> SampleSentence. Each Word has different WordForms and then each form can have one or more SampleSentence

CREATE TABLE [dbo].[Word] (
    [WordId]       VARCHAR (20) NOT NULL,
    [CategoryId]   INT          DEFAULT ((1)) NOT NULL,
    [GroupId]      INT          DEFAULT ((1)) NOT NULL,
    PRIMARY KEY CLUSTERED ([WordId] ASC),
    CONSTRAINT [FK_WordWordCategory] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[WordCategory] ([WordCategoryId]),
    CONSTRAINT [FK_WordWordGroup] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[WordGroup] ([WordGroupId])
);

CREATE TABLE [dbo].[WordForm] (
    [WordFormId]   VARCHAR (20)  NOT NULL,
    [WordId]       VARCHAR (20)  NOT NULL,
    [Primary]      BIT           DEFAULT ((0)) NOT NULL,
    [PosId]        INT           NOT NULL,
    [Definition]   VARCHAR (MAX) NULL,
    PRIMARY KEY CLUSTERED ([WordFormId] ASC),
    CONSTRAINT [FK_WordFormPos] FOREIGN KEY ([PosId]) REFERENCES [dbo].[Pos] ([PosId]),
    CONSTRAINT [FK_WordFormWord] FOREIGN KEY ([WordId]) REFERENCES [dbo].[Word] ([WordId])
);


CREATE TABLE [dbo].[SampleSentence] (
    [SampleSentenceId] INT           IDENTITY (1, 1) NOT NULL,
    [WordFormId]       VARCHAR (20)  NOT NULL,
    [Text]             VARCHAR (MAX) NOT NULL,
    CONSTRAINT [PK_SampleSentence] PRIMARY KEY CLUSTERED ([SampleSentenceId] ASC),
    CONSTRAINT [FK_SampleSentenceWordForm] FOREIGN KEY ([WordFormId]) REFERENCES [dbo].[WordForm] ([WordFormId])
);

I am taking the data from these tables to a front-end client and this then modifies the data and adds or deletes WordForms and SampleSentences.

I then bring the data back to the server.

Is there some way that Entity Framework can check to see changes in the object that I bring back to the server and make changes to the database or do I have to do some form of comparison where I check the before and after of the Word, WordForm and Sample Sentence objects?

For reference here are the C# objects I'm using:

public class Word
    {
        public string WordId { get; set; } // WordId (Primary key) (length: 20)
        public int CategoryId { get; set; } // CategoryId
        public int GroupId { get; set; } // GroupId

        // Reverse navigation
        public virtual System.Collections.Generic.ICollection<WordForm> WordForms { get; set; } // WordForm.FK_WordFormWord

        // Foreign keys
        public virtual WordCategory WordCategory { get; set; } // FK_WordWordCategory
        public virtual WordGroup WordGroup { get; set; } // FK_WordWordGroup

        public Word()
        {
            CategoryId = 1;
            GroupId = 1;
            WordForms = new System.Collections.Generic.List<WordForm>();
        }
    }

public class WordForm
    {
        public string WordFormId { get; set; } // WordFormId (Primary key) (length: 20)
        public string WordId { get; set; } // WordId (length: 20)
        public bool Primary { get; set; } // Primary
        public int PosId { get; set; } // PosId
        public string Definition { get; set; } // Definition

        // Reverse navigation
        public virtual System.Collections.Generic.ICollection<SampleSentence> SampleSentences { get; set; } // SampleSentence.FK_SampleSentenceWordForm

        // Foreign keys
        public virtual Pos Pos { get; set; } // FK_WordFormPos
        public virtual Word Word { get; set; } // FK_WordFormWord

        public WordForm()
        {
            Primary = false;
            SampleSentences = new System.Collections.Generic.List<SampleSentence>();
        }
    }

public class SampleSentence : AuditableTable
    {
        public int SampleSentenceId { get; set; } // SampleSentenceId (Primary key)
        public string WordFormId { get; set; } // WordFormId (length: 20)
        public string Text { get; set; } // Text

        // Foreign keys
        public virtual WordForm WordForm { get; set; } // FK_SampleSentenceWordForm
    }

Here is what I have been able to come up with so far but this does not include checking for the SampleSentence and I am not sure how to do that:

    public async Task<IHttpActionResult> Put([FromBody]Word word)
    {
        var oldObj = db.WordForms
            .Where(w => w.WordId == word.WordId)
            .AsNoTracking()
            .ToList();
        var newObj = word.WordForms.ToList();

        var upd = newObj.Where(n => oldObj.Any(o =>
            (o.WordFormId == n.WordFormId) && (o.PosId != n.PosId || !o.Definition.Equals(n.Definition) )))
            .ToList();
        var add = newObj.Where(n => oldObj.All(o => o.WordFormId != n.WordFormId))
            .ToList();
        var del = oldObj.Where(o => newObj.All(n => n.WordFormId != o.WordFormId))
            .ToList();
        foreach (var wordForm in upd)
        {
            db.WordForms.Attach(wordForm);
            db.Entry(wordForm).State = EntityState.Modified;
        }
        foreach (var wordForm in add)
        {
            db.WordForms.Add(wordForm);
        }
        foreach (var wordForm in del)
        {
            db.WordForms.Attach(wordForm);
            db.WordForms.Remove(wordForm);
        }
        db.Words.Attach(word);
        db.Entry(word).State = EntityState.Modified;
        await db.SaveChangesAsync(User, DateTime.UtcNow);
        return Ok(word);
    }
Alan2
  • 23,493
  • 79
  • 256
  • 450
  • 2
    Related: http://stackoverflow.com/questions/9503286/adding-removing-and-updating-related-entities. Anyway is keeping the existing records important? Otherwise you could delete the `Word` and simply re-insert the POSTed `Word`. – CodeCaster May 13 '16 at 10:20
  • Alan you can reduce the burden if you can use Jquery to detect changes and maintain a flag which you can later send to server side and then decide if you want to update the table or not – Rajshekar Reddy May 13 '16 at 10:26
  • What about Self Tracking Entities? Yes they are not recommended any more but that does not mean they cannot solve your problem. – Evk May 13 '16 at 10:33
  • @CodeCaster - I will look into your suggestion. That sounds like a good idea. Although on the server side I think I will need to have a way to identify all the new WordForm and SampleSentences so that EF will know to add these. – Alan2 May 13 '16 at 10:35
  • @Alan Were you able to address your issue and how did you address it? – CShark May 16 '16 at 15:55
  • There is no longer an EF 7, it's been EF Core 1.0 [for a while now](https://github.com/aspnet/Announcements/issues/144). I know, I know, but still--EF is dead, long live EF and all that. – Marc L. May 18 '16 at 15:13

4 Answers4

18

Sorry, no

The answer to your question literally (as in the title) is no. There is no way to do this automatically with Entity Framework. In what is called disconnected scenarios, properly saving changes from a client is something developers should take care of themselves.

As mentioned, EF used to have self-tracking entities, but soon this approach was deprecated although in official documentation it was never clearly explained why. Probably because "STEs made (change tracking) easier, but at the cost of making almost everything else really hard.." It fitted perfectly in the ObjectContext API with database-first-generated class models with t4 templates, but, as we all know, the DbContext API and code-first have become EF's recommended (and soon the only supported) architecture. Of course, with code-first, EF can't enforce any STE implementation.

Or...?

It's somewhat frustrating that EF never filled this gap later, for instance by supplying an API similar to what GraphDiff offers (or maybe by now I should say offered). There are two reasonable alternatives that I'm aware of.

Entity Framework's proposition

Lerman and Miller, in their book Programming Entity Framework: DbContext, proposed an alternative technique that was the closest to a substitute of self-tracking entities the EF team has come up with so far. It revolves around this interface:

public interface IObjectWithState
{
    State State { get; set; }
    Dictionary<string, object> OriginalValues { get; set; }
}

Where State is

public enum State
{
    Added,
    Unchanged,
    Modified,
    Deleted
}

For this approach to work properly, each entity should implement the interface. Further, each DbContext subclass needs a number of methods. A method to populate the OriginalValues property when an entity is materialized and methods to synchronize its change tracker with changes recorded in entities when they are returned to a context. It's too much to copy all this code here, you can find it in the book, starting at page 102.

Well, if you implement all that, you have self-tracking entities of sorts. It's quite elaborate, although once implemented, it'll "just work". However, a major drawback is that all consumers of your context must set this State property when an entity is added or deleted. That's a daunting responsibility to impose on client code!

Breeze

Breeze offers a complete solution that goes all the way from the DAL in your service to javascript code in the client. That's both incredibly convenient and incredibly scary.

In javascript you get a LINQ-like syntax:

var query = breeze.EntityQuery
           .from("Customers")
           .where("CompanyName", "startsWith", "A")
           .orderBy("CompanyName");

This communicates with Breeze's EntityManager in C# code:

var manager = new Breeze.Sharp.EntityManager(serviceName);
var results = await manager.ExecuteQuery(query);

This EntityManager basically is a wrapper around an EF context. If all the moving parts have been set up properly, it virtually brings the EF context into your javascript, with change tracking, saving changes and all. I work with it in one project and really, it's very convenient.

But if you use Breeze, it's Breeze all the way. It affects everything. A change in the database schema requires changes in javascript. That's scary, but something you can get used to. But if you want to do things your own way, it becomes very hard (though not impossible) to bend Breeze to your needs. Like living with your mother in law. I think in many cases, in the end a combination of Breeze and other patterns becomes inevitable.

But should you want it anyway?

Generally speaking, a major drawback of any automated tracking of disconnected entities is that it makes it far too easy to use original entity objects for data transfer. The thing is, in most cases, full entities contain far more data than the client requires (or is allowed to see). Using dedicated slim DTOs can improve performance dramatically. And of course they act as an abstraction layer between DAL and UI/controllers.

Yes, with DTOs we always have to "repaint the state" server-side. So be it. It really is the recommended approach for disconnected scenarios.

John Papa, in his PluralSight course on the hot-towel template for SPAs, when explaining Breeze, recognizes this problem. He proposes a solution with "partial entities". It is a solution, but quite elaborate and clunky. And of course, still the entities are at the base of the data transfer.

Community
  • 1
  • 1
Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • 5
    _...it becomes very hard (though not impossible) to bend Breeze to your needs. Like living with your mother in law._ **Bravo sir.** – Rob Davis May 16 '16 at 20:58
7

As mentioned by one of the previous posters, this is not natively supported in EF and is one of the most requested features.

However, It is possible, if you are willing to use the GraphDiff library (or write something similar yourself). I highly recommend that you use the open source GraphDiff library to make updates to disconnected graphs in EntityFramework.

Please take a look at this article, by Brent McKendrick, for instructions on how to do this.

The github repository for this project can also be found here

From the above article, updating a graph of related entities is as simple as the below example:

using (var context = new TestDbContext())  
{
    // Update the company and state that the company 'owns' the collection contacts, with contacts having associated advertisement options.
    context.UpdateGraph(company, map => map
        .OwnedCollection(p => p.Contacts, with => with
            .AssociatedCollection(p => p.AdvertisementOptions))
        .OwnedCollection(p => p.Addresses)
    );

    context.SaveChanges();
}

Please note that this project is not being maintained any longer, although I have used it (as well as many others) without any serious issues.

CShark
  • 2,183
  • 1
  • 24
  • 42
  • I would like to add that as a personal preference EF is not well suited to complex database-write operations. A better approach is rather to make use of T-SQL stored procedures for complex database-write operations. – CShark Jul 21 '19 at 12:35
2

Yes you can, by using the following low-impact libraries that do what you need:

https://github.com/AutoMapper/AutoMapper https://github.com/TylerCarlson1/Automapper.Collection

I usually do pass a reduced form of the involved classes to the client (containing only a reduced set of columns/properties), for various reasons.

Note that EF classes are in the ORM namespace, whereas the reduced classes are in the current namespace.

Below you can find some sample code:

private IMapper map;

private void InitMapper()
{
    map = new MapperConfiguration(cfg => {
        cfg.CreateMap<ORM.SampleSentence, SampleSentence>();
        cfg.CreateMap<ORM.WordForm, WordForm>();
        cfg.CreateMap<ORM.Word, Word>();
        cfg.CreateMap<SampleSentence, ORM.SampleSentence>();
        cfg.CreateMap<WordForm, ORM.WordForm>();
        cfg.CreateMap<Word, ORM.Word>();
        cfg.AddProfile<CollectionProfile>();
    }).CreateMapper();

    EquivilentExpressions.GenerateEquality.Add(new GenerateEntityFrameworkPrimaryKeyEquivilentExpressions<YourDbContext>(map));
}

public async Task<IHttpActionResult> Put([FromBody]Word word)
{
    var dbWord = db.Word.Include(w => w.WordForm).Where(w => w.WordId == word.WordId).First();

    if (dbWord != null)
    {
        InitMapper();
        map.Map(word, dbWord);
        db.SaveChanges();

        var ret = map.Map<Word>(dbWord);

        return Ok(ret);
    }
    else
    {
        return NotFound();
    }

}

This solution runs nicely and EF will issue an update containing only the fields that have actually changed.

Note: Using AutoMapper you can also configure which fields have to be updated/mapped (also differently for each direction). Example: you want some fields to be "read-only" for the user, you can mark them with opt.Ignore() in the Object->ORM.Object direction

    cfg.CreateMap<Word, ORM.Word>()
        .ForMember(dest => dest.WordId, opt => opt.Ignore())
zpul
  • 1,535
  • 1
  • 14
  • 19
1

Have a look at this topics: Entity Framework 5 Updating a Record, Update relationships when saving changes of EF4 POCO objects. I feel like it describes well all possibilities and their pros and cons. Basically every tracking system will force you to store somewhere the original entity for comparison or to query for it again. Mentioned Self Tracking Entities do the same - you have to store original object in session or view state which will degrade performance of a page (here more about it: Self Tracking Entities vs POCO Entities). As for me I would just change the state of the object to modified and let ef update the database (just as you do it right now in the Put([FromBody]Word word) method) and this is not only mine perspective - Entity Framework: Update related entities.

A lot of people requested such merge capabilities (http://entityframework.codeplex.com/workitem/864), but for now EF doesn't provide any support for it. However, some developers notice that there are some alternatives like NHibernate that have builtin capabilities like that - http://www.ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html.

Community
  • 1
  • 1
Lesmian
  • 3,932
  • 1
  • 17
  • 28