0

I have a controller action like this (ASP.NET web api)

public HttpResponseMessage<Component> Post(Component c)
{
    //Don't allow equal setup ids in within the same installation when the component is of type 5
    if (db.Components.Any(d => d.InstallationId == c.InstallationId && d.SetupId == c.SetupId && d.ComponentTypeId == 5)) return new HttpResponseMessage<Component>(c, HttpStatusCode.Conflict);

    db.Components.Add(c);
    db.SaveChanges();
    return new HttpResponseMessage<Component>(c, HttpStatusCode.OK);
}      

I send a number of posts request from javascript with 2 of them being equal

{SetupId : 7, InstallationId : 1, ComponentTypeId: 5}

I have verified this both using fiddler and stepping through the code on the server.

However sometimes the constraint that I do above is working as it should and other times it is not. I guess since Post is an async action the request #2 sometimes checks the database for duplicates BEFORE the first request have managed to save to the database.

How can I solve this? Is there a way to lock EF operations to the database from beginning of the post action until the end? Is that even a good idea?

I have though of database restraints, however, since this is only when componenttype is 5 then I'm not sure how to implement that. Or if it's even possible.

Martin Hansen
  • 5,154
  • 3
  • 32
  • 53

2 Answers2

0

This is quite difficult to achieve with EF. In normal SQL you would start transaction and add some table hint to your constraint query to force locking records. The problem is EF doesn't support table hints. You cannot force linq or ESQL query to lock record.

Your options are:

  • Manual locking in your method. Using for example lock will dramatically reduce throughput of your method so you will most probably need some custom clever implementation locking per those Ids
  • Using custom SQL or stored procedure instead of LINQ query and force locking. I think UPDLOCK with HOLDLOCK hints should probably work in this case.
  • Alternatively you can place unique index on your InstallationId, SetupId and ComponentTypeId and simply catch exception if the concurrent request tryes to insert duplicate record. The problem is if that combination must be unique only for some cases but not for other.
Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
0

I solved this in the database with the help of this answer: https://stackoverflow.com/a/5149263/94394 A conditional constraint allowed in SQL server 2008.

create unique nonclustered index [funcix_Components_setupid_installationid_RecordStatus]
on [components]([setupid], [Installationid])
where [componenttypeid] = 5

Then I catched DbUpdateException and checked if i got a constraint exception error code

        try
        {
            db.Components.Add(c);
            db.SaveChanges();                
        }
        catch (DbUpdateException ex) {
            Exception innermostException = ex;
            while (innermostException.InnerException != null)//Get innermost exception
            {
                innermostException = innermostException.InnerException;
            }

            if (((System.Data.SqlClient.SqlException)innermostException).Number == 2601)//Constraint exception id
            {
                return new HttpResponseMessage<Component>(c, HttpStatusCode.Conflict);    
            }
        }
        return new HttpResponseMessage<Component>(c, HttpStatusCode.OK);
Community
  • 1
  • 1
Martin Hansen
  • 5,154
  • 3
  • 32
  • 53