Compartir a través de


Arquitectura de herramientas en tiempo de diseño

Las herramientas en tiempo de diseño forman parte de EF que inicializan las operaciones en tiempo de diseño, como la estructuración de un modelo o la administración de migraciones. Son responsables de crear instancias de objetos DbContext para su uso en tiempo de diseño.

Diagrama Mermaid del flujo de herramientas de alto nivel.

Hay dos puntos de entrada principales: dotnet-ef y las herramientas de EF Core en la consola del Administrador de paquetes NuGet (PMC). Ambos son responsables de recopilar información sobre los proyectos del usuario, compilarlos y llamar a ef.exe que finalmente llama a los puntos de entrada en tiempo de diseño dentro de EFCore.Design.dll.

dotnet-ef

dotnet-ef es una herramienta de .NET. El prefijo dotnet- permite invocarlo como parte del comando dotnet principal: dotnet ef.

Hay dos entradas principales para este comando: el proyecto de inicio y el proyecto de destino. dotnet-ef es responsable de leer información sobre estos proyectos y luego compilarlos.

Diagrama Mermaid del flujo de dotnet-ef.

Lee información sobre los proyectos insertando un archivo .targets de MSBuild y llamando al destino personalizado de MSBuild. El archivo .targets se compila en dotnet-ef como un recurso incrustado. El origen se encuentra en src/dotnet-ef/Resources/EntityFrameworkCore.targets.

Tiene un poco de lógica al principio para controlar proyectos de varios destinos. Básicamente, simplemente selecciona el primer marco objetivo y se reinvoca. Una vez determinada una sola plataforma de destino, obtiene varias propiedades de MSBuild como AssemblyName, OutputPath, RootNamespace, etc.

Después de recopilar la información del proyecto, compilamos el proyecto de inicio. Se supone que el proyecto de destino también se compilará transitivamente.

A continuación, dotnet-ef invoca ef.exe.

Herramientas de PMC

Las herramientas de PMC realizan una función similar a dotnet-ef, pero usan las API de Visual Studio en lugar de MSBuild. Se envían como el paquete Microsoft.EntityFrameworkCore.Tools. Son un módulo especial de PowerShell compatible con VS que se carga automáticamente en la consola del Administrador de paquetes NuGet a través de init.ps1. Al igual que dotnet-ef, cada comando toma dos entradas principales: el proyecto de inicio y el proyecto de destino. Pero los valores predeterminados de estos se toman del IDE. El proyecto de destino tiene como valor predeterminado el proyecto especificado como proyecto predeterminado dentro de la consola de Administrador de paquetes. El proyecto de inicio tiene como valor predeterminado el especificado como proyecto de inicio (a través de Establecer como proyecto de inicio) en el Explorador de soluciones.

Diagrama Mermaid del flujo de herramientas de PMC.

Las herramientas de PMC recopilan información sobre los proyectos a través de las API de EnvDTE siempre que sea posible. En ocasiones, debe colocarse en la lista desplegable Common Project System (CPS) o las API de MSBuild. El origen moderno de implementación del sistema del proyecto de C# está disponible en el proyecto dotnet/project-system en GitHub.

Después de recopilar la información, compila toda la solución.

Sugerencia

El problema #9716 consiste en actualizarlo solo para compilar el proyecto de inicio.

A continuación, como dotnet-ef, invoca ef.exe. Las herramientas de PMC tienen un poco de lógica adicional después de invocar ef.exe para abrir los archivos creados por un comando para proporcionar una experiencia más integrada.

ef.exe

A veces se conoce como el hombre interior, ef.exe (a falta de un nombre mejor) se distribuye como parte de dotnet-ef y PMC Tools como un conjunto de archivos binarios. Hay varios archivos binarios para diferentes plataformas y plataformas de destino.

  • herramientas/
    • net461/
      • any/
        • ef.exe
      • win-x86/
        • ef.exe
      • win-arm64/
        • ef.exe
    • netcoreapp2.0/
      • any/
        • ef.dll

Los ensamblados de .NET Framework solo se invocan para proyectos de EF Core 3.1 y versiones anteriores destinadas a .NET Framework. Por diseño, puede usar la versión más reciente de las herramientas en proyectos que usan versiones anteriores de EF. No hay ningún x64 porque el ensamblado de cualquier directorio tiene como destino la plataforma AnyCPU que se ejecuta como x64 en las versiones x64 y arm64 de Windows.

El ensamblado de .NET Core 2.0 se usa para proyectos destinados a .NET Core o .NET 5 y versiones posteriores.

La responsabilidad principal de ef.exe es cargar el ensamblado de salida del proyecto de inicio e invocar los puntos de entrada en tiempo de diseño dentro de EFCore.Design.dll.

En .NET Framework, usamos un AppDomain independiente para cargar el ensamblado del proyecto pasando el archivo App/Web.config del proyecto para respetar y enlazar redirecciones agregadas por NuGet o el usuario.

En .NET Core/5+, invocamos ef.dll mediante los archivos .deps.json y .runtimeconfig.json del proyecto para emular el comportamiento real de carga de ensamblados y tiempo de ejecución del proyecto.

dotnet exec ef.dll --depsfile startupProject.deps.json --runtimeconfig startupProject.runtimeconfig.json

Sugerencia

El problema #18840 consiste principalmente en usar AssemblyLoadContext en lugar de dotnet exec para cargar el ensamblado del usuario. Esto debe permitir que las herramientas funcionen con más tipos de proyecto, incluidos los que tienen como destino Android e iOS.

Una vez configurado todo para cargarse, ef.exe llama a EFCore.Design.dll a través de reflection y Activator.CreateInstance (o AppDomain.CreateInstance en .NET Framework).

EFCore.Design.dll

EFCore.Design.dll, o más precisamente, Microsoft.EntityFrameworkCore.Design.dll contiene toda la lógica de tiempo de diseño para EF Core. Todos los puntos de entrada están dentro de la clase OperationExecutor. Gran parte de la extrañeza en el diseño de esta clase (MarshallByRefObject, tipos anidados, etc.) se deriva de la necesidad de invocarla en AppDomains en .NET Framework. Se podría simplificar mucho si se quitó este requisito. Todas las firmas están poco tipadas para permitir la compatibilidad con las herramientas tanto hacia delante como hacia atrás. Recuerde que se pueden usar diferentes versiones de las herramientas para invocar proyectos mediante diferentes versiones de EF.

Además del ejecutor, dbContextActivator es otro tipo importante en este ensamblado. Se usa en algunos de los componentes de ASP.NET Web Tools para crear instancias de DbContext de un usuario en tiempo de diseño.

Creación de un dbContext

Antes de que se ejecute una lógica en tiempo de diseño específica, normalmente se requiere una instancia de DbContext. El usuario puede especificar un nombre de tipo simple o completo, sin distinción entre mayúsculas y minúsculas, para DbContext o no especificar ninguno si solo existe un tipo de DbContext. En cualquier caso, es necesario detectar todos los tipos DbContext antes de restringirlos a uno único. La lógica para descubrir los tipos de DbContext se encuentra en el método FindContextTypes de DbContextOperations.

Buscamos tipos DbContext mediante varios orígenes.

  • Se hace referencia a las implementaciones de IDesignTimeDbContextFactory<T> en el ensamblado de inicio.
  • DbContexts agregado al proveedor de servicios de aplicación. Para obtener una lista de todos los tipos de contexto, obtenemos todo lo registrado como DbContextOptions y examinamos la propiedad ContextType. (Consulte a continuación cómo obtenemos el proveedor de servicios de aplicaciones).
  • Tipos derivados de DbContext en los ensamblados de inicio y destino

También utilizamos varias formas de instanciación de tipos. Aquí están en orden de prioridad.

  1. Uso de una implementación de IDesignTimeDbContextFactory<T>
  2. Uso de IDbContextFactory<T> desde el proveedor de servicios de aplicación
  3. Uso de ActivatorUtilities.CreateInstance

Búsqueda de servicios de aplicación

Para obtener la fidelidad más alta al comportamiento en tiempo de ejecución, intentamos obtener la instancia de DbContext directamente desde el proveedor de servicios de la aplicación. Compartimos esta lógica con las herramientas ASP.NET Core. Se mantiene como parte del proyecto dotnet/runtime en GitHub en el directorio Microsoft.Extensions.HostFactoryResolver.

En pocas palabras, estas son algunas de las estrategias que usa.

  • Busque un método denominado BuildWebHost, CreateWebHostBuilder o CreateHostBuilder junto al punto de entrada del ensamblado.
    • Compilación del host y obtención de los servicios de la propiedad Services
  • Llamada al punto de entrada del ensamblado
    • Interceptación de los servicios al compilar el host y finalizar antes de iniciar realmente el host

Servicios en tiempo de diseño

Además de los servicios de aplicación y los servicios de DbContext internos, hay un tercer conjunto de servicios en tiempo de diseño. No se agregan al proveedor de servicios interno, ya que nunca se necesitan en tiempo de ejecución. Los servicios en tiempo de diseño se crean mediante DesignTimeServicesBuilder. Hay dos rutas de acceso principales: una con una instancia de contexto y otra sin. La ruta sin instancia de contexto se usa principalmente al aplicar scaffolding a un nuevo DbContext. Hay varios puntos de extensibilidad aquí para permitir que el usuario, los proveedores y las extensiones invaliden y personalicen los servicios.

El usuario puede personalizar los servicios agregando una implementación de IDesignTimeServices al ensamblado de inicio.

Los proveedores pueden personalizar los servicios agregando el atributo DesignTimeProviderServices a su ensamblado. Esto apunta a una implementación de IDesignTimeServices.

Las extensiones pueden personalizar los servicios añadiendo atributos DesignTimeServicesReference al ensamblado de destino o de inicio. Si el atributo especifica un proveedor, solo se agregará cuando ese proveedor esté en uso.

Registro y excepciones

Después de crear una instancia de DbContext, conectemos su registro a la salida de la herramienta. Esto permite generar la salida desde el tiempo de ejecución. Todas las excepciones no controladas también se escribirán en la salida. Hay un tipo de excepción especial OperationException que se puede producir para finalizar correctamente las herramientas y mostrar un mensaje de error simple sin un seguimiento de la pila.