5

I have ASP.NET Core Razor pages app and I would like to access IWebHostEnvironment in my Program.cs. I seed the DB at the beginning of the application, and I need to pass the IWebHostEnvironment to my initializer. Here is my code:

Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                SeedData.Initialize(services);
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

SeedData.cs

    public static class SeedData
    {
        private static IWebHostEnvironment _hostEnvironment;
        public static bool IsInitialized { get; private set; }

        public static void Init(IWebHostEnvironment hostEnvironment)
        {
            if (!IsInitialized)
            {
                _hostEnvironment = hostEnvironment;
                IsInitialized = true;
            }
        }

        public static void Initialize(IServiceProvider serviceProvider)
        {
            //List<string> imageList = GetMovieImages(_hostEnvironment);

            int d = 0;

            using var context = new RazorPagesMovieContext(
                serviceProvider.GetRequiredService<
                    DbContextOptions<RazorPagesMovieContext>>());

            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }

            var faker = new Faker("en");
            var movieNames = GetMovieNames();
            var genreNames = GetGenresNames();

            foreach(string genreTitle in genreNames)
            {
                context.Genre.Add(new Genre { GenreTitle = genreTitle });
            }

            context.SaveChanges();
            
            foreach(string movieTitle in movieNames)
            {
                context.Movie.Add(
                    new Movie
                    {
                        Title = movieTitle,
                        ReleaseDate = GetRandomDate(),
                        Price = GetRandomPrice(5.5, 30.5),
                        Rating = GetRandomRating(),
                        Description = faker.Lorem.Sentence(20, 100),
                        GenreId = GetRandomGenreId()
                    }
               );
            }

            context.SaveChanges();
        }

Because I have images in wwwroot and I need to get names of of images from there during initializtion. I tried to pass IWebHostEnvironment from Startup.cs inside of configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        int d = 0;
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        SeedData.Init(env); // Initialize IWebHostEnvironment
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }

But it seems that the Startup.Configure method gets executed after the Program.Main method. Then I decided to do it in Startup.ConfigureServices method, but it turns out that this method can only take up to 1 parameter. Is there any way to achieve this? However, I'm not sure that the way I'm trying to seed my data is the best one, I just see this way as the most appropriate for my case, so I would totally appreciate any other suggested approach.

Similar problems I found:

Miraziz
  • 509
  • 7
  • 15
  • 2
    This appears to be an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) and a bit of over-engineering. It also demonstrates how trying to use DI with static classes cause more problems than it solves. You seeder can be a scoped registered class and resolved from the host after it has been built, having the host environment explicitly injected via constructor injection. Another approach could be to have all the seeding done in a `IHostedService` that performs you desired scoped functionality when the application runs. – Nkosi Apr 09 '21 at 21:44
  • Thanks for your advice @Nkosi, I took a look at `XY problem` and found it really useful. – Miraziz Apr 10 '21 at 11:37

2 Answers2

4

The original issue demonstrates how trying to use DI with static classes cause more problems than it solves.

The seeder can be a scoped registered class and resolved from the host after it has been built. The host environment and any other dependencies can be explicitly injected via constructor injection

For example

public class SeedData {
    private readonly IWebHostEnvironment hostEnvironment;
    private readonly RazorPagesMovieContext context;
    private readonly ILogger logger;

    public SeedData(IWebHostEnvironment hostEnvironment, RazorPagesMovieContext context, ILogger<SeedData> logger) {
        this.hostEnvironment = hostEnvironment;
        this.context = context;
        this.logger = logger;
    }

    public void Run() {
        try {
            List<string> imageList = GetMovieImages(hostEnvironment); //<<-- USE DEPENDENCY

            int d = 0;

            if (context.Movie.Any()) {
                return;   // DB has been seeded
            }

            var faker = new Faker("en");
            var movieNames = GetMovieNames();
            var genreNames = GetGenresNames();

            foreach(string genreTitle in genreNames) {
                context.Genre.Add(new Genre { GenreTitle = genreTitle });
            }

            context.SaveChanges();
            
            foreach(string movieTitle in movieNames) {
                context.Movie.Add(
                    new Movie {
                        Title = movieTitle,
                        ReleaseDate = GetRandomDate(),
                        Price = GetRandomPrice(5.5, 30.5),
                        Rating = GetRandomRating(),
                        Description = faker.Lorem.Sentence(20, 100),
                        GenreId = GetRandomGenreId()
                    }
               );
            }

            context.SaveChanges();
        } catch (Exception ex) {
           logger.LogError(ex, "An error occurred seeding the DB.");
        }
    }

    // ... other code

}

Note how there was no longer a need for Service Locator anti-pattern. All the necessary dependencies are explicitly injected into the class as needed.

Program can then be simplified

public class Program {
    public static void Main(string[] args) {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope()) {
            SeedData seeder = scope.ServiceProvider.GetRequiredService<SeedData>();
            seeder.Run();
        }    
        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices(services => {
                services.AddScoped<SeedData>(); //<-- NOTE 
            })
            .ConfigureWebHostDefaults(webBuilder => {
                webBuilder.UseStartup<Startup>();
            });
}

where the seeder is registered with the host and resolved as needed before running the host. Now there is no need to access anything other than the seeder. IWebHostEnvironment and all other dependencies will be resolved by the DI container and injected where needed.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • This way is much better, I don't know why, but because I decided to make my seeder totally static I was thinking in that direction and it lead me to a lot of confusion... I just did not think about injecting it in `Main` and didn't want to seed it using some controllers and so here was the point where I lost... Thank you a lot! – Miraziz Apr 10 '21 at 14:40
  • 1
    @Miraziz glad to help. Sometimes when you hit a block, it helps to just take a step back and try to look at the bigger picture of what you want to do. Happy Coding!!! – Nkosi Apr 10 '21 at 14:41
2

The solution for my problem was to simply request IWebHostEnvironment from ServiceProvider.GetRequiredService<T>:

Main

var host = CreateHostBuilder(args).Build();

using (var scope = host.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var hostEnvironment = services.GetRequiredService<IWebHostEnvironment>();

    try
    {
       SeedData.Initialize(services, hostEnvironment);
    }
    catch (Exception ex)
    {
       var logger = services.GetRequiredService<ILogger<Program>>();
       logger.LogError(ex, "An error occurred seeding the DB.");
    }
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
Miraziz
  • 509
  • 7
  • 15
  • 2
    In .NET 5.0, I used this method to get the environment when writing all the host building, services and app pipeline configurations directly in the Program.cs file, without using a separate Startup.cs file/class. Inside webBuilder.Configure(app => ...) I have this: `IWebHostEnvironment env = app.ApplicationServices.GetRequiredService();` – ryancdotnet Apr 21 '21 at 04:39
  • 1
    I haven't mentioned it, but firstly I also required it from `webBuilder.Configure` and then requested for the `service`. This was surprisingly possible and really convenient I must admit. – Miraziz Apr 21 '21 at 11:33
  • 4
    @ryancdotnet nice, thanks. Its now just `IWebHostEnvironment env = app.Services.GetRequiredService();` – Jeremy Thompson Nov 28 '21 at 01:35