天天看點

.NET Core Web APi類庫如何内嵌運作?

話題

我們知道在.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"      

接下來我們在該類庫中按照規範建立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();
    }
}      
.NET Core Web APi類庫如何内嵌運作?

蕪湖,我們通過Postman模拟調用測試接口,結果驚呆了,404了~~~

.NET Core Web APi類庫如何内嵌運作?

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

public class InitTest : IInitTest
{
    private static readonly string AssemblyName = typeof(InitTest).Assembly.GetName().Name;
    public void Init()
    {
        var builder =.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")]      
.NET Core Web APi類庫如何内嵌運作?

那麼接下來問題又來了,要是需要運作多個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類庫如何内嵌運作?

總結

本節我們重點讨論如何内嵌運作.NET Core Web APi類庫,同時介紹了兩種激活比如控制器特性方案, 希望對您有所幫助,謝謝,我們下節再會