ipjohnson / Grace

Grace is a feature rich dependency injection container library
MIT License
336 stars 33 forks source link

TaskOrThread lifestyle #144

Closed xp-development closed 6 years ago

xp-development commented 6 years ago

Hi @ipjohnson,

I hope that the following question is my last. :)

I think I need custom lifestyle or is there another method out of the box. I want to receive one service per task or if there is no task one service per thread as fallback.

Is there some documentation how to develop an own lifestyle?

In Ninject I develop an own scope resolver:

public class ScopeResolvers
{
  private static readonly AsyncLocal<ConcurrentDictionary<int, object>> asyncLocalTaskToScopeObjectDictionary = new AsyncLocal<ConcurrentDictionary<int, object>>();

  protected static object GetTaskScope(int taskId)
  {
    ConcurrentDictionary<int, object> taskToScopeObjectDictionary = asyncLocalTaskToScopeObjectDictionary.Value;

    if (taskToScopeObjectDictionary == null)
    {
      asyncLocalTaskToScopeObjectDictionary.Value = taskToScopeObjectDictionary = new ConcurrentDictionary<int, object>();
    }

    return taskToScopeObjectDictionary.GetOrAdd(taskId, _ => new object());
  }

  public object GetTaskOrThreadScope() => Task.CurrentId != null ? GetTaskScope(Task.CurrentId.Value) : Thread.CurrentThread;
}
ipjohnson commented 6 years ago

Hi @jan-schubert

I don't have any specific documentation on creating your own lifestyle, that said you should be able to do what you are looking to do pretty easy by looking at SingletonPerScopeLifestyle. You can remove the thread safe bool as you don't need it for your use case. Just replace the GetValueFromScope method.

xp-development commented 6 years ago

This is my implementation.

public static T GetValueFromScope<T>(IExportLocatorScope scope, ActivationStrategyDelegate creationDelegate, string uniqueId)
{
  var id = Task.CurrentId.HasValue ? $"{uniqueId}ta{Task.CurrentId.Value}" : $"{uniqueId}th{Thread.CurrentThread.ManagedThreadId}";
  return (T)(scope.GetExtraData(id) ?? scope.SetExtraData(id, creationDelegate(scope, scope, null), false));
}

It works fine, but the Container.ExtraData collection is getting bigger and bigger. Is it possible to remove extra data from container, if a task was finished?

[Fact]
public void TaskOrThreadLifestyle()
{
  var container = new DependencyInjectionContainer();
  container.Configure(c => c.Export<BasicService>().As<IBasicService>().Lifestyle.Custom(new TaskOrThreadLifestyle(false)));

  IBasicService instance = null;
  IBasicService instance2 = null;

  Task.Run(() =>
  {
    instance = container.Locate<IBasicService>();

    // ------> expected container.ExtraData.Count == 1
  }).Wait();
  //  ------>  expected container.ExtraData.Count == 0, but was 1

  Task.Run(() =>
  {
    instance2 = container.Locate<IBasicService>();
  }).Wait();

  Assert.NotNull(instance);
  Assert.NotNull(instance2);
  Assert.NotSame(instance, instance2);
}
ipjohnson commented 6 years ago

@jan-schubert there isn't a way currently but maybe what you could do is instead of putting it in the extra data directly you put a ConcurrentDictionary in the extra data that you can add and remove easily.

The other option you could do at the moment is set the value to null.

Moving forward it does seem like a worth while feature it might make sense to open a new issue requesting the ability to remove extra data. I don't have a lot of time right at the moment but it could be useful in the future.

ipjohnson commented 6 years ago

@jan-schubert can I close this out or do you have more questions?

xp-development commented 6 years ago

All fine! Thanks.