Search code examples
c#entity-frameworktransactionschange-tracking

C# Entity Framework transaction of non-tracked entities


I was unsuccessful in looking for some specifications of how entity instances obtained by AsNoTracking within a transaction are handled. A colleague of mine told me the transactions are DB layer things and AsNoTracking does not affect the behavior. I wish I had that confidence too. Are they in Entity Framework Core really? Or is it some kind of mix between DB transactions and back-end tracking? I need to use IsolationLevel.Serializable and reading some entity first which I am using later for some update. I am really interested in where I can afford to read the first entity like AsNoTracking.


Solution

  • According to my testing:

    
    namespace test_ef_transaction_asnotracking;
    
    internal class Program
    {
        static async Task Main(string[] args)
        {
            if (File.Exists("test.db"))
            {
                File.Delete("test.db");
            }
    
            var areStep1 = new AutoResetEvent(false);
            var areStep2 = new AutoResetEvent(false);
            var areStep3 = new AutoResetEvent(false);
            var areStep4 = new AutoResetEvent(false);
            var areStep5 = new AutoResetEvent(false);
            var task1 = Task.Run(() =>
            {
                using (var dbContext = new MyDbContext())
                {
                    dbContext.Database.EnsureCreated();
    
                    var p = new Parent() { Name = "ahoj" };
                    dbContext.Add(p);
                    dbContext.SaveChanges();
                }
    
                using (var dbContext = new MyDbContext())
                {
                    //var t = dbContext.Database.BeginTransaction(System.Data.IsolationLevel.Serializable);
                    var p = dbContext.Parents.AsNoTracking().Single();
                    areStep1.Set();
                    areStep2.WaitOne();
    
                    p.Name = "vnější změna";
                    dbContext.Parents.Update(p);
                    dbContext.SaveChanges();
    
                    //t.Commit();
    
                    areStep3.Set();
                }
            });
    
            var task2 = Task.Run(() =>
            {
                areStep1.WaitOne();
                using (var dbContext = new MyDbContext())
                {
                    var t = dbContext.Database.BeginTransaction(System.Data.IsolationLevel.Serializable);
    
                    var p = dbContext.Parents.AsNoTracking().Single();
    
                    var newP = new Parent() { Name = p.Name };
    
                    dbContext.Parents.Add(newP);
                    dbContext.SaveChanges();
    
                    areStep2.Set();
                    areStep3.WaitOne();
                    t.Commit();
    
                    var finalParents = dbContext.Parents.ToList();
                    foreach (var finalParent in finalParents)
                    {
                        Console.WriteLine(finalParent.Name);
                    }
                }
            });
    
            await Task.WhenAll(task1, task2);
    
            Console.WriteLine("...");
            Console.ReadKey();
        }
    }
    
    public sealed class MyDbContext : DbContext
    {
        public DbSet<Parent> Parents { get; private set; } = null!;
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite("Data Source=test.db");
        }
    }
    
    public class Parent
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
    }
    

    It is really matter of database, so the transaction even for untracked entities locking them fully in DB.