Please explain how Update works in the Entity Framework

I don't understand how to work with Entity Framework. After Laravel and its ORM, it seems to me a completely unfriendly technology with a bunch of rakes. I have an entity (model):

    public class Funnel
    {
        public int FunnelId { get; set; }
        public Category Category { get; set; }
        public List<AdvertiserFunnel> AdvertiserFunnels { get; set; }
        public bool IsActive { get; set; }
        public bool IsChoiceAllApps { get; set; }
        public List<string> PackageIds { get; set; }
        public List<PushTemplate> PushTemplates { get; set; }

        public DateTime CreatedAt { get; set; }
        public string Title { get; set; }

        public Funnel()
        {
            CreatedAt = DateTime.UtcNow;
        }
    }

As you can see, there are one-to-many relationships (for example, Category), there are many-to-many (AdvertiserFunnels).

I have a repository. I don't work with pure context in controllers, here is the save method:

        public void Save(Funnel funnel)
        {
            if (funnel.FunnelId == 0)
            {
                _applicationDbContext.Funnels.Add(funnel);
            }
            else
            {
                _applicationDbContext.Funnels.Update(funnel);
            }

            _applicationDbContext.SaveChanges();
        }

I'm doing an Update. And I have two options. Here's the first one where I am updating the IsActive field. This is a banal checkbox on the UI, when activated, ajax is sent to the following method:

 public IActionResult SwitchActiveState([FromBody] JObject json)
        {
            int funnelId;
            bool isActive;
            try
            {
                funnelId = json.SelectToken("templateId").ToObject<int>();
                isActive = json.SelectToken("isActive").ToObject<bool>();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                return BadRequest();
            }

            var item =
                _funnelRepository.Funnels
                    .FirstOrDefault(x => x.FunnelId == funnelId);

            if (item == null)
            {
                return NotFound();
            }

            item.IsActive = isActive;

            _funnelRepository.Save(item);

            return Ok();
        }

I run all this, call the update, and everything works.

And now, I want to update the AdvertiserFunnels field. This is already an edit form where I have a select multiple for a given field. Here is the code for the update:

 public IActionResult Save([FromBody] FunnelViewModel viewModel)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var advertisersTask = _advertiserRepository.Advertisers
                .Where(x => viewModel.Advertisers.Select(y => y.AdvertiserId).Contains(x.AdvertiserId)).ToListAsync();

            var categoryTask = _categoryRepository.Categories
                .FirstOrDefaultAsync(x => viewModel.Category.CategoryId == x.CategoryId);

            Task.WhenAll(advertisersTask, categoryTask);

            var advertisers = advertisersTask.Result;
            var category = categoryTask.Result;

            if (!advertisers.Any())
            {
                ModelState.TryAddModelError("Advertisers", "Нет ни одного рекла");
            }

            if (category == null)
            {
                ModelState.TryAddModelError("Category", "Необходимо выбрать категорию");
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var advertisersFunnels = new List<AdvertiserFunnel>();

            var funnel = new Funnel();
            funnel.Category = category;
            funnel.IsActive = viewModel.IsActive;
            funnel.IsChoiceAllApps = viewModel.IsChoiceAllApps;
            funnel.Title = viewModel.Title;
            funnel.PackageIds = viewModel.Applications.Select(x => x.AppId).ToList();
            funnel.FunnelId = viewModel.FunnelId;

            foreach (var advertiser in advertisers)
            {
                advertisersFunnels.Add(new AdvertiserFunnel
                {
                    Funnel = funnel,
                    Advertiser = advertiser,
                });
            }

            funnel.AdvertiserFunnels = advertisersFunnels;

            _funnelRepository.Save(funnel);

            return Ok();
        }

I launch the update and:

enter a description of the image here

Okay, I think maybe you should write this in the repository:

...
            else
            {
                _applicationDbContext.AdvertiserFunnels.AttachRange(funnel.AdvertiserFunnels);
                _applicationDbContext.Funnels.Update(funnel);
            }
...

I go back to my checkbox (which was discussed earlier), poke and:

enter a description of the image here

Yes, what is it. Maybe you should:

 else
            {
                if (funnel.AdvertiserFunnels != null)
                {
                    _applicationDbContext.AdvertiserFunnels.AttachRange(funnel.AdvertiserFunnels);
                }

                _applicationDbContext.Funnels.Update(funnel);
            }

I poke - it works. Am I writing something wrong? Why are there so many checks and some crazy crutches? I will show you how it would look in Laravel (this is a php framework, if I am read by experienced dotnetchiki and such gentlemen simply do not know about the existence of some Laravel). So:

$funnel = Funnel::where('id', '=', $funnelId); // $funnelId, допустим, я получил из реквеста и это int

$funnel->isActive = true;
$funnel->save(); 

That's it. the first case. And everything works! Now many-to-many:

$funnel = Funnel::where('id', '=', $funnelId);
$funnel->advertisers()->sync($arrayWithAdvertisersIds);

, where advertisers() is the method set in the Funnel model to show the ORM that this is a many-to-many relationship. And $arrayWithAdvertisersIds is an array of int identifiers of the entity Advertiser, for example, obtained from a request. Isn't it more friendly? Who thinks what? Please criticize my code and I will be grateful for useful comments that could simplify the life of readers. Thanks for your attention!

Author: Aleksej_Shherbak, 2019-12-12

1 answers

Entity Framework implements the pattern Unit of work (Unit of Work, UoW). Its purpose is to represent business transactions at the domain level (not at the data access level).

The work unit stores a set of changes, such as adding, modifying, and deleting records. All changes are made to the database when calling the SaveChanges method. Copies of DB records are stored in the work unit in one of several states: not changed, changed, new, and deleted.

This approach is very convenient in some scenarios, but it imposes certain restrictions. We can't just delete the records in the database. We need to load them and mark them as deleted. In some cases, you can construct an entry in The unit of work and mark it as if it was loaded from the database, then "delete".

This may be necessary if you are implementing complex logic, or trying to optimize access to data. My recommendation: first make a simple working solution, and then, if the speed is really low, deal with the records at a low level.

So, an easy way to do something with an existing record is to load it first.

Deleting

    public IAsyncResult DeleteById(int id)
    {
        var record = dbContext.Funnels.SingleOrDefault(x => x.Id == id);
        if (record == null)
            return NotFound();

        dbContext.Funnels.Remove(record);
        dbContext.SaveChanges();
        . . .
    }

Update

    public IAsyncResult UpdateById(int id, FunnelModel model)
    {
        var record = dbContext.Funnels.SingleOrDefault(x => x.Id == id);
        if (record == null)
            return NotFound();

        record.Foo = model.Foo;
        record.Bar = model.Bar;
        record.Baz = model.Baz;

        dbContext.SaveChanges();
        . . .
    }

Note that when updating, you do not need to explicitly mark the record as changed, that is, explicitly call Update. EF when loading from the database saves it in the "no changes" state, and before saving it, independently searches for all the changed records.

If you think it's slow to download records to delete or update them, then you're right. Unit of Work is not about the speed of work, but about the convenience in certain scenarios.

When creating a record, we don't read anything, we just call the Add method:

Creating

    public IAsyncResult Create(FunnelModel model)
    {
        dbContext.Records.Add(new Funnel
        {
            Foo = model.Foo,
            Bar = model.Bar,
            Baz = model.Baz,
        });

        dbContext.SaveChanges();
        . . .
    }

In this scenario, there may be problems with collisions records. If you have a surrogate key, the value of which is generated in the database, usually everything is fine. But if you control the key value yourself, you can try to write something to the database that is already stored there.

This scenario often occurs when you try to add or change an entry that is available through the navigation property.

        var record = dbContext.Funnels.SingleOrDefault(x => x.Id == id);
        if (record == null)
            return NotFound();

        record.AdvertiserFunnels.Add(new AdvertiserFunnel { . . . });

Here we load the record Funnel from the database, but do not load the associated records AdvertiserFunnels. We are creating a new linked record, and maybe this is this will result in an error. For example, EF can delete old records because they are not in its database image.

A big plus Units of work and specifically EF is that it allows you to work with a group of records as a whole. In DDD, there is the concept of an aggregate is such a complex object consisting of several simple entities that are mapped to tables.

To avoid problems, ask EF to load the main record Funnel along with the navigation data. properties that we plan to change. This is done using the Include method.

        var record = dbContext.Funnels
                              .Include(x => x.AdvertiserFunnels)
                              .SingleOrDefault(x => x.Id == id);
        if (record == null)
            return NotFound();

        record.AdvertiserFunnels.Add(new AdvertiserFunnel { . . . });

Now EF knows exactly which child records are in the database and can easily create a new one.

As far as I understand, the error in your program occurs precisely because the navigation properties are not loaded in dbContext.

UPDATE

I will write more about the levels. In classical three-tier applications, the level of the subject domain depended on the level of access to data. As a result, to load the entities, you directly at the level of the subject area, for example, in the cortex of some service, climbed into the database:

class FooService
{
   public void MakeSomething()
   {
       using (var connection = new SqlConnection(_connectionString))
       using (var command = connection.CreateCommand();
       {
           . . .
           using (var dataReader = command.ExecuteReader())
           {
               var foo = Foo.CreatFromDataReader(dataReader);
               . . .
           }
       }
   }
}

This code has two problems. The first is that the subject area is now tied to SQL. Usually this argument is not accepted, do you often have to change the lower level? In my experience, yes, I do, but not often.

The second problem is that the domain classes start to know too much about the details storage. They don't need this information, but we can't do without it. The class Foo must know about System.Data.IDataReader and must be able to read itself from there.

If we need a transaction, we can do it directly for the SQL connection.

By inverting the dependency, and introducing the repository interface, we will remove these details from the domain level. A repository is a large repository of our entities that does not fit into memory - such knowledge is sufficient at the level of subject area.

Everything is great, but the repository methods can only provide atomicity and consistency at the level of individual aggregates.

Aggregates are composite entities. For example, an order in a store in the database is stored in two tables: the order itself and its positions. But at the Java/C#/Python code level, you will have one aggregate, the root entity of which will be the order, and in addition to the order, there will be other entities-order items.

The order repository can make multiple edits to a single order within a single transaction. And what if we have a scenario where different aggregates are involved?

It is clear where to do this - if we have several agragats that we operate with, then we have some kind of business scenario. For business scenarios, we create service classes (service classes). But do not create there SqlTransaction manually?

The Unit of Work pattern is used to hide the transaction. Its interface (as well as repository interfaces), described at the domain level. The implementation is done below - at the data access level.

As for the controllers, they are above the domain level. This is the level of user interaction or interaction with an external program. The classic name is the view level.

 3
Author: Mark Shevchenko, 2019-12-12 10:27:14