How to use configuration from dotnet 6 minimal API in Entity Framework Core cli tools

Viewed 7900

I'm trying to build an API with EF Core as database access, right now on dotnet 6 RC1. I want to use dotnet cli tools to manage migrations (creating, updating the database etc), but the tools do not cooperate with minimal API from the template.

Here is my Program.cs:

void ConfigureApp(WebApplication webApplication)
{
    // Configure the HTTP request pipeline.
    if (webApplication.Environment.IsDevelopment())
    {
        webApplication.UseSwagger();
        webApplication.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Eyespert.Server v1"));
    }

    webApplication.UseHttpsRedirection();

    webApplication.UseAuthentication();
    webApplication.UseAuthorization();

    webApplication.MapControllers();
}

void RegisterServices(WebApplicationBuilder builder)
{
    builder.Services.AddControllers();
    builder.Services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new() { Title = "App", Version = "v1" });
    });
    
    builder.Services.AddDbContext<MyContext>(opt =>
    {
        string connectionString = builder.Configuration.GetConnectionString("MyConnectionString");
        opt.UseNpgsql(connectionString);
    });
}

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

RegisterServices(builder);

WebApplication app = builder.Build();

ConfigureApp(app);

app.Run();

If that code was using the Program/Startup class combo and old builders, I could type in console dotnet ef migrations add InitialCreate and the tool would read appsettings.development.json (even though it's different project than the context) and run the migration on proper database. With minimal API style this is not the case.

As a solution, I made a design time context factory:

public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<MyContext>
    {
        public MyContextCreateDbContext(string[] args)
        {
            DbContextOptionsBuilder<MyContext> dbContextOptionsBuilder =
                new();

            dbContextOptionsBuilder.UseNpgsql(@"myconnectionstring");

            Console.WriteLine("Creating default MyContext");
            
            return new MyContext(dbContextOptionsBuilder.Options);
        }
    }

As you can see, I hardcoded the connection string. I know that I can construct the ConfigurationBuilder and use relative paths to find the correct json file and use it to find the connection string, but it feels like a dirty hack.

What would THE way to do it with dotnet 6?

1 Answers

I created an MinimalAPI project using EFCore Context and it runs fine no major problem ocourred, other than updating ef tools cli and so on, see the complete project:

MinimalApi.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <LangVersion>Preview</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0-rc.1.21452.10">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0-rc.1.21452.10">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.5" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Infra\Infra.csproj" />
  </ItemGroup>

</Project>

Program.cs

using Infra;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<Context>(opts =>
{
    var connString = builder.Configuration.GetConnectionString("MyConnectionString");
    opts.UseSqlServer(connString, options =>
    {
        options.MigrationsAssembly(typeof(Context).Assembly.FullName.Split(',')[0]);
    });
});

await using var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapGet("/", (Func<string>)(() => "Hello World!"));

await app.RunAsync();

Infra.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>Preview</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0-rc.1.21452.10" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0-rc.1.21452.10">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0-rc.1.21452.10" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0-rc.1.21452.10">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Domain\Domain.csproj" />
  </ItemGroup>

</Project>


Infra.Context.cs

using Domain;
using Microsoft.EntityFrameworkCore;

namespace Infra
{
    public class Context : DbContext
    {

        public Context(DbContextOptions options) : base(options)
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }

        public DbSet<MyEntity> MyEntities { get; set; }
    }
}

Domain.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>Preview</LangVersion>
  </PropertyGroup>

</Project>

Entity example (MyEntity.cs)

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Domain
{
    public class MyEntity
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Value { get; set; }
    }
}

Creating migration on nuget package manager

PM> Add-Migration InitialMigration
Build started...
Build succeeded.
Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 6.0.0-rc.1.21452.10 initialized 'Context' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.0-rc.1.21452.10' with options: MigrationsAssembly=Infra 

updating database on nuget package manager

PM> Update-Database
Build started...
Build succeeded.
Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 6.0.0-rc.1.21452.10 initialized 'Context' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.0-rc.1.21452.10' with options: MigrationsAssembly=Infra 
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2,338ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      CREATE DATABASE [MinimalApiDb];
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (933ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      IF SERVERPROPERTY('EngineEdition') <> 5
      BEGIN
          ALTER DATABASE [MinimalApiDb] SET READ_COMMITTED_SNAPSHOT ON;
      END;
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (29ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (18ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [__EFMigrationsHistory] (
          [MigrationId] nvarchar(150) NOT NULL,
          [ProductVersion] nvarchar(32) NOT NULL,
          CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
      );
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (17ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT OBJECT_ID(N'[__EFMigrationsHistory]');
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [MigrationId], [ProductVersion]
      FROM [__EFMigrationsHistory]
      ORDER BY [MigrationId];
Microsoft.EntityFrameworkCore.Migrations[20402]
      Applying migration '20211001150743_InitialMigration'.
Applying migration '20211001150743_InitialMigration'.
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (176ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [MyEntities] (
          [Id] uniqueidentifier NOT NULL,
          [Name] nvarchar(max) NOT NULL,
          [Value] nvarchar(max) NOT NULL,
          CONSTRAINT [PK_MyEntities] PRIMARY KEY ([Id])
      );
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (23ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
      VALUES (N'20211001150743_InitialMigration', N'6.0.0-rc.1.21452.10');
Done.
PM> 

To use dotnet ef tools you will need to update to the latest rc to match your projetc designer/tools

dotnet tool update --global dotnet-ef --version 6.0.0-rc.1.21452.10
  • Note that is the same version of package you are using in the project packages

Creating a migration via ef tools cli

PS D:\Repositorios\MinimalApi\MinimalApi> dotnet ef migrations add eftoolsmigration -s "D:\Repositorios\MinimalApi\MinimalApi\MinimalApi.csproj" -p "D:\Repositorios\MinimalApi\Infra\Infra.csproj"
Build started...
Build succeeded.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 6.0.0-rc.1.21452.10 initialized 'Context' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.0-rc.1.21452.10' with options: MigrationsAssembly=Infra
Done. To undo this action, use 'ef migrations remove'
PS D:\Repositorios\MinimalApi\MinimalApi>

Updating database via ef tools cli

 dotnet ef database update -s "D:\Repositorios\MinimalApi\MinimalApi\MinimalApi.csproj" -p "D:\Repositorios\MinimalApi\Infra\Infra.csproj"
  • Note that -s is your Startup project path;
  • Note that -p is your target project path (where context is configured);

Automatically created Migrations folder

Automatically created Migrations folder

Final project structure

Final project structure

Created database

Created database

  • Note that you will have to set "include prerelease" on you package manager to get the versions compatibles with .Net 6

  • Note tha you are using Postgree DB, it will have to have a client compatible with EF Core 6.xxx

Runtimes and SDKs intalled (.NET 6 only for clarity)

PS C:\Users\Daniel> dotnet --list-runtimes
Microsoft.NETCore.App 6.0.0-rc.1.21451.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 6.0.0-rc.1.21451.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

PS C:\Users\Daniel> dotnet --list-sdks
6.0.100-rc.1.21463.6 [C:\Program Files\dotnet\sdk]
Related