C# 9 新特性:代码生成器、编译时反射

前言

今天 .NET 官方博客宣布 C# 9 Source Generators 第一个预览版发布,这是一个用户已经喊了快 5 年特性,今天终于发布了。

简介

Source Generators 顾名思义代码生成器,它允许开发者在代码编译过程中获取查看用户代码并且生成新的 C# 代码参与编译过程,并且可以很好的与代码分析器集成提供 Intellisense、调试信息和报错信息,可以用它来做代码生成,因此也相当于是一个加强版本的编译时反射。

使用 Source Generators,可以做到这些事情:

  • 获取一个 Compilation 对象,这个对象表示了所有正在编译的用户代码,你可以从中获取 AST 和语义模型等信息
  • 可以向 Compilation 对象中插入新的代码,让编译器连同已有的用户代码一起编译

Source Generators 作为编译过程中的一个阶段执行:

编译运行 -> [分析源代码 -> 生成新代码] -> 将生成的新代码添加入编译过程 -> 编译继续。

上述流程中,中括号包括的内容即为 Source Generators 所参与的阶段和能做到的事情。

作用

.NET 明明具备运行时反射和动态 IL 织入功能,那这个 Source Generators 有什么用呢?

编译时反射 - 0 运行时开销

拿 ASP.NET Core 举例,启动一个 ASP.NET Core 应用时,首先会通过运行时反射来发现 Controllers、Services 等的类型定义,然后在请求管道中需要通过运行时反射获取其构造函数信息以便于进行依赖注入。然而运行时反射开销很大,即使缓存了类型签名,对于刚刚启动后的应用也无任何帮助作用,而且不利于做 AOT 编译。

Source Generators 将可以让 ASP.NET Core 所有的类型发现、依赖注入等在编译时就全部完成并编译到最终的程序集当中,最终做到 0 运行时反射使用,不仅利于 AOT 编译,而且运行时 0 开销。

除了上述作用之外,gRPC 等也可以利用此功能在编译时织入代码参与编译,不需要再利用任何的 MSBuild Task 做代码生成啦!

另外,甚至还可以读取 XML、JSON 直接生成 C# 代码参与编译,DTO 编写全自动化都是没问题的。

AOT 编译

Source Generators 的另一个作用是可以帮助消除 AOT 编译优化的主要障碍。

许多框架和库都大量使用反射,例如System.Text.Json、System.Text.RegularExpressions、ASP.NET Core 和 WPF 等等,它们在运行时从用户代码中发现类型。这些非常不利于 AOT 编译优化,因为为了使反射能够正常工作,必须将大量额外甚至可能不需要的类型元数据编译到最终的原生映像当中。

有了 Source Generators 之后,只需要做编译时代码生成便可以避免大部分的运行时反射的使用,让 AOT 编译优化工具能够更好的运行。

例子

INotifyPropertyChanged

写过 WPF 或 UWP 的都知道,在 ViewModel 中为了使属性变更可被发现,需要实现 INotifyPropertyChanged 接口,并且在每一个需要的属性的 setter 处触发属性更改事件:

class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            PropertyChanged?.Invoke(this,new PropertyChangedEventArgs(nameof(Text)));
        }
    }
}

当属性多了之后将会非常繁琐,先前 C# 引入了 CallerMemberName 用于简化属性较多时候的情况:

class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            OnPropertyChanged();
        }
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this,new PropertyChangedEventArgs(propertyName));
    }
}

即,用 CallerMemberName 指示参数,在编译时自动填充调用方的成员名称。

但是还是不方便。

如今有了 Source Generators,我们可以在编译时生成代码做到这一点了。

为了实现 Source Generators,我们需要写个实现了 ISourceGenerator 并且标注了 Generator 的类型。

完整的 Source Generators 代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace MySourceGenerator
{
    [Generator]
    public class AutoNotifyGenerator : ISourceGenerator
    {
        private const string attributeText = @"
using System;
namespace AutoNotify
{
    [AttributeUsage(AttributeTargets.Field,Inherited = false,AllowMultiple = false)]
    sealed class AutoNotifyAttribute : Attribute
    {
        public AutoNotifyAttribute()
        {
        }
        public string PropertyName { get; set; }
    }
}
";

        public void Initialize(InitializationContext context)
        {
            // 注册一个语法接收器,会在每次生成时被创建
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }

        public void Execute(SourceGeneratorContext context)
        {
            // 添加 Attrbite 文本
            context.AddSource("AutoNotifyAttribute",SourceText.From(attributeText,Encoding.UTF8));

            // 获取先前的语法接收器 
            if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
                return;

            // 创建处目标名称的属性
            CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
            Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText,Encoding.UTF8),options));

            // 获取新绑定的 Attribute,并获取INotifyPropertyChanged
            INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
            INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

            // 遍历字段,只保留有 AutoNotify 标注的字段
            List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
            foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
            {
                SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
                foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
                {
                    // 获取字段符号信息,如果有 AutoNotify 标注则保存
                    IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                    if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol,SymbolEqualityComparer.Default)))
                    {
                        fieldSymbols.Add(fieldSymbol);
                    }
                }
            }

            // 按 class 对字段进行分组,并生成代码
            foreach (IGrouping<INamedTypeSymbol,IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
            {
                string classSource = ProcessClass(group.Key,group.ToList(),attributeSymbol,notifySymbol,context);
               context.AddSource($"{group.Key.Name}_autoNotify.cs",SourceText.From(classSource,Encoding.UTF8));
            }
        }

        private string ProcessClass(INamedTypeSymbol classSymbol,List<IFieldSymbol> fields,ISymbol attributeSymbol,ISymbol notifySymbol,SourceGeneratorContext context)
        {
            if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace,SymbolEqualityComparer.Default))
            {
                // TODO: 必须在顶层,产生诊断信息
                return null;
            }

            string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();

            // 开始构建要生成的代码
            StringBuilder source = new StringBuilder($@"
namespace {namespaceName}
{{
    public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
    {{
");

            // 如果类型还没有实现 INotifyPropertyChanged 则添加实现
            if (!classSymbol.Interfaces.Contains(notifySymbol))
            {
                source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
            }

            // 生成属性
            foreach (IFieldSymbol fieldSymbol in fields)
            {
                ProcessField(source,fieldSymbol,attributeSymbol);
            }

            source.Append("} }");
            return source.ToString();
        }

        private void ProcessField(StringBuilder source,IFieldSymbol fieldSymbol,ISymbol attributeSymbol)
        {
            // 获取字段名称
            string fieldName = fieldSymbol.Name;
            ITypeSymbol fieldType = fieldSymbol.Type;

            // 获取 AutoNotify Attribute 和相关的数据
            AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol,SymbolEqualityComparer.Default));
            TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;

            string propertyName = chooseName(fieldName,overridenNameOpt);
            if (propertyName.Length == 0 || propertyName == fieldName)
            {
                //TODO: 无法处理,产生诊断信息
                return;
            }

            source.Append($@"
public {fieldType} {propertyName} 
{{
    get 
    {{
        return this.{fieldName};
    }}
    set
    {{
        this.{fieldName} = value;
        this.PropertyChanged?.Invoke(this,new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
    }}
}}
");

            string chooseName(string fieldName,TypedConstant overridenNameOpt)
            {
                if (!overridenNameOpt.IsNull)
                {
                    return overridenNameOpt.Value.ToString();
                }

                fieldName = fieldName.TrimStart('_');
                if (fieldName.Length == 0)
                    return string.Empty;

                if (fieldName.Length == 1)
                    return fieldName.ToUpper();

                return fieldName.Substring(0,1).ToUpper() + fieldName.Substring(1);
            }

        }

        // 语法接收器,将在每次生成代码时被按需创建
        class SyntaxReceiver : ISyntaxReceiver
        {
            public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();

            // 编译中在访问每个语法节点时被调用,我们可以检查节点并保存任何对生成有用的信息
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                // 将具有至少一个 Attribute 的任何字段作为候选
                if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
                    && fieldDeclarationSyntax.AttributeLists.Count > 0)
                {
                    CandidateFields.Add(fieldDeclarationSyntax);
                }
            }
        }
    }
}

有了上述代码生成器之后,以后我们只需要这样写 ViewModel 就会自动生成通知接口的事件触发调用:

public partial class MyViewModel
{
    [AutoNotify]
    private string _text = "private field text";

    [AutoNotify(PropertyName = "Count")]
    private int _amount = 5;
}

上述代码将会在编译时自动生成以下代码参与编译:

public partial class MyViewModel : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    public string Text
    {
        get 
        {
            return this._text;
        }
        set
        {
            this._text = value;
            this.PropertyChanged?.Invoke(this,new System.ComponentModel.PropertyChangedEventArgs(nameof(Text)));
        }
    }

    public int Count
    {
        get 
        {
            return this._amount;
        }
        set
        {
            this._amount = value;
            this.PropertyChanged?.Invoke(this,new System.ComponentModel.PropertyChangedEventArgs(nameof(Count)));
        }
    }
}

非常方便!

使用时,将 Source Generators 部分作为一个独立的 .NET Standard 2.0 程序集(暂时不支持 2.1),用以下方式引入到你的项目即可:

<ItemGroup>
  <Analyzer Include="..\MySourceGenerator\bin\$(Configuration)\netstandard2.0\MySourceGenerator.dll" />
</ItemGroup>

<ItemGroup>
  <ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj" />
</ItemGroup>

注意需要最新的 .NET 5 preview(写文章时还在 artifacts 里没正式 release),并指定语言版本为 preview

<PropertyGroup>
  <LangVersion>preview</LangVersion>
</PropertyGroup>

另外,Source Generators 需要引入两个 nuget 包:

<ItemGroup>
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.final" PrivateAssets="all" />
  <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
</ItemGroup>

限制

Source Generators 仅能用于访问和生成代码,但是不能修改已有代码,这有一定原因是出于安全考量。

文档

Source Generators 处于早期预览阶段,docs.microsoft.com 上暂时没有相关文档,关于它的文档请访问在 roslyn 仓库中的文档:

设计文档

使用文档

后记

目前 Source Generators 仍处于非常早期的预览阶段,API 后期还可能会有很大的改动,因此现阶段不要用于生产。

另外,关于与 IDE 的集成、诊断信息、断点调试信息等的开发也在进行中,请期待后续的 preview 版本吧。

原文地址: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的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便