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

序言:

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

abp vnexthttps://abp.io

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

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


正文:

首先先创建我们的解决方案如下:

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 项目。


要实现动态 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 框架它是不知道的,我们需要把这个规则告诉它。

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

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 接口,这个是我们一开始所定下的规则。

接下来,1、如果一个接口即使它实现了 IApplicationService,但它仍然不能是一个控制器,那是因为接口是无法实例化的;2、抽象类同理,也是因为无法实例化;3、泛型类也不允许,因为需要确切的类型才能实例化;4、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

做了三件事情。

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

[ApiExplorerSettings(IgnoreApi = true)]

具体这里就不说了,大家可以自行 google。

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

3、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

posted @ 2020-03-10 11:19  h82258652  阅读(8369)  评论(14编辑  收藏  举报