Autofac shared objects require different registrat

2019-08-08 17:31发布

As detailed in InstancePerApiControllerType not working, I am unable to use the InstancePerApiControllerType to configure my solution. The answer provided there works so long as I am directly injecting a ConnectionContext into the controller, or otherwise know that a class is only used by a specific controller. Unfortunately that is not the case in my situation:

ControllerA -> EngineA -> RepositoryA -> GenericEntityAccessor

ControllerB -> EngineB -> RepositoryB -> GenericEntityAccessor

The issue is when we come in through ControllerA, GenericEntityAccessor needs "string A" and from ControllerB it needs "string B".

Of course, the real situation is a little more complicated and there are some bad practices such as code that directly "news"-up a ConnectionContext (it's legacy code). I'm currently exploring providing another component that provides the connection string that is injected via Autofac and configured in the controller using Lazy, but the bad practices are causing problems there also (i.e. once I start to change things in the interface, all the dominoes start to fall over and I end up 15 classes later wondering how I got there).

Are there any patterns, techniques, etc. that address this type of thing? I can't imagine it's all that uncommon.

UPDATE:

To provide a few more specifics, since I'm having some trouble getting this to work, in general we have the following hierarchy, showing which scopes I've applied

Controller -> InstancePerApiRequest()
I*Repository -> ?
I*Manager -> ?
I*Builder -> ?
I*Adapter -> ?
ISqlServerConnectionContext -> ?
IConnectionContextCache -> InstancePerApiRequest()

I've got a number of components that directly take ISqlServerConntectionContext and I'm trying to provide it like so:

container.Register(c =>
{
    var connectionContextCache = c.Resolve<IConnectionContextCache>();
    var connection = (ISqlServerConnectionContext)connectionContextCache.CurrentConnectionContext;

    return connection;
}).As<ISqlServerConnectionContext>().InstancePerDependency();

Unfortunately at that point I'm getting a null for CurrectConnectionContext. My guess at this point is I've got some component that isn't rooted from the controller and I'm currently going through the dependencies manually attempting to find it (AFAIK the isn't a way for my to find out which object triggered Autofac to attempt to provide the ISqlServerConnectionContext when I'm debugging).

UPDATE 2:

It turns out I did have some issues where I was registering things improperly, and creating a dependency on ISqlServerConnectionContext for DocumentController, even though it did not have one (this was created through the delegate for something it did depend on).

Now I've got a circular reference that I'm pretty sure I've created myself in the registrations:

container.Register(x =>
{
    if (x.IsRegistered<HttpRequestMessage>())
    {
        var httpRequestMethod = x.Resolve<HttpRequestMessage>();

        var tokenHelper = x.Resolve<ITokenHelper>();
        var token = tokenHelper.GetToken(httpRequestMethod);

        return token ?? new NullMinimalSecurityToken();
    }

    return new NullMinimalSecurityToken();
}).As<IMinimalSecurityToken>().InstancePerApiRequest();

container.Register(c =>
{
    var connectionContextCache = c.Resolve<IConnectionContextCache>();
    var token = c.Resolve<IMinimalSecurityToken>();
    var connection = (ISqlServerConnectionContext)connectionContextCache.CurrentConnectionContext;

    connection.Token = token;

    return connection;
}).As<ISqlServerConnectionContext>().InstancePerApiRequest();

The problem is ISqlServerConnectionContext has a property of type IMinimalSecurityToken which is optional, and definitely not used when the ISqlServerConnectionContext is being used to look up IMinimalSecurityToken, which depends on ISqlServerConnectionContext through ITokenHelper.

UPDATE 3:

For completeness, in order to solve my circular reference problem I needed to use named services, and use a SqlServerConnectionContext that did not have the IMinimalSecurityToken property set for the IOAuthTokenManager registration. Now I'm getting the dreaded

No scope with a Tag matching 'AutofacWebRequest' is visible

error, but I think that warrants a new question if I'm not able to solve it.

container.Register(c =>
{
    var productId = WellKnownIdentifierFactory.Instance.GetWellKnownProductIdentifier(WellKnownProductIdentifiers.RESTSearchService);
    var connectionString = ConfigurationManager.AppSettings[AppSettingsNames.DatabaseConnection];

    var newConnectionContext = new SqlServerConnectionContext(connectionString) { ProductID = productId };
    newConnectionContext.Open();

    return newConnectionContext;
}).Named<ISqlServerConnectionContext>("OAuthTokenConnectionContext").InstancePerApiRequest();
container.Register(c => new SqlServerBuilderFactory(c.ResolveNamed<ISqlServerConnectionContext>("OAuthTokenConnectionContext"))).Named<IBuilderFactory>("OAuthTokenBuilderFactory").InstancePerApiRequest();
container.Register(c =>new OAuthTokenManager(c.ResolveNamed<IBuilderFactory>("OAuthTokenBuilderFactory"))).As<IOAuthTokenManager>().InstancePerApiRequest();

1条回答
戒情不戒烟
2楼-- · 2019-08-08 18:00

This can be solved using AutoFac's support for object graph lifetime scoping.

  1. Cache the current SqlServerConnectionContext in an object scoped to the lifetime of your controller.
  2. Within the SqlServerConnectionContext factory type, once the connection is created assign it to the backing field of the current lifetime-scoped cache
  3. Any types scoped within the lifetimes scope of a controller can then access the connection associated with that controller through the cache

The only complexities I can think of are:

  1. If the controller is not actually the root of a lifetime scope for all types with a dependency on a specific connection. I.e. if they fall outside the lifetime of the controller.
  2. If any of the dependencies are registered as single instance. In which case they will not be able to resolve the Cache as it is currently implemented as it is PerApiRequest.

For example:

public interface ISqlServerConnectionContextCache
{
    ISqlServerConnectionContext CurrentContext { get; set; }
}

public class SqlServerConnectionContextScopeCache : ISqlServerConnectionContextCache
{
    public ISqlServerConnectionContext CurrentContext { get; set; }
}

public interface ISqlServerConnectionContextFactory
{
  ISqlServerConnectionContext Create();
}

// The factory has the cache as a dependancy
// This will be the first use of the cache and hence
// AutoFac will create a new one at the scope of the controller
public class SqlServerConnectionContextFactory : ISqlServerConnectionContextFactory
{
  private string _connectionString;
  private ISqlServerConnectionContextCache _connectionCache;

  public SqlServerConnectionContextFactory(ISqlServerConnectionContextCache connectionCache,
    string connectionString)
  {
    _connectionCache = connectionCache;
    _connectionString = connectionString;  
  }

  public ISqlServerConnectionContext Create()
  {
    var connectionContext = new SqlServerConnectionContext(_connectionString);
    connectionContext.Open();
    _sqlServerConnectionContextProvider.CurrentContext = connectionContext;
    return connectionContext;
  }
}

public class MyController : ApiController
{
  private ISqlServerConnectionContext _sqlServerConnectionContext;

  public MyController(Func<string, ISqlServerConnectionContextFactory> connectionFactory)
  {
    _sqlServerConnectionContext = connectionFactory("MyConnectionString");
  }
}

// As the cache is lifetime scoped it will receive the single instance
// of the cache associated with the current lifetime scope
// Assuming we are within the scope of the controller this will receive
// the cache that was initiated by the factory
public class MyTypeScopedByController
{
    public MyTypeScopedByController(ISqlServerConnectionContextCache connectionCache)
    {
        var sqlServerConnectionContext = connectionCache.CurrentContext;
    }
}

// AutoFac wiring
builder.RegisterType<SqlServerConnectionContextScopeCache>()
    .As<ISqlServerConnectionContextCache>()
    .InstancePerApiRequest();
builder.RegisterType<SqlServerConnectionContextFactory>()
    .As<ISqlServerConnectionContextFactory>()
    .InstancePerDependency();
查看更多
登录 后发表回答