SINETHEMBA PAULA
SINETHEMBA PAULA

Reputation: 31

DbContext.SaveChanges() not updating database after modifying the entity

I am reading some data from a database using entity framework(reads successfully)and modifying the entity works perfectly but it does not save changes to the database.

The code reads 15 records but it does not iterate through them all inside the loop. It goes through the first element of the list and stops at DbContext.SaveChanges() without saving changes.

Any idea what is wrong with my code?

public async Task LinkCardAsync(postcardContext DbContext)
        {
            HttpClient cons = new HttpClient();
            cons.BaseAddress = new Uri("http://cbsswfint01:53303/");
            cons.DefaultRequestHeaders.Accept.Clear();
            cons.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
            using (cons)
            {
                try
                {
                    var cardsData = DbContext.MasterpassBulkLoad.Take(15).ToList();
                    foreach (var cardDetails in cardsData)
                    {
                        Console.WriteLine(cardDetails.Msisdn);
                        Console.WriteLine(cardDetails.Status == null);
                        HttpResponseMessage res = await cons.PostAsJsonAsync("api/Cardmanagement/CardLoad", cardDetails);
                        cardDetails.Status = (int)res.StatusCode;
                        Console.WriteLine("before save");
                        DbContext.SaveChanges();
                        Console.WriteLine("After Save");
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.InnerException.Message);
                    _logger.LogError(ex.InnerException.StackTrace);
                }
            }
        }
public void Run()
        {
            using (var DbContext = new postcardContext())
            {
                _cardLinkService.LinkCardAsync(DbContext);
            }
            Console.ReadKey();


        }

Upvotes: 1

Views: 1594

Answers (1)

Steve Py
Steve Py

Reputation: 34773

Firstly, your calling code should be awaiting the call to the card link service.

However, I believe the crux of the problem is that DbContexts are not thread-safe so you should avoid accessing a context after a non-context related awaited async operation. (PostAsJsonAsync) Depending on the synchronization context, awaited code can resume on a different thread.

For instance where [#] denotes a thread ID you will have:

      [1]  using (var DbContext = new postcardContext())
      [1]  {
      [1]      _cardLinkService.LinkCardAsync(DbContext);

-- > (into method)

       ...
      [1]          var cardsData = DbContext.MasterpassBulkLoad.Take(15).ToList();
      [1]          foreach (var cardDetails in cardsData)
      [1]          {
      [1]              Console.WriteLine(cardDetails.Msisdn);
      [1]              Console.WriteLine(cardDetails.Status == null);
      [1]              HttpResponseMessage res = await cons.PostAsJsonAsync("api/Cardmanagement/CardLoad", cardDetails);
      [2]              cardDetails.Status = (int)res.StatusCode;
      [2]              Console.WriteLine("before save");
      [2]              DbContext.SaveChanges();

.. in the first iteration, the next iteration would continue on thread #2, then after the await, resume on another thread. Maybe #1, maybe #3.

To observe thread switching, you can add:

Console.WriteLine(cardDetails.Status == null);
Console.WriteLine(string.Format("Thread #{0} (before)", Thread.CurrentThread.ManagedThreadId));
HttpResponseMessage res = await cons.PostAsJsonAsync("api/Cardmanagement/CardLoad", cardDetails);
Console.WriteLine(string.Format("Thread #{0} (after)", Thread.CurrentThread.ManagedThreadId));
cardDetails.Status = (int)res.StatusCode;

First up, remove the async calls/signature and try a simple synchronous solution to verify the data is persisting as expected. From there you can perform an async operation but I would wrap the data load, web service call, and data update/save operation into a single awaitable method with a single SaveChangesAsync call with the DbContext scoped to the method call.

Disclaimer: If this is going to be running as a console application, async/await is purely unnecessary. If this were processing on a web server as part of servicing a request then using Async/await can make the server more responsive to requests by freeing up request handling threads while a background thread churns through the HTTP notification and DB updates. The design does not look to be parallel-processing friendly (A different barrel of fun vs. async/await) given the way records are selected for processing. Parallel runs would end up fetching potentially some of the same 15 records in a run with each other. From what I've seen in the code I'd say async provides no benefit, if anything it will make it run slower overall. But to outline the async method, it would look something like this:

using (var cons = new HttpClient())
{
    cons.BaseAddress = new Uri("http://cbsswfint01:53303/");
    cons.DefaultRequestHeaders.Accept.Clear();
    cons.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
    try
    {
       using(var context = new DbContext())
       {
           var cardsData = await context.MasterpassBulkLoad.Take(15).ToListAsync();
           foreach (var cardDetails in cardsData)
           {
                Console.WriteLine(cardDetails.Msisdn);
                Console.WriteLine(cardDetails.Status == null);
                HttpResponseMessage res = cons.PostAsJson("api/Cardmanagement/CardLoad", cardDetails);
                        cardDetails.Status = (int)res.StatusCode;
           }
           Console.WriteLine("before save");
           await context.SaveChangesAsync();
           Console.WriteLine("After Save");
       }
   }
   catch (Exception ex)
   {
           Console.WriteLine(ex.InnerException.Message);
           _logger.LogError(ex.InnerException.StackTrace);
   }
}

The above code will save the results in batches of 15, so if any 1 row fails for any reason, none will save. You should also check the "Take 15" behaviour as this looks like it would always pick up the same 15 rows. When using Take you should always include an OrderBy for consistent behaviour. I'm assuming that is pointing at a View which is checking Status?

Upvotes: 1

Related Questions