HiLo for EntityFramework

I wrote a little implementation of the HiLo pattern for EntityFramework. Those who are using NHibernate currently already knows how this pattern works but I am going to give a little explanation.

The Hi/Lo pattern describe a mechanism for generating safe-ids on the client side rather than the database. Safe in this context means without collisions. This pattern is interesting for three reasons:

  • It doesn’t break the Unit of Work pattern (check  this link and this other one)
  • It doesn’t need many round-trips as the Sequence generator in other DBMS.
  • It generates human readable identifier unlike to GUID techniques.

How it works?

The identifier generated is composed of two parts; the Hi part and the Lo part. The Hi part of the identifier is controlled by the database while the Lo part is controlled by the client side. Applications need to agree in only one parameter called “MaxLo”.

Imagine we agree to use a MaxLo of 100. Once the application needs to generate an ID for the first time; it will ask a Hi to the database; suppose the database returns “1”.  Now the application knows, that it can use identifiers from 1 to 100. Once the application run out of “Lo” because it hits the MaxLo; the application will ask for another Lo to the database.

NHibernate support this out of the box while EntityFramework is far from that.

Test case

I didn’t write much tests cases for this feature but this should be very descriptive:

[Test]
public void ShouldWork()
{
    var expected = Enumerable.Range(0, 200).Select(i => (long)i).ToList();

    using(var context = new SampleContext())
    {
        //create 200 products:
        var products = Enumerable.Range(0, 200)
            .Select(i => new Product {Name = string.Format("Test product {0}", i) }).ToList();
        
        //add to the dbSet: DO NOT FLUSH THE CHANGES YET.
        products.ForEach(p => context.Orders.Add(p));
        
        //Assert
        products.Select(p => p.Id)
            .Should().Have.SameSequenceAs(expected);


        context.SaveChanges();
    }
}

First; create 200 new products; then call the method Add of the DbSet. One of the interesting things about this, is that I want the ID right when I call Add, despite if I flush the changes or no.

Solution

The first class is the HiLoGenerator:

public class HiLoGenerator<TDbContext> : IHiLoGenerator
    where TDbContext : DbContext, new()
{
    private static readonly object ConcurrencyLock = new object();
    private readonly int maxLo;
    private string connectionString;
    private long currentHi = -1;
    private int currentLo;

    public HiLoGenerator(int maxLo)
    {
        this.maxLo = maxLo;
    }

    #region IHiLoGenerator Members

    public long GetIdentifier()
    {
        long result;
        lock (ConcurrencyLock)
        {
            if (currentHi == -1)
            {
                MoveNextHi();
            }
            if (currentLo == maxLo)
            {
                currentLo = 0;
                MoveNextHi();
            }
            result = (currentHi*maxLo) + currentLo;
            currentLo++;
        }
        return result;
    }

    public void SetConnectionString(string newConnectionString)
    {
        connectionString = newConnectionString;
    }

    #endregion

    private void MoveNextHi()
    {
        using (var connection = CreateConnection())
        {
            connection.Open();
            using (var tx = connection.BeginTransaction(IsolationLevel.Serializable))
            using (var selectCommand = connection.CreateCommand())
            {
                selectCommand.Transaction = tx;
                selectCommand.CommandText =
                    "SELECT next_hi FROM entityframework_unique_key;" +
                     "UPDATE entityframework_unique_key SET next_hi = next_hi + 1;";
                currentHi = (int)selectCommand.ExecuteScalar();

                tx.Commit();
            }
            connection.Close();
        }
        
    }

    private IDbConnection CreateConnection()
    {
        using (var dbContext = new TDbContext())
        {
            var connectionType = dbContext.Database.Connection.GetType();
            connectionString = connectionString ?? dbContext.Database.Connection.ConnectionString;
            return (IDbConnection) Activator.CreateInstance(connectionType, connectionString);
        }
    }
}

This class is intented to be used as singleton; for the whole application. That’s why it has a concurrency look. The most important thing happens here:

result = (currentHi * maxLo) + currentLo;

Then we need to tweak our DbContext as follows:

public class SampleContext : DbContext
{
    private static readonly HiLoGenerator<SampleContext> HiloGenerator 
        = new HiLoGenerator<SampleContext>(100);

    private IDbSet<Product> orders;
    public IDbSet<Product> Orders
    {
        get { return orders; }
        set { orders = new HiLoSet<Product>(value, HiloGenerator); }
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<StoreGeneratedIdentityKeyConvention>();
        modelBuilder.Entity<Product>().HasKey(p => p.Id);
    }
}

And then the HiLoSet is a wrapper arround DbSet;

public class HiLoSet<T> : IDbSet<T> where T : EntityBase
{
    private readonly IDbSet<T> dbSet;
    private readonly IHiLoGenerator generator;

    public HiLoSet(IDbSet<T> dbSet, IHiLoGenerator generator)
    {
        this.dbSet = dbSet;
        this.generator = generator;
    }

    public T Add(T entity)
    {
        var add = dbSet.Add(entity);
        TrySetId(entity);
        return add;
    }

    private void TrySetId(EntityBase entity)
    {
        if (entity.Id == default(long))
        {
            entity.Id = generator.GetIdentifier();
            HandleChildsObjects(entity);
        }
    }

    private void HandleChildsObjects(EntityBase entity)
    {
        var propertyInfos = new List<PropertyInfo>();
        var typeToLook = entity.GetType();
        do
        {
            var newProps = entity.GetType()
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => typeof(IEnumerable<EntityBase>).IsAssignableFrom(p.PropertyType)).ToList();
            propertyInfos.AddRange(newProps);
            typeToLook = typeToLook.BaseType;
        } while (typeToLook != typeof (object));
        

        var aggregated = propertyInfos
            .Select(p => p.GetValue(entity, null))
            .OfType<IEnumerable<EntityBase>>()
            .Where(col => col != null).SelectMany(col => col);
        foreach (var child in aggregated)
        {
            TrySetId(child);
        }
    }

    #region no op methods
        public IEnumerator<T> GetEnumerator()
        {
            return dbSet.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)dbSet).GetEnumerator();
        }

        public Expression Expression
        {
            get { return dbSet.Expression; }
        }

        public Type ElementType
        {
            get { return dbSet.ElementType; }
        }

        public IQueryProvider Provider
        {
            get { return dbSet.Provider; }
        }

        public T Find(params object[] keyValues)
        {
            return dbSet.Find(keyValues);
        }

        public T Remove(T entity)
        {
            return dbSet.Remove(entity);
        }

        public T Attach(T entity)
        {
            return dbSet.Attach(entity);
        }

        public T Create()
        {
            return dbSet.Create();
        }

        public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
        {
            return dbSet.Create<TDerivedEntity>();
        }

        public ObservableCollection<T> Local
        {
            get { return dbSet.Local; }
        }
        #endregion
}

When we call the Add method, we set the ids for the object and we even explore the child objects to do the same.
It is a hacky solution but will work for most of the cases.
That is all the code! I hope you find useful.


blog comments powered by Disqus
  • Categories

  • Archives