如何在ASP.NET Core中从Program.cs访问IWebHostEnvironment

5

我有一个ASP.NET Core Razor页面应用程序,我想在Program.cs中访问IWebHostEnvironment。 我在应用程序开始时播种DB,并且我需要将IWebHostEnvironment传递给我的初始化程序。 这是我的代码:

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();
        }

由于我的 wwwroot 目录中有一些 图片,并且我需要在初始化期间从那里获取这些图片的名称。我尝试在 Startup.cs 中的 configure 方法中传递 IWebHostEnvironment

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();
        });
    }

但是似乎 Startup.Configure 方法在 Program.Main 方法之后被执行。然后我决定在 Startup.ConfigureServices 方法中完成,但是这个方法只能接受一个参数。有没有办法实现这个目标?不过,我不确定我正在尝试种植数据的方式是否是最佳的,我只是认为这种方式最适合我的情况,所以我非常感谢任何其他建议的方法。
类似的问题:

2
这似乎是一个XY问题,并且有点过度工程化。它还展示了尝试使用DI与静态类会带来更多问题而不是解决问题。您的seeder可以是一个作用域注册类,并在构建后从主机解析,通过构造函数注入显式注入主机环境。另一种方法是在IHostedService中完成所有播种操作,在应用程序运行时执行所需的作用域功能。 - Nkosi
谢谢你的建议@Nkosi,我查看了"XY问题"并发现它非常有用。 - Miraziz
2个回答

4

这个问题最初的展示表明,试图使用 DI(依赖注入)与静态类结合使用会带来更多问题而不是解决问题。

种子数据生成器可以被注册为范围内的类,并在主机构建后从主机中解析。可以通过构造函数注入方式显式地注入主机环境和任何其他依赖项。

例如:

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

}

请注意,不再需要使用服务定位器反模式。 所有必要的依赖项将根据需要显式注入到类中。
然后可以简化程序
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>();
            });
}

种子被注册到主机并在运行主机之前根据需要解析。现在除了种子之外,没有必要访问任何其他东西。 IWebHostEnvironment 和所有其他依赖项将由 DI 容器解析并注入到需要的位置。


这种方式更好,我不知道为什么,但因为我决定使我的seeder完全静态,所以我一直在思考这个方向,结果导致了很多混乱...我只是没有考虑将其注入到“Main”中,并且不想使用某些控制器来进行播种,所以这就是我迷失的地方...非常感谢! - Miraziz
1
@Miraziz 很高兴能帮助你。有时候当你遇到难题时,退一步并试着看待你想要做的整体情况会有所帮助。祝编码愉快! - Nkosi

2
我的问题的解决方案是简单地从ServiceProvider.GetRequiredService请求IWebHostEnvironment:
主要
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.");
    }
}

2
在.NET 5.0中,我使用了这种方法来获取环境,直接在Program.cs文件中编写所有主机构建、服务和应用程序管道配置,而不使用单独的Startup.cs文件/类。在webBuilder.Configure(app => ...)中,我有这个:IWebHostEnvironment env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>(); - ryancdotnet
1
我之前没有提到,但首先我也需要从 webBuilder.Configure 中获取它,然后请求 service。这是令人惊讶的可能性,而且非常方便,我必须承认。 - Miraziz
4
@ryancdotnet 很好,谢谢。现在只需要这个 IWebHostEnvironment env = app.Services.GetRequiredService<IWebHostEnvironment>(); - Jeremy Thompson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接