How to handle async calls with Ninject InRequestScope?
Asked Answered
M

1

6

We are using Ninject in an ASP.NET Web Api application, and we bind our DbContext with InRequestScope. This works well with most of our requests, because they do all their work synchronously, so the context can be safely disposed after the request is completed.

However, we have on request in which we do an asynchronous web service call, that has a continuation method passed as a callback, and that callback method needs to use the database context. However our request shouldn't wait for the asynchronous service call to finish, but return immediately (this is an explicit requirement).

Here is a simplified example of the situation.

public class MyController : ApiController
{
    private readonly MyDbContext dbContext;
    private readonly SomeWebService service;

    public MyController(MyDbContext dbContext, SomeWebService service)
    {
        this.dbContext = dbContext;
        this.service = service;
    }

    public IHttpActionResult MyActionWithAsyncCall()
    {
        // Doing stuff.

        // Calling webservice method, passing the Callback as the continuation.
        service.MethodWithCallback(param1, param2, this.Callback);

        // Returning without waiting for the service call to be completed.
        return Ok();
    }

    private void Callback()
    {
        // Trying to use the DbContext:
        var person = dbContext.People.First();
        // The above line sometimes throws exception, because the context has been disposed.
    }
}

How should this situation be handled with Ninject? Is there a way to somehow "prolong" the lifetime of a bound DbContext instance explicitly? Or should the Callback method create completely new DbContext? If it should, what scope should it use?

Meleager answered 6/11, 2014 at 8:44 Comment(1)
Sounds like a job for message queueing.Orjonikidze
W
8

There's is no way to explicitly prolong the lifetime of an object with .InRequestScope() to extend to after the request end.

If there's not a business requirement that the work during the request and @ callback must happen in a single transaction i would go for using two DbContext instances. One during the request and one during the callback. Note: As far as i know this also means you can't take an entity from the first context and update/save it in the second context. This means you must only pass identifier (and other data relevant to the operation) from request to callback. The callback has to "create" a new DbContext and retrieve the according entitites from the context.

Conditional Binding Alternative

As an alternative you might declare a special binding for this special case. Ninject supports so called contextual bindings. This means you would have two bindings, the standard binding and a contextual, special case binding:

Bind<DbContext>().ToSelf().InRequestScope();

Bind<DbContext>().ToSelf()
    .WhenInjectedInto<SomeController>();

Notice that the second binding does not specify a scope - that means SomeController is responsible to call .Dispose(). In your case that would mean the callback would have to dispose the context. You'd also need to dispose of the context in all errors cases (errors in the callback code, errors occurring before callback is triggered,....).

Also, in reality your application is probably a bite more complex and .WhenInjectedInto<SomeController> is not going to be enough/correct, because you might want to inject the same instance into the controller plus a repository plus a query object.. what not.

That means you will need scoping, but a scope different from .InRequestScope(). You might use .InCallScope() or named scope - both are included in the named scope extension.

Furthermore you would need to adapt the When condition. You could adapt it so to traverse the requests and see if there is FooController anywhere in the request chain. But that's not very performant, instead i would recommend using a ninject IParameter to specify that you want special case treatment. The parameter would be:

public class NonRequestScopedParameter : Ninject.Parameters.IParameter
{
    public bool Equals(IParameter other)
    {
        if (other == null)
        {
            return false;
        }

        return other is NonRequestScopedParameter;
    }

    public object GetValue(IContext context, ITarget target)
    {
        throw new NotSupportedException("this parameter does not provide a value");
    }

    public string Name
    {
        get { return typeof(NonRequestScopedParameter).Name; }
    }

    // this is very important
    public bool ShouldInherit
    {
        get { return true; }
    }
}

which would be applied at the bindings like:

kernel.Bind<SomeController>().ToSelf()
      .WithParameter(new NonRequestScopedParameter());

kernel.Bind<DbContext>().ToSelf()
       .When(x => x.Parameters.OfType<NonRequestScopedParameter>().Any())
       .InCallScope(); // or whatever scope you're using
Whispering answered 6/11, 2014 at 10:24 Comment(1)
Thanks for the detailed answer. Finally we chose the first approach, the async callback uses a separate context (actually the callback is implemented in another class, that has an IServiceLocator interface injected, and asks for a new context in every callback call.)Meleager

© 2022 - 2024 — McMap. All rights reserved.