使用.net6 WebApplication打造最小API

  .net6在preview4時給我們帶來了一個新的API:WebApplication,通過這個API我們可以打造更小的輕量級API服務。今天我們來嘗試一下如何使用WebApplication設計一個小型API服務系統。

  環境準備

  .NETSDK   v6.0.0-preview.6.21355.2

  Visual Studio 2022 Preview

  首先看看原始版本的WebApplication,官方已經提供了樣例模板,打開我們的vs2022,選擇新建項目選擇asp.net core empty,framework選擇.net6.0(preview)點擊創建,即可生成一個簡單的最小代碼示例:

  如果我們在.csproj裏在配置節PropertyGroup增加使用C#10新語法讓自動進行類型推斷來隱式的轉換成委託,則可以更加精簡:

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

 

   當然僅僅是這樣,是無法用於生產的,畢竟不可能所有的業務單元我們塞進這麼一個小小的表達式裏。不過藉助WebApplication我們可以打造一個輕量級的系統,可以滿足基本的依賴注入的小型服務。比如通過自定義特性類型,在啓動階段告知系統爲哪些服務注入哪些訪問路徑,形成路由鍵和終結點。具體代碼如下:

  首先我們創建一個簡易的特性類,只包含httpmethod和path:

    [AttributeUsage(AttributeTargets.Method)]
    public class WebRouter : Attribute
    {
        public string path;
        public HttpMethod httpMethod;
        public WebRouter(string path)
        {
            this.path = path;
            this.httpMethod = HttpMethod.Post;
        }
        public WebRouter(string path, HttpMethod httpMethod)
        {
            this.path = path;
            this.httpMethod = httpMethod;
        }
    }

  接着我們按照一般的分層設計一套DEMO應用層/倉儲服務:

    public interface IMyService
    {
        Task<MyOutput> Hello(MyInput input);
    }
    public interface IMyRepository
    {
        Task<bool> SaveData(MyOutput data);
    }
    public class MyService : IMyService
    {
        private readonly IMyRepository myRepository;
        public MyService(IMyRepository myRepository)
        {
            this.myRepository = myRepository;
        }
        [WebRouter("/", HttpMethod.Post)]
        public async Task<MyOutput> Hello(MyInput input)
        {
            var result = new MyOutput() { Words = $"hello {input.Name ?? "nobody"}" };
            await myRepository.SaveData(result);
            return await Task.FromResult(result);
        }
    }
    public class MyRepository : IMyRepository
    {
        public async Task<bool> SaveData(MyOutput data)
        {
            Console.WriteLine($"保存成功:{data.Words}");
            return await Task.FromResult(true);
        }
    }

  最後我們需要將我們的服務接入到WebApplication的map裏,怎麼做呢?首先我們需要定義一套代理類型用來反射並獲取到具體的服務類型。這裏爲了簡單的演示,我只設計包含一個入參和沒有入參的情況下:

    public abstract class DynamicPorxy
    {
        public abstract Delegate Instance { get; set; }
    }
    public class DynamicPorxyImpl<Tsvc, Timpl, Tinput, Toutput> : DynamicPorxy where Timpl : class where Tinput : class where Toutput : class
    {
        public override Delegate Instance { get; set; }
        public DynamicPorxyImpl(MethodInfo method)
        {
            Instance = ([FromServices] IServiceProvider sp, Tinput input) => ExpressionTool.CreateMethodDelegate<Timpl, Tinput, Toutput>(method)(sp.GetService(typeof(Tsvc)) as Timpl, input);
        }
    }
    public class DynamicPorxyImpl<Tsvc, Timpl, Toutput> : DynamicPorxy where Timpl : class where Toutput : class
    {
        public override Delegate Instance { get; set; }
        public DynamicPorxyImpl(MethodInfo method)
        {
            Instance = ([FromServices] IServiceProvider sp) => ExpressionTool.CreateMethodDelegate<Timpl, Toutput>(method)(sp.GetService(typeof(Tsvc)) as Timpl);
        }
    }

  接着我們創建一個代理工廠用於創建服務的方法委託並創建代理類型實例返回給調用端

    public class DynamicPorxyFactory
    {
        public static IEnumerable<(WebRouter, DynamicPorxy)> RegisterDynamicPorxy()
        {
            foreach (var methodinfo in DependencyContext.Default.CompileLibraries.Where(x => !x.Serviceable && x.Type != "package")
                .Select(x => AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(x.Name)))
                .SelectMany(x => x.GetTypes().Where(x => !x.IsInterface && x.GetInterfaces().Any()).SelectMany(x => x.GetMethods().Where(y => y.CustomAttributes.Any(z => z.AttributeType == typeof(WebRouter))))))
            {
                var webRouter = methodinfo.GetCustomAttributes(typeof(WebRouter), false).FirstOrDefault() as WebRouter;
                DynamicPorxy dynamicPorxy;
                if (methodinfo.GetParameters().Any())
                    dynamicPorxy = Activator.CreateInstance(typeof(DynamicPorxyImpl<,,,>).MakeGenericType(methodinfo.DeclaringType.GetInterfaces()[0], methodinfo.DeclaringType, methodinfo.GetParameters()[0].ParameterType , methodinfo.ReturnType), new object[] { methodinfo }) as DynamicPorxy;
                else
                    dynamicPorxy = Activator.CreateInstance(typeof(DynamicPorxyImpl<,,>).MakeGenericType(methodinfo.DeclaringType.GetInterfaces()[0], methodinfo.DeclaringType,  methodinfo.ReturnType), new object[] { methodinfo }) as DynamicPorxy;
                yield return (webRouter, dynamicPorxy);
            }
        }
    }
    internal class ExpressionTool
    {
        internal static Func<TObj, Tin, Tout> CreateMethodDelegate<TObj, Tin, Tout>(MethodInfo method)
        {
            var mParameter = Expression.Parameter(typeof(TObj), "m");
            var pParameter = Expression.Parameter(typeof(Tin), "p");
            var mcExpression = Expression.Call(mParameter, method, Expression.Convert(pParameter, typeof(Tin)));
            var reExpression = Expression.Convert(mcExpression, typeof(Tout));
            return Expression.Lambda<Func<TObj, Tin, Tout>>(reExpression, mParameter, pParameter).Compile();
        }
        internal static Func<TObj, Tout> CreateMethodDelegate<TObj, Tout>(MethodInfo method)
        {
            var mParameter = Expression.Parameter(typeof(TObj), "m");
            var mcExpression = Expression.Call(mParameter, method);
            var reExpression = Expression.Convert(mcExpression, typeof(Tout));
            return Expression.Lambda<Func<TObj, Tout>>(reExpression, mParameter).Compile();
        }
    }

  最後我們創建WebApplication的擴展方法來調用代理工廠以及注入IOC容器:

    public static class WebApplicationBuilderExtension
    {
        static Func<string, Delegate, IEndpointConventionBuilder> GetWebApplicationMap(HttpMethod httpMethod, WebApplication webApplication) => (httpMethod) switch
                {
                    (HttpMethod.Get) => webApplication.MapGet,
                    (HttpMethod.Post) => webApplication.MapPost,
                    (HttpMethod.Put) => webApplication.MapPut,
                    (HttpMethod.Delete) => webApplication.MapDelete,
                    _ => webApplication.MapGet
                };
        public static WebApplication RegisterDependencyAndMapDelegate(this WebApplicationBuilder webApplicationBuilder, Action<IServiceCollection> registerDependencyAction, Func<IEnumerable<(WebRouter webRouter, DynamicPorxy dynamicPorxy)>> mapProxyBuilder)
        {
            webApplicationBuilder.Host.ConfigureServices((ctx, services) =>
            {
                registerDependencyAction(services);
            });
            var webApplication = webApplicationBuilder.Build();
            mapProxyBuilder().ToList().ForEach(item => GetWebApplicationMap(item.webRouter.httpMethod, webApplication)(item.webRouter.path, item.dynamicPorxy.Instance));
            return webApplication;
        }
    }

  當然包括我們的自定義容器注入方法:

    public class MyServiceDependency
    {
        public static void Register(IServiceCollection services)
        {
            services.AddScoped<IMyService, MyService>();
            services.AddScoped<IMyRepository, MyRepository>();
        }
    }

  最後改造我們的program.cs的代碼,通過擴展來注入容器和代理委託並最終生成路由-終結點:

await WebApplication.CreateBuilder().RegisterDependencyAndMapDelegate(MyServiceDependency.Register,DynamicPorxyFactory.RegisterDynamicPorxy).RunAsync("http://*:80");

  這樣這套小型API系統就基本完成了,可以滿足日常的依賴注入和獨立的業務單元類型編寫,最後我們啓動並調用一下,可以看到確實否符合我們的預期成功的調用到了應用服務並且倉儲也被正確的執行了:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章