Mvc: How to Seed WebHost Data When Using WebApplicationFactory<Startup> ?

Created on 30 May 2018  路  12Comments  路  Source: aspnet/Mvc

Is this a Bug or Feature request?:

Bug? Lack of docs if it's already available.

Steps to reproduce (preferrably a link to a GitHub repo with a repro project):

I can seed data using WebHostBuilder using code like this:
https://github.com/dotnet-architecture/eShopOnWeb/blob/netcore2.1/src/Web/Program.cs#L16-L39

I'd like to be able to seed data before my functional tests. I'm using the new WebApplicationFactory<T> but am not seeing a simple way to hook into this.

Description of the problem:

I can configure the WebHostBuilder using code like this:

    public class CatalogControllerIndex : IClassFixture<WebApplicationFactory<Startup>>
    {
        public CatalogControllerIndex(WebApplicationFactory<Startup> fixture)
        {
            var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
            Client = factory.CreateClient();
        }

        private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
            builder.UseEnvironment("Testing");

        public HttpClient Client { get; }
...

However, I don't see how I can get to an actual instance of the built host, so that I can get to its dbContext, so that I can use it to add test data. Should I just try and do it using something like builder.Configure() ?

Version of Microsoft.AspNetCore.Mvc or Microsoft.AspNetCore.App or Microsoft.AspNetCore.All:

2.1

investigate

Most helpful comment

Ah ha! The Server property is populated after calling CreateClient() (obviously...). This works!

public class CatalogControllerIndex : IClassFixture<WebApplicationFactory<Startup>>
{
public CatalogControllerIndex(WebApplicationFactory<Startup> fixture)
{
    var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
    Client = factory.CreateClient();
    var host = factory.Server?.Host;
    SeedData(host);
}

private void SeedData(IWebHost host)
{
    if(host == null) { throw new ArgumentNullException("host"); }
    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        var loggerFactory = services.GetRequiredService<ILoggerFactory>();
        try
        {
            var catalogContext = services.GetRequiredService<CatalogContext>();
            CatalogContextSeed.SeedAsync(catalogContext, loggerFactory)
    .Wait();

            var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
            AppIdentityDbContextSeed.SeedAsync(userManager).Wait();
        }
        catch (Exception ex)
        {
            var logger = loggerFactory.CreateLogger<CatalogControllerIndex>();
            logger.LogError(ex, "An error occurred seeding the DB.");
        }
    }
}

private static void ConfigureWebHostBuilder(IWebHostBuilder builder)
{
    builder.UseEnvironment("Testing");
}

public HttpClient Client { get; }

// tests
}

All 12 comments

Tried using builder.Configure(), but of course that only works instead of, not in addition to, using UseStartup<Startup>(). So, that's out.

This looked promising, but Server is null here:

var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
var host = factory.Server?.Host;
SeedData(host);

@danroth27 Have you used WebApplicationFactory with seeded data like this? Thanks in advance.

Ah ha! The Server property is populated after calling CreateClient() (obviously...). This works!

public class CatalogControllerIndex : IClassFixture<WebApplicationFactory<Startup>>
{
public CatalogControllerIndex(WebApplicationFactory<Startup> fixture)
{
    var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
    Client = factory.CreateClient();
    var host = factory.Server?.Host;
    SeedData(host);
}

private void SeedData(IWebHost host)
{
    if(host == null) { throw new ArgumentNullException("host"); }
    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        var loggerFactory = services.GetRequiredService<ILoggerFactory>();
        try
        {
            var catalogContext = services.GetRequiredService<CatalogContext>();
            CatalogContextSeed.SeedAsync(catalogContext, loggerFactory)
    .Wait();

            var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
            AppIdentityDbContextSeed.SeedAsync(userManager).Wait();
        }
        catch (Exception ex)
        {
            var logger = loggerFactory.CreateLogger<CatalogControllerIndex>();
            logger.LogError(ex, "An error occurred seeding the DB.");
        }
    }
}

private static void ConfigureWebHostBuilder(IWebHostBuilder builder)
{
    builder.UseEnvironment("Testing");
}

public HttpClient Client { get; }

// tests
}

Thanks for contacting us, @ardalis. Looks like you've went through a good investigation here. Glad it worked out. After looking back now, it took you just an hour to figure it out. Which part specifically was the hard part? What/ how you'd like us to improve to make it even simpler?

/cc @danroth27, @javiercn

  1. WebApplicationFactory doesn't actually have a CreateWebApplication method or a Create method that returns a WebApplication.

  2. Server property has no way of being created that makes any sense. You need to create a Client and then realize that this in fact populates the Server. I made a PR to fix this. https://github.com/aspnet/Mvc/pull/7838

These were the two biggest issues. You don't want to have properties that magically do or don't have values based on other method calls that aren't related to them. You also don't want to use the Factory suffix name convention when the start of the type name isn't actually what the Factory is instantiating and returning from its Create method(s). If we can correct both of these design issues, it will make the type much easier and more intuitive. In the meantime, more docs including XML docs will help.

Realizing we're way late to get anything into 2.1 I think at a bare minimum you could raise an exception if someone tries to access the Server property, and in the exception explain that they need to call CreateClient in order to create the Server. But my PR is probably the better solution.

Thanks!

Also I thought "just an hour" comment was great. :) This should have been 30 seconds... 馃

Thanks for your feedback.

There's a point on the testing doc specifically on how to seed the database:
https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1#customize-webapplicationfactory

This package focuses on allowing developers to boot up your MVC app, there's really nothing we can do in this package to simplify how to seed data in EF, that said, it could be done in the same way that its done in a regular app. There are a couple of things that come to my mind though:

  • The first one is the SeedData feature for EF.
  • The second one is just to write an IStartupFilter and do the initialization in there (where you have access to the service provider and can resolve the context).

Can you tell us what you were doing to require accessing the Server property? We expect the common use case is to just create a client and start issuing requests.

Thanks for the docs link - that wasn't there a couple of days ago when I first was looking into this but it looks like good stuff from @GuardRex / Luke. I'll have a look at following that approach and will follow up.

You can see from this issue + comments how I was basically just trying to find where to hook in seeding of test data for my functional test. My previous (pre 2.1) tests did this in a base test class by getting access to the host and creating a scope and then getting a dbContext and seeding data (as you see in my later comments). In trying to do basically the same thing I was trying to get access to the host and its services collection. Server seemed promising, but was null, but then it wasn't if I attempted to access it after calling CreateClient. That's not intuitive.

Between the docs update and my current understanding of the WebApplicationFactory I'm all set, but I do still think it would be worth revisiting the naming and design of the type for the reasons I cited previously. Cheers.

Thanks,

I'm closing this for now as there's no action we can take here until our next major version.

Got into same issue with Server == null

We expect the common use case is to just create a client and start issuing requests.

This is a temporal dependency which is a very bad thing from design POV.

http://blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling/
https://www.yegor256.com/2015/12/08/temporal-coupling-between-method-calls.html

I tried to seed data (specifically users) for integration testing with an inmemory database. I encounter the same error than the one explained here, server is null. I try to build a UserManager to call CreateAsync. I could not get the example above working. Any help is welcome.

public class CustomWebApplicationFactory<TStartup> 
    : WebApplicationFactory<TStartup> where TStartup : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                services.AddDbContext<DocIntelContext>(options => 
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                    options.UseInternalServiceProvider(serviceProvider);
                });
            });
        }
    }
    public class BasicTests : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        protected readonly CustomWebApplicationFactory<Startup> _factory;

        public BasicTests(CustomWebApplicationFactory<Startup> factory)
        {}

        [Fact]
        public async void MyTest()
        {
            // Arrange
            var client = _factory.CreateClient();
            Assert.NotNull(_factory.Server);
        } 
    }

I also tried the approach described here : https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1#customize-webapplicationfactory but I cannot get an instance of UserManager.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CezaryRynkowski picture CezaryRynkowski  路  4Comments

workmonitored picture workmonitored  路  3Comments

karthicksundararajan picture karthicksundararajan  路  4Comments

nefcanto picture nefcanto  路  3Comments

michaelbowman1024 picture michaelbowman1024  路  3Comments