Design Meeting Notes - September 27, 2012

Dependency resolver scoping

It is possible that in the future EF will need to resolve services within a given scope and release (dispose) of those services when the scope ends. The obvious example of this are services that are used by a given context instance and then released when the context is disposed.

This kind of scoping can be further broken down as follows:

  • The ability to know when a scope is starting and when it ends. This is important when using IoC containers because they often have mechanisms for beginning and ending scopes which are designed to handle the lifetimes of services in the scope.
  • Knowledge about the type of scope being created. For example, is the scope for a context lifetime, for a connection lifetime, or for something else that we haven’t even thought of yet?
  • Specifics about the scope. For example, services might need to be resolved one way for BlogContext, but a different way for MembershipContext.

Previous ideas

The Release method was intended to solve part of this problem—namely the releasing (disposal) of services when they go out of scope. However, it doesn’t integrate well with IoC containers (as per feedback from MVC team) and doesn’t cover the other aspects of scoping. We also never currently call Release for any service. Therefore the Release method will be removed.

The WebAPI mechanism for scoping doesn’t provide any information about the scope and also adds complexity to the dependency resolver interfaces. It also assumes only one dependency resolver, rather than the chain of resolvers that we have. It seems inappropriate to follow the WebAPI design given that we don’t even need scoping at this point and that the WebAPI design doesn’t meet the general requirements we have.

Prototype/proposal

The current proposal is to do nothing now (YAGI principle) but to have prototyped a mechanism that can work if/when we need it.

The mechanism of this proposal/prototype is to use the IDbDependencyResolver against itself as the general mechanism for resolving scopes. For example, let’s say that EF needs to resolve services scoped by a context instance. To do this the context instance will ask the global dependency resolver for a new resolver and store that resolver:

    _resolver = DbConfiguration.GetService<IDbDependencyResolver><idbdependencyresolver>(context.GetType());

We would have a default implementation of this in the root resolver as we do for other services. Someone using an IoC container would add a resolver that uses the IoC container’s scoping mechanism to return a new resolver. For example:

public class UnityResolver : IDbDependencyResolver, IDisposable
{
    private IUnityContainer _container;

    public UnityResolver(IUnityContainer container)
    {
        _container = container;
    }

     public object GetService(Type type, object key)
     {
         if (type == typeof(IDbDependencyResolver)
             && typeof(DbContext).IsAssignableFrom(key as Type))
         {
             return new UnityResolver(_container.CreateChildContainer());
         }

         return null;
     }

     public void Dispose()
     {
         _container.Dispose();
     }
}

Notice that the key object is used to provide information about scope that is being created. In this case we are passing the context type name which indicates both that this is a scope for a DbContext instance and also the name of the context type. The key used could be anything that makes sense for the scope being created so long as it is well-defined.

After the context has obtained the scoped resolver it can be used to request scoped services in the normal way:

_cacheKeyFactory = _resolver.GetService<idbmodelcachekeyfactory>();

The code that created the scoped resolver can choose what to do when services are not resolved by the scoped resolver. In some cases it may be appropriate to ask the global resolver to resolve the service. In other cases, especially when the service must be disposed, it may be required that the scoped resolver always resolves the service and the caller will throw if that doesn’t happen.

When the scope ends (e.g. the context is disposed) then the scoped resolver will also be disposed if it implements IDisposable.

public virtual void DisposeContext()
{
    var disposableResolver = _resolver as IDisposable;
    if (disposableResolver != null)
    {
        disposableResolver.Dispose(); ;
    }
}

Additional notes:

  • General proposal accepted; we won’t implement anything now.
  • Release method will be removed.
  • If we do do this, then consider
    • Having a way to call the root scoped resolver if services are not resolved by the resolver in use
    • Adding an explicit interface that also includes Dispose or equivalent since there is nothing in the API that requires the scoped resolver to return an IDisposable
    • Sugar methods for registering a scoped resolver

Sugar methods for services

The fundamental building block for configuring EF is to add a dependency resolver on DbConfiguration. For example, the following sets a default connection factory when used in your DbConfiguration constructor:

AddDependencyResolver(new SingletonDependencyResolver<IDbConnectionFactory>(new SqlConnectionFactory()));

The question is whether or not and to what degree we should provide “sugar” methods that simply this in some cases. Sugar methods make it easier to configure EF without knowing anything about the dependency resolver mechanism.

This breaks down further in two ways: general purpose sugar methods and service-specific sugar methods.

General purpose sugar methods

Many of the registered services are effectively Singletons. We can make this easier by providing methods that allow registration of Singletons, or indeed other types of lifetime such as transients and thread locals. For example:

RegisterSingleton<IDbConnectionFactory>(new SqlConnectionFactory());

We currently have the following lifetime types:

  • Singleton: the same instance is returned every time GetService is called
  • Transient: a new instance is returned each time GetService is called
  • Thread local: the same instance is returned every time GetService is called for a given thread, but a new instance is used for each thread

Service-specific sugar methods

In cases where we want more discoverability and documentation points for certain services we can provide specific methods for those services. For example:

SetDefaultConnectionFactory(new SqlConnectionFactory())

In particular, we may choose to do this for places where:

  • Existing methods have been obsoleted (currently only default connection factories)
  • It is very important to make setting the service discoverable and/or documented (e.g. registering an EF provider)
  • We anticipate that setting the service will be very common

We should also choose in such cases whether or not a getter method is also useful.

We currently resolve the following services:

  • IDatabaseInitializer<TContext> (for each TContext)
  • MigrationSqlGenerator
  • DbProviderServices
  • IDbConnectionFactory
  • IManifestTokenService
  • IDbCommandInterceptor
  • IDbProviderFactoryService
  • IDbModelCacheKeyFactory

Decisions:

  • We will add service-specific sugar methods for all services. This will improve discoverability and documentation and allow people to configure EF without an increase in concept count.
  • Given that we will have service-specific methods we do not need to have the general purpose methods.

Simplified API for common conventions

The idea here is to provide a simple way for developers to change/add common conventions without needing to understand the full pluggable conventions API. Some initial API ideas:

//Set Decimal precision, column type not necesary
modelBuilder.AllEntities()
    .Properties()
    .OfTypeDecimal()
    .HasPrecision(25,10)
    .HasColumnType("Decimal");

modelBuilder.AllEntities()
    .Properties()
    .OfTypeDecimal()
    .Where(d => d.Name == "DecimalProperty")
    .HasPrecision(18, 10);

//Set all guids with Key at the end of their name to be a key.
modelBuilder.AllEntities()
    .Properties()
    .OfType<guid>()
    .Where(x => x.Name.EndsWith("Key"))
    .IsKey();

//Convert CamelCase class names to tables with lowercase names with underscores
modelBuilder.AllEntities()
    .ToTable(x => Regex.Replace(x.Name, "(?<=[a-z])(?<x>[A-Z])|(?<=.)(?<x>[A-Z])(?=[a-z])", "_${x}").ToLower());

//Add TableForBlog to the end of the blog table, but only for the Blog entity. 
modelBuilder.AllEntities()
    .Where(x => x.Name == "Blog")
    .ToTable(x => x.Name + "TableForBlog");

modelBuilder.AllEntities()
    .Properties()
    .Where(x => x.Name.EndsWith("Ignore"))
    .Ignore();

Notes:

  • AllEntities or similar method is a good way of covering a lot of the common cases for replacing conventions without needing to actually know about conventions
  • Consider using nested closure pattern instead of new bespoke fluent API
  • Implementation ideas:
    • Could be implemented as conventions or by applying the configuration directly to all matching entities
    • Order my matter—probably should just choose last wins
    • OfType Should be able to work with non-mapped interfaces and classes

Last edited Dec 14, 2012 at 6:14 PM by ajcvickers, version 2

Comments

No comments yet.