Design Meeting Notes - June 21, 2012

Code-based configuration

Background

In EF 4.1 most configuration was done through code. In EF 4.3 we added the EntityFramework configuration section to allow configuration via config file.

Both code-based configuration and file-based configuration are useful. Code-based configuration can make use of IDE and compiler services (strong typing, Intellisense, etc.) and is flexible especially when coupled with dependency injection. File-based configuration can allow the same code to run in different environments without re-compiling.

The main problem with code-based configuration is making sure that the configuration is available to design-time tooling that does not run the application. The tooling must be able to find and execute (or otherwise interpret) the code. This is not possible if the configuration is performed by some arbitrary call made at some point during app startup.

Goals

  • If I don’t know about or care about using code-based configuration, then everything still works
    • In particular, EF 4.3 config file configuration is not changed
  • If I do want to use code-based configuration then it should be simple to do for the main developer scenarios
    • If I use it in this simple way, then tooling will also be able to find and use my code-based configuration
  • Whatever we add now should also form the basis for adding new configuration going forward
  • Less common scenarios (no context, multiple contexts, using Code First building blocks, etc.) should still be easy with code-based configuration
    • These scenarios may require additional steps but it should be easy to find out what the steps are

Basic Idea

We provide a DbConfiguration base class. To use code-based configuration a developer creates a class derived from DbConfiguration and places it in the same assembly as their context. Configuration settings are made in the constructor of this class.

public class MyConfiguration : DbConfiguration
{
    public MyConfiguration()
    {
        SetDefaultConnectionFactory(new LocalDbConnectionFactory("v11.0"));
        AddEntityFrameworkProvider("My.New.Provider", new MyProviderServices());
    }
}

Additional details

  • Can I specify the configuration type to use in the config file?
    • Yes. This overrides discovery and allows your DbConfiguration class to be contained in any assembly.
    • Design-time still works in this case because the config can (and should) be made available to the tooling
  • What if I run some code that needs to use configuration before I use my context type?
    • This just works if you’re not using code-based configuration or if you have specified your DbConfiguration class in the config file.
    • If you are using code-based configuration then you must set the DbConfiguration to use explicitly:
      DbConfiguration.Instance = new MyConfiguration();
    • If you don’t do this then we will throw when you use your context and we discover that you have a DbConfiguration class but didn’t set it.
    • We will also throw if you set the DbConfiguration to something we can’t discover.
  • What if I want to use EF without a derived context type at all?
    • This is the same as the previous bullet point except that we will never actually do the discovery
    • There is an assumption here that tooling will always make use of a derived context.
  • What if I have multiple contexts in multiple assemblies?
    • The easiest option is to specify the DbConfiguration class in the config file.
    • We may also choose to allow a special type of DbConfiguration class that just acts as a proxy to a class in another assembly.
  • What if I have a context that shouldn’t impact application configuration?
    • A good example of this is the HistoryContext used by Migrations. Using this context shouldn’t affect DbConfiguration resolution—it should just use whatever configuration the application is using.
    • If this context is in the same assembly as the DbConfiguration you want to use then it’s not a problem.
    • If it’s in a different assembly then you can put a type derived from DbNullConfiguration in this assembly. This tells EF to ignore DbConfiguration discovery for contexts in this assembly. In particular, it tells EF not to throw if a DbConfiguration is discovered in both this assembly and another assembly.
  • How does this work with dependency injection?
    • DbConfiguration is actually the place where the IDbDependencyResolver chain is rooted.
    • All configuration settings are resolved using the resolver chain.
    • When a configuration value is set this is implemented by adding a new resolver to the chain.
    • You can also add your own resolvers directly when constructing the configuration
  • Can I mutate the code-based configuration after it has been set?
    • Not directly because it encourages code that will be different when run in the application than when run at design-time.
    • The setter methods are protected to encourage usage from the constructor.
    • Once the configuration is set (either implicitly or explicitly) then it is locked and further attempts to modify will throw with info on the correct way to use DbConfiguration.
    • However, you can add a dependency resolver that can have behavior that changes as the application runs.
      • This doesn’t pose as much of a risk for design-time since the resolver is still added and must function in some way at design-time.
      • I’m currently using this in the functional tests to change the DefaultConnectionFactory to target SQL CE or LocalDb for some tests.
  • What if I set some config in both code and using the config file?
    • Config always wins.
    • The code below shows a CompositeResolver that ensures dependencies are always resolved from the config before other resolvers are tried.
  • What happens to the existing code-based configuration APIs?
    • We will obsolete (or remove) SetDefaultConnectionFactory
    • We may obsolete SetInitializer, but this will impact a lot of people.

Comments/suggestions from the meeting

  • Consider having NuGet package generate a DbConfiguration class instead of creating/updating config
    • This would avoid the potential confusion of some config we create overriding config the user then sets
    • But it is harder to parse/update DbConfiguration code—for example, to switch connection factory
    • Also not clear how other packages would update/add to this code
  • Consider allowing packages to create code snippets that are collected together
    • No clear idea on how this would work
  • We should update existing APIs to allow their dependencies to be injected
    • We could in the future then try to remove the non-context based use of the configuration, but this would be a lot of changes to existing APIs
    • Alternately we could throw if non-context use happens and the config is not specified in the config file
    • Either way, we should understand which APIs currently use configuration
    • We will implement this as is for now, then iterate on it

Current code for DbConfiguration

public class DbConfiguration
{
    private readonly CompositeResolver<resolverchain resolverchain ,> _resolvers
        = new CompositeResolver<resolverchain resolverchain ,>(new ResolverChain(), new ResolverChain());
    
    private bool _isLocked;

    protected internal DbConfiguration()
        : this(new AppConfigDependencyResolver(AppConfig.DefaultInstance), new RootDependencyResolver())
    {
    }

    internal DbConfiguration(IDbDependencyResolver appConfigResolver, IDbDependencyResolver rootResolver)
    {
        _resolvers.First.Add(appConfigResolver);
        _resolvers.Second.Add(rootResolver);
    }

    public static DbConfiguration Instance
    {
        get { return DbConfigurationManager.Instance.GetConfiguration(); }
        set
        {
            Contract.Requires(value != null);

            DbConfigurationManager.Instance.SetConfiguration(value);
        }
    }

    internal void Lock()
    {
        _isLocked = true;
    }

    internal void AddAppConfigResolver(IDbDependencyResolver resolver)
    {
        Contract.Requires(resolver != null);
        CheckNotLocked();

        _resolvers.First.Add(resolver);
    }

    protected void AddDependencyResolver(IDbDependencyResolver resolver)
    {
        Contract.Requires(resolver != null);
        CheckNotLocked();

        // New resolvers always run after the config resolvers so that config always wins over code
        _resolvers.Second.Add(resolver);
    }

    [CLSCompliant(false)]
    protected void AddEntityFrameworkProvider(string providerInvariantName, DbProviderServices provider)
    {
        CheckNotLocked();

        AddDependencyResolver(new SingletonDependencyResolver<dbproviderservices>(provider, providerInvariantName));
    }

    [CLSCompliant(false)]
    public DbProviderServices GetEntityFrameworkProvider(string providerInvariantName)
    {
        // TODO: use generic version of Get
        return (DbProviderServices)_resolvers.Get(typeof(DbProviderServices), providerInvariantName);
    }

    protected void SetDatabaseInitializer<tcontext>(IDatabaseInitializer<tcontext> strategy) where TContext : DbContext
    {
        CheckNotLocked();

        AddDependencyResolver(new SingletonDependencyResolver<idatabaseinitializer><tcontext>>(strategy));
    }

    public IDatabaseInitializer<tcontext> GetDatabaseInitializer<tcontext>() where TContext : DbContext
    {
        // TODO: Make sure that access to the database initializer now uses this method
        return (IDatabaseInitializer<tcontext>)_resolvers.Get(typeof(IDatabaseInitializer<tcontext>), null);
    }

    public void SetDefaultConnectionFactory(IDbConnectionFactory value)
    {
        CheckNotLocked();

        AddDependencyResolver(new SingletonDependencyResolver<idbconnectionfactory>(value));
    }

    public IDbConnectionFactory GetDefaultConnectionFactory()
    {
        return Database.DefaultConnectionFactoryChanged
paggma warning disable 612,618
                   ? Database.DefaultConnectionFactory
pragma warning restore 612,618
                   : (IDbConnectionFactory)_resolvers.Get(typeof(IDbConnectionFactory), null);
    }

    private void CheckNotLocked()
    {
        if (_isLocked)
        {
            throw new InvalidOperationException("Configuration can only be changed before the configuration is used. Try setting configuration in the constructor of your DbConfiguration class.");
        }
    }
}

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

Comments

No comments yet.