9

I have a MVC application that uses Unity as its IoC container and have multiple services defined in my application using the PerRequestLifetimeManager.

container.RegisterType<IFileService, FileService>();

Everything works fine, except when I tried to roll my solution to automate tasks (like SharePoint TimerJobs), started in various intervals.

For that, I've defined a ServiceLocator-Type class ContainerManager in a separate project, that does essentially this:

    public static object Resolve(string typeName)
    {
        var type = Type.GetType(typeName);
        return Resolve(type);
    }

    public static object Resolve(Type type)
    {
        object result = DependencyResolver.Current.GetService(type);
        return result;
    }

    public static T Resolve<T>() where T : class
    {
        object result = DependencyResolver.Current.GetService<T>();
        return (T)result;
    }

    public static object Resolve(string typeName)
    {
        var type = Type.GetType(typeName);
        return Resolve(type);
    }

    public static object Resolve(Type type)
    {
        object result = DependencyResolver.Current.GetService(type);
        return result;
    }

    public static T Resolve<T>() where T : class
    {
        object result = DependencyResolver.Current.GetService<T>();
        return (T)result;
    }

And inside my "TaskManager" I do the following:

var unitOfWork = ContainerManager.Resolve<IFileService>();

Now this works when started manually (when originating from an HttpRequest). However, this does not work when started via my background thread.

I've tried calling unity directly (without my ServiceLocator), but then I'll get the exception: PerRequestLifetimeManager can only be used in the context of an HTTP request

That's how I create my tasks:

    private ITask CreateTask()
    {
        ITask task = null;
        if (IsEnabled)
        {
            var type = System.Type.GetType(Type);
            if (type != null)
            {
                object instance = ContainerManager.Resolve(type);
                if (instance == null)
                {
                    // Not resolved
                    instance = ContainerManager.ResolveUnregistered(type);
                }

                task = instance as ITask;
            }
        }

        return task;
    }

What am I missing?

SeToY
  • 5,777
  • 12
  • 54
  • 94
  • I am trying to understand your question. When you say "However, this does not work when started via my background thread", what do you mean? do you get an exception? So, TimerJobs are not executed per HTTP requests, they are execute on some schedule, right? You want them to use their own instance of the `FileService`, right? – Yacoub Massad Oct 17 '15 at 23:44
  • @YacoubMassad That's right. When I create a new Task to be run in my controller (originating from an HTTP Request), Unity is able to resolve the dependencies. However when my background timer does it, unity does not resolve the dependencies and returns "null" after a call to my ServiceLocator. – SeToY Oct 18 '15 at 11:32
  • When you say Task, do you mean [this class](https://msdn.microsoft.com/en-us/library/system.threading.tasks.task(v=vs.110).aspx) from the TPL? I though your were using other kind of Tasks from SharePoint? Can you provide some code on how you create tasks? – Yacoub Massad Oct 18 '15 at 12:33
  • In your Timer, do you want to use the same instance of the service that was used for the request that scheduled the task? Or you don't care about this, and simply want a new instance of the service? – Yacoub Massad Oct 18 '15 at 13:02
  • @YacoubMassad My `Task` is a custom class. SharePoint was just used to illustrate the type of code I'm writing :) I don't really care which instance I get... I just want to get an instance. I've added the code how I create my tasks. – SeToY Oct 18 '15 at 13:15
  • Also take a look at [this answer](http://stackoverflow.com/a/25671643/1370166). – TylerOhlsen Oct 19 '15 at 07:07

3 Answers3

15

You are using Serivice Location which is considered an anti-pattern.

Having said that, here is a direct answer to your question:

One way to solve your problem is using named registrations:

Let say that you are registering IService to Service using the PerRequestLifetimeManager lifetime manager like this:

container.RegisterType<IService, Service>(new PerRequestLifetimeManager());

You can also add another registration for the same types but with a different lifetime manager. However, to distinguished between this and the previous registration, you have to give it a name like this:

container.RegisterType<IService, Service>("transient_service", new TransientLifetimeManager());

Here I am registering IService with Service and using the transient lifetime manager. The name I am giving to this registration is "transient_service" , but you can use any name here.

Now, from your background thread, you can locate this service like this:

var service = container.Resolve<IService>("transient_service");

I am assuming here that you have access to the container (which you are doing through the service locator). You might need to update your service locator to enable it to locate services by name.

UPDATE:

Here is another solution:

You can create a custom lifetime manager that acts as the PerRequestLifetimeManager lifetime manager if there is an HttpContext in the current thread, and that will fallback to a TransientLifetimeManager if there isn't.

Here is how such lifetime manager would look like:

public class PerRequestOrTransientLifeTimeManager : LifetimeManager
{
    private readonly PerRequestLifetimeManager m_PerRequestLifetimeManager = new PerRequestLifetimeManager();
    private readonly TransientLifetimeManager m_TransientLifetimeManager = new TransientLifetimeManager();

    private LifetimeManager GetAppropriateLifetimeManager()
    {
        if (System.Web.HttpContext.Current == null)
            return m_TransientLifetimeManager;

        return m_PerRequestLifetimeManager;
    }

    public override object GetValue()
    {
        return GetAppropriateLifetimeManager().GetValue();
    }

    public override void SetValue(object newValue)
    {
        GetAppropriateLifetimeManager().SetValue(newValue);
    }

    public override void RemoveValue()
    {
        GetAppropriateLifetimeManager().RemoveValue();
    }
}

You need to modify your registrations to use such lifetime manager.

UPDATE 2:

The custom LifetimeManger code won't work with Unity 3.0 or later since it was completely rewritten and further abstracted into new Nuget packages as well. Here is an updated code:

public class PerRequestOrTransientLifeTimeManager : LifetimeManager
{
    private readonly PerRequestLifetimeManager _perRequestLifetimeManager = new PerRequestLifetimeManager();
    private readonly TransientLifetimeManager _transientLifetimeManager = new TransientLifetimeManager();

    private LifetimeManager GetAppropriateLifetimeManager()
    {
        if (HttpContext.Current == null)
        {
            return _transientLifetimeManager;
        }

        return _perRequestLifetimeManager;
    }

    public override object GetValue(ILifetimeContainer container = null)
    {
        return GetAppropriateLifetimeManager().GetValue();
    }

    public override void SetValue(object newValue, ILifetimeContainer container = null)
    {
        GetAppropriateLifetimeManager().SetValue(newValue);
    }

    public override void RemoveValue(ILifetimeContainer container = null)
    {
        GetAppropriateLifetimeManager().RemoveValue();
    }

    protected override LifetimeManager OnCreateLifetimeManager()
    {
        return this;
    }
}
Major
  • 5,948
  • 2
  • 45
  • 60
Yacoub Massad
  • 27,509
  • 2
  • 36
  • 62
  • 1
    Thank you for your answer. But that wouldn't work with constructor injected dependencies, would it? – SeToY Oct 18 '15 at 15:22
  • Can you explain the object graph that you are trying to create from the background thread and how such graph is using constructor injection? – Yacoub Massad Oct 18 '15 at 15:37
  • One of my tasks may be called `CleanupTask` that cleans up files from disk (hence the need of an `IFileService`) and therefore the constructor of said `CleanupTask` has an IFileService parameter. – SeToY Oct 18 '15 at 16:42
5

I would suggest you to have 2 separate containers, with different configuration, for the web environment and for the background environment. So, for your web environment, you can control the lifetime per request and in a background task you can do it per thread.

As you are using service locator, you could have 2 locators, like WebServiceLocator.Resolve<> and BackgroundServiceLocator.Resolve<>

Francesc Castells
  • 2,692
  • 21
  • 25
  • This is a good solution. +1. The only possible issue is that we would need to maintain two composition roots that might be identical. – Yacoub Massad Oct 18 '15 at 17:26
  • @YacoubMassad the container configuration itself shouldn't cause duplication because you could factor the lifetimemanager out and decide which one to use before hand. If the duplication comes from code using one service locator or the other, then the best would be to replace the service locator by injected factories or even injecting an IContainer. Then you would just have to reference one container or the other in the entry point of each environment. Not sure if this makes sense. – Francesc Castells Oct 18 '15 at 18:37
  • I think you are right. You can make the code that configures the container take a lifetime manager type as input. And each app will consume such container building code using a different lifetime manager. – Yacoub Massad Oct 18 '15 at 18:56
0

2023 update to the accepted answer

Now code should look like this:

public class PerRequestOrHierarchicalLifeTimeManager : LifetimeManager, ITypeLifetimeManager,
    IFactoryLifetimeManager
{
    private readonly PerRequestLifetimeManager perRequestLifetimeManager = new PerRequestLifetimeManager();
    private readonly HierarchicalLifetimeManager hierarchicalLifetimeManager = new HierarchicalLifetimeManager();

    private LifetimeManager GetAppropriateLifetimeManager()
    {
        if (HttpContext.Current == null)
        {
            return hierarchicalLifetimeManager;
        }

        return perRequestLifetimeManager;
    }

    public override object GetValue(ILifetimeContainer container = null)
    {
        return GetAppropriateLifetimeManager().GetValue(container);
    }

    public override void SetValue(object newValue, ILifetimeContainer container = null)
    {
        GetAppropriateLifetimeManager().SetValue(newValue, container);
    }

    public override void RemoveValue(ILifetimeContainer container = null)
    {
        GetAppropriateLifetimeManager().RemoveValue(container);
    }

    protected override LifetimeManager OnCreateLifetimeManager()
    {
        return this;
    }

    public LifetimeManager CreateLifetimePolicy()
    {
        return this;
    }
}
Yehor Androsov
  • 4,885
  • 2
  • 23
  • 40