dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.76k stars 3.18k forks source link

Is there any workarround for using DbContext in multiple threads #25099

Closed komdil closed 2 years ago

komdil commented 3 years ago

I have some scenarios related to getting entities from the database that I need to use one DbContext in multiple threads. I want to implement a queue for DbContext. I tried to do this by implementing IQuerable to a new class and lock query provider. It is working in some use cases, but I am still getting the error Asecond operation was started on this context before a previous operation completed... in some use cases

Here is my workaround:

public class CustomQueryProvider : IQueryProvider
    {
        private readonly IQueryProvider _databaseQueryProvider;
        private readonly MainContext _dbContext;

        public CustomQueryProvider(IQueryProvider databaseQueryProvider, MainContext dbContext)
        {
            _databaseQueryProvider = databaseQueryProvider;
            _dbContext = dbContext;
        }

        public IQueryable CreateQuery(Expression expression)
        {
            var res = _databaseQueryProvider.CreateQuery(expression);
            var customDBSet = new CustomDBSet(_dbContext, this, res);
            return customDBSet;
        }

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            var res = _databaseQueryProvider.CreateQuery<TElement>(expression);
            CustomDBSet<TElement> customDBSet = new CustomDBSet<TElement>(this, res);
            return customDBSet;
        }

        public object Execute(Expression expression)
        {
            return Execute<object>(expression);
        }

        public TResult Execute<TResult>(Expression expression)
        {
            lock (_databaseQueryProvider)
            {
                return _databaseQueryProvider.Execute<TResult>(expression);
            }
        }
    }

public class CustomDBSet<T> : IQueryable<T>
    {
        private readonly IQueryProvider _provider;
        private readonly IQueryable<T> _source;
        internal CustomDBSet(CustomQueryProvider provider, IQueryable<T> source)
        {
            _provider = provider;
            _source = source;
        }

        public IEnumerator<T> GetEnumerator()
        {
            return ((IEnumerable<T>)Provider.Execute(Expression)).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)Provider.Execute(Expression)).GetEnumerator();
        }

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

        public Type ElementType
        {
            get
            {
                return typeof(T);
            }
        }

        public IQueryProvider Provider
        {
            get
            {
                return _provider;
            }
        }
    }

GetEntities method:

public IQueryable<T> GetEntities<T>() where T : class
        {
            var sourceQuery = Set<T>().AsQueryable();
            var query = new CustomDBSet<T>( new CustomQueryProvider(sourceQuery.Provider, this), sourceQuery);
            return query;
        }

This workaround is working for getting a single entity. Here is the usage:

static void Main(string[] args)
        {
            MainContext mainContext = new MainContext();
            CreateData(mainContext);
            var task1 = Task.Run(new Action(() => GetStudents(mainContext)));
            var task2 = Task.Run(new Action(() => GetBackpacks(mainContext)));
            Task.WaitAll(task1, task2);
        }

        static void GetStudents(MainContext mainContext)
        {
            Console.WriteLine("Loading Students started");
            Console.WriteLine("Students Task Id is " + Task.CurrentId);
            var students = mainContext.GetEntities<Student>().FirstOrDefault(s => s.FirstName == "FirstName1");
            Console.WriteLine("Students loaded");
        }

        static void GetBackpacks(MainContext mainContext)
        {
            Console.WriteLine("Loading Backpacks started");
            Console.WriteLine("Backpacks Task Id is " + Task.CurrentId);
            var backpacks = mainContext.GetEntities<Backpack>().FirstOrDefault(s => s.Name == "Name1");
            Console.WriteLine("Backpacks loaded");
        }

I know DbContext has not supported multithreading, but in some use cases, I really need it. When the first operation is not completed I want the second operation to wait till completion. Is there any way to implement this? My workaround is working only for getting single entity and I am getting error on Where, Select... queries EF Core version: Database provider: (e.g. Microsoft.EntityFrameworkCore.SqlServer) Target framework: (e.g. .NET 5.0) Operating system: IDE: (e.g. Visual Studio 2019 16.3)

AndriySvyryd commented 3 years ago

Using a wrapping IQueryProvider is not recommended for this, as you would need to reimplement a lot of EF Core functionality to make it work as intended.

Consider using separate DbContext instances via DbContextFactory

komdil commented 3 years ago

I found another way to implement this. But I don't think it is a good idea. However, we can use in some use cases: https://github.com/komdil/efcoreMultiThreadingContext