Spiga

ABP成长系列3:动态API

2024-04-27 16:12:33

前面我们介绍了Abp的模块化,号称能随意拆分,随意组装, 能各种复用,是真的超牛。

但是这样就可以了吗?虽然是可以任意组装了,比如A项目被拆分成A、B两个项目了。如何才能做的优雅,做的有意义?

ABP的答案是这样的: 最强CP——ADO(Auto API+Dynamic Client+Options)

模块化===能力复用

  • Auto API Controller:轻松组合到一个进程,也可以模块化独立部署,拆分分布式
  • Dynamic C# Client:服务之间的调用(跟本地调用一样),都不用改代码,就可以轻松替换成分布式调用
  • 定制化需求:Options选项模式完成定制

一、Auto API Controllers

创建应用程序服务后, 通常需要创建API控制器以将此服务公开为 HTTP(REST)API端点

  • 典型的API控制器除了将方法调用重定向到应用程序服务并使用[HttpGet],[HttpPost],[Route]等属性配置REST API之外什么都不做
  • 瘦控制器:本身是可以不要的
  • ABP可以按照惯例自动将应用程序服务配置为API控制器
  • 大多数时候不关心它的详细配置,但它可以完全被自定义,而且还可以做好复用

Auto-API-Controllers自动API控制器,可以不用写控制器Action了,就可以直接把应用服务暴露成WebAPI

1. 配置方法

  • 服务标记成IRemoteService
  • 配置AbpAspNetCoreMvcOptions
    • TypePredicate
    • ConventionalControllers.Create

以上就完成了Auto-API-Controller

2. 自定义HttpMethod

ABP在确定服务方法的HTTP Method时使用命名约定:

  • Get:如果方法名称以GetList,GetAll或Get开头
  • Put:如果方法名称以Put或Update开头
  • Delete:如果方法名称以Delete或Remove开头
  • Post:如果方法名称以Create,Add,Insert或Post开头
  • Patch:如果方法名称以Patch开头
  • 其他情况,Post 为 默认方式

自定义HTTP Method,则可以使用标准ASP.NET Core的属性 ([HttpPost], [HttpGet], [HttpPut]... 等等.)。需要添加 Microsoft.AspNetCore.Mvc.Core的Nuget包

3. 自定义路由规则

  • 始终以 /api开头
  • 接着是路由路径,默认值为“/app”,可以在初始化修改

4. 服务筛选

  • RemoteServiceAttribute设置IsEnabled=false,可以禁用
  • 配置项TypePredicate 为false
  • 避免Swagger呈现,但是服务还在,RemoteServiceAttribute设置 IsMetadataEnabled为false

以上Auto API Controller,靠的是约定俗成来生成的,但是也支持了一 系列的自定义,比如路由、HttpMethod、名字、服务筛选。

实际开发中,尽量使用默认的规则

5. 源码分析:上帝视角

  1. WebAPI控制器没啥做的,都转到业务逻辑那边处理了,写一遍还麻烦,所以有这个自动的API Controller
  2. 真有WebAPI控制器了,反而不方便模块化组装复用了
  3. 标记IRemote接口,模块启动时Create,在ConfigureService里面找到dll,筛选出合适的类和方法,用Conventions的形式写入,等同于写了Controller和Action代码

Auto API Controller的源码在Volo.Abp.AspNetCore.Mvc.Conventions命名空间下:

6. 核心1:模型约定

  • 接口IAbpServiceConvention是一个ASP.NET Core框架的约定,它会通过包装器AbpServiceConventionWrapper,将规则延迟加载到配置中去。
//AbpServiceConventionWrapper.cs
public class AbpServiceConventionWrapper : IApplicationModelConvention
{
    private readonly Lazy<IAbpServiceConvention> _convention;

    public AbpServiceConventionWrapper(IServiceCollection services)
    {
        _convention = services.GetRequiredServiceLazy<IAbpServiceConvention>();
    }

    public void Apply(ApplicationModel application)
    {
        _convention.Value.Apply(application);
    }
}

//AbpMvcOptionsExtensions.cs
private static void AddConventions(MvcOptions options, IServiceCollection services)
{
    options.Conventions.Add(new AbpServiceConventionWrapper(services));
}
  • 在实现类AbpServiceConvention里面,有个Apply方法,会在程序启动过程执行,内容在ApplyForControllers方法,检测特性或者接口,移除后缀
/// <summary>
/// 模型约定的方法---会在程序启动时,执行ConfigureService--AddController会执行
/// </summary>
/// <param name="application"></param>
public void Apply(ApplicationModel application)
{
    ApplyForControllers(application);
}
/// <summary>
/// 把类型的route规则都安排好
/// </summary>
/// <param name="application"></param>
protected virtual void ApplyForControllers(ApplicationModel application)
{
    RemoveDuplicateControllers(application);

    foreach (var controller in GetControllers(application))
    {
        var controllerType = controller.ControllerType.AsType();

        var configuration = GetControllerSettingOrNull(controllerType);

        //TODO: We can remove different behaviour for ImplementsRemoteServiceInterface. If there is a configuration, then it should be applied!
        //TODO: But also consider ConventionalControllerSetting.IsRemoteService method too..!

        if (ImplementsRemoteServiceInterface(controllerType))
        {
            controller.ControllerName = controller.ControllerName.RemovePostFix(ApplicationService.CommonPostfixes);
            configuration?.ControllerModelConfigurer?.Invoke(controller);
            ConfigureRemoteService(controller, configuration);
        }
        else
        {
            var remoteServiceAttr = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(controllerType.GetTypeInfo());
            if (remoteServiceAttr != null && remoteServiceAttr.IsEnabledFor(controllerType))
            {
                ConfigureRemoteService(controller, configuration);
            }
        }
    }
}

7. 核心2:配置

  • ConfigureApiExplorer(controller); 配置展示
  • ConfigureSelector(controller, configuration); 配置筛选
  • ConfigureParameters(controller); 配置参数

就是基础配置的生效,都是可以通过源码看到的,甚至去修改定制。

protected virtual void ConfigureRemoteService(ControllerModel controller, [CanBeNull] ConventionalControllerSetting configuration)
{
    ConfigureApiExplorer(controller);
    ConfigureSelector(controller, configuration);
    ConfigureParameters(controller);
}

//3个方法都在这个类里面,这里不代码了

控制器直接就找到了,控制器其实靠的Convertion在扫描 dll 环节写入的,控制器和Action其实也就是一个描述,在程序启动时会收集起来放入一个集合,这个Convertion就是有权限写入这个集合,MVC就当成有这么个控制器和Action。

8. 核心3:添加约定

如果写过ASP.MVC插件式Controller,肯定会熟悉ControllerFeatureProvider类,ABP继承这个类,将模型约定添加到MVC中去

public class AbpConventionalControllerFeatureProvider : ControllerFeatureProvider
{
    protected override bool IsController(TypeInfo typeInfo)
    {
        //TODO: Move this to a lazy loaded field for efficiency.
        if (_application.ServiceProvider == null)
        {
            return false;
        }

        var configuration = _application.ServiceProvider
            .GetRequiredService<IOptions<AbpAspNetCoreMvcOptions>>().Value
            .ConventionalControllers
            .ConventionalControllerSettings
            .GetSettingOrNull(typeInfo.AsType());

        return configuration != null;
    }
}

//AbpAspNetCoreMvcModule中,添加上面的Provider
partManager.FeatureProviders.Add(new AbpConventionalControllerFeatureProvider(application));

9. 知识点拓展:Conventions约定

  1. ApplicationModel:描述了MVC应用中的各种对象和行为,包括 Application、Controller、Action、Parameter、Router、Page、 Property、Filter——程序启动时收集到各种信息的集合
  2. Asp.Net Core框架本身内置一套规则(Convention)用来处理这些模型,同时也提供了接口给我们自定义约定来扩展模型以实现更符合需要的应用
  3. ModelConvention就是用来扩展该规则的,自定义个 CustomControllerModelConvention

10. ModelConventions源码流程

  1. 启动环节:EndpointMiddleware注册——MapRouteController
  2. ApplicationModelConventions.ApplyConventions写入到 ActionDescriptor
  3. 后续操作都是基于ActionDescriptor

简而言之:ApplyConventions程序反射读取控制器、Action、Filter之后,留了一个后门可以自由添加这些元数据

AutoAPI靠的就是这个ModelConvertions模型约定 在程序启动的过程中,把Service变成了Controller+Action

二、Dynamic C# Api Client

AutoAPI提供了轻松拆分,但调用方有依赖咋办?得修改调用的代码呀,还是很麻烦。如果说不用修改调用方代码,那就牛了!

ABP可以自动创建C# API 客户端代理来调用远程HTTP服务(REST APIS)。

通过这种方式,不需要通过 HttpClient或者其他低级的HTTP功能调用远程服务并获取数据,非常方便。

独立部署一个模块,提供HttpApi的服务,常规就是HttpClient/HttpWebRequest 去请求。

这个就是提供一个动态代理,可以用Proxy的模式去调用,就像WebService/WCF的代理,就像调用本地实例方法的方式去调用远程API。

服务端基本不需要配置,只是提供常规的服务而已。

1. 客户端配置

  1. Nuget引用和模块依赖 Volo.Abp.Http.Client
  2. 依赖目标服务的抽象
  3. 创建代理
  4. 配置地址(多名称多地址)
  5. WebModule依赖Client
  6. 注入+使用
  7. ABP还可以生成客户端,动态生成js调用
context.Services.AddHttpClientProxies(typeof(DashboardCenterApplicationContractsModule).Assembly, "Dashboard");

2. 源码分析:上帝视角

  1. 对方是HttpAPI,终归是发起Http请求,然后调用
  2. 需要:ip端口,需要路径,httpmethod,各种参数
  3. 其实就是封装了HttpClient

3. 核心1:注入代理

在类ServiceCollectionHttpClientProxyExtensions里面,通过IsSuitableForClientProxying方法来判断。核心逻辑是找出assambly里面合适做代理的接口

 public static IServiceCollection AddStaticHttpClientProxies(
     [NotNull] this IServiceCollection services,
     [NotNull] Assembly assembly,
     [NotNull] string remoteServiceConfigurationName = RemoteServiceConfigurationDictionary.DefaultName)
 {
     Check.NotNull(services, nameof(assembly));

     var serviceTypes = assembly.GetTypes().Where(IsSuitableForClientProxying).ToArray();

     foreach (var serviceType in serviceTypes)
     {
         AddHttpClientFactory(services, remoteServiceConfigurationName);

         services.Configure<AbpHttpClientOptions>(options =>
         {
             options.HttpClientProxies[serviceType] = new HttpClientProxyConfig(serviceType, remoteServiceConfigurationName);
         });
     }

     return services;
 }

private static bool IsSuitableForClientProxying(Type type)
{
	//TOD0: Add option to change type filter
	return type.IsInterface
		&& type.IsPublic
		&& !type.IsGenericType
		&& typeof(IRemoteService).IsAssignableFrom(type),
}

4. 核心2:AddHttpClientProxy方法

循环类型,依次创建代理,注册到IOC。动态代理生成过程在Volo.abp.Castle.Core

public static IServiceCollection AddHttpClientProxy(
    [NotNull] this IServiceCollection services,
    [NotNull] Type type,
    [NotNull] string remoteServiceConfigurationName = RemoteServiceConfigurationDictionary.DefaultName,
    bool asDefaultService = true)
{
    //省略其他
    //...

    //服务注册:接口类型注册给一个委托,委托负责生成实例,通过Castle动态代理
    if (asDefaultService)
    {
        services.AddTransient(
            type,
            serviceProvider => ProxyGeneratorInstance
                .CreateInterfaceProxyWithoutTarget(
                    type,
                    (IInterceptor)serviceProvider.GetRequiredService(validationInterceptorAdapterType),
                    (IInterceptor)serviceProvider.GetRequiredService(interceptorAdapterType)
                )
        );
    }
    
	//代理注册:一直会——IHttpClientProxy<T> 做注册,给一个委托,委托创建一个proxy,一个属性service等于这个实例
    services.AddTransient(
        typeof(IHttpClientProxy<>).MakeGenericType(type),
        serviceProvider =>
        {
            var service = ProxyGeneratorInstance
                .CreateInterfaceProxyWithoutTarget(
                    type,
                    (IInterceptor)serviceProvider.GetRequiredService(validationInterceptorAdapterType),
                    (IInterceptor)serviceProvider.GetRequiredService(interceptorAdapterType)
                );

            return Activator.CreateInstance(
                typeof(HttpClientProxy<>).MakeGenericType(type),
                service
            );
        });

    return services;
}

5. Castle.Core

  1. 第三方开源类库,支持.NET Framework/.NET Core。autofac的AOP就是这个实现的
  2. GitHub地址:https://github.com/castleproject/Core
  3. 基于emit技术,动态生成代码,ProxyGenerator---ProxyGenerator---InterfaceProxyWithTargetInterfaceGenerator--父类 BaseInterfaceProxyGenerato--GenerateType方法

Dynamic C# Client就是这样生成的代码。

Castle.Core的源码很复杂,知道怎么用就可以了。

6. 动态客户端限制

  • 基于Contract,基于IRemoteService,必须依赖,所以得是同版本的ABP vNext,同运行时环境CLR
  • 也可以拆出来这个封装

三、Options选项模式

1. Option多种注册方式

  • Configure
  • ConfigureAll
  • PostConfigure
  • PostConfigureAll
  • AddOptions
  • ABP封装Configure

2. Option三种获取方式

  • IOptions
  • IOptionsMonitor
  • IOptionsSnapshot

3. Options大致原理和建议

  • 通过Configure<>泛型注册
  • 获取环境OptionsFactory 实例化Option
  • 缓存

4. ABP中的Options

  • Configure方法,就是个封装,可以不用
  • 还有个PreConfigure,用的不多
  • 位置问题,写在不同层次。其实只是在组装委托,在第一次获取时才真实计算,所以可以在不同层按需求去做配置
  • Option其实就是保存各层次的配置,然后在程序运行时使用,所以才能定制化

5. AbpAspNetCoreMvcOptions

  • ConventionalControllerSetting.TypePredicate
  • ConventionalControllers.Create

6. AbpRemoteServiceOptions

  • RemoteServices
  • IsMetadataEnabled