In this recipe, we will be setting up a management class that allows us to create and manage multiple contexts on a per thread basis.
We will be using NuGet
Package Manager to install the Entity Framework 4.1 assemblies.
The package installer can be found at http://nuget.org.
We will also be using a database for connecting to the data and updating it.
Open the Improving Parallel Context Management solution in the included source code examples.
using System.Threading; using DataAccess; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Test { [TestClass] public class MultiThreadedRepoManagementTests { private IBlogRepository repo1 = null; private IBlogRepository repo2 = null; private IBlogRepository repo3 = null; [TestMethod] public void ShouldAllowCommitsToMultipleContextsWithoutConcernForThread() { //Arrange RepositoryFactory.Get("Test"); //Act Thread thread = new Thread(GetRepositories); repo1 = RepositoryFactory.Get("Test"); thread.Start(); while (thread.ThreadState == ThreadState.Running) { Thread.Sleep(1); } //Assert Assert.IsNotNull(repo1); Assert.IsNotNull(repo2); Assert.IsNotNull(repo3); Assert.AreSame(repo2, repo3); Assert.AreNotSame(repo1, repo2); Assert.AreNotSame(repo1, repo3); } private void GetRepositories() { repo2 = RepositoryFactory.Get("Test"); repo3 = RepositoryFactory.Get("Test"); } } }
Blog
to the BusinessLogic
project with the following code:using System; namespace BusinessLogic { public class Blog { public int Id { get; set; } public DateTime Creationdate { get; set; } public string ShortDescription { get; set; } public string Title { get; set; } public double Rating { get; set; } } }
BlogMapping
to the DataAccess
project with the following code:using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using BusinessLogic; namespace DataAccess.Mappings { public class BlogMapping : EntityTypeConfiguration<Blog> { public BlogMapping() { this.ToTable("Blogs"); this.HasKey(x => x.Id); this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("BlogId"); this.Property(x => x.Title).IsRequired().HasMaxLength(250); this.Property(x => x.Creationdate).HasColumnName("CreationDate").IsRequired(); this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description"); } } }
BlogContext
to the DataAccess
project with the following code:using System; using System.Data.Entity; using System.Linq; using BusinessLogic; using DataAccess.Mappings; namespace DataAccess { public class BlogContext : DbContext, IUnitOfWork { public BlogContext(string connectionString) : base(connectionString) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new BlogMapping()); base.OnModelCreating(modelBuilder); } public IQueryable<T> Find<T>() where T : class { return this.Set<T>(); } public void Refresh() { this.ChangeTracker.Entries().ToList().ForEach(x=>x.Reload()); } public void Commit() { this.SaveChanges(); } } }
RepositoryFactory
into the DataAccess
project. The class will have another class defined in it that will handle identifying contexts with the following code:using System.Collections.Generic; using System.Configuration; using System.Threading; namespace DataAccess { public class RepositoryFactory { private static Dictionary<string, IBlogRepository> repositories = new Dictionary<string, IBlogRepository>(); public static IBlogRepository Get(string connectionString) { var id = new RepositoryIdentifier(Thread.CurrentThread, connectionString); if(!repositories.ContainsKey(id)) { //This would more than likely not new up the blog //repository but supply it from an IoC implementation. repositories.Add(id, new BlogRepository(new BlogContext(connectionString))); } return repositories[id]; } public static void Dispose(string connectionString) { var id = new RepositoryIdentifier(Thread.CurrentThread, connectionString); if (!repositories.ContainsKey(id)) { repositories.Remove(id); } } private class RepositoryIdentifier { private readonly Thread _currentThread; private readonly string _connectionString; public RepositoryIdentifier(Thread currentThread, string connectionString) { _currentThread = currentThread; _connectionString = connectionString; } public override string ToString() { return _currentThread.ManagedThreadId + _connectionString; } public static implicit operator string(RepositoryIdentifier r) { return r.ToString(); } } } }
The test defines how the repository factory should be used to get a context per thread. It also ensures that the same thread will get a reusable context if it calls the factory with the same connection string. This definition of the problem that we are trying to solve gives us a clear model to solve.
The addition of the Blog
object, the mappings, and the context will allow us to connect and communicate with a database, so that our example code is fully functional to the database. These pieces are essential for the full-testing of our solution.
The repository factory will act as a central creation point for all the repositories. This ensures that all repositories created this way are specific to the thread that they are in. The importance of this is that the DbContext
is not thread-safe, and will cause problems if the same object is shared across many threads. We can avoid these problems by separating the context by thread and allowing for reuse without conflicts.
When dealing with multi-threaded applications, there are several issues that we need to be aware of so that we can avoid them, and the following sections provide more details on some of those issues:
When we have two threads that have access to the shared data (such as a database), and one of them is somehow dependent on the execution happening in the right sequence, we have created a race condition. For example, when we have one thread adding a post to blog 1, and the second thread is selecting all posts from blog 1, and then modifying them to have a new title. If thread 2 completes its select operation before the save operation of thread 1, then it will not achieve the goal, but if save operation of thread 1 occurs first, then it will succeed. These two threads are now racing.
When resources are shared between threads, the resource may need to be locked. Locking ensures that only one thread can interact with the object at a time. This will allow shared resources to be used, without sacrificing the integrity of the system. This will enforce a serialization concurrency control.