Chris Walsh
Chris Walsh

Reputation: 3523

DbContextScope Transaction

I am using mehdime/DbContextScope as documented here to implemeent a bulk user refresh/import solution for our product.

Under the heading "Changes are only persisted when SaveChanges() is called", Mehdi states in code sample:

Do not call SaveChanges() until the business transaction is complete - i.e. no partial or intermediate saves. SaveChanges() must be called exactly once per business transaction.

If you find yourself needing to call SaveChanges() multiple times within a business transaction, it means that you are in fact implementing multiple business transactions within a single service method.

However, I want to add a new row, obtain its generated ID (IDENTITY column) and then add an audit log entry to the database referencing the generated ID. To do this, I need to perform an intermediate SaveChanges() in order for EF to perform the INSERT and get an ID, however I want to do this in a single atomic transaction.

Am I missing something or is this actually possible without breaking the Medhi's rule?

Here is my code sample (which current breaks the rule)

    /// <summary>
    /// Creates a new Location
    /// </summary>
    /// <param name="orgID">ID of Organisation</param>
    /// <param name="name">Location Name</param>
    /// <returns></returns>
    public IActionResult Create(int orgID, string name)
    {
        if (string.IsNullOrEmpty(name))
            throw new ArgumentNullException(nameof(name));

        // New Location
        var location = new Location {
            OrganisationID = orgID,
            Name = name,
            Enabled = true
        };

        using (var dbContextScope = _dbContextScopeFactory.Create())
        {
            var ctx = dbContextScope.DbContexts.Get<PlatformEntities2014>();

            // Duplicate name?
            var existing = ctx.Locations.Where(l => l.OrganisationID == orgID && l.Name == name).FirstOrDefault();
            if (existing != null)
                return new ActionResult(ActionResultCode.ErrorAlreadyExists) { ReferencedObject = existing };

            ctx.Locations.Add(location);
            // ---POSITION A---
            dbContextScope.SaveChanges();   // (Assigns location.ID)

            // Log
            GeneralLog log = new GeneralLog() {
                DateTime = DateTime.Now,
                Code = "LOC",
                SubCode = "NEW",
                OrganisationID = orgID,
                Information = $"Location {location.ID} \"{location.Name}\" created during Bulk Refresh."
            };
            ctx.GeneralLogs.Add(log);
            // ---POSITION B---
            dbContextScope.SaveChanges();

            return new ActionResult(ActionResultCode.Success) { ReferencedObject = location };
        }
    }

Would it be acceptable to replace the first dbContextScope.SaveChanges(); at POSITION A with ctx.SaveChanges(); to obtain the ID and call dbContextScope.SaveChanges(); at the POSITION B?

Thanks.

Upvotes: 1

Views: 855

Answers (1)

Preet Singh
Preet Singh

Reputation: 1861

Further on from your explanation of what the relationships are in comments. these are 2 separate business transactions. Look at it this way your transaction is not considered as failed if it fails to create a 'GeneralLog'. Hence I would say do it as 2 separate transactions. Logging should not fail your business transaction. What you can do is create the objects first in one transaction. The get the IDs and create logs in a separate transaction.

Upvotes: 2

Related Questions