asp.net core之Startup

2023-07-24 18:01:18

Startup介紹

Startup是Asp.net Core的應用啟動入口。在.NET5及之前一般會使用startup.cs類進行程式初始化構造。如下:

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

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

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

        app.UseRouting();

        app.UseAuthorization();

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

並在Program中使用IHostBuilder構造Host程式:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

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

在.NET5之後的版本中,簡化了這一操作(當然也可以繼續保留這種方式),我們可以直接在Program的程式入口Main函數中直接構造設定我們的Startup,或者直接使用頂級語句的方式,在Program類中直接編寫。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseAuthorization();

app.MapGet("/hi", () => "Hello!");

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

對比之下,很容易發現,其中在var app = builder.Build();之前的程式碼這是做我們應用的初始化,比如依賴注入,設定載入等等操作,相當於Startup.cs中的ConfigureServices方法。
對應的,下面的操作就是我們的中介軟體設定,對應Startup.cs中的Configure方法。
同時我們可以發現,在新版的中介軟體設定中,少了UseRouting和UseEndpoints用來註冊路由的中介軟體,是因為使用最小託管模型時,終結點路由中介軟體會包裝整個中介軟體管道,因此無需顯式呼叫 UseRouting 或 UseEndpoints 來註冊路由。 UseRouting 仍可用於指定進行路由匹配的位置,但如果應在中介軟體管道開頭匹配路由,則無需顯式呼叫 UseRouting。
app.MapRazorPages(); 就相當於
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});

擴充套件Startup

在asp.net core中有一個IStartupFilter的介面,用於擴充套件Startup。
IStartupFilter 在不顯式註冊預設中介軟體的情況下將預設值新增到管道的開頭。
IStartupFilter 實現 Configure,即接收並返回 Action。 IApplicationBuilder 定義用於設定應用請求管道的類。
每個 IStartupFilter 可以在請求管道中新增一個或多箇中介軟體。 篩選器按照新增到服務容器的順序呼叫。 篩選器可在將控制元件傳遞給下一個篩選器之前或之後新增中介軟體,從而附加到應用管道的開頭或末尾。
我們來實踐一下,首先建立一個空的asp.net core模板很簡單,只有一個Program檔案。

再來新增一個IStartupFilter的實現,只用於控制檯輸出執行內容。

using Microsoft.AspNetCore.Hosting;

namespace LearnStartup
{
    public class StartupFilterOne : IStartupFilter
    {
        public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
        {
            return builder => 
            {
                builder.Use(async (httpContext, _next) => 
                {
                    Console.WriteLine("-----StartupFilterOne-----");
                    await _next(httpContext);
                });
                next(builder);
            };
        }
    }
}

在Program中新增一行程式碼註冊StartupFilterOne

using LearnStartup;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IStartupFilter, StartupFilterOne>(); //注入StartupFilterOne

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

啟動程式,可以看到如下結果,中介軟體正常執行:

當我們有多個IStartupFilter時,我們怎麼控制中介軟體執行順序呢?上面所說,跟我們注入的順序有關。
新增一個StartupFilterTwo,在修改一下Program

using LearnStartup;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IStartupFilter, StartupFilterTwo>();
builder.Services.AddTransient<IStartupFilter, StartupFilterOne>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

可以看到是先執行StartupFilterTwo中的中介軟體,然後再執行StartupFilterOne的中介軟體。

以上寫法都是把中介軟體註冊在中介軟體管道頭部,那麼如何讓他在尾部執行呢?
在IStartupFilter.Configure(Action next)中的引數next,本質其實就是Startup中的Configure(感興趣可以翻原始碼檢視),只要調整next的執行順序即可。
我們調整一下StartupFilterTwo的程式碼

public class StartupFilterTwo : IStartupFilter
    {
        public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
        {
            return builder =>
            {
                next(builder);
                builder.Use(async (httpContext, _next) => 
                {
                    Console.WriteLine("-----StartupFilterTwo-----");
                    await _next(httpContext);
                });
            };
        }
    }

將next(builder)放在前面執行,我們來看看效果

此時,發現我們StartupFilterTwo並沒有執行,那是因為app.MapGet("/", () => "Hello World!");是一個終結點中介軟體,而StartupFilterTwo註冊到了中介軟體末尾,執行到這個中介軟體時就直接返回沒有繼續執行下一個中介軟體。
當我們修改Url路徑為/test時,沒有匹配到HelloWorld的中介軟體,StartupFilterTwo中的內容成功輸出。

淺談一下IStartupFilter的應用場景

IStartupFilter可以用於模組化開發的方案,在各自類庫中載入對應的中介軟體。
在請求頭部管道做一些請求的校驗or資料處理。
在請求管道尾部時,如上圖404,無法匹配到路由,我們可以做哪些處理。

注意事項:

IStartupFilter只能註冊在中介軟體管道頭部或者尾部,請確保中介軟體的使用順序。
若中介軟體需要在管道中間插入使用,請使用正常的app.use在startup中正確設定。

IHostingStartup

可在啟動時從應用的 Program.cs 檔案之外的外部程式集嚮應用新增增強功能,比如我們一些0程式碼侵入的擴充套件服務,在SkyApm中的.NET實現就是基於這種方式。
我們新建一個StartupHostLib類庫,新增一下Microsoft.AspNetCore.Hosting的nuget包
然後新增一個Startup類庫實現IHostingStartup。
注意,必須需要新增標記,否則無法識別HostingStartup
[assembly: HostingStartup(typeof(LearnStartup.OneHostingStartup))]

using Microsoft.AspNetCore.Hosting;

[assembly: HostingStartup(typeof(LearnStartup.OneHostingStartup))]
namespace StartupHostLib
{

    public class OneHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureAppConfiguration((config) => 
            {
                Console.WriteLine("ConfigureAppConfiguration");
            });

            builder.ConfigureServices(services =>
            {

                Console.WriteLine("ConfigureServices");
            });

            builder.Configure(app =>
            {
                Console.WriteLine("Configure");
            });
        }
    }
}

在LearnStartup中參照專案,並在launchSettings的環境變數中新增
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "StartupHostLib"
然後啟動專案

這裡可以發現,HostingStartup的執行順序是優於應用的。
但是出現一個問題,發現原本的HelloWorld中介軟體消失了,但是我們依賴注入載入的中介軟體依舊生效。我們註釋builder.Configure方法之後再啟動程式。

public void Configure(IWebHostBuilder builder)
{
    builder.ConfigureAppConfiguration((config) => 
    {
        Console.WriteLine("ConfigureAppConfiguration");
    });

    builder.ConfigureServices(services =>
    {

        Console.WriteLine("ConfigureServices");
    });

    //builder.Configure(app =>
    //{
    //    Console.WriteLine("Configure");
    //});
}


可以發現,應用中介軟體正常了。說明HostingStartup中設定中介軟體和應用的中介軟體設定衝突,並覆蓋應用中介軟體。
我們將StartupFilterOne和StartupFilterTwo放到OneHostingStartup中去設定依賴注入,再次啟動專案觀察。

public void Configure(IWebHostBuilder builder)
{
    builder.ConfigureAppConfiguration((config) => 
    {
        Console.WriteLine("ConfigureAppConfiguration");
    });
    builder.ConfigureServices(services =>
    {
        services.AddTransient<IStartupFilter, StartupFilterTwo>();
        services.AddTransient<IStartupFilter, StartupFilterOne>();
        Console.WriteLine("ConfigureServices");
    });

    //builder.Configure(app =>
    //{
    //    Console.WriteLine("Configure");
    //});
}


可以發現,依賴注入中載入的中介軟體是生效的。

淺談IHostingStartup應用場景

由上面表現可以發現
IHostingStartup執行順序由於應用執行順序。
IHostingStartup中設定中介軟體管道會覆蓋應用中介軟體管道。
依賴注入中IStartupFilter設定中介軟體可以正常使用不覆蓋應用中介軟體。
所以我們使用HostingStartup的場景可以為:
對程式碼0侵入的場景,比如AOP資料收集(如SkyApm)。
沒有中介軟體的場景OR符合IStartupFilter中介軟體的場景。
想深入瞭解的可以自行翻看原始碼,本文淺嘗即止。

歡迎進群催更。