0

I have a hosted service that processes objects PUT/POST via an API endpoint, that is, as soon as a new entity is given or an existing one is edited, (a) the hosted service starts processing it (a long running process), and (b) the received/modified object returned (as a JSON object) to the API caller.

When PUT/POST an entity, I see run-time errors here and there (e.g., at object JSON serializer) complaining for different issues, such as:

ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.

or:

InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext.

Initially I was using a database context pool, but according to this, it seems the pooling has known issues with hosted services. Therefore, I switched to regular AddDbContext; however, neither that has solved the problem.

This is how I define the database context and the hosted service:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCustomDbContext(Configuration);

        // This is the hosted service:
        services.AddHostedService<MyHostedService>();
    }
}

public static class CustomExtensionMethods
{
    public static IServiceCollection AddCustomDbContext(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddDbContext<MyContext>(
            options =>
            {
                options
                .UseLazyLoadingProxies(true)
                .UseSqlServer(
                    configuration.GetConnectionString("DefaultConnection"),
                    sqlServerOptionsAction: sqlOptions => { sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); });
            });

        return services;
    }
}

and I access the database context in hosted service as the following (as recommended here):

using(var scope = Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<MyContext>();
}

Edit 1

As mentioned, the errors happen all around the code; however, since I mentioned the errors occurring on the serializer, I am sharing the serializer code in the following:

public class MyJsonConverter : JsonConverter
{
    private readonly Dictionary<string, string> _propertyMappings;

    public MyJsonConverter()
    {
        _propertyMappings = new Dictionary<string, string>
        {
            {"id", nameof(MyType.ID)},
            {"name", nameof(MyType.Name)}
        };
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject obj = new JObject();
        Type type = value.GetType();

        foreach (PropertyInfo prop in type.GetProperties())
        {
            if (prop.CanRead)
            {
                // The above linked errors happen here. 
                object propVal = prop.GetValue(value, null);
                if (propVal != null)
                    obj.Add(prop.Name, JToken.FromObject(propVal, serializer));
            }
        }

        obj.WriteTo(writer);
    }
}

Update 2

An example API endpoint is as the following:

[Route("api/v1/[controller]")]
[ApiController]
public class MyTypeController : ControllerBase
{
    private readonly MyContext _context;
    private MyHostedService _service;

    public MyTypeController (
        MyContext context,
        MyHostedService service)
    {
        _context = context;
        _service = service
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<IEnumerable<MyType>>> GetMyType(int id)
    {
        return await _context.MyTypes.FindAsync(id);
    }

    [HttpPost]
    public async Task<ActionResult<MyType>> PostMyType(MyType myType)
    {
        myType.Status = State.Queued;
        _context.MyTypes.Add(myType);
        _context.MyTypes.SaveChangesAsync().ConfigureAwait(false);

        // the object is queued in the hosted service for execution.
        _service.Enqueue(myType);

        return CreatedAtAction("GetMyType", new { id = myType.ID }, myType);
    }
}
Dr. Strangelove
  • 2,725
  • 3
  • 34
  • 61
  • Generally speaking, `ObjectDisposedException` exceptions with JSON serialization indicate that the db context was disposed prior to the JSON serializer attempting to serialize the object. This is because the serializer will look at all the properties and trigger a lazy load which will attempt to use the underlying context (which has been disposed). I would not recommend directly passing db entity classes to the front end/view/serializer. Create a model instead and map only what you need, then pass that. – JuanR Jan 28 '20 at 19:46
  • I have a model and all the navigational properties are loaded via lazy load; all was working fine until I decided to add the hosted service. – Dr. Strangelove Jan 28 '20 at 19:51
  • It's hard to tell without seeing the actual code that causes the error, because what you posted is **not** it. Please post the code that consumes the context and produces the JSON response. – JuanR Jan 28 '20 at 19:52
  • By the way, the documentation clearly states that: `Entity Framework Core does not support multiple parallel operations being run on the same DbContext instance`. Again, impossible to tell what is causing the threading issue without the actual code. – JuanR Jan 28 '20 at 19:55
  • @JuanR agree, please see the text in bold in my question: I have been using db pooling, but had to revert because the of the know issue pooling has with the hosted services. – Dr. Strangelove Jan 28 '20 at 20:14
  • @JuanR also, I am not sharing/passing a common instance of database between different processes, rather I am relying on the DI, hopping it will inject scoped contexts for different services. – Dr. Strangelove Jan 28 '20 at 20:15
  • Thank you for updating the code. As I mentioned before, this is a fairly common issue when using contexts and serializers. At what point are you attempting to serialize? I don't see it in the code. – JuanR Jan 28 '20 at 20:28
  • @JuanR Please see the `Update 2`. – Dr. Strangelove Jan 28 '20 at 20:42
  • Why you are using background task for processing entities if caller still waiting for the result. When you need to return result of long process to the api caller it can be successfully executed within request thread/process – Fabio Jan 28 '20 at 21:23
  • This is not waiting for the results of background task to be ready, instead it queues the object to be processed, and updates the status of the object to "queued" (which will be changed to "running" or "completed" by the background task). – Dr. Strangelove Jan 28 '20 at 21:26

1 Answers1

1

The following lines are most likely causing the ObjectDisposedException error:

return await _context.MyTypes.FindAsync(id);

and

return CreatedAtAction("GetMyType", new { id = myType.ID }, myType);

This is because you are relying on this variable:

private readonly MyContext _context;

Since object myType has been attached to that context.

As I mentioned before, it is not a good idea to send context entities for serialization because by the time the serializer has a chance to fire, the context might have been disposed. Use a model (meaning a class in the Models folder) instead and map all the relevant properties from your real entity to it. for instance, you could create a class called MyTypeViewModel that contains only the properties that you need to return:

public class MyTypeViewModel
{
    public MyTypeViewModel(MyType obj)
    {
        Map(obj);
    }

    public int ID { get; set; }

    private void Map(MyType obj)
    {
        this.ID = obj.ID;
    }
}

Then instead of returning the entity, use the view model:

var model = new MyTypeViewModel(myType);
return CreatedAtAction("GetMyType", new { id = myType.ID }, model);

As far as the InvalidOperationException, my educated guess is that since you are not awaiting the SaveChangesAsync method, the serializer is firing while the original operation is still in progress, causing a double hit to the context, resulting in the error.

Using await on the SaveChangesAsync method should fix that, but you still need to stop sending lazy-loaded entities for serialization.

Upon further review, the service itself might also be causing issues since you are passing it a reference to object myType:

_service.Enqueue(myType);

The same two issues may occur if the service is doing something with the object that causes a call to a now-disposed context or at the same time as other asynchronous parts (e.g. serialization) attempt to lazy-load stuff.

JuanR
  • 7,405
  • 1
  • 19
  • 30
  • "you still need to stop sending lazy-loaded entities for serialization" any thoughts on what is the alternative? I have Model, and as I mentioned, all this code was fine until I changed from pooled database context to the current one due to known issues with HostedServices. – Dr. Strangelove Jan 28 '20 at 21:18
  • @Hamed: Please read my last note on enqueing the object. After further review, I think this might be the most likely source of your issues, since it's a long running process. – JuanR Jan 28 '20 at 21:19
  • service modifies the object, that is its role; however, as I mentioned in the question, the service creates its own scoped context. Should not that be sufficient? – Dr. Strangelove Jan 28 '20 at 21:22
  • @Hamed: Precisely, the context used by the service is not the same context as the one object `myType` is attached to. – JuanR Jan 28 '20 at 21:33
  • As I guessed; so, if the service is lazy-loading or modifying the properties of the given entity, it should be using its own, and independent context, so not causing any such issues?! – Dr. Strangelove Jan 28 '20 at 21:34
  • @Hamed: Exactly! Think about it. You are setting up a long running process but you are passing it an object that is bound to a context that will be destroyed when the current request ends. At the same time, it's possible the service starts performing work on the object while the context hasn't been disposed, so it works fine but the serializer attempts to return the object back, causing the double hit on the context, which results in the `InvalidOperationException` error. – JuanR Jan 28 '20 at 21:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/206822/discussion-between-juanr-and-hamed). – JuanR Jan 28 '20 at 21:46
  • thanks, as per your suggestion, I started sending `ID` of the objects to the queue, and let the service fetch them from database again instead of sending the object itself. And it seems it worked, I am not seeing any issues now, hope that persists :) Thanks again. – Dr. Strangelove Jan 28 '20 at 21:57