0

Been doing some sample code with ASP.NET Core to try to understand how it fits together and I am stumped as to why I am unable to successfully resolve a service.

The configure services method has the call to add ISeedDataService

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions();
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddDbContext<CustomerDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddScoped<ICustomerDbContext, CustomerDbContext>();
    services.AddScoped<ICustomerRepository, CustomerRepository>();
    services.AddScoped<ISeedDataService, SeedDataService>();
}

In Configure I am calling AddSeedData() as below

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.AddSeedData();
}

which is calling the extension method below

public static async void AddSeedData(this IApplicationBuilder app)
{
    var seedDataService = app.ApplicationServices.GetRequiredService<ISeedDataService>();
    await seedDataService.EnsureSeedData();
}

and the SeedDataService is below

public class SeedDataService : ISeedDataService
{
    private ICustomerDbContext _context;
    public SeedDataService(ICustomerDbContext context)
    {
        _context = context;
    }

    public async Task EnsureSeedData()
    {
        _context.Database.EnsureCreated();

        _context.Customers.RemoveRange(_context.Customers);
        _context.SaveChanges();

        Customer customer = new Customer();
        customer.FirstName = "Chuck";
        customer.LastName = "Norris";
        customer.Age = 30;
        customer.Id = Guid.NewGuid();

        _context.Add(customer);

        Customer customer2 = new Customer();
        customer2.FirstName = "Fabian";
        customer2.LastName = "Gosebrink";
        customer2.Age = 31;
        customer2.Id = Guid.NewGuid();

        _context.Add(customer2);

        await _context.SaveChangesAsync();
    }
}

Totally unsure as to what I am doing wrong, the error is System.InvalidOperationException: 'Cannot resolve scoped service 'secondapp.Services.ISeedDataService' from root provider.'

Bernard
  • 995
  • 2
  • 9
  • 20

2 Answers2

2

You are (and should be) adding the ISeedDataService as scoped service. However, you are attempting to resolve it from the root service provider (e.g. app.ApplicationServices) which is not scoped. This means that scoped services resolved from it effectively are turned into a singleton service and are not disposed until the application shuts down or it will result in an error.

The solution here is to create a scope yourself:

public void Configure(IApplicationBuilder app)
{
    using (var scope = app.ApplicationServices.CreateScope())
    {
        var seedDataService = scope.ServiceProvider.GetRequiredService<ISeedDataService>();
        // Use seedDataService here
    }
}

Please take a look at the documentation regarding dependency injection scopes.


On a second note: your AddSeedData extension method is async void and you are not waiting for the result. You should return a task (async Task) call AddSeedData().GetAwaiter().GetResult() to make sure you block until the seeding is complete.

Henk Mollema
  • 44,194
  • 12
  • 93
  • 104
  • 1
    Please note that this is the old way of seeding/migrating the database. This isn't advised with ASP.NET Core 2.x, since it will create issues with the new EF Core dotnet tools (i.e. `dotnet ef migrations add Xxx` as it will trigger seeding). The new recommended seeding and migration pattern can be found [here](https://stackoverflow.com/a/38704828/455493). – Tseng Sep 18 '18 at 14:33
  • @Tseng: I wish I could upvote your comment more. Unfortunately, when people search for how to seed with EF Core, there's a ton of outdated posts out there advocating the old approach, which used to be the only approach. Definitely a better way now. – Chris Pratt Sep 18 '18 at 16:31
  • @Tseng Since when is this the new recommended seeding and migration pattern? EF Core 2.1 introduced [Data Seeding](https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding) which will populate seed data during a migration. – Brad Sep 19 '18 at 00:06
  • @Brad: as in the linked answer, since EF Core 2.0. EF Core tools will now use the DI configuration of the main application. On each `dotnet ef ...` command, it will execute the `ConfigureServices` and `Configure` methods. Then problem is, when you run `dotnet ef migrations remove` it will apply all migrations and seeding, but in order to **reverse** an already applied migration, you have to do `dotnet ef database update `. When you now run `dotnet ef migrations remove` it will reapply the migration and tell you its not possible to migrate until you reverse it on DB first – Tseng Sep 19 '18 at 06:08
  • For that reason there have been added the `BuildWebHost` method (and now `CreateWebHostBuilder`) which will do the same, but not **RUN** the host (since that happens outside the `CreateWebHostBuilder` method. Also see the announcement in the linked answer: https://stackoverflow.com/a/38704828/455493. P.S. `.HasData` is meant to add fixed/mandatory data (i.e. when you add a table named `DocumentTypes` to initially seed it. Seeding testdata like adding an default admin user or test data for development is done outside the context – Tseng Sep 19 '18 at 06:11
-1

The Configure() method allows parameter dependency injection so you can do the following.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ISeedDataService seedService)
{
    seedService.EnsureSeedData().Wait(); // Configure() is not async so you have to wait
}
Brad
  • 4,493
  • 2
  • 17
  • 24
  • `.Wait()` isn't recommended in general, it has different throw semantics. It throws `AggregateException` rather than the specific exception that was triggered. `.GetAwaiter().GetResult()` is the correct way of async awaiting to be able to catch the specific rather than the generic (and possibly containing mulltiple) exceptions within the AggregateException – Tseng Sep 19 '18 at 06:15
  • @Tseng Thanks for the explainer and good to know the difference, I wasn't actually aware of that. I was trying to highlight the fact it needed some sort of waiting because the OP was not doing it in their original question so their seed method would not have even executed. – Brad Sep 19 '18 at 06:29
  • `ISeedDataService` is a scoped service, therefore you can't inject it into the `Configure` method as it will be resolved from the root service provider. This will either result in an error or your scoped service will be become a singleton service. – Henk Mollema Sep 19 '18 at 07:39