Modelo de proveedor de Entity Framework 6

El modelo de proveedor de Entity Framework permite usar Entity Framework con diferentes tipos de servidor de bases de datos. Por ejemplo, se puede conectar un proveedor para permitir que EF se use en Microsoft SQL Server, mientras que otro proveedor se puede conectar para permitir que EF se use en Microsoft SQL Server Compact Edition. Los proveedores de EF6 que conocemos se pueden encontrar en la página de proveedores de Entity Framework.

Se requerían ciertos cambios en la forma en que EF interactúa con los proveedores para permitir que EF se publicase bajo una licencia de código abierto. Estos cambios requieren la regeneración de los proveedores de EF en los ensamblados de EF6, junto con nuevos mecanismos para el registro del proveedor.

Regeneración

Con EF6, el código principal que formaba parte de .NET Framework ahora se está enviando como ensamblados fuera de banda (OOB). Puede encontrar más información sobre cómo compilar aplicaciones en EF6 en la página Actualización de aplicaciones en EF6. Los proveedores también deberán volver a compilarse mediante estas instrucciones.

Información general de los tipos de proveedor

Un proveedor de EF es una colección de servicios específicos del proveedor definidos por tipos CLR desde los que estos servicios se extienden (para una clase base) o implementan (para una interfaz). Dos de estos servicios son fundamentales y necesarios para que EF funcione. Otros son opcionales y solo deben implementarse si se requiere una funcionalidad específica o las implementaciones predeterminadas de estos servicios no funcionan para el servidor de bases de datos específico al que se destina.

Tipos de proveedor fundamentales

DbProviderFactory

EF depende de tener un tipo derivado de System.Data.Common.DbProviderFactory para realizar todos los accesos de bajo nivel a la base de datos. DbProviderFactory no forma parte de EF, sino que es una clase de .NET Framework que sirve como punto de entrada para los proveedores ADO.NET que EF puede usar, otros O/RMs o directamente para obtener instancias de conexiones, comandos, parámetros y otras abstracciones de ADO.NET de forma independiente del proveedor. Puede encontrar más información sobre DbProviderFactory en la documentación de MSDN de para ADO.NET.

DbProviderServices

EF depende de tener un tipo derivado de DbProviderServices para proporcionar la funcionalidad adicional necesaria que necesita EF además de la funcionalidad ya proporcionada por el proveedor ADO.NET. En versiones anteriores de EF, la clase DbProviderServices formaba parte de .NET Framework y se encontraba en el espacio de nombres System.Data.Common. A partir de EF6, esta clase forma parte de EntityFramework.dll y está en el espacio de nombres System.Data.Entity.Core.Common.

Puede encontrar más detalles sobre la funcionalidad fundamental de una implementación de DbProviderServices en MSDN. Sin embargo, tenga en cuenta que en el momento de escribir este artículo esta información no está actualizada para EF6, aunque la mayoría de los conceptos siguen siendo válidos. Las implementaciones de SQL Server y SQL Server Compact de DbProviderServices también se registran en la base de código abierto y pueden servir como referencias útiles para otras implementaciones.

En versiones anteriores de EF, la implementación de DbProviderServices que se va a usar se obtenía directamente desde un proveedor ADO.NET. Esto se realizó mediante la conversión de DbProviderFactory a IServiceProvider y la llamada al método GetService. De este modo, el proveedor de EF se ha acoplado estrictamente a DbProviderFactory. Este acoplamiento impidió que EF saliera de .NET Framework y, por lo tanto, para EF6 se ha quitado este acoplamiento estricto y ahora se registra una implementación de DbProviderServices directamente en el archivo de configuración de la aplicación o en la configuración basada en código, tal como se describe con más detalle en la sección Registro de DbProviderServices a continuación.

Servicios adicionales

Además de los servicios fundamentales descritos anteriormente, también hay muchos otros servicios usados por EF que son siempre o a veces específicos del proveedor. Las implementaciones predeterminadas específicas del proveedor de estos servicios se pueden proporcionar mediante una implementación de DbProviderServices. Las aplicaciones también pueden invalidar las implementaciones de estos servicios, o proporcionar implementaciones cuando un tipo DbProviderServices no proporciona un valor predeterminado. Esto se describe con más detalle en la sección Resolución de servicios adicionales a continuación.

A continuación se enumeran los tipos de servicio adicionales que pueden ser de interés para un proveedor. Puede encontrar más detalles sobre cada uno de estos tipos de servicio en la documentación de la API.

IDbExecutionStrategy

Se trata de un servicio opcional que permite a un proveedor implementar reintentos u otro comportamiento cuando se ejecutan consultas y comandos en la base de datos. Si no se proporciona ninguna implementación, EF simplemente ejecutará los comandos y propagará las excepciones producidas. Para SQL Server, este servicio se usa para proporcionar una directiva de reintento que es especialmente útil cuando se ejecuta en servidores de bases de datos basados en la nube, como SQL Azure.

IDbConnectionFactory

Se trata de un servicio opcional que permite a un proveedor crear objetos DbConnection por convención cuando solo se proporciona un nombre de base de datos. Tenga en cuenta que, aunque este servicio se puede resolver mediante una implementación de DbProviderServices, ha estado presente desde EF 4.1 y también se puede establecer explícitamente en el archivo de configuración o en el código. El proveedor solo tendrá la oportunidad de resolver este servicio si se registró como proveedor predeterminado (consulte a continuación El proveedor predeterminado) y si no se ha establecido una fábrica de conexiones predeterminada en otro lugar.

DbSpatialServices

Se trata de un servicio opcional que permite al proveedor agregar soporte para tipos espaciales geográficos y geométricos. Se debe proporcionar una implementación de este servicio para que una aplicación use EF con tipos espaciales. DbSptialServices se solicita de dos maneras. La primera, solicitando servicios espaciales específicos del proveedor mediante un objeto DbProviderInfo (que contiene un nombre invariable y un token de manifiesto) como clave. La segunda, solicitando DbSpatialServices sin clave. Se usa para resolver el “proveedor espacial global” que se usa al crear tipos DbGeography o DbGeometry independientes.

MigrationSqlGenerator

Se trata de un servicio opcional que permite que las migraciones de EF se usen para la generación de SQL que se usa para crear y modificar esquemas de base de datos mediante Code First. Se requiere una implementación para admitir migraciones. Si se proporciona una implementación, también se usará cuando se creen bases de datos mediante inicializadores de base de datos o el método Database.Create.

Func<DbConnection, cadena, HistoryContextFactory>

Se trata de un servicio opcional que permite a un proveedor configurar la asignación de HistoryContext a la tabla __MigrationHistory usada por migraciones de EF. HistoryContext es un DbContext de Code First y se puede configurar mediante la API fluida normal para cambiar cosas como el nombre de la tabla y las especificaciones de asignación de columnas. La implementación predeterminada de este servicio devuelto por EF para todos los proveedores puede funcionar para un servidor de base de datos determinado si ese proveedor admite todas las asignaciones de columnas y tablas predeterminadas. En tal caso, el proveedor no necesita proporcionar una implementación de este servicio.

IDbProviderFactoryResolver

Este es un servicio opcional para obtener el DbProviderFactory correcto de un objeto DbConnection determinado. La implementación predeterminada de este servicio devuelto por EF para todos los proveedores está pensada para funcionar para todos los proveedores. Sin embargo, cuando se ejecuta en .NET 4, DbProviderFactory no es accesible públicamente desde una de sus DbConnections. Por lo tanto, EF usa algunos métodos heurísticos para buscar en los proveedores registrados una coincidencia. Es posible que para algunos proveedores se produzca un error en esta heurística y, en tales situaciones, el proveedor debe proporcionar una nueva implementación.

Registro de DbProviderFactories

La implementación de DbProviderServices que se va a usar se puede registrar en el archivo de configuración de la aplicación (app.config o web.config) o mediante la configuración basada en código. En cualquier caso, el registro usa el “nombre invariable” del proveedor como clave. Esto permite registrar y usar varios proveedores en una sola aplicación. El nombre invariable que se usa para los registros de EF es el mismo que el que se usa para el registro del proveedor ADO.NET y las cadenas de conexión. Por ejemplo, para SQL Server se usa el nombre invariable “System.Data.SqlClient”.

Registro en el archivo de configuración

El tipo DbProviderServices que se va a usar se registra como elemento de proveedor en la lista de proveedores de la sección entityFramework del archivo de configuración de la aplicación. Por ejemplo:

<entityFramework>
  <providers>
    <provider invariantName="My.Invariant.Name" type="MyProvider.MyProviderServices, MyAssembly" />
  </providers>
</entityFramework>

La cadena tipo debe ser el nombre de tipo calificado con el ensamblado de la implementación DbProviderServices que se va a usar.

Registro basado en código

A partir de EF6, los proveedores también pueden registrarse mediante código. Esto permite usar un proveedor de EF sin ningún cambio en el archivo de configuración de la aplicación. Para usar la configuración basada en código, una aplicación debe crear una clase DbConfiguration como se describe en la documentación de configuración basada en código. El constructor de la clase DbConfiguration debe llamar a SetProviderServices para registrar el proveedor de EF. Por ejemplo:

public class MyConfiguration : DbConfiguration
{
    public MyConfiguration()
    {
        SetProviderServices("My.New.Provider", new MyProviderServices());
    }
}

Resolución de servicios adicionales

Como se mencionó anteriormente en la sección Información general de los tipos de proveedor, también se puede usar una clase DbProviderServices para resolver servicios adicionales. Esto es posible porque DbProviderServices implementa IDbDependencyResolver y cada tipo de DbProviderServices registrado se agrega como “solucionador predeterminado”. El mecanismo IDbDpendencyResolver se describe con más detalle en Resolución de dependencias. Sin embargo, no es necesario comprender todos los conceptos de esta especificación para resolver servicios adicionales en un proveedor.

La forma más común para que un proveedor resuelva servicios adicionales es llamar a DbProviderServices.AddDependencyResolver para cada servicio en el constructor de la clase DbProviderServices. Por ejemplo, SqlProviderServices (el proveedor de EF para SQL Server) tiene un código similar al siguiente para la inicialización:

private SqlProviderServices()
{
    AddDependencyResolver(new SingletonDependencyResolver<IDbConnectionFactory>(
        new SqlConnectionFactory()));

    AddDependencyResolver(new ExecutionStrategyResolver<DefaultSqlExecutionStrategy>(
        "System.data.SqlClient", null, () => new DefaultSqlExecutionStrategy()));

    AddDependencyResolver(new SingletonDependencyResolver<Func<MigrationSqlGenerator>>(
        () => new SqlServerMigrationSqlGenerator(), "System.data.SqlClient"));

    AddDependencyResolver(new SingletonDependencyResolver<DbSpatialServices>(
        SqlSpatialServices.Instance,
        k =>
        {
            var asSpatialKey = k as DbProviderInfo;
            return asSpatialKey == null
                || asSpatialKey.ProviderInvariantName == ProviderInvariantName;
        }));
}

Este constructor usa las siguientes clases auxiliares:

  • SingletonDependencyResolver: proporciona una manera sencilla de resolver los servicios Singleton, es decir, servicios para los que se devuelve la misma instancia cada vez que se llama a GetService. A menudo, los servicios transitorios se registran como una fábrica Singleton que se usará para crear instancias transitorias a petición.
  • ExecutionStrategyResolver: un solucionador específico para devolver implementaciones de IExecutionStrategy.

En lugar de usar DbProviderServices.AddDependencyResolver, también es posible invalidar DbProviderServices.GetService y resolver servicios adicionales directamente. Se llamará a este método cuando EF necesite un servicio definido por un tipo determinado y, en algunos casos, para una clave determinada. El método debe devolver el servicio si puede, o devolver null para no devolver el servicio y, en su lugar, permitir que otra clase lo resuelva. Por ejemplo, para resolver la fábrica de conexiones predeterminada, el código de GetService podría tener un aspecto similar al siguiente:

public override object GetService(Type type, object key)
{
    if (type == typeof(IDbConnectionFactory))
    {
        return new SqlConnectionFactory();
    }
    return null;
}

Orden de registro

Cuando se registran varias implementaciones de DbProviderServices en un archivo de configuración de una aplicación, se agregarán como solucionadores secundarios en el orden en que se muestran. Dado que los solucionadores siempre se agregan a la parte superior de la cadena de solucionadores secundarios, esto significa que el proveedor al final de la lista tendrá la oportunidad de resolver las dependencias antes que las demás. (Esto puede parecer poco intuitivo al principio, pero tiene sentido si imagina que saca cada proveedor de la lista y lo apila encima de los proveedores existentes).

Normalmente, este orden no importa porque la mayoría de los servicios de proveedor son específicos del proveedor y se codifican por el nombre invariable de este. Sin embargo, para los servicios cuya clave no sea el nombre invariable del proveedor o alguna otra clave específica del proveedor, el servicio se resolverá en función de este orden. Por ejemplo, si no se establece explícitamente de forma diferente en otro lugar, la fábrica de conexiones predeterminada provendrá del proveedor más arriba en la cadena.

Registros adicionales de archivos de configuración

Es posible registrar explícitamente algunos de los servicios de proveedor adicionales descritos anteriormente directamente en el archivo de configuración de una aplicación. Cuando esto se realiza, el registro en el archivo de configuración se usará en lugar de cualquier elemento devuelto por el método GetService de la implementación de DbProviderServices.

Registro de la fábrica de conexiones predeterminada

A partir de EF5, el paquete NuGet de EntityFramework registra automáticamente la fábrica de conexiones SQL Express o la fábrica de conexiones LocalDb en el archivo de configuración.

Por ejemplo:

<entityFramework>
  <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" >
</entityFramework>

El tipo es el nombre de tipo calificado para el ensamblado de la fábrica de conexiones predeterminada, que debe implementar IDbConnectionFactory.

Se recomienda que un paquete NuGet de proveedor establezca la fábrica de conexiones predeterminada de esta manera cuando esté instalado. Consulte Paquetes NuGet para proveedores a continuación.

Cambios adicionales del proveedor de EF6

Cambios del proveedor espacial

Los proveedores que admiten tipos espaciales ahora deben implementar algunos métodos adicionales en las clases derivadas de DbSpatialDataReader:

  • public abstract bool IsGeographyColumn(int ordinal)
  • public abstract bool IsGeometryColumn(int ordinal)

También hay nuevas versiones asincrónicas de métodos existentes que se recomiendan invalidar, ya que las implementaciones predeterminadas delegan en los métodos sincrónicos y, por lo tanto, no se ejecutan de forma asincrónica:

  • public virtual Task<DbGeography> GetGeographyAsync(int ordinal, CancellationToken cancellationToken)
  • public virtual Task<DbGeometry> GetGeometryAsync(int ordinal, CancellationToken cancellationToken)

Compatibilidad nativa con Enumerable.Contains

EF6 presenta un nuevo tipo de expresión, DbInExpression, que se agregó para solucionar problemas de rendimiento relacionados con el uso de Enumerable.Contains en consultas LINQ. La clase DbProviderManifest tiene un nuevo método virtual, SupportsInExpression, al que llama EF para determinar si un proveedor controla el nuevo tipo de expresión. Para la compatibilidad con las implementaciones de proveedor existentes, el método devuelve false. Para beneficiarse de esta mejora, un proveedor de EF6 puede agregar código para controlar DbInExpression e invalidar SupportsInExpression para que devuelva true. Se puede crear una instancia de DbInExpression llamando al método DbExpressionBuilder.In. Una instancia de DbInExpression se compone de una DbExpression, que normalmente representa una columna de la tabla y una lista de DbConstantExpression para comprobar la coincidencia.

Paquetes NuGet para proveedores

Una manera de hacer que un proveedor de EF6 esté disponible es liberarlo como un paquete NuGet. El uso de un paquete NuGet tiene las siguientes ventajas:

  • Es fácil de usar para agregar el registro del proveedor al archivo de configuración de la aplicación
  • Se pueden realizar cambios adicionales en el archivo de configuración para establecer la fábrica de conexiones predeterminadas para que las conexiones realizadas por convención usen el proveedor registrado
  • NuGet se encarga de agregar redirecciones de enlace para que el proveedor de EF6 siga funcionando incluso después de que se publique un nuevo paquete de EF

Un ejemplo de esto es el paquete EntityFramework.SqlServerCompact que se incluye en el código base de código abierto. Este paquete proporciona una buena plantilla para crear paquetes NuGet del proveedor de EF.

Comandos de PowerShell

Cuando el paquete NuGet de EntityFramework se instala, registra un módulo de PowerShell que contiene dos comandos muy útiles para los paquetes de proveedor:

  • Add-EFProvider agrega una nueva entidad para el proveedor en el archivo de configuración del proyecto de destino y se asegura de que está al final de la lista de proveedores registrados.
  • Add-EFDefaultConnectionFactory agrega o actualiza el registro defaultConnectionFactory en el archivo de configuración del proyecto de destino.

Ambos comandos se encargan de agregar una sección entityFramework al archivo de configuración y de agregar una colección de proveedores si es necesario.

Se pretende que estos comandos sean llamados desde el script NuGet install.ps1. Por ejemplo, install.ps1 para el proveedor de SQL Compact tiene un aspecto similar al siguiente:

param($installPath, $toolsPath, $package, $project)
Add-EFDefaultConnectionFactory $project 'System.Data.Entity.Infrastructure.SqlCeConnectionFactory, EntityFramework' -ConstructorArguments 'System.Data.SqlServerCe.4.0'
Add-EFProvider $project 'System.Data.SqlServerCe.4.0' 'System.Data.Entity.SqlServerCompact.SqlCeProviderServices, EntityFramework.SqlServerCompact'</pre>

Puede obtener más información sobre estos comandos utilizando get-help en la ventana Consola del administrador de paquetes.

Proveedores de encapsulado

Un proveedor de encapsulado es un proveedor de EF o ADO.NET que encapsula un proveedor existente para ampliarlo con otras funcionalidades, como la generación de perfiles o las funcionalidades de seguimiento. Los proveedores de encapsulado se pueden registrar de la manera normal, pero a menudo es más conveniente configurarlo en tiempo de ejecución interceptando la resolución de los servicios relacionados con el proveedor. Para ello se puede usar el evento estático OnLockingConfiguration de la clase DbConfiguration.

Se llama a OnLockingConfiguration después de que EF haya determinado de dónde se obtendrá toda la configuración de EF para el dominio de aplicación, pero antes de que se bloquee para su uso. Al iniciar la aplicación (antes de usar EF), esta debe registrar un controlador de eventos para este evento. (Estamos pensando en agregar soporte para registrar este controlador en el archivo de configuración, pero aún no es compatible). A continuación, el controlador de eventos debe realizar una llamada a ReplaceService para cada servicio que deba encapsularse.

Por ejemplo, para encapsular IDbConnectionFactory y DbProviderService, se debe registrar un controlador similar al siguiente:

DbConfiguration.OnLockingConfiguration +=
    (_, a) =>
    {
        a.ReplaceService<DbProviderServices>(
            (s, k) => new MyWrappedProviderServices(s));

        a.ReplaceService<IDbConnectionFactory>(
            (s, k) => new MyWrappedConnectionFactory(s));
    };

El servicio que se ha resuelto y que ahora debe encapsularse junto con la clave que se usó para resolver el servicio se pasan al controlador. Después, el controlador puede encapsular este servicio y reemplazar el servicio devuelto por la versión encapsulada.

Resolución de DbProviderFactory con EF

DbProviderFactory es uno de los tipos de proveedor fundamentales que necesita EF, tal como se describe anteriormente en Información general de los tipos de proveedor. Como ya se ha mencionado, no es un tipo de EF y el registro normalmente no forma parte de la configuración de EF, sino que es el registro normal del proveedor ADO.NET en el archivo machine.config o el archivo de configuración de la aplicación.

A pesar de esto, EF sigue usando su mecanismo de resolución de dependencias normal al buscar un DbProviderFactory para usarlo. El solucionador predeterminado usa el registro normal de ADO.NET en los archivos de configuración y, por lo tanto, suele ser transparente. Pero debido a que se usa el mecanismo de resolución de dependencias normal, significa que se puede usar IDbDependencyResolver para resolver un DbProviderFactory incluso cuando no se ha realizado el registro normal de ADO.NET.

Resolver DbProviderFactory de esta manera tiene varias implicaciones:

  • Una aplicación que use la configuración basada en código puede agregar llamadas en su clase DbConfiguration para registrar el DbProviderFactory adecuado. Esto es especialmente útil para las aplicaciones que no quieren (o no pueden) hacer uso de ninguna configuración basada en archivos.
  • El servicio se puede encapsular o reemplazar mediante ReplaceService como se describe anteriormente en Proveedores de encapsulado
  • Teóricamente, una implementación de DbProviderServices podría resolver un DbProviderFactory.

El punto importante que hay que tener en cuenta al realizar cualquiera de estas cosas es que solo afectarán a la búsqueda de DbProviderFactory por EF. Es posible que otro código que no sea EF todavía espere que el proveedor ADO.NET se registre de la manera normal y que se produzca un error si no se encuentra el registro. Por esta razón, normalmente es mejor que DbProviderFactory se registre de la manera normal de ADO.NET.

Si EF se usa para resolver DbProviderFactory, también debe resolver los servicios IProviderInvariantName e IDbProviderFactoryResolver.

IProviderInvariantName es un servicio que se usa para determinar un nombre invariable de proveedor para un tipo determinado de DbProviderFactory. La implementación predeterminada de este servicio usa el registro de proveedor ADO.NET. Esto significa que si el proveedor ADO.NET no está registrado de la manera normal porque DbProviderFactory está siendo resuelto por EF, entonces también será necesario resolver este servicio. Tenga en cuenta que se agrega automáticamente una resolución para este servicio al usar el método DbConfiguration.SetProviderFactory.

Tal como se describe anteriormente en Información general de los tipos de proveedor, IDbProviderFactoryResolver se usa para obtener el DbProviderFactory correcto a partir de un objeto DbConnection determinado. La implementación predeterminada de este servicio cuando se ejecuta en .NET 4 usa el registro del proveedor ADO.NET. Esto significa que si el proveedor ADO.NET no está registrado de la manera normal porque DbProviderFactory está siendo resuelto por EF, entonces también será necesario resolver este servicio.