3

I am already using transactions inside my repository functions in some cases because I sometimes need to insert data into two tables at once and I want the whole operation to fail if one of the inserts fails.

Now I ran into a situation where I had to wrap calls to multiple repositories / functions in another transaction, but when one of those functions already uses a transaction internally I will get the error The connection is already in a transaction and cannot participate in another transaction.

I do not want to remove the transaction from the repository function because this would mean that I have to know for which repository functions a transaction is required which I would then have to implement in the service layer. On the other hand, it seems like I cannot use repository functions in a transaction when they already use a transaction internally. Here is an example for where I am facing this problem:

// Reverse engineered classes
public partial class TblProject
{
    public TblProject()
    {
        TblProjectStepSequences = new HashSet<TblProjectStepSequence>();
    }
    
    public int ProjectId { get; set; }

    public virtual ICollection<TblProjectStepSequence> TblProjectStepSequences { get; set; }
}

public partial class TblProjectTranslation
{
    public int ProjectId { get; set; }
    public string Language { get; set; }
    public string ProjectName { get; set; }

    public virtual TblProject Project { get; set; }
}

public partial class TblProjectStepSequence
{
    public int SequenceId { get; set; }
    public int ProjectId { get; set; }
    public int StepId { get; set; }
    public int SequencePosition { get; set; }

    public virtual TblStep Step { get; set; }
    public virtual TblProject Project { get; set; }
}

// Creating a project in the ProjectRepository
public async Task<int> CreateProjectAsync(TblProject project, ...)
{
    using (var transaction = this.Context.Database.BeginTransaction())
    {
        await this.Context.TblProjects.AddAsync(project);
        await this.Context.SaveChangesAsync();
        // Insert translations... (project Id is required for this)
        await this.Context.SaveChangesAsync();
        transaction.Commit();
        
        return entity.ProjectId;
    }
}

// Creating the steps for a project in the StepRepository
public async Task<IEnumerable<int>> CreateProjectStepsAsync(int projectId, IEnumerable<TblProjectStepSequence> steps)
{
    await this.Context.TblProjectStepSequences.AddRangeAsync(steps);
    await this.Context.SaveChangesAsync();

    return steps.Select(step =>
    {
        return step.SequenceId;
    }
    );
}

// Creating a project with its steps in the service layer
public async Task<int> CreateProjectWithStepsAsync(TblProject project, IEnumerable<TblProjectStepSequence> steps)
{
    // This is basically a wrapper around Database.BeginTransaction() and IDbContextTransaction
    using (Transaction transaction = await transactionService.BeginTransactionAsync())
    {
        int projectId = await projectRepository.CreateProjectAsync(project);
        await stepRepository.CreateProjectStepsAsync(projectId, steps);

        return projectId;
    }
}

Is there a way how I can nest multiple transactions inside each other without already knowing in the inner transactions that there could be an outer transaction?

I know that it might not be possible to actually nest those transactions from a technical perspective but I still need a solution which either uses the internal transaction of the repository or the outer one (if one exists) so there is no way how I could accidentally forget to use a transaction for repository functions which require one.

Matt
  • 25,467
  • 18
  • 120
  • 187
Chris
  • 1,417
  • 4
  • 21
  • 53
  • Are you using `TransactionScope` or `Database.BeginTransaction`? – Matthew Feb 02 '22 at 15:54
  • @Matthew I am using `Database.BeginTransaction` – Chris Feb 02 '22 at 15:57
  • 3
    *I want the whole operation to fail if one of the inserts fails* -- The best way to do that is by designing your code toward saving everything in one SaveChanges call. Usually a repository layer is more of a handicap than a help here. That said, it's impossible to help you without seeing an example. – Gert Arnold Feb 02 '22 at 16:04
  • @GertArnold I added an example to my question. I simplified it a little bit to focus on my problem related to this question. I get your point but I see no way how I could possibly design my code to just use one `SaveChanges()` call in my example because I cannot access the project Id for inserting the translations before saving the changes to the database after inserting the project. I also cannot insert the translations directly via the project because there is no `virtual ICollection` in the project and I cannot edit the database. – Chris Feb 03 '22 at 08:18
  • All because these repositories get in your way. The repositories shouldn't save changes by themselves to begin with. They should be part of an encompassing Unit of Work. Better is to just remove this layer altogether and work with DbSets and DbContext directly (= repository and UoW, respectively). – Gert Arnold Feb 03 '22 at 08:22
  • @GertArnold If they shouldn't save changes by themselves then how can I create a project inside them because this seems impossible without at least one `SaveChanges()` call so I can get the new project id and then insert the project name translations afterwards? I also do not want to ditch the repositories because they actually abstract away a few things (such as taking care of the translations) which I absolutely do not want to do in the upper layers. My service layer shouldn't care if the data for a project belongs to two, three or even more tables in the database. – Chris Feb 03 '22 at 08:28
  • @GertArnold I do not even think that my problem regarding the transactions has anything to do with whether or not I use repositories. The main problem is that creating a project requires two things (insert project data + insert translations with the id of the new project) while creating a "full" project also requires two steps (create project + create project steps). Therefore, I need a transaction for creating a project but also a transaction for creating a "full" project, hence the problem with the nested transactions. I do not see how this problem goes away by ditching the repositories. – Chris Feb 03 '22 at 08:37
  • 1
    No, you must create the project, the translations, and the steps as one object graph and save them all at once. I already said something along these lines on one of you other questions on the same subject. Your architecture makes it impossible to work with EF the way it's intended. – Gert Arnold Feb 03 '22 at 08:41
  • @GertArnold Okay, then the main thing I do not understand about that approach is this: How can I do everything as one object graph when I at one point need the ID of a previous insert for the next insert? I do not know the ID before saving the changes to the database. In my case, I cannot even work with the translations without raw SQL because they do not have a primary key in the database and I cannot do anything about that a this point. So how could I possibly work with EF the way it's intended in this situation? Afaik, I cannot, so I need an alternative. – Chris Feb 03 '22 at 08:54
  • https://learn.microsoft.com/en-us/ef/core/saving/related-data – Gert Arnold Feb 03 '22 at 09:15
  • @GertArnold After reading through the linked article, it seems like I need to have a related entity in my reverse engineered class but as you can see in my `TblProject` class, there is no related entity for the translations there. This is a current limitation in the database which I cannot do anything about at this point, so I have to work around it. Did I miss something in the article that would help me in this situation? – Chris Feb 03 '22 at 09:46
  • 1
    You can set `TblProject Project` in `TblProjectTranslation`. – Gert Arnold Feb 03 '22 at 10:26
  • @Chris I don't know if anyone else already mentioned this, but the question suggests a serious design error. EF doesn't even need transactions, much less nested ones. If you have to use database transactions, something is **seriously** wrong. A DbContext is a Unit-Of-Work already, it tracks all changes *in memory* and doesn't even open a connection until you call `SaveChanges`. If you don't call that, all changes are discarded. If you need transactions it means you misused DbContext and tried to use it as a SqlConnection – Panagiotis Kanavos Mar 18 '22 at 07:37
  • 1
    @Chris `I sometimes need to insert data into two tables at once` you don't need transactions for this. A DbContext is a *multi*-entity UoW. It will track all entities and persist all of them inside a single internal transaction when you call `SaveChanges`. The code in `CreateProjectAsync` *causes* the bug. Remove all transactions and leave only a single `SaveChangesAsync` at the very end of the method. – Panagiotis Kanavos Mar 18 '22 at 07:44
  • 1
    @PanagiotisKanavos I am aware of that but calling `SaveChanges` once at the very end does not work in my current project architecture. To make this work, I would have to expose the database context to classes which are currently not aware of its existence. Alternatively, I would have to ditch a whole layer of my project but that comes with a whole lot of other problems I'd have to solve then. I've already spent days thinking about this but there does not seem to be a perfect solution. I absolutely need to compromise somewhere. – Chris Mar 18 '22 at 08:50
  • @Chris then your architecture is seriously broken. you need to fix it. Or stop using EF Core or any other ORM. Looks like you tried to use the "generic" repository **anti**pattern. That's a *low* level abstraction compared to an ORM like EF Core. You're already encountering the problems with that antipattern. It only gets work from here. – Panagiotis Kanavos Mar 18 '22 at 09:03
  • 1
    A DbContext isn't a database connection or model. It doesn't deal with tables, it deals with entities. It's designed to work *dis*connected, only opening a connection when needed, and use optimistic concurrency. These last two things have resulted in **several** orders of magnitude improvements in throughput since the late 1990s. By trying to use EF Core like a 1990s recordset you reintroduce all the problems that were solved back then. You aren't improving abstraction either, as the "generic" repository actually leaves below the `DbSet` level – Panagiotis Kanavos Mar 18 '22 at 09:10
  • 1
    In DDD terms a DbContext handles the entities in a bounded context - the entities needed for a specific scenario or use case. In DDD a repository doesn't return any entity, it returns *aggregate roots*. How these are loaded and modified is hidden by the implementation. EF Core already provides 99.5 of that implementation. The only way to abstract EF Core any further is to use *specialized* repositories that return aggregate root objects and their dependencies. – Panagiotis Kanavos Mar 18 '22 at 09:16
  • 2
    @PanagiotisKanavos So what would you suggest? All I could find so far are things I shouldn't do, but not a single feasible solution. I need to abstract EF Core away at some point because I have many other data sources in my project, not just a single database. – Chris Mar 18 '22 at 11:13
  • That's not what your code does though, it tries to abstract low-level table access. EF Core is what abstracts databases. You can use EF Core to connect to MySQL, SQLite and SQL Server databases in the same project. You can use the same DbContext to target different databases, simply by changing the DbContextOptions parameter. – Panagiotis Kanavos Mar 18 '22 at 11:18
  • Besides, even if you use a "generic" repository there's no reason to call `SaveChanges` after every object modification, only to impose an explicit transaction to revert those modifications. – Panagiotis Kanavos Mar 18 '22 at 11:22
  • In `// Insert translations... (project Id is required for this)` no it's not. EF Core will update related **entities** and the FK fields by itself, based on the relations between objects. Because once again, EF works with entities, not tables. ΅When you call SaveChanges, EF will insert the new parent entities first and use the generated IDs as FK values in the child entities – Panagiotis Kanavos Mar 18 '22 at 11:46
  • 2
    @PanagiotisKanavos I already updated my code to take this into account by directly adding the related entities before calling `SaveChanges()` about a month ago. However, I still run into problems with the "only call `SaveChanges()` once" approach because of different reasons. For example, I have one entity where the value of a column should be set to a string which contains the entity ID when creating a new entity. As I do not know the ID before saving the changes to the database, I need to call `SaveChanges()` before I can get the ID to set the value of the other column. – Chris Mar 18 '22 at 13:15
  • 2
    @PanagiotisKanavos - It is ok to criticize. But instead of writing a ton of comments about what we are doing wrong you could have spent the time to post a good answer, don't you agree? ;-) – Matt Mar 19 '22 at 11:28
  • @Matt the actual answer is shown in all EF Core tutorials - just call `SaveChanges` once to commit the changes. Not earlier. It's not needed. In the classic Blog/Posts example, add a single Blog, add 10 posts to the Blog, then call SaveChanges at the end. EF Core will resolve relations and insert parent/child rows properly. There's no reason to save the parents first, then the children. – Panagiotis Kanavos Mar 24 '22 at 10:38
  • @Chris - I am curious, which of the answers given actually helped you? Or is there an answer you can give yourself how you approached (and solved) the issue? You can mark the one as "accepted answer" - to help other visitors reading your question. – Matt Apr 06 '22 at 09:03
  • 1
    @Matt Actually, I have not found a "good" solution so far, neither in the anwers here, nor by myself. Ideally, I would've liked to change the architecture of my project so that `SaveChanges` is only called once and not multiple times. The problem is that in a few cases this wasn't possible for me to do, because I have not found a way to avoid calling `SaveChanges` multiple times in a few situations. To avoid nested transactions I simply moved the code from `CreateProjectAsync` into `CreateProjectWithStepsAsync`. So far I did not encounter another situation where I needed nested transactions. – Chris Apr 06 '22 at 10:10
  • 1
    @Chris - Well, I think you can only try to minimize SaveChanges calls, but not avoid it being called more than once. Also, transactions IMHO are still needed if you have objects that need to be updated together (not always necessarily dependent by relations one to many, many to many, or parent-child relationships). If a transaction fails (is rolled back) all of them need to be rolled back for consistency reasons. It doesn't need to be (but can be) nested. But it is now clear the question title (how to nest transactions?) doesn't quite fit to your use case. – Matt Apr 06 '22 at 11:58

3 Answers3

0

You could check the CurrentTransaction property and do something like this:

var transaction = Database.CurrentTransaction ?? Database.BeginTransaction()

If there is already a transaction use that, otherwise start a new one...

Edit: Removed the Using block, see comments. More logic is needed for Committing/Rollback the transcaction though...

Poiter
  • 441
  • 2
  • 9
0

I am answering the question you asked "How to nest transactions in EF Core 6?"

Please note that this is just a direct answer, but not an evaluation what is best practice and what not. There was a lot of discussion going around best practices, which is valid to question what fits best for your use case but not an answer to the question (keep in mind that Stack overflow is just a Q+A site where people want to have direct answers).

Having said that, let's continue with the topic:

Try to use this helper function for creating a new transaction:

public CommittableTransaction CreateTransaction() 
    => new System.Transactions.CommittableTransaction(new TransactionOptions()
    {
        IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted
    });

Using the Northwind database as example database, you can use it like:

public async Task<int?> CreateCategoryAsync(Categories category)
{         
    if (category?.CategoryName == null) return null;
    using(var trans = CreateTransaction()) 
    {
        await this.Context.Categories.AddAsync(category);
        await this.Context.SaveChangesAsync();

        trans.Commit();

        return category?.CategoryID;
    }
}

And then you can call it from another function like:

/// <summary>Create or use existing category with associated products</summary>
/// <returns>Returns null if transaction was rolled back, else CategoryID</returns>
public async Task<int?> CreateProjectWithStepsAsync(Categories category)
{
    using var trans = CreateTransaction();
    
    int? catId = GetCategoryId(category.CategoryName) 
                ?? await CreateCategoryAsync(category);

    if (!catId.HasValue || string.IsNullOrWhiteSpace(category.CategoryName))
    {
        trans.Rollback(); return null;
    }
    
    var product1 = new Products()
    {
        ProductName = "Product A1", CategoryID = catId
    };
    await this.Context.Products.AddAsync(product1);
    
    var product2 = new Products()
    {
        ProductName = "Product A2", CategoryID = catId
    };
    await this.Context.Products.AddAsync(product2);

    await this.Context.SaveChangesAsync();

    trans.Commit();
    
    return catId;
}

To run this with LinqPad you need an entry point (and of course, add the NUGET package EntityFramework 6.x via F4, then create an EntityFramework Core connection):

// Main method required for LinqPad
UserQuery Context;

async Task Main()
{
    Context = this;
    var category = new Categories()
    {
        CategoryName = "Category A1"
        // CategoryName = ""
    };
    var catId = await CreateProjectWithStepsAsync(category);
    Console.WriteLine((catId == null) 
           ? "Transaction was aborted." 
           : "Transaction successful.");
}

This is just a simple example - it does not check if there are any product(s) with the same name existing, it will just create a new one. You can implement that easily, I have shown it in the function CreateProjectWithStepsAsync for the categories:

int? catId = GetCategoryId(category.CategoryName) 
            ?? await CreateCategoryAsync(category);

First it queries the categories by name (via GetCategoryId(...)), and if the result is null it will create a new category (via CreateCategoryAsync(...)).

Also, you need to consider the isolation level: Check out System.Transactions.IsolationLevel to see if the one used here (ReadCommitted) is the right one for you (it is the default setting).

What it does is creating a transaction explicitly, and notice that here we have a transaction within a transaction.


Note:

  • I have used both ways of using - the old one and the new one. Pick the one you like more.
Matt
  • 25,467
  • 18
  • 120
  • 187
  • This only tries to cover up the problem - using `SaveChanges` all the time. The only real solution is to either use EF Core properly by only using `SaveChanges` when the changes need to be committed, or not at all. – Panagiotis Kanavos Mar 18 '22 at 07:38
  • Even if this answers the OP's (bad) question, it will cause harm to other people that find this question and don't realize what the real problem is. Besides, it's not possible to have nested transactions unless the database supports them. In SQL Server, a nested `BEGIN TRAN` has no effect, a `ROLLBACK TRAN` would roll back the root transaction – Panagiotis Kanavos Mar 18 '22 at 07:39
  • 1
    Finally, `ReadUncommitted`? That's essentially a NOLOCK and a *major* bug. This means that dirty and *duplicate* data will be returned, while other rows will be missed. Aaron Bertrand (among others) explains what's wrong with this in [Bad Habits -NOLOCK everywhere](https://www.sentryone.com/blog/aaronbertrand/bad-habits-nolock-everywhere) – Panagiotis Kanavos Mar 18 '22 at 07:51
  • @PanagiotisKanavos - Do you really think that Chris would ask about nested transactions if the DBMS he is using does not support it? Besides, what is "the real problem" others don't recognize? – Matt Mar 19 '22 at 18:34
  • Yes. People often misunderstand the concept of nested transactions while the code shows an overall misunderstanding and misuse of Entity Framework. SQL Server [has no nested transactions](https://stackoverflow.com/questions/527855/nested-transactions-in-sql-server). MySQL [doesn't have nested transactions](https://stackoverflow.com/questions/1306869/are-nested-transactions-allowed-in-mysql). [Neither does Oracle](https://stackoverflow.com/questions/24506847/using-nested-transactions-in-oracle#:~:text=Oracle%20doesn%27t%20support%20nested,if%20your%20transaction%20semantics%20differ.) – Panagiotis Kanavos Mar 20 '22 at 14:25
  • PostgreSQL [dosn't have nested transactions either](https://dba.stackexchange.com/questions/81011/transactions-within-a-transaction). Neither does DB2. All those databases support savepoints though, with different calls and not-quite-the-same semantics. Nested transactions were an attempt to implement long lived transactions (as in hours, not seconds) in the early 2000s, which simply wouldn't scale to services and distributed systems. It wasn't even necessary in disconnected scenarios like those supported by ORMs like NHibernate and Entity Framework – Panagiotis Kanavos Mar 20 '22 at 14:31
  • @PanagiotisKanavos - according to this you are wrong, SQL Server *does* have nested transactions: [https://dotnettutorials.net/lesson/sql-server-savepoints-transaction/](https://dotnettutorials.net/lesson/sql-server-savepoints-transaction/) You can give the transactions names, which is called savepoints. The article describes nesting quite well. – Matt Mar 23 '22 at 21:28
  • And those are quite important as the article shows when it comes to code executing stored procedures within a transaction, where the stored procedure itself is using a transaction. – Matt Mar 23 '22 at 21:36
  • Those are savepoints, not nested transactions. They behave very differently. They aren't exposed through either EF or ADO.NET. That's not what your answer shows either. Committing multiple times by calling `SaveChanges` needlessly is the real bug. Adding more code on top of the bug won't fix it – Panagiotis Kanavos Mar 24 '22 at 10:31
-1

Just don't call SaveChanges multiple times.

The problem is caused by calling SaveChanges multiple times to commit changes made to the DbContext instead of calling it just once at the end. It's simply not needed. A DbContext is a multi-entity Unit-of-Work. It doesn't even keep an open connection to the database. This allows 100-1000 times better throughput for the entire application by eliminating cross-connection blocking.

A DbContext tracks all modifications made to the objects it tracks and persists/commits them when SaveChanges is called using an internal transaction. To discard the changes, simply dispose the DbContext. That's why all examples show using a DbContext in a using block - that's actually the scope of the Unit-of-Work "transaction".

There's no need to "save" parent objects first. EF Core will take care of this itself inside SaveChanges.

Using the Blog/Posts example in the EF Core documentation tutorial :


public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    public string DbPath { get; }

    // The following configures EF to create a Sqlite database file in the
    // special "local" folder for your platform.
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer($"Data Source=.;Initial Catalog=tests;Trusted_Connection=True; Trust Server Certificate=Yes");
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; } = new();
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

The following Program.cs will add a Blog with 5 posts but only call SaveChanges once at the end :


using (var db = new BloggingContext())
{
    Blog blog = new Blog { Url = "http://blogs.msdn.com/adonet" };
    IEnumerable<Post> posts = Enumerable.Range(0, 5)
                                       .Select(i => new Post {
                                            Title = $"Hello World {i}",
                                            Content = "I wrote an app using EF Core!"
                                         });
    blog.Posts.AddRange(posts);

    db.Blogs.Add(blog);

    await db.SaveChangesAsync();
}

The code never specifies or retrieves the IDs. Add is an in-memory operation so there's no reason to use AddAsync. Add starts tracking both the blog and the related Posts in the Inserted state.

The contents of the tables after this are :

select * from blogs

select * from posts;
-----------------------

BlogId  Url
1   http://blogs.msdn.com/adonet

PostId  Title   Content BlogId
1   Hello World 0   I wrote an app using EF Core!   1
2   Hello World 1   I wrote an app using EF Core!   1
3   Hello World 2   I wrote an app using EF Core!   1
4   Hello World 3   I wrote an app using EF Core!   1
5   Hello World 4   I wrote an app using EF Core!   1

Executing the code twice will add another blog with another 5 posts.

PostId  Title   Content BlogId
1   Hello World 0   I wrote an app using EF Core!   1
2   Hello World 1   I wrote an app using EF Core!   1
3   Hello World 2   I wrote an app using EF Core!   1
4   Hello World 3   I wrote an app using EF Core!   1
5   Hello World 4   I wrote an app using EF Core!   1
6   Hello World 0   I wrote an app using EF Core!   2
7   Hello World 1   I wrote an app using EF Core!   2
8   Hello World 2   I wrote an app using EF Core!   2
9   Hello World 3   I wrote an app using EF Core!   2
10  Hello World 4   I wrote an app using EF Core!   2

Using SQL Server XEvents Profiler shows that these SQL calls are made:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Blogs] ([Url])
VALUES (@p0);
SELECT [BlogId]
FROM [Blogs]
WHERE @@ROWCOUNT = 1 AND [BlogId] = scope_identity();
',N'@p0 nvarchar(4000)',@p0=N'http://blogs.msdn.com/adonet'

exec sp_executesql N'SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([PostId] int, [_Position] [int]);
MERGE [Posts] USING (
VALUES (@p1, @p2, @p3, 0),
(@p4, @p5, @p6, 1),
(@p7, @p8, @p9, 2),
(@p10, @p11, @p12, 3),
(@p13, @p14, @p15, 4)) AS i ([BlogId], [Content], [Title], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([BlogId], [Content], [Title])
VALUES (i.[BlogId], i.[Content], i.[Title])
OUTPUT INSERTED.[PostId], i._Position
INTO @inserted0;
SELECT [i].[PostId] FROM @inserted0 i
ORDER BY [i].[_Position];
',N'@p1 int,@p2 nvarchar(4000),@p3 nvarchar(4000),@p4 int,@p5 nvarchar(4000),@p6 nvarchar(4000),@p7 int,@p8 nvarchar(4000),@p9 nvarchar(4000),@p10 int,@p11 nvarchar(4000),@p12 nvarchar(4000),@p13 int,@p14 nvarchar(4000),@p15 nvarchar(4000)',@p1=3,@p2=N'I wrote an app using EF Core!',@p3=N'Hello World 0',@p4=3,@p5=N'I wrote an app using EF Core!',@p6=N'Hello World 1',@p7=3,@p8=N'I wrote an app using EF Core!',@p9=N'Hello World 2',@p10=3,@p11=N'I wrote an app using EF Core!',@p12=N'Hello World 3',@p13=3,@p14=N'I wrote an app using EF Core!',@p15=N'Hello World 4'

The unusual SELECT and MERGE are used to ensure IDENTITY values are returned in the order the objects were inserted, so EF Core can assign them to the object properties. After calling SaveChanges all Blog and Post objects will have the correct database-generated IDs

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • 1
    How do you add related objects? SaveChanges generates the ID (PK) for you. What if you have 1:n or m:n relationships? How to get those keys? Not all databases are using Guids for the key... – Matt Mar 26 '22 at 14:55
  • That's all the code you need. I already explained that the code comes from the EF Core getting started tutorial and posted the link to it. I didn't use GUIDs anywhere, I used `int`- GUID makes a terrible primary key. Generating and executing the migrations created tables with `ID` columns with IDENTITY default values. `Posts` is a related object already. It already is a `1:N` relation. I posted the SQL trace to show how EF Core handles related objects out-of-the-box. EF Core 6 even handles M:N based on conventions by automatically generating a bridge table without a corresponding class – Panagiotis Kanavos Mar 26 '22 at 18:39
  • Now I see - EF "knows" it has to translate lists into relations and generate the necessary keys automatically, so there is only one SaveChanges needed at the end. – Matt Mar 27 '22 at 19:36
  • However, if you "bundle" a couple of updates/inserts you might still do this in a transaction because usually you want either everything to either succeed or fail, and not having the last update fail but everything else succeed (atomicity). More about this here: [https://database.guide/what-is-acid-in-databases/](https://database.guide/what-is-acid-in-databases/) – Matt Mar 28 '22 at 05:56
  • @Matt and once again, that's what EF Core does. NHibernate too. `SaveChanges` bundles all modifications and executes all of them in a single database transaction. That's why all those `Update` and `Delete` methods in the "generic repository" antipattern are so dangerous - they can easily execute 42 DELETEs and 67 UPDATEs for every INSERT, if those changes are pending – Panagiotis Kanavos Mar 28 '22 at 07:03
  • Thank you for the comprehensive explanations! :-) – Matt Mar 28 '22 at 09:36