1

To start here are my tables. Not very interesting, just a primary key, data and foreign key connecting them. Everything with a unique constraint.

CREATE TABLE [dbo].[Providers] (
    [id]   BIGINT         IDENTITY (1, 1) NOT NULL,
    [name] NVARCHAR (850) NOT NULL,
    CONSTRAINT [PK_Providers] PRIMARY KEY CLUSTERED ([id] ASC),
    CONSTRAINT [UQ_Providers_name] UNIQUE NONCLUSTERED ([name] ASC)
);

CREATE TABLE [dbo].[Templates] (
    [id]           BIGINT         IDENTITY (1, 1) NOT NULL,
    [provider]     BIGINT         NOT NULL,
    [repositoryId] NVARCHAR (842) NOT NULL,
    CONSTRAINT [PK_Templates] PRIMARY KEY CLUSTERED ([id] ASC),
    CONSTRAINT [UQ_Templates_repositoryId] UNIQUE NONCLUSTERED ([provider] ASC, [repositoryId] ASC),
    CONSTRAINT [FK_Templates_ToProviders] FOREIGN KEY ([provider]) REFERENCES [dbo].[Providers] ([id])
);

Using Entity Framework, I want to insert a row into Templates. However, I don't know at the time of insert whether the matching row in Providers exists yet. I want to be defensive against race conditions if many people are inserting at the same time (two templates with the same Provider).

I believe that in order to accomplish this I need to first get-or-create the provider, then after that is persisted (at least one full DB round trip) create the template.

I have come up with a solution that I think will work, but it seems incredibly complicated for what I have to imagine is a pretty common scenario. I'm hoping someone can show me what I am doing wrong or how I can do it better.

The following code lives inside my DbContext implementation.

public async Task<Provider> GetOrCreateProvider(String name)
{
    var provider = new Provider { name = name };
    return await GetOrCreateEntity(Providers, provider);
}

public async Task<Template> GetOrCreateTemplate(Provider provider, String repositoryId)
{
    var template = new Template { Provider = provider, repositoryId = repositoryId };
    return await GetOrCreateEntity(Templates, template);
}

private async Task<T> GetOrCreateEntity<T>(DbSet<T> dbSet, T newEntity) where T : class
{
    var maybeEntity = await dbSet.Where(entity => entity.Equals(newEntity)).FirstOrDefaultAsync();
    if (maybeEntity != null)
        return maybeEntity;

    try
    {
        dbSet.Add(newEntity);
        await SaveChangesAsync();
        return newEntity;
    }
    catch (UpdateException exception) when ((exception.InnerException as SqlException)?.Number == 2601 || (exception.InnerException as SqlException)?.Number == 2627)
    {
        return await dbSet.Where(entity => entity.Equals(newEntity)).FirstOrDefaultAsync();
    }
}

Entity classes are auto-generated by VS from my DB tables with a .Equals override added to them. Here is Provider, Template is similar but with a reference to a single Provider rather than a collection:

public partial class Provider
{
    public Provider()
    {
        Templates = new HashSet<Template>();
    }

    public long id { get; set; }

    [Required]
    [StringLength(850)]
    public string name { get; set; }

    public virtual ICollection<Template> Templates { get; set; }

    public override Boolean Equals(Object otherObject)
    {
        if (Object.ReferenceEquals(this, otherObject))
            return true;

        if (otherObject == null)
            return false;

        var other = otherObject as Provider;
        if (other == null)
            return false;

        return name == other.name;
    }

    public override Int32 GetHashCode()
    {
        return name.GetHashCode();
    }
}

Usage would look like this:

var provider = await GetOrCreateProvider("foo");
var template = await GetOrCreateTemplate(provider, "bar");

The primary problem with this (aside from the amount of boilerplate necessary to do such a simple operation) is that I have to make two full round trips to the server in the best case scenario (when both the provider and the template already exist). In the worst case scenario I have to make 6 round trips:

  1. fail to get provider
  2. constraint violation creating provider
  3. get provider
  4. fail to get template
  5. constraint violation creating template
  6. get template

I'm not terribly concerned about the worst case scenario but I am concerned about the best case scenario as the number of round trips goes up with the complexity of my object graph. If I have something else that references something in the Template table I am now up to 3 round trips.

Micah Zoltu
  • 6,764
  • 5
  • 44
  • 72
  • How do you create your entity classes? Can you show them? – Kosala W Mar 06 '16 at 05:16
  • `new Provider("foo")` – Micah Zoltu Mar 06 '16 at 05:17
  • I also simplified the code a bit so you can see the `new Provider("foo")` more obviously now. – Micah Zoltu Mar 06 '16 at 05:19
  • I realize now you meant the class itself. I have edited with one of my classes. It was auto-created from the DB by Visual Studio using the new ADO.NET Entity Framework wizard. – Micah Zoltu Mar 06 '16 at 05:23
  • I will always think about this from SQL perspective before start coding in C#. Do I have to worry about all the above steps if I do this purely in SQL?. If so, I will have to think about all those steps when I do the same in EF. – Kosala W Mar 06 '16 at 05:25
  • If this was pure SQL I would have to execute the entire operation inside of a transaction. My research suggests that attempting to do get-or-create type operations from within a transaction in entity framework is not recommended. The transaction would be, 1. `select provider`, 2. if no results then `insert provider; select provider`, 3. `select template`, 4. if no results then `insert template; select template` – Micah Zoltu Mar 06 '16 at 05:39
  • Even with multiple transactions you will have to do the same to avoid SQL server from throwing errors. Other than those general errors, In EF, you may also get timeouts due to database locks, network failures etc. – Kosala W Mar 06 '16 at 06:01

1 Answers1

0

A stored procedure would do all steps in a single call to the database, and in a single transaction. Have a look at the merge command, for example introduction or reference.

You can use this method to execute a stored procedure from EF:

private static int ExecuteSqlCount(string statement, SqlParameter[] paramsSql)
{
    using (Entities dbContext = new Entities())
    {
        var total = dbContext.Database.SqlQuery<int>(statement, paramsSql).First();
        return total;
    }
}

The return value is whatever the USP returns. The method is called like (for a USP that returns an int):

    var parameters = new List<SqlParameter>();
    string statement = "exec uspPersonAdd @personName = @name, @activeFlag = @active";
    parameters.Add(new SqlParameter("@name", person.PersonName));
    parameters.Add(new SqlParameter("@active", person.Active));
    int id = ExecuteSqlCount(statement, parameters.ToArray());

You need using System.Data.SqlClient;

Peter Bill
  • 508
  • 3
  • 12
  • I'm looking for a way to do this from Entity Framework. Is what you suggest possible from Entity Framework? – Micah Zoltu Mar 06 '16 at 21:22
  • Yes, I use this method to execute a stored procedure, as shown in my editted answer. – Peter Bill Mar 06 '16 at 21:40
  • If I understand what you are doing here correctly, that isn't really leveraging Entity Framework. It is just making a regular SQL call to a database. If I am going to go this route, I might as well drop Entity Framework and use traditional SQL. In particular, https://msdn.microsoft.com/en-us/data/jj592907.aspx says: "The results returned from SqlQuery on Database will never be tracked by the context even if the objects are instances of an entity type." so doing it like this is likely to lead to long term maintenance issues. – Micah Zoltu Mar 06 '16 at 22:55