ipjohnson / Grace

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

Combination with SimpleFixture #162

Closed SeriousM closed 6 years ago

SeriousM commented 6 years ago

Hi! I'm trying to combine Grace with SimpleFixture. My attempt is as follows:

public class FixtureBase : SimpleFixture.Fixture
{
  [TestInitialize]
  public void TestSetup()
  {
    var container = new DependencyInjectionContainer();
    container.Configure(reg =>
      reg.ExportFactory<StaticInjectionContext, ILogger>(context =>
        LoggerFactory.GetLogger(context.TargetInfo.InjectionType)));

    Add(new GraceInjectorConvention(container));
    Populate(this);
  }
}

public class GraceInjectorConvention : SimpleFixture.IConvention
{
  private readonly ILocatorService locatorService;
  public GraceInjectorConvention(ILocatorService locatorService)
  {
    this.locatorService = locatorService;
  }

  public object GenerateData(DataRequest request)
  {
    if (locatorService.TryLocate(request.RequestedType, out object instance))
      return instance;
    return Convention.NoValue;
  }

  // this convention should be ask as last instance. this way the fixture can override types.
  public ConventionPriority Priority { get; } = ConventionPriority.Last;
  public event EventHandler<PriorityChangedEventArgs> PriorityChanged;
}

Sorry for the code but it's the smallest example I could create.

The problem is now that context.TargetInfo.InjectionType is empty. I guess it's because I use Populate from SimpleFixture instead of Locate from the container instance.

Is there a way to tell Grace which instance is about to get populated? I don't need SimpleFixture yet I would have the same issue if I would populate my Fixture properties myself.

Thank you @ipjohnson for Grace, it's awesome :)

/ref: #141

SeriousM commented 6 years ago

I know there is a ILogService which is used for Common.Logging, NLog and log4net wrappers, but I use Serilog in combination with SerilogAnalyzer which forbids me to use another interface type for logging than Serilog.ILogger. The use of another type would break the c# code analyzer. /edit: just have seen that the ILogService is from 4.x, I use the latest version 6.x

ipjohnson commented 6 years ago

@SeriousM very interesting use case I'll see if I can take a look this evening. I actually haven't tried combining the two together like that.

SeriousM commented 6 years ago

Oh thank you very much! That'd be awesome! Populating / completing an created object is a common usecase for us.

SeriousM commented 6 years ago

I've implemented a populate function for existing objects:

private void populate(IInjectionScope scope, object instance)
{
  var request = scope.StrategyCompiler.CreateNewRequest(instance.GetType(), 1, scope);
  var members = scope.MemberInjectionSelectors.Aggregate(
    Enumerable.Empty<MemberInjectionInfo>(),
    (list, selector) => list.Concat(selector.GetPropertiesAndFields(instance.GetType(), scope, request))).Distinct().ToArray();

  foreach (var member in members)
  {
    var propertyInfo = (PropertyInfo)member.MemberInfo;
    object createdValue;

    if (!member.IsRequired)
    {
      scope.TryLocate(propertyInfo.PropertyType, out createdValue, withKey: member.LocateKey);
    }
    else
    {
      createdValue = scope.Locate(propertyInfo.PropertyType, withKey: member.LocateKey);
    }

    propertyInfo.SetValue(instance, createdValue);
  }
}

I tried to use your objects. One problem I have is that the information in MemberInjectionInfo is empty except of the MemberInfo. Now I need to pass in the instance to tell Grace what's the target is.

ipjohnson commented 6 years ago

@SeriousM I took a look last night and ultimately I think the reason context.TargetInfo.InjectionType is empty is because it's only present when the type is being injected into something (that the container knows about) and there is no way to assign it as StaticContext is created when the method is first executed.

I think what you could do instead is pass the data in through the extra data parameter in locate

  public object GenerateData(DataRequest request)
  {
    // get injected type from request.ExtraData (is a PropertyInfo or MemberInfo)
    if (locatorService.TryLocate(request.RequestedType, out object instance, new { InjectedType = injectedType }))
      return instance;
    return Convention.NoValue;
  }

ILogger registration

    container.Configure(reg =>
      reg.ExportFactory<StaticInjectionContext, IInjectionContext, ILogger>((context, injectionContext) =>
        LoggerFactory.GetLogger(context.TargetInfo.InjectionType ?? injectionContext.GetExtraData("InjectedType"))));

I'll be honest it's a bit hard to follow what you are trying to do with out a working sample.

SeriousM commented 6 years ago

I'll be honest it's a bit hard to follow what you are trying to do with out a working sample.

But you guessed pretty well! The logger of Serilog (and NLog, log4net, etc.) can be created for a context in which the logger logs. Usually this is the class name so that one is able to determine the source of the log. To achieve this I create the ILogger on the fly in context with the target type.

Your suggestion was a good starting point, but now I have two problems:

I hope you can help me again?

ipjohnson commented 6 years ago

@SeriousM you are correct the injection context is not shared through the Singleton lifestyle. That said the logic for the is in the lifestyle itself so it can be overridden. I'll see if I can put together lifestyle that does what you want to do.

SeriousM commented 6 years ago

Great, thank you!

ipjohnson commented 6 years ago

I've added a test showing custom singleton here that passes the injection context through during construction