ASP.NET Core3.0 RazorPages事始め(12)番外編 - Startup.csとProgram.cs



ASP.NET Core 3.0 Razor Pagesの[公式チュートリアル](https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/razor-pages/?view=aspnetcore-3.0)をやって感じたのは、Startup.cs が一番の鬼門かな、ということ。

もう少し、Startup.csの理解を深めておく必要がありそうです。

僕もまだわかっていないことが多いので、もし間違い等あれば指摘していただけると嬉しいです。


## Startup.cs

ということで、チュートリアルで作成した `Startup.cs` を開いてみます。
このクラスでは、アプリの動作を構成するコードを記述するようです。


```cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RazorPagesMovie.Models;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();

            services.AddDbContext<RazorPagesMovieContext>(options =>
                options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            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();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

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

```

`ConfigureServices` と `Configure`の２つのメソッドがあります。呼び出される順番は、`ConfigureServices` → `Configure` の順です。

### ConfigureServicesメソッド

コメントを読むと、ランタイムから呼び出されるメソッドで、このメソッド内で、コンテナにサービスを追加するコードを書くということのようです。

コンテナとサービスが何かが良くわかってませんが、このWebアプリに必要な機能をここで追加するということだと思います。ASP.NET Coreはプラガブル(といっていいのかな)な構造になっていて、開発者が必要な機能を明示的に組み込むようになっています。

最初の

```cs
services.AddRazorPages();
```

では、Razor pagesの機能を有効にしています。

次の

```c#
services.AddDbContext<RazorPagesMovieContext>(options => options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
```

では、チュートリアルで作成した DBアクセスのための `RazorPagesMovieContext`をアプリケーションから利用できるようにするためにコードです。

SQLiteを利用し、その接続文字列は、構成ファイルの "MovieContext" から取得しています。
これによって、各ページモデルで `RazorPagesMovieContext`のインスタンスを生成する必要はなくなります。

チュートリアルのコードでは、

```c#
    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)
        {
            _context = context;
        }
    ...
```

と、ページモデルのコンストラクタで、`RazorPagesMovieContext` のインスタンスを受け取っていましたが、`ConfigureServices`メソッドで、サービスを登録していることでこれが実現できていたということですね。

`AddEntityFrameworkStores` といったメソッドも用意されているようです。これら `Add` で始まるメソッドは、`IServiceCollection`インターフェースに対する拡張メソッドとして定義されています。

なお、ここで追加したサービスは、依存関係の挿入(DI) または `ApplicationServices` を利用して利用することができます。

### Configure メソッド

Configure メソッドもランタイムから呼び出されます。 
コメントには、このメソッドを使ってHTTPリクエストパイプラインを設定します、とあります。

ということは、この順番が意味をも持つってことですね。


まだ、完全に理解していないけど、

```cs
app.UseHttpsRedirection();
```

は、httpをhttpsにリダイレクトさせる。

```cs
app.UseStaticFiles();
```

は、静的ファイルを提供できるようにする。

```cs            
app.UseRouting();
```

は、ルーティングを標準設定で構成する。

```cs
app.UseAuthorization();
```

は、認証を構成する。

ということをやっているようです。

Useで始まるメソッドは、IApplicationBuilder の拡張メソッドとして定義されています。


## env.IsDevelopment

それと、Configureメソッドの最初では、`env.IsDevelopment`の値を見て、if文で分岐させている個所があります。`env`は、引数で渡ってくる `IWebHostEnvironment` のインスタンスです。

開発環境と運用環境で動作を変更するために利用しています。

調べたところ、ASP.NET Core はアプリの起動時に環境変数 `ASPNETCORE_ENVIRONMENT` の値を読み込み、このプロパティの値を設定しているようです。

`ASPNETCORE_ENVIRONMENT` には、"Development"、"Staging"、"Production" という 3 つの値を指定できます。ASPNETCORE_ENVIRONMENT が設定されていない場合、既定で Production になります。

Visual Studio Codeの、`launch.json` を見ると、

```json
    "configurations": [
        {
            "name": ".NET Core Launch (web)",
            "type": "coreclr",
            "request": "launch",
            "preLaunchTask": "build",
            "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/RazorPagesMovie.dll",
            "args": [],
            "cwd": "${workspaceFolder}",
            "stopAtEntry": false,
            "serverReadyAction": {
                "action": "openExternally",
                "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
            },
            "env": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            },
            "sourceFileMap": {
                "/Views": "${workspaceFolder}/Views"
            }
        },
```        

となっています。なので、VS Codeから起動する場合は、`env.IsDevelopment`プロパティは、trueになります。

なお、`env.IsDevelopment`プロパティが、falseの時には、

```c#
    app.UseExceptionHandler("/Error");
    app.UseHsts();
```                

となっているので、例外発生時は、"/Error"にリダイレクトされるようです。つまり、Error.cshtml, Error.cshtml.cs が利用されるということですね。

`app.UseHsts()` は、 HSTS (Hypertext Strict Transport Security) をブラウザに通知するようにしているコードです。

Visual Studio IDEでは、プロジェクトのプロパティページでASPNETCORE_ENVIRONMENT 環境変数の値を設定できます。


## appsettings.Development.json

そういえば、チュートリアルで `appsettings.json` についてすこし触れましたが、`appsettings.Development.json` というファイルもプロジェクトには存在していました。

開発時(`ASPNETCORE_ENVIRONMENT=Development`)には、appsettings.jsonの内容に、appsettings.Development.jsonの内容が上書きされて、利用されることになるようです。

利用される接続文字列は、appsettings.jsonに書かれているのですが、appsettings.Development.jsonにも書けば、デバッグ時は、

appsettings.Development.jsonに書かれているConnectionStringsの値が利用されるということですね。

つまり、開発中と運用で接続文字列を簡単に切り替えることができるということです。

`web.config`と`web.debug.config`,`web.release.config`との関係に似ていますね。
でも、`web.debug.config`,`web.release.config`での特殊な記法が必要ないので、理解しやすいですね。

## Startup コンストラクター


Startコンストラクタは、以下のパラメータを受け取りことができます。


- IHostingEnvironment  (環境別にサービスを構成するため)。

- IConfiguration  (スタートアップ時にアプリケーションを構成するため)。

- ILoggerFactory  （ロギングを構成するため）

これらのコンストラクタは省略することもできます。


実際、チュートリアルで利用したコンストラクタは、以下のように `IConfiguration`だけを受け取っています。

```c#
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
```

全てを受け取る場合は、以下のように書きます。

```c#
        public Startup(IHostingEnvironment env, IConfiguration configuration, ILoggerFactory loggerFactory) {
            Configuration = configuration;
            _env = env;
            _loggerFactory = loggerFactory;
        }

        public IConfiguration Configuration { get; }

        private readonly IHostingEnvironment _env;
        private readonly ILoggerFactory _loggerFactory;
```




## Program.cs


これまで見てきた `Startup`クラスは、Program.csの`Main`メソッドから呼び出される`CreateWebHostBuilder`メソッドで指定されています。

チュートリアルで作成した Program.cs は、以下の通り。

```c#
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Models;

namespace RazorPagesMovie
{
    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>();
                });
    }
}
```

ドキュメントによると、Mainメソッドで、ホスト といわれるものを組み立てています。ホストとは、以下をカプセル化するオブジェクトです。


- HTTP サーバーの実装
- ミドルウェア コンポーネント
- ログの記録
- DI
- 構成

上記のコードでは、

- Web サーバーとして Kestrel を使用し、IIS 統合を有効にする。
- appsettings.json、"appsettings.{環境名}.json"、環境変数、コマンド ライン引数、およびその他の構成ソースから構成を読み込む。
- ログ出力をコンソールとデバッグ プロバイダーに送る。

というオプションとともにホストを構成しています。


なお、サービスの追加とリクエストパイプラインの構成以外の初期化が必要ならば、Program.cs で行うってことですね。

Mainメソッドでは、DIが利用できないので、

```c#
SeedData.Initialize(services);
```

や

```c#
var logger = services.GetRequiredService<ILogger<Program>>();
```

のように `IServiceProvider` のインスタンスを使って、サービスにアクセスしているってことですね。

デバッグで確かめたところ

Main → CreateHostBuilder →  Startup.ConfigureServices →  SeedData.Initialize →  host.Run() → Startup.Configure

の順で呼び出されていました。


Startup クラスがすこし理解できたように思います。