通过 .NET NativeAOT 实现用户体验升级

前言

TypedocConverter 是我先前因帮助维护 monaco-editor-uwp 但苦于 monaco editor 的 API 实在太多,手写 C# 的类型绑定十分不划算而发起的一个项目。

这个工具可以将 typedoc 根据 TypeScript 生成的 JSON 文件直接生成对应的 C# 类型绑定代码,并提供完整的 JSON 序列化支持,因此使用这个工具可以大大降低移植 TypeScript 库到 .NET 上的困难。(至于为什么是从 typedoc 而不是从 TypeScript 直接 parse,其实只是因为太懒了不想写 TypeScript 的 parser)

TypedocConverter 使用 F# 编写,虽然使用 .NET 5 可以做到程序集裁剪后使用单文件自托管发布,但是我一直在想如果能使用 AOT 技术将整个程序编译为 native binary 那就好了,这样的话用户在使用的时候将不需要运行 .NET 的运行时,也不需要 JIT,而是直接运行机器代码。

工具除了功能性之外,最重要的就是用户体验,这样做将大大提升程序的启动速度(虽然原本已经够快了,但是我想将 100ms 的启动时间缩短到不到 1ms),使得用户使用该工具时不需要任何的等待。

AOT 方案调研

.NET 一直以来都有一个叫做 CoreRT 的项目,使用该工具可以将 .NET 程序集编译到 native binary,然而这个项目自从 2018 年官方就没有再积极维护。但是由于社区的强烈呼声以及某个微软的合作伙伴的项目需要 AOT 技术,并表示如果没有这项技术将不再使用 .NET,于是这个项目原地复活,以 NativeAOT 的名字转移到了 runtimelab 并作为 .NET 6 的 P0(最高) 优先级实验性工作项(即提供带支持的官方 preview,而不再是原来的万年 alpha),目前支持 win-x64linux-x64osx-x64,对于 ARM64 、移动平台和浏览器(WebAssembly)的支持在计划当中。

借着这个契机,我决定使用该方案将项目编译为原生镜像。

NativeAOT 原理

.NET 的 NativeAOT 的思路其实很简单:

  • 首先需要一个 AOT 友好的、用于 NativeAOT 的核心库 (System.Private.CoreLib)实现,提供类型和实现查找、类型解析等方法
  • 扫描程序集,记录用到的类型和方法
  • 调用 RyuJIT 接口,生成类型的元数据,为所有的方法生成代码,最终产生出 obj 二进制文件
  • 调用链接器(MSVC 或 clang),将产生的 obj 与 GC 和系统库等链接成为最终的可执行文件

现阶段 NativeAOT 基本已经完成,剩余的部分工作则是一些修补和完善,以及对新版本 .NET 的跟进(目前还没有跟进 C# 8 之后牵扯到运行时修改的特性,如默认接口方法实现和模块初始化器等等)。

可能你会问这和 .NET Native 技术有何不同?不同之处在于 .NET Native 使用 UTC 编译器(MSVC 后端)进行代码生成,而 NativeAOT 使用 RyuJIT 进行代码生成。

关于 .NET NativeAOT 完整的使用文档可以参考:using-native-aot

针对 NativeAOT 改造项目

NativeAOT 使用非常简单,只需要修改 csproj 项目文件即可:

<PropertyGroup>
  <IlcOptimizationPreference>Speed</IlcOptimizationPreference>
  <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
</PropertyGroup>
<ItemGroup>
  <PackageReference Include="Microsoft.DotNet.ILCompiler" Version="6.0.0-*" />
</ItemGroup>

IlcOptimizationPreference 指定 Speed 表示以最大性能为目标生成代码(如果指定 Size 则表示以最小程序为目标生成代码)。

IlcFoldIdenticalMethodBodies 参数则可以将相同的方法体合并,有助于减小体积。

最后则是新的 Microsoft.DotNet.ILCompiler,这是 NativeAOT 编译器本体,通过 wildcard 指定 6.0.0-* 版本,这样每次编译都会获取最新的版本。

由于 Microsoft.DotNet.ILCompiler 来自实验仓库的 artifacts,而没有发布在官方的 nuget 源,需要新建 nuget.config 额外将实验仓库的 artifacts 作为源引入:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
  </packageSources>
</configuration>

如此一来便大功告成了,这就开始编译:

dotnet publish -c Release -r win-x64

伴随而来的是大量的警告:

AOT analysis warning IL9700: Microsoft.FSharp.Reflection.FSharpType.MakeFunctionType(Type,Type): Calling 'System.Type.MakeGenericType(Type[])' which has `RequiresDynamicCodeAttribute` can break functionality when compiled fully ahead of time. The native code for this instantiation might not be available at runtime.
AOT analysis warning IL9700: Microsoft.FSharp.Reflection.FSharpValue.MakeFunction(Type,FSharpFunc`2<Object,Object>): Calling 'System.Type.MakeGenericType(Type[])' which has `RequiresDynamicCodeAttribute` can break functionality when compiled fully ahead of time. The native code for this instantiation might not be available at runtime.
...

观察警告可以发现,这是分析器报出来的,理由很简单:NativeAOT 是不支持运行时动态代码生成的,但是 MakeGenericType 在需要在运行时产生类型,因此可能不受支持。

为什么说是可能呢?因为 NativeAOT 条件下,不支持运行时产生新的类型,但是对于已经生成代码的类型则是完全支持的。

由于项目没有用到 System.Reflection.Emit 在运行时动态织入 IL,也没有用到 Assembly.LoadFile 等动态加载程序集,更没有用到 C++/CLI 和 COM,因此是 NativeAOT 兼容的。

编译速度尚可,只等待了半分钟。编译完成后产生了一个 29mb 的 exe,体积还不够优秀,但是先运行看看:

> ./TypedocConverter
[Error] No input file
Typedoc Converter Arguments:
--inputfile [file]: input file
--namespace [namespace]: specify namespace for generated code
--splitfiles [true|false]: whether to split code to different files
--outputdir [path]: used for place code files when splitfiles is true
--outputfile [path]: used for place code file when splitfiles is false
--number-type [int/decimal/double...]: config for number type mapping
--promise-type [CLR/WinRT]: config for promise type mapping,CLR for Task and WinRT for IAsyncAction/IAsyncOperation
--any-type [object/dynamic...]: config for any type mapping
--array-type [Array/IEnumerable/List...]: config for array type mapping
--nrt-disabled [true|false]: whether to disable Nullable Reference Types
--use-system-json [true|false]: whether to use System.Text.Json instead of Newtonsoft.Json

一瞬间就运行了起来,完全感受不到启动时间(体感小于 1ms),这个体验太爽了。

可是正当我高兴的时候,使用一个实际的 JSON 文件对功能进行测试,却报错了:

Unhandled Exception: EETypeRva:0x013EC198(System.Reflection.MissingRuntimeArtifactException): MakeGenericMethod() cannot create this generic method instantiation because no code was generated for it: 'Microsoft.FSharp.Collections.ListModule.OfSeq<System.Int32>(System.Collections.Generic.IEnumerable<System.Int32>)'.
   at Internal.Reflection.Core.Execution.ExecutionEnvironment.GetMethodInvoker(RuntimeTypeInfo,QMethodDefinition,RuntimeTypeInfo[],MemberInfo) + 0x144
   at System.Reflection.Runtime.MethodInfos.NativeFormat.NativeFormatMethodCommon.GetUncachedMethodInvoker(RuntimeTypeInfo[],MemberInfo) + 0x50
   at System.Reflection.Runtime.MethodInfos.RuntimeMethodInfo.get_MethodInvoker() + 0xa1
   at System.Reflection.Runtime.MethodInfos.RuntimeNamedMethodInfo`1.MakeGenericMethod(Type[]) + 0x104
   ...

可以看到方法 Microsoft.FSharp.Collections.ListModule.OfSeq<System.Int32>(System.Collections.Generic.IEnumerable<System.Int32> 缺失了。

这是因为 NativeAOT 编译器并没有通过代码路径分析出该类型,因此没有为该类型生成代码,导致运行时尝试创建该类型时由于找不到实现代码而出错。

因此,需要通过 Runtime Directives 指示编译器生成指定类型和方法的代码,方法是创建一个 rd.xml 并引入项目:

 <ItemGroup>
  <RdXmlFile Include="rd.xml" />
 </ItemGroup>

然后在 rd.xml 中编写需要编译器额外生成的类型和方法。经过一番试错之后,我写出了如下的代码:

<Directives>
  <Application>
    <Assembly Name="FSharp.Core" Dynamic="Required All">
      <Type Name="Microsoft.FSharp.Collections.ListModule" Dynamic="Required All">
        <Method Name="OfSeq" Dynamic="Required">
          <GenericArgument Name="System.Int32,System.Private.CoreLib" />
        </Method>
      </Type>
      <Type Name="Microsoft.FSharp.Core.PrintfImpl+Specializations`3[[System.Object,System.Private.CoreLib],[System.Object,System.Private.CoreLib]]" Dynamic="Required All">
        <Method Name="CaptureFinal1" Dynamic="Required">
          <GenericArgument Name="System.Object,System.Private.CoreLib" />
        </Method>
        <Method Name="CaptureFinal2" Dynamic="Required">
          <GenericArgument Name="System.Object,System.Private.CoreLib" />
          <GenericArgument Name="System.Object,System.Private.CoreLib" />
        </Method>
        <Method Name="CaptureFinal3" Dynamic="Required">
          <GenericArgument Name="System.Object,System.Private.CoreLib" />
        </Method>
        <Method Name="OneStepWithArg" Dynamic="Required">
          <GenericArgument Name="System.Object,System.Private.CoreLib" />
        </Method>
        <Method Name="Capture1" Dynamic="Required">
          <GenericArgument Name="System.Object,System.Private.CoreLib" />
        </Method>
        <Method Name="Capture2" Dynamic="Required">
          <GenericArgument Name="System.Object,System.Private.CoreLib" />
        </Method>
        <Method Name="Capture3" Dynamic="Required">
          <GenericArgument Name="System.Object,System.Private.CoreLib" />
        </Method>
      </Type> 
    </Assembly>

    <Assembly Name="System.Linq" Dynamic="Required All">
      <Type Name="System.Linq.Enumerable" Dynamic="Required All">
        <Method Name="ToArray" Dynamic="Required">
          <GenericArgument Name="System.Int32,System.Private.CoreLib" />
        </Method>
      </Type>
    </Assembly>

  </Application>
</Directives>

稍微对上面的东西进行一下解释:Name 用于指定类型,, 前后分别是类型的完整名称和类型来自的程序集名称,.NET 中的各种基础类型都来源于 System.Private.CoreLibmscorlib。详细的格式说明可以参考 rd-xml-format

在 .NET 中,编译器会为所有的值类型的泛型参数特化一份实现,而所有的引用类型参数共享一份实现。这么做其实原因显而易见,因为引用类型背后只是一个指针罢了。因此根据这个特点,所有的引用类型都无需指定实际的类型参数,统一指定一个 System.Object 就好了;而对于值类型作为类型参数则需要指出生成什么类型的代码。

经过上面一番折腾之后,重新编译运行,这次所有的功能均正常了,启动速度飞快,运行时性能也非常棒,并且纯静态链接无需安装任何运行时就能运行,体验几乎和 C++ 编写出来的程序一样。

程序体积优化

上面一系列操作之后,虽然启动和运行速度很快,但是生成的程序大小有 30 mb,还是有些大,那么接下来在不牺牲运行时代码性能的情况下,针对程序体积进行优化。

首先指定 TrimMode为 Link,这可以使 NativeAOT 采用更加激进的程序集剪裁方案,将代码路径中没有被引用的代码以方法为粒度删掉;另外,想到自己的程序不需要国际化支持,因此可以删除掉没有用的多语言支持及其资源文件。

<PropertyGroup>
  <TrimMode>Link</TrimMode>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

重新进行编译,这个时候产生的 exe 大小只有 27mb 了,运行测试:

Unhandled Exception: Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type Definitions+Reflection. A class should either have a default constructor,one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'id',line 2,position 6.
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(JsonReader,JsonObjectContract,JsonProperty,String,Boolean&) + 0x1d1
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader,Type,JsonContract,JsonContainerContract,Object) + 0x2cc
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader,Object) + 0xa4
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader,Boolean) + 0x26e
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader,Type) + 0xf8
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String,JsonSerializerSettings) + 0x93
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String,JsonSerializerSettings) + 0x2b
   at Program.main$cont@84(JsonSerializerSettings,Definitions.Config,Unit) + 0x31
   at TypedocConverter!<BaseAddress>+0x83a0ca

根据报错信息我们知道是 JSON 反序列化过程出了问题,问题在于 Definitions+Reflection 类型被裁剪掉了。由于我知道我自己的程序内进行 JSON 反序列化的目标类型都是来自于我自己的程序集本身,因此不必使用 rd.xml 那么麻烦,只需要告诉编译器不要裁剪我自己的程序集中的类型即可(这对于泛型类实例无效,因为泛型类型实现是需要特化的):

<ItemGroup>
  <TrimmerRootAssembly Include="TypedocConverter" />
</ItemGroup>

接下来重新编译运行,这次没问题了。

最终程序的大小是 27mb,相比 30mb 并没有小太多,不过这也正常,毕竟前面写的 rd.xml 中,由于偷懒,通过 Dynamic="Require All" 保留了 F# 核心库中的所有类型。如果我去掉 Dynamic="Require All" 的话,最终编译出 22mb 的二进制文件,但是需要更多的精力调研有哪些类型需要写进 rd.xml。其实即使到这一步,22mb 里面起码有一半的体积都是生成的元数据用来给反射提供类型信息的,如果直接把反射禁用不生成对应的元数据的话,编译出来的东西只有不到 10mb。

此外,可以通过告诉编译器只给用到反射的类型生成元数据来进一步节省体积(大约能节省 30%):

<ItemGroup>
  <IlcArg Include="--reflectedonly" />
</ItemGroup>

通过 zip 压缩之后只剩下 11mb,这个体积我觉得已经不错了。

当然,要注意的是,Windows 下调试符号文件默认作为单独的 pdb 文件提供,而在 *inx 下调试符号是直接内嵌到程序二进制数据中的,因此在非 Windows 平台下需要使用 strip 命令将符号裁剪掉,否则你将得到一个非常大的二进制程序文件。

strip ./TypedocConverter

想要看看最终效果的可以去此处下载含 Native 名称的 Release 文件体验:https://github.com/hez2010/TypedocConverter/releases

到这里有人可能好奇 NativeAOT 最小能做到多小?经过实验,禁用反射并取消 root 所有程序集后的 hello world 项目可以做到不到 1mb 的体积。

已知问题和限制

.NET NativeAOT 预计会在 .NET 6 将会为尝鲜者提供带支持的预览(其实已经足够稳定),现阶段有一些比较影响使用的已知问题,我将在这里列出。

由于缺少实现而不支持(主要是 C# 8 之后的需要运行时改变的特性),但是短期内会被解决的问题:

  • 不支持含泛型方法的默认接口方法实现
  • 不支持协变返回
  • try-catch 语句中不支持 catch (T),即将泛型参数作为 catch 的异常类型
  • 不支持模块初始化器

短期内不会被解决的问题:

  • 不支持 C++/CLI
  • 无限泛型递归调用

受限于运行时无 JIT 而无法实现的:

  • 运行时动态生成代码(如:System.Reflection.Emit
  • 运行时动态加载程序集(如:Assembly.LoadFile
  • 无限泛型递归调用

有人可能不理解什么叫做无限泛型递归调用,我通过代码解释一下,假如你编写了如下代码:

public void Foo<T>()
{
    if (bar)
    {
        Foo<U<T>>();
    }
}

那么会导致编译器 Stack Overflow。原因是因为代码中将 U<T> 类型代入了 T,如果是不改变泛型嵌套层数调用的话(比如将 U 带入 T),只需要通过 rd.xml 指定一下用到的类型即可解决;但是对于前后嵌套层数不一致的情况,编译器在编译时并不知道你到底会展开多少层代码(NativeAOT 编译器需要在编译时展开所有的泛型并为涉及到的所有的方法和类型生成代码),于是会无限的生成用于 TU<T>U<U<T>>... 的代码,最终导致无法完成编译。 而为什么有 JIT 的情况下不存在问题呢?是因为可以根据 bar 这个条件在运行时按需产生类型和生成代码。

我曾经为 ReactvieX 和 Entity Framework Core 修复过类似的问题,如果想要了解详情的话可以参考:

GUI 解决方案

由于短期内不支持 COM 和 C++/CLI,意味着 WPF 目前无法经过 NativeAOT 编译为本机程序,但是好在 WPF 的跨平台(基于 Skia 自绘)实现版本 Avalonia 完全不需要 COM,也不包含我上述列出的已知问题,因此今天就已经能够使用它开发跨平台的 UI 程序。

由于 0.10.0 版本做了大量优化,并引入了编译时绑定,性能有极大的提升,并且所有动画都以 60fps 呈现,还自带一套 Fluent Design 的主题库,体验非常舒适。我经过尝试之后,将自己的可视化通用旅行商问题解算器应用使用 NativeAOT 编译后得到了一个 40mb 大小的应用程序(无需运行时),可以瞬间启动且运行时内存占用不到 20mb,什么才是小而美(战术后仰)。

SATSP

左侧是一个包含接近 70 万个节点的折线图,可以 60 fps 的体验(其实可以更高,但对于桌面 GUI 应用来说 60 fps 渲染是一个默认的设定)随意滑动、缩放和跟踪点,完全不带一点卡顿(某 WebGL 实现的 echart 这时候早已经停止了思考)。

Web 解决方案

自然,ASP.NET Core 是支持 NativeAOT 的(MVC 中的 View 暂时除外),而 Entity Framework Core 由于使用了含泛型的默认接口方法实现暂时不支持 NativeAOT,随着 NativeAOT 编译器和库的更新会解决。

至于重度依赖运行时织入 IL 的 Dapper,可能永远也不会支持 NativeAOT,毕竟熊掌和鱼不可兼得。

当然,通过 Source Generator 将动态生成代码转为静态生成代码不失为一种解决方案。

不过对于 ASP.NET Core,有一点需要注意:该框架通过反射程序集加载 Controller,因此代码路径中没有直接引用 Controller 类型的代码,编译时所有的 Controller 都会被剪裁掉导致访问所有的 API 都是 404。这一个问题同样也是通过编写 rd.xml 告知编译器保留类型来解决。

我将自己的一个没有使用 ORM,只是使用 Microsoft.Data.Sqlite 的用于人员管理的 Web 服务经过 NativeAOT 编译,得到了一个 30mb 的程序,运行后瞬间就能提供服务,内存占用只需要 20mb,且首次请求只需要 0.7ms,体验非常的棒。这意味着在云原生环境下,尤其是扩容时,新建节点中的应用可以在极短时间内(一秒都不到)启动并投入使用,而不是都启动不久了还在等健康检查的响应。预热是什么?不存在的!

总结和展望

毫无疑问,NativeAOT 将能极大的改善 .NET 程序的启动速度和运行性能,并自带反破解属性,真正做到 C# 的编写效率,C++ 的运行效率。在 .NET 5 的今天这套工具链其实发展状况已经较为成熟了,想用的话已经可以提前体验,国外其实已经有使用这套工具链上线生产项目的例子了。

另外,该技术同样可以用于编译 native 的动态链接库供其他语言(如 C++)使用,甚至可以用来构建 EFI 裸机引导程序进行系统编程(参考 GitHub 项目 ZeroSharp)。

.NET NativeAOT 目前还在不断探索各种可能性,其中一个我认为比较有趣的是:

在 NativeAOT 编译中,先将 IL 借助 RyuJIT 编译到 LLVM IR,这个过程会对代码进行 IL 特有模式相关的优化;然后将 LLVM IR 编译到原生二进制程序,这个过程将会通过 LLVM 进行进一步的优化,使得编译后的体积更小、运行时性能更强。

先前编译到 LLVM IR 的实验 LLILC 的问题在于直接 target 到 LLVM IR 导致 RyuJIT 针对 IL 特定模式的优化缺失。而新的实验当中,RyuJIT 作为“中端”,做好针对 IL 特定模式的优化后再送到 LLVM,避免了该不足之处。

未来 .NET NativeAOT 技术同样会被带到移动平台和浏览器(WebAssembly)上,对于这套技术以后的发展我也会长期关注和跟进。

最后,希望 .NET 平台越来越好。

原文地址:https://www.cnblogs.com/hez2010

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发、订阅和处理的流程。这种实现太简单了,百十行代码就展示了一个基本工作原理。然而,要将这样的解决方案运用到实际生产环境,还有很长的路要走。今天,我们就研究一下在事件处理器中,对象生命周期的管理问题。事实上,不仅仅是在事件处理器
上文已经介绍了Identity Service的实现过程。今天我们继续,实现一个简单的Weather API和一个基于Ocelot的API网关。 回顾 《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)》 Weather API Weather
最近我为我自己的应用开发框架Apworks设计了一套案例应用程序,并以Apache 2.0开源,开源地址是:https://github.com/daxnet/apworks-examples,目的是为了让大家更为方便地学习和使用.NET Core、最新的前端开发框架Angular,以及Apwork
HAL(Hypertext Application Language,超文本应用语言)是一种RESTful API的数据格式风格,为RESTful API的设计提供了接口规范,同时也降低了客户端与服务端接口的耦合度。很多当今流行的RESTful API开发框架,包括Spring REST,也都默认支
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论,就需要结合一个实际的架构案例来进行分析。在领域驱动设计的讨论范畴,CQRS架构本身就是事件驱动的,因此,
HAL,全称为Hypertext Application Language,它是一种简单的数据格式,它能以一种简单、统一的形式,在API中引入超链接特性,使得API的可发现性(discoverable)更强,并具有自描述的特点。使用了HAL的API会更容易地被第三方开源库所调用,并且使用起来也很方便
何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architecture Pattern),它也不是一种软件开发方法论,所以,是否应该使用领域驱动设计,以及什么时候使用
《在ASP.NET Core中使用Apworks快速开发数据服务》一文中,我介绍了如何使用Apworks框架的数据服务来快速构建用于查询和管理数据模型的RESTful API,通过该文的介绍,你会看到,使用Apworks框架开发数据服务是何等简单快捷,提供的功能也非常多,比如对Hypermedia的
在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。 回顾 《Angular SPA基于
Keycloak是一个功能强大的开源身份和访问管理系统,提供了一整套解决方案,包括用户认证、单点登录(SSO)、身份联合、用户注册、用户管理、角色映射、多因素认证和访问控制等。它广泛应用于企业和云服务,可以简化和统一不同应用程序和服务的安全管理,支持自托管或云部署,适用于需要安全、灵活且易于扩展的用
3月7日,微软发布了Visual Studio 2017 RTM,与之一起发布的还有.NET Core Runtime 1.1.0以及.NET Core SDK 1.0.0,尽管这些并不是最新版,但也已经从preview版本升级到了正式版。所以,在安装Visual Studio 2017时如果启用了
在上文中,我介绍了如何在Ocelot中使用自定义的中间件来修改下游服务的response body。今天,我们再扩展一下设计,让我们自己设计的中间件变得更为通用,使其能够应用在不同的Route上。比如,我们可以设计一个通用的替换response body的中间件,然后将其应用在多个Route上。 O
不少关注我博客的朋友都知道我在2009年左右开发过一个名为Apworks的企业级应用程序开发框架,旨在为分布式企业系统软件开发提供面向领域驱动(DDD)的框架级别的解决方案,并对多种系统架构风格提供支持。这个框架的开发和维护我坚持了很久,一直到2015年,我都一直在不停地重构这个项目。目前这个项目在
好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4ʺSP.NET Identity实现身份认证与授权》,然而如你所见,这样的名字实在是太长了。所以,我不得不缩写“单页面应用”几个字
在前面两篇文章中,我介绍了基于IdentityServer4的一个Identity Service的实现,并且实现了一个Weather API和基于Ocelot的API网关,然后实现了通过Ocelot API网关整合Identity Service做身份认证的API请求。今天,我们进入前端开发,设计
Ocelot是ASP.NET Core下的API网关的一种实现,在微服务架构领域发挥了非常重要的作用。本文不会从整个微服务架构的角度来介绍Ocelot,而是介绍一下最近在学习过程中遇到的一个问题,以及如何使用中间件(Middleware)来解决这样的问题。 问题描述 在上文中,我介绍了一种在Angu
在大数据处理和人工智能时代,数据工厂(Data Factory)无疑是一个非常重要的大数据处理平台。市面上也有成熟的相关产品,比如Azure Data Factory,不仅功能强大,而且依托微软的云计算平台Azure,为大数据处理提供了强大的计算能力,让大数据处理变得更为稳定高效。由于工作中我的项目
在上文中,我们讨论了事件处理器中对象生命周期的问题,在进入新的讨论之前,首先让我们总结一下,我们已经实现了哪些内容。下面的类图描述了我们已经实现的组件及其之间的关系,貌似系统已经变得越来越复杂了。其中绿色的部分就是上文中新实现的部分,包括一个简单的Event Store,一个事件处理器执行上下文的接
在之前《在ASP.NET Core中使用Apworks快速开发数据服务》一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语法解析提供支持。在此非常感激友哥给我的建议,也让我了解了一些Roslyn的知识。使用Roslyn的一个很大的好处
很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构。这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便