Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make EF Core tools obtain DbContext instance from service provider of a console application?

I have read Design-time DbContext Creation that there are 3 ways the EF Core tools (for example, the migration commands) obtain derived DbContext instance from the application at design time as opposed to at run time.

  • From application service provider
  • From any parameterless ctor
  • From a class implementing IDesignTimeDbContextFactory<T>

Here I am only interested in the first method by mimicking the pattern used in Asp.net Core. This code does not compile because I have no idea how to make EF Core tool obtain TheContext instance.

Minimal Working Example

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;


public class TheContext : DbContext
{
    public TheContext(DbContextOptions<TheContext> options) : base(options) { }
    public DbSet<User> Users { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}


class Program
{
    private static readonly IConfiguration _configuration;
    private static readonly string _connectionString;

    static Program()
    {
        _configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).Build();

        _connectionString = _configuration.GetConnectionString("SqlServer");
    }

    static void ConfigureServices(IServiceCollection isc)
    {
        isc.AddSingleton(_ => _configuration);

        isc.AddDbContextPool<TheContext>(options => options
            .UseSqlServer(_connectionString));

        isc.AddSingleton<TheApp>();
    }

    static void Main()
    {
        IServiceCollection isc = new ServiceCollection();
        ConfigureServices(isc);

        IServiceProvider isp = isc.BuildServiceProvider();

        isp.GetService<TheApp>().Run();
    }
}

class TheApp
{
    readonly TheContext _theContext;

    public TheApp(TheContext theContext) => _theContext = theContext;

    public void Run()
    {
        // Do something on _theContext            
    }
}

Question

How to make EF Core tools obtain DbContext instance from service provider of a console application?

Edit:

I forgot to mention the appsettings.json as follows:

{
  "ConnectionStrings": {
    "Sqlite": "Data Source=MyDatabase.db",
    "SqlServer": "Server=(localdb)\\mssqllocaldb;Database=MyDatabase;Trusted_Connection=True"
  }
}
like image 947
xport Avatar asked Dec 23 '22 03:12

xport


2 Answers

Although the documentation topic is called From application services, it starts with

If your startup project is an ASP.NET Core app, the tools try to obtain the DbContext object from the application's service provider.

Looks like they don't expect project types other than ASP.NET Core app to use application service provider :)

Then it continues with

The tools first try to obtain the service provider by invoking Program.BuildWebHost() and accessing the IWebHost.Services property.

and example:

public static IWebHost BuildWebHost(string[] args) => ...

And here is the trick which works with the current (EF Core 2.1.3) bits. The tools actually are searching the class containing your entry point (usually Program) for a static (does not need to be public) method called BuildWebHost with string[] args parameters, and the important undocumented part - the return type does not need to be IWebHost! It could be any object having public property like this

public IServiceProvider Services { get; } 

Which gives us the following solution:

class Program
{
    // ...

    // Helper method for both Main and BuildWebHost
    static IServiceProvider BuildServiceProvider(string[] args)
    {
        IServiceCollection isc = new ServiceCollection();
        ConfigureServices(isc);
        return isc.BuildServiceProvider();
    }

    static void Main(string[] args)
    {
         BuildServiceProvider(args).GetService<TheApp>().Run();
    }

    // This "WebHost" will be used by EF Core design time tools :)
    static object BuildWebHost(string[] args) =>
        new { Services = BuildServiceProvider(args) };
}

Update: Starting from v2.1, you could also utilize the new CreateWebHostBuilder pattern, but IMHO it just adds another level of complexity not needed here (the previous pattern is still supported). It's similar, but now we need a method called CreateWebHostBuilder which returns an object having public method Build() returning object having public Services property returning IServiceProvider. In order to be reused from Main, we can't use anonymous type and have to create 2 classes, and also this makes it's usage from Main more verbose:

class AppServiceBuilder
{
    public ServiceCollection Services { get; } = new ServiceCollection();
    public AppServiceProvider Build() => new AppServiceProvider(Services.BuildServiceProvider());
}

class AppServiceProvider
{
    public AppServiceProvider(IServiceProvider services) { Services = services; }
    public IServiceProvider Services { get; }
}

class Program
{
    // ...

    static void Main(string[] args)
    {
         CreateWebHostBuilder(args).Build().Services.GetService<TheApp>().Run();
    }

    // This "WebHostBuilder" will be used by EF Core design time tools :)
    static AppServiceBuilder CreateWebHostBuilder(string[] args)
    {
        var builder = new AppServiceBuilder();
        ConfigureServices(builder.Services);
        return builder;
    }
}
like image 108
Ivan Stoev Avatar answered Dec 29 '22 07:12

Ivan Stoev


Try this, i've added some changes to make it run :

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.FileExtensions;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;

namespace IOCEFCore
{

    public class TheContext : DbContext
    {
        public TheContext(DbContextOptions<TheContext> options) : base(options) { }
        public DbSet<User> Users { get; set; }
    }

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }


    class Program
    {
        private static readonly IConfigurationRoot _configuration;
        private static readonly string _connectionString;

        static Program()
        {
            _configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).Build();

        }

        static void ConfigureServices(IServiceCollection isc)
        {
            isc.AddSingleton(_ => _configuration);

            isc.AddDbContextPool<TheContext>(options => options.UseInMemoryDatabase("myContext"));

            isc.AddSingleton<TheApp>();
        }

        static void Main()
        {
            IServiceCollection isc = new ServiceCollection();
            ConfigureServices(isc);

            IServiceProvider isp = isc.BuildServiceProvider();

            isp.GetService<TheApp>().Run();
            Console.ReadLine();
        }

        class TheApp
        {
            readonly TheContext _theContext;

            public TheApp(TheContext theContext) => _theContext = theContext;

            public void Run()
            {
                // Do something on _theContext  
                _theContext.Users.Add(new User {Id = 1, Name = "Me"});
                _theContext.SaveChanges();

                foreach (var u in _theContext.Users)
                {
                    Console.WriteLine("{0} : {1}", u.Id, u.Name);
                }
            }
        }
    }
}
like image 26
Mauricio Atanache Avatar answered Dec 29 '22 07:12

Mauricio Atanache