Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cache DataContext instances in a consumer type application?

We have an application using SDK provided by our provider to integrate easily with them. This SDK connects to AMQP endpoint and simply distributes, caches and transforms messages to our consumers. Previously this integration was over HTTP with XML as a data source and old integration had two ways of caching DataContext - per web request and per managed thread id. (1)

Now, however, we do not integrate over HTTP but rather AMQP which is transparent to us since the SDK is doing all the connection logic and we are only left with defining our consumers so there is no option to cache DataContext "per web request" so only per managed thread id is left. I implemented chain of responsibility pattern, so when an update comes to us it is put in one pipeline of handlers which uses DataContext to update the database according to the new updates. This is how the invocation method of pipeline looks like:

public Task Invoke(TInput entity)
{
    object currentInputArgument = entity;

    for (var i = 0; i < _pipeline.Count; ++i)
    {
        var action = _pipeline[i];
        if (action.Method.ReturnType.IsSubclassOf(typeof(Task)))
        {
            if (action.Method.ReturnType.IsConstructedGenericType)
            {
                dynamic tmp = action.DynamicInvoke(currentInputArgument);
                currentInputArgument = tmp.GetAwaiter().GetResult();
            }
            else
            {
                (action.DynamicInvoke(currentInputArgument) as Task).GetAwaiter().GetResult();
            }
        }
        else
        {
            currentInputArgument = action.DynamicInvoke(currentInputArgument);
        }
    }

    return Task.CompletedTask;
}

The problem is (at least what I think it is) that this chain of responsibility is chain of methods returning/starting new tasks so when an update for entity A comes it is handled by managed thread id = 1 let's say and then only sometime after again same entity A arrives only to be handled by managed thread id = 2 for example. This leads to:

System.InvalidOperationException: 'An entity object cannot be referenced by multiple instances of IEntityChangeTracker.'

because DataContext from managed thread id = 1 already tracks entity A. (at least that's what I think it is)

My question is how can I cache DataContext in my case? Did you guys have the same problem? I read this and this answers and from what I understood using one static DataContext is not an option also.(2)

  1. Disclaimer: I should have said that we inherited the application and I cannot answer why it was implemented like that.
  2. Disclaimer 2: I have little to no experience with EF.

Comunity asked questions:

  1. What version of EF we are using? 5.0
  2. Why do entities live longer than the context? - They don't but maybe you are asking why entities need to live longer than the context. I use repositories that use cached DataContext to get entities from the database to store them in an in-memory collection which I use as a cache.

This is how entities are "extracted", where DatabaseDataContext is the cached DataContext I am talking about (BLOB with whole database sets inside)

protected IQueryable<T> Get<TProperty>(params Expression<Func<T, TProperty>>[] includes)
{
    var query = DatabaseDataContext.Set<T>().AsQueryable();

    if (includes != null && includes.Length > 0)
    {
        foreach (var item in includes)
        {
            query = query.Include(item);
        }
    }

    return query;
}

Then, whenever my consumer application receives AMQP message my chain of responsibility pattern begins checking if this message and its data I already processed. So I have method that looks like that:

public async Task<TEntity> Handle<TEntity>(TEntity sportEvent)
            where TEntity : ISportEvent
{
    ... some unimportant business logic

    //save the sport
    if (sport.SportID > 0) // <-- this here basically checks if so called 
                           // sport is found in cache or not
                           // if its found then we update the entity in the db
                           // and update the cache after that
    {
        _sportRepository.Update(sport); /* 
                                         * because message update for the same sport can come
                                         * and since DataContext is cached by threadId like I said
                                         * and Update can be executed from different threads
                                         * this is where aforementioned exception is thrown
                                        */

    }
    else                   // if not simply insert the entity in the db and the caches
    {
        _sportRepository.Insert(sport);
    }

    _sportRepository.SaveDbChanges();

    ... updating caches logic
}

I thought that getting entities from the database with AsNoTracking() method or detaching entities every time I "update" or "insert" entity will solve this, but it did not.

like image 779
kuskmen Avatar asked Dec 23 '19 11:12

kuskmen


1 Answers

Whilst there is a certain overhead to newing up a DbContext, and using DI to share a single instance of a DbContext within a web request can save some of this overhead, simple CRUD operations can just new up a new DbContext for each action.

Looking at the code you have posted so far, I would probably have a private instance of the DbContext newed up in the Repository constructor, and then new up a Repository for each method.

Then your method would look something like this:

public async Task<TEntity> Handle<TEntity>(TEntity sportEvent)
        where TEntity : ISportEvent
{
        var sportsRepository = new SportsRepository()

        ... some unimportant business logic

        //save the sport
        if (sport.SportID > 0) 
        {
            _sportRepository.Update(sport);
        }
        else
        {
            _sportRepository.Insert(sport);
        }

        _sportRepository.SaveDbChanges();

}

public class SportsRepository
{
    private DbContext _dbContext;

    public SportsRepository()
    {
        _dbContext = new DbContext();
    }

}

You might also want to consider the use of Stub Entities as a way around sharing a DbContext with other repository classes.

like image 77
ste-fu Avatar answered Nov 08 '22 04:11

ste-fu