.NET Core Web APi類庫如何內嵌執行?

語言: CN / TW / HK

話題

我們知道在.NET Framework中可以嵌入執行Web APi,那麼在.NET Core(.NET 6+稱之為.NET)中如何內嵌執行Web Api呢,在實際專案中這種場景非常常見,那麼我們本節以.NET 6.0作為演示示例一起來瞅瞅

內嵌執行.NET Core Web APi

接下來我們通過控制檯作為主程式來啟動Web APi,首先我們建立名為EmbedWebApi的控制檯程式,然後建立Embed.WebApi類庫執行Web APi,我們在此Web APi中建立如下介面,並實現相關方法來執行Web APi

public class InitTest : IInitTest
{
    public void Init()
    {
        var builder = WebApplication.CreateBuilder();

        builder.Services.AddControllers();

        var app = builder.Build();

        app.UseRouting();

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

        app.Run();
    }
}

public interface IInitTest
{
    void Init();
}

通過寫介面並在對應方法中執行Web APi主要是達到在控制中呼叫該介面進行模擬實現,這裡需要注意一點的是,因為我們建立的Web APi是類庫,要想使用Web裡面的Api等等,直接在專案檔案中新增如下一行以表明我們要引用框架,這樣一來框架裡面所包含的APi等等版本都一致統一, 而不是通過NuGet一一下載,這是錯誤的做法

<ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

接下來我們在該類庫中按照規範建立Controllers資料夾,並建立測試控制器,如下

using Microsoft.AspNetCore.Mvc;

namespace Embed.WebApi.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public IActionResult Test()
        {
            return Ok("Hello World");
        }
    }
}

最後我們在控制檯程式中註冊上述介面並呼叫初始化方法,如下:

internal class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();
        
        services.AddTransient<IInitTest, InitTest>();

        var serviceProvider = services.BuildServiceProvider();

        var initTest = serviceProvider.GetRequiredService<IInitTest>();

        initTest.Init();

        Console.Read();
    }
}

蕪湖,我們通過Postman模擬呼叫測試介面,結果驚呆了,404了~~~

當我們將類庫中的控制器移動到控制檯中,此時請求測試介面併成功返回對世界的問候,這是什麼原因呢? 不難猜測可知,預設WebAPi控制器的啟用以作為入口的主程式集進行查詢啟用。 雖然這樣看似解決了問題,假設呼叫嵌入執行的主程式是底層已經封裝好的基礎設施,那麼豈不是遭到了程式碼入侵,所以我們就想在執行的Web APi類庫裡面去啟用,此時我們想到將類庫作為Web APi應用程式一部分應用手動載入並激活,在初始化方法裡面修改為如下即可請求測試介面成功

public class InitTest : IInitTest
{
    private static readonly string AssemblyName = typeof(InitTest).Assembly.GetName().Name;
    public void Init()
    {
        var builder = WebApplication.CreateBuilder();

        builder.Services.AddControllers()
            .AddApplicationPart(Assembly.Load(new AssemblyName(AssemblyName)));

        var app = builder.Build();

        app.UseRouting();

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

        app.Run();
    }
}

上述直接在執行Web APi類庫中新增控制器啟用,這種場景完全限定於底層主入口已封裝好,所以只能採用這種方式,若是主入口我們自己可控制,當然還有另外一種方式,來,我們瞧瞧擷取的關鍵性原始碼

/// <summary>
/// Populates the given <paramref name="feature"/> using the list of
/// <see cref="IApplicationFeatureProvider{TFeature}"/>s configured on the
/// <see cref="ApplicationPartManager"/>.
/// </summary>
/// <typeparam name="TFeature">The type of the feature.</typeparam>
/// <param name="feature">The feature instance to populate.</param>
public void PopulateFeature<TFeature>(TFeature feature)
{
    if (feature == null)
    {
        throw new ArgumentNullException(nameof(feature));
    }

    foreach (var provider in FeatureProviders.OfType<IApplicationFeatureProvider<TFeature>>())
    {
        provider.PopulateFeature(ApplicationParts, feature);
    }
}

internal void PopulateDefaultParts(string entryAssemblyName)
{
    var assemblies = GetApplicationPartAssemblies(entryAssemblyName);

    var seenAssemblies = new HashSet<Assembly>();

    foreach (var assembly in assemblies)
    {
        if (!seenAssemblies.Add(assembly))
        {
            // "assemblies" may contain duplicate values, but we want unique ApplicationPart instances.
            // Note that we prefer using a HashSet over Distinct since the latter isn't
            // guaranteed to preserve the original ordering.
            continue;
        }

        var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly);
        foreach (var applicationPart in partFactory.GetApplicationParts(assembly))
        {
            ApplicationParts.Add(applicationPart);
        }
    }
}

private static IEnumerable<Assembly> GetApplicationPartAssemblies(string entryAssemblyName)
{
    var entryAssembly = Assembly.Load(new AssemblyName(entryAssemblyName));

    // Use ApplicationPartAttribute to get the closure of direct or transitive dependencies
    // that reference MVC.
    var assembliesFromAttributes = entryAssembly.GetCustomAttributes<ApplicationPartAttribute>()
        .Select(name => Assembly.Load(name.AssemblyName))
        .OrderBy(assembly => assembly.FullName, StringComparer.Ordinal)
        .SelectMany(GetAssemblyClosure);

    // The SDK will not include the entry assembly as an application part. We'll explicitly list it
    // and have it appear before all other assemblies \ ApplicationParts.
    return GetAssemblyClosure(entryAssembly)
        .Concat(assembliesFromAttributes);
}

private static IEnumerable<Assembly> GetAssemblyClosure(Assembly assembly)
{
    yield return assembly;

    var relatedAssemblies = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false)
        .OrderBy(assembly => assembly.FullName, StringComparer.Ordinal);

    foreach (var relatedAssembly in relatedAssemblies)
    {
        yield return relatedAssembly;
    }
}

從上述原始碼可知,通過主入口程式集還會載入引用的程式集去查詢並激活相關特性(比如控制器),當然前提是實現ApplicationPartAttribute特性,此特性必須在主入口程式集裡定義,定義在程式集上,所以我們只需一行程式碼即可搞定,我們在控制檯主入口名稱空間頂部新增特性,引入Web APi類庫程式集作為應用程式的一部分,如下:

[assembly: ApplicationPart("Embed.WebApi")]

那麼接下來問題又來了,要是需要執行多個Web APi我們又當如何呢?按照上述方式一一新增未嘗不可,我們也可以通過MSBuild任務來進行構建將相關特性自動新增到主入口程式集描述資訊裡面去,例如:

<ItemGroup>
    <AssemblyAttribute Include="Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartAttribute">
        <_Parameter1>Embed.WebApi</_Parameter1>
    </AssemblyAttribute>
</ItemGroup>

有的童鞋就問了,這不寫死了麼,那還不如通過新增特性的方式去處理,請注意這裡只是使用示例,實際情況下,我們可將多個Web APi放在同一解決方案下,然後在此解決方案下建立可構建任務的.targets檔案,並在主專案檔案裡引入,將程式集名稱作為變數引入,剩下事情自行統一處理,若不清楚怎麼搞,就在程式碼中使用特性方式也未嘗不可,例如如下:

<ItemGroup>
    <AssemblyAttribute Include="Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartAttribute">
        <_Parameter1>$(AssemblyName)</_Parameter1>
    </AssemblyAttribute>
</ItemGroup>

總結

本節我們重點討論如何內嵌執行.NET Core Web APi類庫,同時介紹了兩種啟用比如控制器特性方案, 希望對您有所幫助,謝謝,我們下節再會