专栏名称: 微软中国MSDN
微软中国MSDN开发社区官方微信。
目录
相关文章推荐
BioArt  ·  Nat Immunol | ... ·  18 小时前  
BioArt  ·  Immunity | ... ·  2 天前  
生信宝典  ·  生物从业者必看!我问DeepSeek“如何跻 ... ·  4 天前  
生物学霸  ·  上海交大 2025 首篇 Cell:解决 ... ·  5 天前  
51好读  ›  专栏  ›  微软中国MSDN

使用 Roslyn 和 .NET Core 生成跨平台代码

微软中国MSDN  · 公众号  ·  · 2017-07-20 10:18

正文

.NET Core 是一组开放源代码的模块式跨平台工具,可方便你生成在 Windows、Linux 和 macOS 上运行的下一代 .NET 应用程序 (microsoft.com/net/core/platform)。它还可以安装在 Windows 10 上以供 IoT 分发,并可在 Raspberry PI 等设备上运行。作为功能强大的平台,.NET Core 包含运行时、库和编译器,完全支持 C#、F# 和 Visual Basic 等语言。也就是说,不仅可以在 Windows 上编写 C# 代码,还可以在其他 OS 上编写,因为 .NET 编译器平台 (github.com/dotnet/roslyn) 亦称为“项目 Roslyn”,提供包含丰富代码分析 API 的开放源代码跨平台编译器。重要意义在于,可以使用 Roslyn API 在不同 OS 上执行许多与代码相关的操作,如代码分析、代码生成和编译。本文逐一介绍了在 .NET Core 上创建使用 Roslyn API 的 C# 项目所需执行的步骤,并介绍了一些有趣的代码生成和编译方案。此外,还介绍了一些基本反射技巧,用于在 .NET Core 上调用和运行使用 Roslyn 编译的代码。如果对 Roslyn 不熟悉,不妨先阅读下列文章:


  • “使用 Roslyn 编写 API 的实时代码分析器”(msdn.com/magazine/dn879356)

  • “将代码修补程序添加到 Roslyn 分析器中”(msdn.com/magazine/dn904670)

  • “使用 Roslyn 最大限度地提升‘模型-视图-视图模型’体验”(msdn.com/magazine/mt703435)。


安装 .NET Core SDK


第一步是安装 .NET Core 和 SDK。如果使用的是 Windows 并且已安装 Visual Studio 2017,那么 .NET Core 已包含在内,但前提是在安装期间在 Visual Studio 安装程序中选择了 .NET Core 跨平台开发工作负载。否则,只需打开 Visual Studio 安装程序,然后选择此工作负载并单击“修改”即可。如果使用的是 Windows 但不依赖 Visual Studio 2017,或使用的是 Linux 或 macOS,可以手动安装 .NET Core,并将 Visual Studio Code 用作开发环境 (code.visualstudio.com)。我将在本文中介绍后一种情况,因为 Visual Studio Code 本身就是跨平台产品;所以也是 .NET Core 的绝佳伴侣。此外,请务必安装适用于 Visual Studio Code 的 C# 扩展 (bit.ly/29b1Ppl)。由于 .NET Core 的安装步骤因 OS 而异,因此,请按 bit.ly/2mJArWx 上的说明操作。请务必安装最新版本。值得一提的是,.NET Core 的最新版本不再支持 project.json 文件格式,改为支持 MSBuild 内更通用的 .csproj 文件格式。


搭建 .NET Core C# 应用程序的基架


借助 .NET Core,可以创建控制台应用程序和 Web 应用程序。对于 Web 应用程序,在 .NET Core 按照路线图不断发展的过程中,Microsoft 会发布除 ASP.NET Core 模板以外的更多模板。由于 Visual Studio Code 是轻型编辑器,因此不会像 Visual Studio 一样提供项目模板。也就是说,需要在与应用程序同名的文件夹内通过命令行创建应用程序。下面的示例是以适用于 Windows 的操作说明为依据,但相同的概念也适用于 macOS 和 Linux。首先,打开命令提示符,然后转到磁盘上的文件夹。例如,假设文件夹名为 C:\Apps。请转到此文件夹,然后使用下面的命令新建子文件夹 RoslynCore:


> cd C:\Apps
> md RoslynCore
> cd RoslynCore


因此,RoslynCore 就是本文中介绍的示例应用程序的名称。这是一个控制台应用程序,不仅非常适用于说明用途,并且简化了 Roslyn 编码方式。还可以对 ASP.NET Core Web 应用程序使用相同的技术。若要为控制台应用程序新建空项目,只需键入下面的命令行即可:


> dotnet new console


这样一来,.NET Core 可以为 RoslynCore 控制台应用程序搭建 C# 项目基架。现在可以使用 Visual Studio Code 打开项目的文件夹。最简单的方法是键入下面的命令行:


> code .


当然,也可以从 Windows“开始”菜单打开 Visual Studio Code,然后手动打开项目文件夹。进入任意 C# 代码文件后,文件便会请求获取生成一些必需资产并还原一些 NuGet 包的权限(见图 1)。


图 1:Visual Studio Code 需要更新项目


下一步是添加使用 Roslyn 所需的 NuGet 包。


添加 Roslyn NuGet 包


正如你可能知道的,可以通过从 Microsoft.CodeAnalysis 层次结构安装一些 NuGet 包来使用 Roslyn API。安装这些包之前,请务必说明 Roslyn API 在 .NET Core 系统中的作用。如果曾在 .NET Framework 上使用过 Roslyn,可能会习惯于使用全套 Roslyn API。.NET Core 依赖 .NET Standard 库。也就是说,只能在 .NET Core 中使用支持 .NET Standard 的 Roslyn 库。截至本文撰写之时,大多数 Roslyn API 已对 .NET Core 可用,包括(但不限于)编译器 API(包含发出和诊断 API)和工作区 API。只有少数 API 尚不可移植,但由于 Microsoft 对 Roslyn 和 .NET Core 的投资巨大,因此有望在今后推出的版本中实现完整的 .NET Standard 兼容性。在 .NET Core 上运行的跨平台应用程序真实示例是 OmniSharp (bit.ly/2mpcZeF),其使用 Roslyn API 强力驱动代码编辑器的大部分功能,如完成列表和语法突出显示。


本文将介绍如何使用编译器和诊断 API。为此,需要将 Microsoft.CodeAnalysis.CSharp NuGet 包添加到项目中。借助基于 MSBuild 的新 .NET Core 项目系统,NuGet 包列表现已包含在 .csproj 项目文件中。在 Visual Studio 2017 中,可以使用 NuGet 的客户端 UI 来下载、安装和管理包,但 Visual Studio Code 中却没有等效选项。幸运的是,可以直接打开 .csproj 文件并查找包含 元素的 节点,每个元素分别表示一个必需的 NuGet 包。修改节点,如下所示:


<ItemGroup>  ...
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp"    Version="2.0.0 " />  <PackageReference Include="System.Runtime.Loader" Version="4.3.0" />ItemGroup>


请注意,添加对 Microsoft.CodeAnalysis.C­Sharp 包的引用可以访问 C# 编译器的 API,System.Runtime.Loader 包是执行反射所必需,将在本文后面用到。


保存更改后,Visual Studio Code 会检测缺少的 NuGet 包,并提议还原它们。


代码分析: 分析源代码文本和生成语法节点


第一个示例与代码分析有关,展示了如何分析源代码文本和生成新语法节点。例如,假设你有以下简单业务对象,并且希望根据此对象生成视图模型类:


namespace Models
{
  public class Item
  {
    public string ItemName { get; set }
  }
}


此业务对象的文本可能来自不同的源,如 C# 代码文件、代码中的字符串或用户输入。借助代码分析 API,可以分析源代码文本,并生成编译器可以理解和控制的新语法节点。例如,假设代码如图 2 中所示,用于分析包含类定义的字符串,获取其对应的语法节点,并调用新的静态方法以通过语法节点生成视图模型。


图 2:分析源代码和检索语法节点


using System;using RoslynCore;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp;class Program
{
  static void Main(string[] args)
  {
    GenerateSampleViewModel();
  }
  static void GenerateSampleViewModel()
  {
    const string models = @"namespace Models
{
  public class Item
  {
    public string ItemName { get; set }
  }
}
";
    var node = CSharpSyntaxTree.ParseText(models).GetRoot();
    var viewModel = ViewModelGeneration.GenerateViewModel(node);
    if(viewModel!=null)
      Console.WriteLine(viewModel.ToFullString());
    Console.ReadLine();
  }
}


由于将在 ViewModelGeneration 静态类中定义 GenerateViewModel 方法,因此,请向项目添加新的 ViewModelGeneration.cs 文件。此方法将在输入语法节点(为了方便本文演示,即 ClassDeclarationSyntax 对象的第一个实例)中查找类定义,然后根据类名和类成员构造新的视图模型。图 3 展示了此过程。


图 3:生成新的语法节点


using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis;using System.Linq;using Microsoft.CodeAnalysis.CSharp;namespace RoslynCore
{
  public static class ViewModelGeneration
  {
    public static SyntaxNode GenerateViewModel(SyntaxNode node)
    {
      // Find the first class in the syntax node      var classNode = node.DescendantNodes()
       .OfType().FirstOrDefault();
      if(classNode!=null)
      {
        // Get the name of the model class        string modelClassName = classNode.Identifier.Text;
        // The name of the ViewModel class        string viewModelClassName = $"{modelClassName}ViewModel";
        // Only for demo purposes, pluralizing an object is done by        // simply adding the "s" letter. Consider proper algorithms        string newImplementation =
          $@"public class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection _{modelClassName}s;
public ObservableCollection {modelClassName}s
{{
  get {{ return _{modelClassName}s; }}
  set
  {{
    _{modelClassName}s = value;
    OnPropertyChanged(nameof({modelClassName}s));
  }}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
          var newClassNode =
            CSharpSyntaxTree.ParseText(newImplementation).GetRoot()
            .DescendantNodes().OfType()
            .FirstOrDefault();
          // Retrieve the parent namespace declaration          if(!(classNode.Parent is NamespaceDeclarationSyntax)) return null;
          var parentNamespace = (NamespaceDeclarationSyntax)classNode.Parent;
          // Add the new class to the namespace and adjust the white spaces          var newParentNamespace =
            parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
          return newParentNamespace;
        }
      }
      else      {
        return null;
      }
    }
  }
}


图 3 中的第一部分代码展示了如何将视图模型表示为字符串(使用字符串内插以轻松根据原始类名指定对象和成员名称)。在此示例方案中,只需向对象/成员名称添加“s”即可生成复数;在真实的代码中,应使用更为明确的复数算法。


在图 3 的第二部分中,代码调用 CSharpSyntaxTree.ParseText,以在 SyntaxTree 中分析源代码文本。将调用 GetRoot 来检索新树的 SyntaxNode;使用 DescendantNodes().OfType(),代码只会检索表示类的语法节点,同时仅使用 FirstOrDefault 选择第一个类。检索语法节点中的第一个类就足以获取其中插入了新视图模型类的父级命名空间。可以通过将 ClassDeclarationSyntax 的 Parent 属性转换成 NamespaceDeclarationSyntax 对象来获取命名空间。由于可以将一个类嵌套到另一个类中,因此代码首先会验证 Parent 是否属于 NamespaceDeclarationSyntax 类型,从而检查是否存在这种可能性。最后一部分代码将视图模型类的新语法节点添加到父级命名空间中,同时将此结果作为语法节点返回。如果现在按 F5,则会在调试控制台中看到生成的代码结果,如图 4 中所示。


图 4:视图模型类已正确生成


生成的视图模型类是可与 C# 编译器结合使用的 SyntaxNode,因此可以进一步控制它,并能通过分析它来获取诊断信息,同时还可以使用发出 API 将它编译到程序集中,并通过反射加以利用。


获取诊断信息


无论源代码文本的源是字符串、文件还是用户输入,均可使用诊断 API 检索代码问题(如错误和警告)的相关诊断信息。请注意,使用诊断 API,不仅可以检索错误和警告,还可以编写分析器和重构代码。继续以前面的示例为例,最好先检查原始源代码文本中是否有语法错误,然后再生成视图模型类。为此,可以调用 SyntaxNode.GetDiagnostics 方法,该方法返回 IEnumerable 对象(若有)。有关 ViewModelGeneration 类的扩展版,请参阅图 5。代码会检查 GetDiagnostics 的调用结果是否包含任何诊断信息。如果不包含,代码会生成视图模型类。如果调用结果包含一系列诊断信息,代码会显示所有诊断信息并返回 null。诊断类可提供所有代码问题的详细信息。例如,Id 属性返回诊断 ID;GetMessage 方法返回完整的诊断消息;GetLineSpan 返回在源代码中的诊断位置;Severity 属性返回诊断严重级别,如 Error、Warning 或 Information。


图 5:使用诊断 API 检查是否存在代码问题


using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis;using System.Linq;using Microsoft.CodeAnalysis.CSharp;using System;namespace RoslynCore
{
  public static class ViewModelGeneration
  {
    public static SyntaxNode GenerateViewModel(SyntaxNode node)
    {
      // Find the first class in the syntax node      var classNode =
        node.DescendantNodes().OfType().FirstOrDefault();
      if(classNode!=null)
      {
        var codeIssues = node.GetDiagnostics();
        if(!codeIssues.Any())
        {
          // Get the name of the model class          var modelClassName = classNode.Identifier.Text;
          // The name of the ViewModel class          var viewModelClassName = $"{modelClassName}ViewModel";
          // Only for demo purposes, pluralizing an object is done by          // simply adding the "s" letter. Consider proper algorithms          string newImplementation =
            $@"public class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection _{modelClassName}s;
public ObservableCollection {modelClassName}s
{{
  get {{ return _{modelClassName}s; }}
  set
  {{
    _{modelClassName}s = value;
    OnPropertyChanged(nameof({modelClassName}s));
  }}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
            var newClassNode =
              SyntaxFactory.ParseSyntaxTree(newImplementation).GetRoot()
              .DescendantNodes().OfType()
              .FirstOrDefault();
            // Retrieve the parent namespace declaration            if(!(classNode.Parent is NamespaceDeclarationSyntax)) return null;
            var parentNamespace = (NamespaceDeclarationSyntax)classNode.Parent;
            // Add the new class to the namespace            var newParentNamespace =
              parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
            return newParentNamespace;
          }
          else          {
            foreach(Diagnostic codeIssue in codeIssues)
          {
            string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()},
              Location: {codeIssue.Location.GetLineSpan()},
              Severity: {codeIssue.Severity}";
            Console.WriteLine(issue);
          }
          return null;
        }
      }
      else      {
        return null;
      }
    }
  }
}


现在,如果故意将一些错误引入模型变量(位于 Program.cs 中的 GenerateSampleViewModel 方法内)中包含的源代码文本,然后运行应用程序,就可以看到 C# 编译器返回所有代码问题的完整详情。图 6 展示了相关示例。


图 6:使用诊断 API 检测代码问题


值得注意的是,即使 C# 编译器包含诊断信息,仍会生成语法树。这样不仅可以完全忠实于源代码文本,还可以让开发者视需要使用新语法节点修复这些问题。


执行代码:发出 API


使用发出 API,可以将源代码编译到程序集中。然后,可以使用反射来调用并执行代码。下一个示例结合了代码生成、发出和诊断检测。将新文件 EmitDemo.cs 添加到项目中,然后假设代码列表如图 7 中所示。如你所见,SyntaxTree 通过定义 Helper 类的源代码文本生成,此类包含用于计算圆面积的静态方法。我们的目标是通过此类生成 .dll,然后将半径作为自变量传递,从而执行 CalculateCircleArea 方法。





请到「今天看啥」查看全文