【asp.net core】实现动态 Web API

序言:

远程工做已经一个月了,最近也算是比较闲,天天早上起床打个卡,快速弄完当天要作的工做以后就快乐摸鱼去了。以前在用 ABP 框架(旧版)的时候就以为应用服务层写起来真的爽,为何实现了个 IApplicationService 的空接口就能够变成 Web API,惋惜的是以前一直没空去研究这一块的原理及其实现,园子里也找不到相关实现原理的文章(旧版 ABP 的却是有,可是 asp.net core 没法参考)。最近闲起来,就看了一下 abp vnext 的源码,而且也参考了一下晓晨Master 介绍的 Panda.DynamicWebApi。我本身也简单实现了一遍动态 Web API,不由感叹 asp.net core 设计之精妙。git

abp vnexthttps://abp.iogithub

Panda.DynamicWebApihttps://github.com/pdafx/Panda.DynamicWebApiapi

这里先感谢这两个库的相关人员,没有他们的工做,本文也出现不了。另外在此声明,本文意在探究其实现原理并实现一个简易版本,若无把握请勿用于生产环境。服务器


正文:

首先先建立咱们的解决方案以下:mvc

Snipaste_2020-03-09_20-34-28

由于动态 Web API 这一功能是与业务无关的,并且为了复用,咱们应该把这一功能的实现写到一个单独的类库当中。上图中 Demo 项目是 asp.net core 3.1 版本的 Web API 项目,用于演示咱们的简易动态 Web API,而 SimpleDynamicWebAPI 的 .net standard 2.0 项目则是咱们的简易动态 Web API 项目。app


要实现动态 Web API,首先要作的第一件事情就是要有一个规则,来断定一个类是否是动态 Web API。在 abp vnext 当中,主要提供两种方式一个是实现 IRemoteService 接口(实际开发过程当中通常都是实现 IApplicationService 接口),另外一种方式标记 RemoteServiceAttribute。而在 Panda.DynamicWebApi 中,则是实现 IDynamicWebApi 接口而且标记 DynamicWebApi。由于本文是要实现简易版本,所以只选空接口方式。在 SimpleDynamicWebAPI 项目中建立以下空接口:框架

namespace SimpleDynamicWebAPI
{
    public interface IApplicationService
    {
    }
}

接下来,咱们有了 IApplicationService 接口,咱们也知道实现了这个接口的类是要成为动态 Web API 的,但这个是咱们所知道的规则,asp.net core 框架它是不知道的,咱们须要把这个规则告诉它。asp.net

这一块 abp vnext 有点复杂,咱们参考 Panda.DynamicWebAPI 的实现:ide

Snipaste_2020-03-09_20-55-02

https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiServiceExtensions.cs#L46测试

Snipaste_2020-03-09_20-55-48

https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiControllerFeatureProvider.cs

上面图中 DynamicWebApiControllerFeatureProvider 的 IsController 方法很明显了。查看 msdn

Snipaste_2020-03-09_21-03-07

粗俗点翻译过来就是判断一个类是否是控制器。


接下来开始依样画葫芦。首先一点 ControllerFeatureProvider 类是属于 asp.net core 的,理论上是位于 Microsoft.AspNetCore.Mvc.Core 这个 nuget 包的,可是这个包的 3.x 版本并无发布在 nuget 上。若是咱们的 SimpleDynamicWebAPI 引用 2.x 版本的,而 Demo 项目又是 3.x 版本的,则极可能会引发冲突。保险起见,咱们修改 SimpleDynamicWebAPI 为一个 asp.net core 的类库。反正这个库原本也不可能会被其它类型诸如 WPF 的项目引用。

修改 SimpleDynamicWebAPI.csproj 以下:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <OutputType>Library</OutputType>
  </PropertyGroup>
</Project>

接下来建立 ApplicationServiceControllerFeatureProvider 类,并修改代码以下:

using Microsoft.AspNetCore.Mvc.Controllers;
using System.Reflection;

namespace SimpleDynamicWebAPI
{
    public class ApplicationServiceControllerFeatureProvider : ControllerFeatureProvider
    {
        protected override bool IsController(TypeInfo typeInfo)
        {
            if (typeof(IApplicationService).IsAssignableFrom(typeInfo))
            {
                if (!typeInfo.IsInterface &&
                    !typeInfo.IsAbstract &&
                    !typeInfo.IsGenericType &&
                    typeInfo.IsPublic)
                {
                    return true;
                }
            }

            return false;
        }
    }
}

首先先要判断是否是实现了 IApplicationService 接口,这个是咱们一开始所定下的规则。

接下来,一、若是一个接口即便它实现了 IApplicationService,但它仍然不能是一个控制器,那是由于接口是没法实例化的;二、抽象类同理,也是由于没法实例化;三、泛型类也不容许,由于须要确切的类型才能实例化;四、public 表明着公开,可被外界访问,若是一个类不是 public 的,那么就不该该成为一个动态 Web API 控制器。

接下来就是要把这个 ApplicationServiceControllerFeatureProvider 加入到 asp.net core 框架中。

建立 SimpleDynamicWebApiExtensions 扩展类,修改代码以下:

using Microsoft.Extensions.DependencyInjection;
using System;

namespace SimpleDynamicWebAPI
{
    public static class SimpleDynamicWebApiExtensions
    {
        public static IMvcBuilder AddDynamicWebApi(this IMvcBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.ConfigureApplicationPartManager(applicationPartManager =>
            {
                applicationPartManager.FeatureProviders.Add(new ApplicationServiceControllerFeatureProvider());
            });
            return builder;
        }

        public static IMvcCoreBuilder AddDynamicWebApi(this IMvcCoreBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.ConfigureApplicationPartManager(applicationPartManager =>
            {
                applicationPartManager.FeatureProviders.Add(new ApplicationServiceControllerFeatureProvider());
            });
            return builder;
        }
    }
}

由于 ConfigureApplicationPartManager 扩展方法分别在 IMvcBuilder 和 IMvcCoreBuilder 上都有,因此咱们也只能写两遍。固然参照 abp vnext 或 Panda.DynamicWebApi 从 services 中获取 ApplicationPartManager 对象实例也是可行的。

接下来回到 Demo 项目,在 AddControllers 后面加上 AddDynamicWebApi:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddDynamicWebApi();
}


如今咱们已经完成第一步了,实现了 IApplicationService 接口的类将被视做控制器处理。但仅仅这样并不足够,假设有多个类同时实现 IApplicationService 接口,那应该如何映射呢,若是没错的话,这个时候你应该会想到是——路由。咱们还须要作的工做就是把这些控制器与路由配置起来。

abp vnext 这块为了在配置过程当中获取 services 而延迟加载致使包了一层,有点复杂。这里参考 Panda.DynamicWebApi

Snipaste_2020-03-09_22-11-13

https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiServiceExtensions.cs#L51

注释告诉了咱们这里是配置控制器的路由,感谢做者大大。

继续画葫芦,建立 ApplicationServiceConvention 类并实现 IApplicationModelConvention 接口:

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;

namespace SimpleDynamicWebAPI
{
    public class ApplicationServiceConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            throw new NotImplementedException();
        }
    }
}

Apply 方法的实现等下再考虑,先把它注册到 asp.net core 框架,修改 SimpleDynamicWebApiExtensions 扩展类以下:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace SimpleDynamicWebAPI
{
    public static class SimpleDynamicWebApiExtensions
    {
        public static IMvcBuilder AddDynamicWebApi(this IMvcBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.ConfigureApplicationPartManager(applicationPartManager =>
            {
                applicationPartManager.FeatureProviders.Add(new ApplicationServiceControllerFeatureProvider());
            });

            builder.Services.Configure<MvcOptions>(options =>
            {
                options.Conventions.Add(new ApplicationServiceConvention());
            });

            return builder;
        }

        public static IMvcCoreBuilder AddDynamicWebApi(this IMvcCoreBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.ConfigureApplicationPartManager(applicationPartManager =>
            {
                applicationPartManager.FeatureProviders.Add(new ApplicationServiceControllerFeatureProvider());
            });

            builder.Services.Configure<MvcOptions>(options =>
            {
                options.Conventions.Add(new ApplicationServiceConvention());
            });

            return builder;
        }
    }
}

对服务容器中的 MvcOptions 进行配置,添加上 ApplicationServiceConvention。ok,接下来回到考虑 Apply 方法实现的问题了。

这里参考 abp vnext:

Snipaste_2020-03-09_22-26-48

https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L33

上图中的 ApplyForControllers 方法的方法体关键部分很好懂,foreach 遍历了全部的控制器,若是控制器实现了 IRemoteService 接口或者标记了 RemoteServiceAttribute,则调用 ConfigureRemoteService 进一步处理。由于咱们的简易版本是只有接口,else 部分的咱们就不须要了。

修改 ApplicationServiceConvention 代码以下:

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;

namespace SimpleDynamicWebAPI
{
    public class ApplicationServiceConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            foreach (var controller in application.Controllers)
            {
                if (typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))
                {
                    ConfigureApplicationService(controller);
                }
            }
        }

        private void ConfigureApplicationService(ControllerModel controller)
        {
            throw new NotImplementedException();
        }
    }
}

那 abp vnext 的 ConfigureRemoteService 方法中又干了什么呢?跳转到 ConfigureRemoteService 的实现:

Snipaste_2020-03-09_22-36-53

作了三件事情。

一、ConfigureApiExplorer。ApiExplorer,简单点说就是 API 是否可被发现。举个栗子,加入你写了一个 Web API,项目又配置了 swagger,并且你又想 swagger 不显示这个 Web API 的话,那么能够在 Action 上加上:

[ApiExplorerSettings(IgnoreApi = true)]

具体这里就不说了,你们能够自行 google。

二、ConfigureSelector。Selector,选择器,可能不太好理解。可是第三个明显是配置参数,那么第二这个只能是配置路由了,这个方法将会是咱们的关键。

三、ConfigureParameters。第二点说了,配置参数。


那么继续修改咱们的 ApplicationServiceConvention 类而且实现咱们的 ConfigureApiExplorer:

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;

namespace SimpleDynamicWebAPI
{
    public class ApplicationServiceConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            foreach (var controller in application.Controllers)
            {
                if (typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))
                {
                    ConfigureApplicationService(controller);
                }
            }
        }

        private void ConfigureApplicationService(ControllerModel controller)
        {
            ConfigureApiExplorer(controller);
            ConfigureSelector(controller);
            ConfigureParameters(controller);
        }

        private void ConfigureApiExplorer(ControllerModel controller)
        {
            if (!controller.ApiExplorer.IsVisible.HasValue)
            {
                controller.ApiExplorer.IsVisible = true;
            }

            foreach (var action in controller.Actions)
            {
                if (!action.ApiExplorer.IsVisible.HasValue)
                {
                    action.ApiExplorer.IsVisible = true;
                }
            }
        } private void ConfigureSelector(ControllerModel controller) { throw new NotImplementedException(); }

        private void ConfigureParameters(ControllerModel controller)
        {
            throw new NotImplementedException();
        }
    }
}

ConfigureApiExplorer 这块,IsVisible 只要没有值,就无脑设为 true 好了。

接下来 ConfigureSelector 看 abp vnext 的实现:

Snipaste_2020-03-09_23-01-01

https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L170

首先第一行 RemoveEmptySelectors 这是一个关键点。虽然咱们的动态 Web API 控制器一开始并无配置路由,但实际上 asp.net core 框架会为今生成一些空白信息。abp vnext 在这里就抹除掉了这些空白信息。而 Panda.DynamicWebApi 虽然没有这样干,可是后面的判断逻辑就相对复杂了一些(大大别打我)。

咱们的 RemoveEmptySelectors:

private void RemoveEmptySelectors(IList<SelectorModel> selectors)
{
    for (var i = selectors.Count - 1; i >= 0; i--)
    {
        var selector = selectors[i];
        if (selector.AttributeRouteModel == null &&
            (selector.ActionConstraints == null || selector.ActionConstraints.Count <= 0) &&
            (selector.EndpointMetadata == null || selector.EndpointMetadata.Count <= 0))
        {
            selectors.Remove(selector);
        }
    }
}

使用倒序删除小技巧,就不须要担忧下标越界的问题了。

if 第一行明显能够看出判断路由信息是否存在,第二行判断的 Action 的约束,而约束则是指 HttpGet、HttpPost 这种约束,第三行判断了端点元数据信息,例如标记了什么 Attribute 之类的。假如这些都没有,那么这条 selector 就能够判定为空白信息了。

接下来回到 abp vnext 代码截图的 181 行:

Snipaste_2020-03-09_23-25-51

https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L181

假如移除过空白信息后仍然有路由的话,则后续不进行处理。

接下来的 foreach 就开始处理 Action 了。先完善咱们的代码,再开始处理 Action 的路由:

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SimpleDynamicWebAPI
{
    public class ApplicationServiceConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            foreach (var controller in application.Controllers)
            {
                if (typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))
                {
                    ConfigureApplicationService(controller);
                }
            }
        }

        private void ConfigureApplicationService(ControllerModel controller)
        {
            ConfigureApiExplorer(controller);
            ConfigureSelector(controller);
            ConfigureParameters(controller);
        }

        private void ConfigureApiExplorer(ControllerModel controller)
        {
            if (!controller.ApiExplorer.IsVisible.HasValue)
            {
                controller.ApiExplorer.IsVisible = true;
            }

            foreach (var action in controller.Actions)
            {
                if (!action.ApiExplorer.IsVisible.HasValue)
                {
                    action.ApiExplorer.IsVisible = true;
                }
            }
        }

        private void ConfigureSelector(ControllerModel controller)
        {
            RemoveEmptySelectors(controller.Selectors);

            if (controller.Selectors.Any(temp => temp.AttributeRouteModel != null))
            {
                return;
            }

            foreach (var action in controller.Actions)
            {
                ConfigureSelector(action);
            }
        }

        private void ConfigureSelector(ActionModel action)
        {
            throw new NotImplementedException();
        }

        private void ConfigureParameters(ControllerModel controller)
        {
            throw new NotImplementedException();
        }

        private void RemoveEmptySelectors(IList<SelectorModel> selectors)
        {
            for (var i = selectors.Count - 1; i >= 0; i--)
            {
                var selector = selectors[i];
                if (selector.AttributeRouteModel == null &&
                    (selector.ActionConstraints == null || selector.ActionConstraints.Count <= 0) &&
                    (selector.EndpointMetadata == null || selector.EndpointMetadata.Count <= 0))
                {
                    selectors.Remove(selector);
                }
            }
        }
    }
}

开始处理 Action 的路由,参考 abp vnext 的 194 行到 212 行:

Snipaste_2020-03-09_23-32-13

第一行仍然是移除空白信息。

关键在最后的判断,假如没有 selector 的话,加上就是了。可是若是已经有了呢?那就修改呗。举个栗子,假如咱们实现 IApplicationService 接口的类的一个方法标记了 HttpGet,那么这个 Action 是有约束的,可是它倒是没有路由的。这几行不管是 abp vnext 仍是 Panda.DynamicWebApi 都是同样的。

初步实现添加 selector 方法,这里我叫它 AddApplicationServiceSelector:

private void AddApplicationServiceSelector(ActionModel action)
{
    var selector = new SelectorModel();
    selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));
    selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));

    action.Selectors.Add(selector);
}

private string CalculateRouteTemplate(ActionModel action)
{
    throw new NotImplementedException();
}

private string GetHttpMethod(ActionModel action)
{
    throw new NotImplementedException();
}

接下来咱们须要添加路由而且配置约束。

要计算路由,咱们先举个栗子(嗯,第三颗栗子了)。假设咱们有一个叫 BookController 的 API 控制器,有一个叫 Save 的 Action,那么它的路由通常就是:

api/books/{id}/save

也就是说,通常 API 控制器的路由以下:

api/[controller]s(/{id})?(/[action])?

那么咱们大概能写出以下代码:

private string CalculateRouteTemplate(ActionModel action)
{
    var routeTemplate = new StringBuilder();
    routeTemplate.Append("api");

    // 控制器名称部分
    var controllerName = action.Controller.ControllerName;
    if (controllerName.EndsWith("ApplicationService"))
    {
        controllerName = controllerName.Substring(0, controllerName.Length - "ApplicationService".Length);
    }
    else if (controllerName.EndsWith("AppService"))
    {
        controllerName = controllerName.Substring(0, controllerName.Length - "AppService".Length);
    }
    controllerName += "s";
    routeTemplate.Append($"/{controllerName}");

    // id 部分
    if (action.Parameters.Any(temp => temp.ParameterName == "id"))
    {
        routeTemplate.Append("/{id}");
    }

    // Action 名称部分
    var actionName = action.ActionName;
    if (actionName.EndsWith("Async"))
    {
        actionName = actionName.Substring(0, actionName.Length - "Async".Length);
    }
    var trimPrefixes = new[]
    {
        "GetAll","GetList","Get",
        "Post","Create","Add","Insert",
        "Put","Update",
        "Delete","Remove",
        "Patch"
    };
    foreach (var trimPrefix in trimPrefixes)
    {
        if (actionName.StartsWith(trimPrefix))
        {
            actionName = actionName.Substring(trimPrefix.Length);
            break;
        }
    }
    if (!string.IsNullOrEmpty(actionName))
    {
        routeTemplate.Append($"/{actionName}");
    }

    return routeTemplate.ToString();
}

以 api 开头。

控制器部分,若是名字结尾是 ApplicationService 或者 AppService,那就裁掉。而且变为复数。由于这里是简易版,直接加 s 了是。实际建议使用 Inflector 等之类的库。否则 bus 这种词直接加 s 就太奇怪了。

id 部分没啥好说的。

最后是 Action 部分,假如是 Async 结尾的,裁掉。接下来看开头是否是以 Get、Post、Create 等等这些开头,是的话也裁掉,注意要先判断 GetAll 和 GetList 而后再判断 Get。由于最后裁掉以后有多是空字符串,因此还须要判断一下再肯定是否添加到路由中。

经过 Action 部分的计算,以前咱们剩下的 GetHttpMethod 方法也很好写了:

private string GetHttpMethod(ActionModel action)
{
    var actionName = action.ActionName;
    if (actionName.StartsWith("Get"))
    {
        return "GET";
    }

    if (actionName.StartsWith("Put") || actionName.StartsWith("Update"))
    {
        return "PUT";
    }

    if (actionName.StartsWith("Delete") || actionName.StartsWith("Remove"))
    {
        return "DELETE";
    }

    if (actionName.StartsWith("Patch"))
    {
        return "PATCH";
    }

    return "POST";
}

根据 Action 名开头返回 Http 方法就是了,若是什么都匹配不上就假定 POST。


添加 Selector 总算写完了,修改 Selector 还难么?实现咱们本身的 NormalizeSelectorRoutes 方法:

private void NormalizeSelectorRoutes(ActionModel action)
{
    foreach (var selector in action.Selectors)
    {
        if (selector.AttributeRouteModel == null)
        {
            selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));
        }

        if (selector.ActionConstraints.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods?.FirstOrDefault() == null)
        {
            selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));
        }
    }
}

没有路由就给它补路由,没有约束就给它补约束。


如今咱们的 ApplicationServiceConvention 的代码应该以下:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SimpleDynamicWebAPI
{
    public class ApplicationServiceConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            foreach (var controller in application.Controllers)
            {
                if (typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))
                {
                    ConfigureApplicationService(controller);
                }
            }
        }

        private void ConfigureApplicationService(ControllerModel controller)
        {
            ConfigureApiExplorer(controller);
            ConfigureSelector(controller);
            ConfigureParameters(controller);
        }

        private void ConfigureApiExplorer(ControllerModel controller)
        {
            if (!controller.ApiExplorer.IsVisible.HasValue)
            {
                controller.ApiExplorer.IsVisible = true;
            }

            foreach (var action in controller.Actions)
            {
                if (!action.ApiExplorer.IsVisible.HasValue)
                {
                    action.ApiExplorer.IsVisible = true;
                }
            }
        }

        private void ConfigureSelector(ControllerModel controller)
        {
            RemoveEmptySelectors(controller.Selectors);

            if (controller.Selectors.Any(temp => temp.AttributeRouteModel != null))
            {
                return;
            }

            foreach (var action in controller.Actions)
            {
                ConfigureSelector(action);
            }
        }

        private void ConfigureSelector(ActionModel action)
        {
            RemoveEmptySelectors(action.Selectors);

            if (action.Selectors.Count <= 0)
            {
                AddApplicationServiceSelector(action);
            }
            else
            {
                NormalizeSelectorRoutes(action);
            }
        }

        private void ConfigureParameters(ControllerModel controller)
        {
            throw new NotImplementedException();
        }

        private void NormalizeSelectorRoutes(ActionModel action)
        {
            foreach (var selector in action.Selectors)
            {
                if (selector.AttributeRouteModel == null)
                {
                    selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));
                }

                if (selector.ActionConstraints.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods?.FirstOrDefault() == null)
                {
                    selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));
                }
            }
        }

        private void AddApplicationServiceSelector(ActionModel action)
        {
            var selector = new SelectorModel();
            selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(CalculateRouteTemplate(action)));
            selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { GetHttpMethod(action) }));

            action.Selectors.Add(selector);
        }

        private string CalculateRouteTemplate(ActionModel action)
        {
            var routeTemplate = new StringBuilder();
            routeTemplate.Append("api");

            // 控制器名称部分
            var controllerName = action.Controller.ControllerName;
            if (controllerName.EndsWith("ApplicationService"))
            {
                controllerName = controllerName.Substring(0, controllerName.Length - "ApplicationService".Length);
            }
            else if (controllerName.EndsWith("AppService"))
            {
                controllerName = controllerName.Substring(0, controllerName.Length - "AppService".Length);
            }
            controllerName += "s";
            routeTemplate.Append($"/{controllerName}");

            // id 部分
            if (action.Parameters.Any(temp => temp.ParameterName == "id"))
            {
                routeTemplate.Append("/{id}");
            }

            // Action 名称部分
            var actionName = action.ActionName;
            if (actionName.EndsWith("Async"))
            {
                actionName = actionName.Substring(0, actionName.Length - "Async".Length);
            }
            var trimPrefixes = new[]
            {
                "GetAll","GetList","Get",
                "Post","Create","Add","Insert",
                "Put","Update",
                "Delete","Remove",
                "Patch"
            };
            foreach (var trimPrefix in trimPrefixes)
            {
                if (actionName.StartsWith(trimPrefix))
                {
                    actionName = actionName.Substring(trimPrefix.Length);
                    break;
                }
            }
            if (!string.IsNullOrEmpty(actionName))
            {
                routeTemplate.Append($"/{actionName}");
            }

            return routeTemplate.ToString();
        }

        private string GetHttpMethod(ActionModel action)
        {
            var actionName = action.ActionName;
            if (actionName.StartsWith("Get"))
            {
                return "GET";
            }

            if (actionName.StartsWith("Put") || actionName.StartsWith("Update"))
            {
                return "PUT";
            }

            if (actionName.StartsWith("Delete") || actionName.StartsWith("Remove"))
            {
                return "DELETE";
            }

            if (actionName.StartsWith("Patch"))
            {
                return "PATCH";
            }

            return "POST";
        }

        private void RemoveEmptySelectors(IList<SelectorModel> selectors)
        {
            for (var i = selectors.Count - 1; i >= 0; i--)
            {
                var selector = selectors[i];
                if (selector.AttributeRouteModel == null &&
                    (selector.ActionConstraints == null || selector.ActionConstraints.Count <= 0) &&
                    (selector.EndpointMetadata == null || selector.EndpointMetadata.Count <= 0))
                {
                    selectors.Remove(selector);
                }
            }
        }
    }
}

嗯,咱们差很少完成了,就剩最后一个 ConfigureParameters。继续参考 abp vnext(这块 Panda.DynamicWebApi 实现也几乎同样了):

Snipaste_2020-03-10_00-31-04

https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L67

没啥东西,主要是配置控制器的每一个 Action 的每一个参数的 Binding。

关键就后面两个判断,TypeHelper.IsPrimitiveExtended 相似于判断是否是基础类型。例如 int、long 这些就是基础类型,是不该该加上 FromBody 绑定的,而且 abp vnext 进一步判断像 Nullable<int>、Nullable<long>、DateTime 这些也不该该加 FromBody 绑定。这个阅读 TypeHelper 的源码仍是很好懂的。第二个判断则判断了当前 Http 约束是否能用 FormBody,例如 GET、DELETE 请求是没办法用 FromBody 的。

由于是简易版,咱们能够实现以下:

private void ConfigureParameters(ControllerModel controller)
{
    foreach (var action in controller.Actions)
    {
        foreach (var parameter in action.Parameters)
        {
            if (parameter.BindingInfo != null)
            {
                continue;
            }

            if (parameter.ParameterType.IsClass &&
                parameter.ParameterType != typeof(string) &&
                parameter.ParameterType != typeof(IFormFile))
            {
                var httpMethods = action.Selectors.SelectMany(temp => temp.ActionConstraints).OfType<HttpMethodActionConstraint>().SelectMany(temp => temp.HttpMethods).ToList();
                if (httpMethods.Contains("GET") ||
                    httpMethods.Contains("DELETE") ||
                    httpMethods.Contains("TRACE") ||
                    httpMethods.Contains("HEAD"))
                {
                    continue;
                }

                parameter.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromBodyAttribute() });
            }
        }
    }
}

固然第一个判断没有 abp vnext 和 Panda.DynamicWebApi 严谨,但 90% 状况足够用了。第二个判断则把 Http 约束统统查出来,若是有 GET、DELETE 等等这些则不能加 FromBody 约束,反之则加上。


演示:

历经千辛万苦,咱们的简易版动态 Web API 终于完成了。接下来咱们能够给 Demo 项目添加一下测试代码以及配置 swagger 来看一下效果。

在 Demo 项目中添加测试代码 PersonAppService:

using SimpleDynamicWebAPI;
using System.Collections.Generic;
using System.Linq;

namespace Demo.Application
{
    public class CreateUpdatePersonInput
    {
        public string Name { get; set; }
    }

    public class PersonDto
    {
        public string Name { get; set; }
    }

    public class PersonAppService : IApplicationService
    {
        public string Create(CreateUpdatePersonInput input)
        {
            return $"你造了个名字叫:{input.Name} 的人";
        }

        public string Delete(int id)
        {
            return $"你把 Id:{id} 的人干掉了";
        }

        public string Get(int id)
        {
            return $"你输入的 Id 是:{id}";
        }

        public List<PersonDto> GetAll()
        {
            return "服务器向你扔了一堆人"
                .ToCharArray()
                .Select(temp => new PersonDto
                {
                    Name = temp.ToString()
                })
                .ToList();
        }

        public string Update(int id, CreateUpdatePersonInput input)
        {
            return $"你把 Id:{id} 的人的名字改为了 {input.Name}";
        }
    }
}

配置 swagger 的文章园子里多得是,这里就不贴代码了。

完事以后跑起来。

Snipaste_2020-03-10_01-19-16

Snipaste_2020-03-10_01-19-27

感受还行。


结语:

咱们总算实现了一个很是简易的动态 Web API,也至关于又造了一遍轮子,但在这造轮子的过程当中,咱们了解到了其实现的原理,假如之后发现 abp vnext 等框架的动态 Web API 知足不了咱们的时候,咱们也有必定能力进行修改。最后我再次声明,若是没有把握的话,千万别用于生产环境。abp vnext 这种是通过大量项目验证的,即便有 bug,abp vnext 官方也有足够人力去修复。

最后附上 Gayhub 源码:https://github.com/h82258652/SimpleDynamicWebAPI

相关文章
相关标签/搜索