2024年2月

在对
应用模型的基本构建方式
具有大致的了解之后,我们来系统地认识一下描述应用模型的ApplicationModel类型。对于一个描述MVC应用模型的ApplicationModel对象来说,它承载的元数据绝大部分是由默认注册的DefaultApplicationModelProvider对象提供的,在接下来针对ApplicationModel及其相关类型(ControllerModel、ActionModel和ParameterModel等)的介绍中,我们还会着重介绍DefaultApplicationModelProvider对象采用怎样的方式提取并设置这些元数据。

一、几个重要的接口
二、ApplicationModel
三、自定义IApplicationModelProvider
四、自定义IApplicationModelConvention

一、几个重要的接口

在正式介绍ApplicationModel及其相关类型的定义之前,我们先来认识如下几个重要的接口,针对不同模型节点的类型分别实现了这些接口的一个或者多个。认识这些接口有助于我们更好地理解应用模型的层次结构以及每种模型节点的用途。

IPropertyModel

为了让应用模型的构建方式具有更好的扩展性,ApplicationModel类型以及描述其他描述模型节点的类型(ControllerModel、ActionModel和ParameterModel等)都提供了一个字典类型的Properties属性,自定义的IApplicationModelProvider实现类型以及各种形式的约定类型都可以将任意属性存储到这个字典中。这个Properties属性是对IPropertyModel接口的实现。

public interface IPropertyModel
{
    IDictionary<object, object> Properties { get; }
}

ICommonModel

描述MVC应用模型的ApplicationModel对象由描述所有Controller类型的ControllerModel对象组成,而ControllerModel对象则通过描述其Action方法和属性的ActionModel和PropertyModel对下组成。这三种分别描述类型、方法和属性的模型节点本质上都是对一个MemberInfo对象的封装,描述对应节点的元数据主要由标注在它们上面的特性来提供,所以标注的特性成了这些模型节点重要的元素。除此之外,这些模型节点还应该具有一个唯一的命名。综上这些元素被统一定义在如下这个ICommonModel接口中,该接口派生于IPropertyModel接口。

public interface ICommonModel : IPropertyModel
{
    MemberInfo 		        MemberInfo { get; }
    string 			Name { get; }
    IReadOnlyList<object> 	Attributes { get; }
}

IFilterModel

针对MVC应用的请求总是被路由到某个匹配的Action,针对请求的处理体现在对目标Action的执行。这里所谓的“执行Action”不仅仅包括针对目标方法的执行,还需要执行应用在该Action上的一系列过滤器。过滤器使我们可以很容易地“干预”针对目标Action的执行流程,它们可以直接注册到Action方法上,也可以注册到Controller类型,甚至可以在应用范围进行全局注册,所以MVC框架为这些包含过滤器注册的模型节点(ApplicationModel、ControllerModel和ActionModel)定义了如下这个IFilterModel接口。

public interface IFilterModel
{
    IList<IFilterMetadata> 	Filters { get; }
}

如上面的代码片段所示,IFilterModel接口定义了唯一的Filters属性返回一个IFilterMetadata对象的列表,IFilterMetadata接口是对过滤器元数据的描述。

IApiExplorerModel

当我们在面向Controller的MVC编程模型上开发API的时候,我们希望应用能够提供在API层面的元数据。这些面向开发人员的元数据告诉我们当前应用提供了怎样的API终结点,每个终结点的路径是什么、支持何种HTTP方法、需要怎样的输入、输入和响应具有怎样的结构等。MVC框架专门提供了一个名为“ApiExplorer”的模块来完成针对API元数据的导出任务。我们可以利用API元数据自动生成在线开发文档(比如著名的Swagger就是这么干的),也可以针对不同的语言生成调用API的客户端代码。

如果说ActionDescriptor对象是Action面向运行时的描述,那么Action面向API的描述就体现为一个ApiDescription对象。我们可以在Controller类型或者具体的Action方法上标注实现IApiDescriptionGroupNameProvider接口的特性对ApiDescription对象进行分组(设置GroupName属性),也可以标注实现了IApiDescriptionVisibilityProvider接口的特性控制对应API的可见性(如果IgnoreApi属性设置为True,ApiExplorer将不会生成对应的ApiDescription对象)。如下所示的ApiExplorerSettingsAttribute特性是对这两个接口的实现。IApiExplorerModel接口定义的ApiExplorer属性返回的ApiExplorerModel对象于此对应。

public interface IApiDescriptionGroupNameProvider
{
    string GroupName { get; }
}
public interface IApiDescriptionVisibilityProvider
{
    bool IgnoreApi { get; }
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited=true)]
public class ApiExplorerSettingsAttribute : Attribute, IApiDescriptionGroupNameProvider, IApiDescriptionVisibilityProvider
{
    public string 	GroupName { get; set; }
    public bool 	IgnoreApi { get; set; }
}

针对API分组和可见性的设置体现在面向应用(ApplicationModel)、Controller类型(ControllerModel)和Action方法(ActionModel)的模型节点上,所以它们都会实现如下这个IApiExplorerModel,两个设置体现在ApiExplorer返回的ApiExplorerModel对象上。

public interface IApiExplorerModel
{
    ApiExplorerModel 	ApiExplorer { get; set; }
}
public class ApiExplorerModel
{
    public bool? 	IsVisible { get; set; }
    public string 	GroupName { get; set; }
}

IBindingModel

MVC框架采用“模型绑定”的机制来绑定目标Action方法的参数列表和定义在Controller类型中相应的属性,所以描述参数的ParameterModel对象和描述Controller属性的PropertyModel对象需要提供服务于模型绑定的元数据。MVC为这两种模型节点定义了如下这个IBindingModel接口,它利用BindingInfo属性返回的BindingInfo对象提供绑定元数据。

public interface IBindingModel
{
    BindingInfo BindingInfo { get; set; }
}

二、ApplicationModel

如下所示的是描述应用模型的ApplicationModel类型的定义,它的核心是Controllers属性返回的一组ControllerModel对象。该类型实现了IPropertyModel、IFilterModel和IApiExplorerModel接口,DefaultApplicationModelProvider对象只会提取在应用级别全局注册的过滤器,并生成相应的IFilterMetadata对象添加到Filters属性中。

public class ApplicationModel : IPropertyModel, IFilterModel, IApiExplorerModel
{
    public IList<ControllerModel> 		Controllers { get; }
    public IList<IFilterMetadata> 		Filters { get; }
    public ApiExplorerModel 			ApiExplorer { get; set; }
    public IDictionary<object, object> 	Properties { get; }
}

在了解了DefaultApplicationModelProvider对象针对应用模型的大致构建规则之后,我们利用一个简单的实例演示来对此做一个验证。由于构建应用模型的ApplicationModelFactory是一个内部类型,所以我们在作为演示程序的MVC应用中定义了如下这个ApplicationModelProducer类型。如代码片段所示,它会利用注入的IServiceProvider对象来提供ApplicationModelFactory对象。在定义的Create方法中,ApplicationModelProducer根据反射的方式调用ApplicationModelFactory的CreateApplicationModel方法根据指定的Controller类型创建出描述应用模型的ApplicationModel对象。

public class ApplicationModelProducer
{
    private readonly Func<Type[], ApplicationModel> _factory;
    public ApplicationModelProducer(IServiceProvider serviceProvider)
    {
        var assemblyName = new AssemblyName("Microsoft.AspNetCore.Mvc.Core");
        var assemly = Assembly.Load(assemblyName);
        var typeName ="Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModelFactory";
        var factoryType = assemly.GetTypes().Single(it => it.FullName ==typeName);
        var factory = serviceProvider.GetService(factoryType);
        var method = factoryType.GetMethod("CreateApplicationModel");
        _factory = controlerTypes =>
        {
            var typeInfos = controlerTypes.Select(it => it.GetTypeInfo());
            return (ApplicationModel)method.Invoke(factory, new object[] { typeInfos });
        };
    }
    public ApplicationModel Create(params Type[] controllerTypes) => _factory(controllerTypes);
}

为了验证针对全局过滤器的注册,我们定义了如下这个FoobarAttribute特性。如代码片段所示,FoobarAttribute派生于ActionFilterAttribute特性。从标注的AttributeUsage特性来看,多个FoobarAttribute特性可以同时标注到Controller类型或者Action方法上。

[AttributeUsage(AttributeTargets.Class| AttributeTargets.Method, AllowMultiple = true)]
public class FoobarAttribute : ActionFilterAttribute
{
}

在如下所示的应用承载程序中,我们调用IWebHostBuilder接口的ConfigureServices方法添加了针对ApplicationModelProducer类型的服务注册。在调用AddControllersWithViews扩展方法的过程中,我们创建了一个FoobarAttribute对象并将它添加到MvcOptions对象的Filters属性中,意味着我们在应用范围内全局注册了这个FoobarAttribute过滤器。

class Program
{
    static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                .ConfigureServices(services => services
                    .AddSingleton<ApplicationModelProducer>()
                    .AddRouting()
                    .AddControllersWithViews(options=>options.Filters.Add(new FoobarAttribute())))
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(endpoints => endpoints.MapControllers())))
            .Build()
            .Run();
    }
}

我们在定义了如下三个用于测试的Controller类型(FooController、BarController和BazController)。我们将用于呈现主页的Action方法定义在HomeController类型中。简单起见,我们直接将ApplicationModelProducer对象注入到Index方法中,并通过标注的FromServicesAttribute特性指示利用注册的服务来绑定该参数。Index方法利用这个ApplicationModelProducer对象构建出根据三个测试Controller类型创建的ApplicationModel对象,并将其作为Model呈现在默认View中。

public class FooController
{
    public void Index() => throw new NotImplementedException();
}
public class BarController
{
    public void Index() => throw new NotImplementedException();
}
public class BazController
{
    public void Index() => throw new NotImplementedException();
}

public class HomeController: Controller
{
    [HttpGet("/")]
    public IActionResult Index([FromServices]ApplicationModelProducer producer)
    {
        var applicationModel = producer.Create(typeof(FooController), typeof(BarController), typeof(BazController));
        return View(applicationModel);
    }
}

如下所示的就是Action方法Index对应View的定义。如代码片段所示,这是一个Model类型为ApplicationModel的强类型View。在这个View中,我们将构成ApplicationModel对象的所有ControllerModel的名称、过滤器的类型以及ApiExplorer相关的两个对象以表格的形式呈现出来。

@model Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModel
@{
    var controllers = Model.Controllers;
    var filters = Model.Filters;
}
<html>
<head>
    <title>Application</title>
</head>
<body>
    <table border="1" cellpadding="0" cellspacing="0">
        <tr>
            <td rowspan="@controllers.Count">Controllers</td>
            <td>@controllers[0].ControllerName</td>
        </tr>
        @for (int index = 1; index < controllers.Count; index++)
        {
            <tr><td>@controllers[index].ControllerName</td></tr>
        }
        <tr>
            <td rowspan="@filters.Count">Filters</td>
            <td>@filters[0].GetType().Name</td>
        </tr>
        @for (int index = 1; index < filters.Count; index++)
        {
            <tr><td>@filters[index].GetType().Name</td></tr>
        }
        <tr>
            <td rowspan="2">ApiExplorer</td>
            <td>IsVisible = @Model.ApiExplorer.IsVisible </td>
        </tr>
        <tr><td>GroupName = @Model.ApiExplorer.GroupName </td></tr>

    </table>
</body>
</html>

演示程序启动之后,如果利用浏览器访问其根路径,我们会得到如图1所示的输出结果。我们可以从输出结果中看到组成ApplicationModel对象的三个Controller的名称。ApplicationModel对象的Filters属性列表中包含三个全局过滤器,除了我们显式注册的FoobarAttribute特性之外,还具有一个在不支持提供媒体类型情况下对请求进行处理的UnsupportedContentTypeFilter过滤器,它是在AddMvcCore扩展方法中注册的。另一个用来保存临时数据的SaveTempDataAttribute特性则是通过AddControllersWithViews扩展方法注册的。默认下,ApplicationModel对象的ApiExplorer属性返回的ApiExplorerModel对象并没有做相应的设置。

clip_image002

图1 应用模型的默认构建规则

三、自定义IApplicationModelProvider

由于MVC框架针对目标Action的处理行为完全由描述该Action的ActionDescriptor对象决定,而最初的元数据则来源于应用模型,所以有时候一些针对请求流程的控制需要间接地利用针对应用模型的定制来实现。通过前面的内容,我们知道应用模型的定制可以通过注册自定义的IApplicationModelProvider实现类型,接下来我们就来做相应的演示。

通过上面演示的势力可以看出,默认情况下构建出来的ApplicationModel对象的ApiExplorer属性并没有作具体的设置,接下来我们将此设置实现在一个IApplicationModelProvider实现类型中。具体来说,我们希望在MVC应用所在项目的程序集上标注如下这个ApiExplorerAttribute特性来设置与ApiExplorer相关的两个属性。我们将针对该特性的标注按照如下的方式定义在Program.cs中,该特性将GroupName设置为 “Foobar” 。

[AttributeUsage(AttributeTargets.Assembly)]
public class ApiExplorerAttribute:Attribute
{
    public bool 	IsVisible => true;
    public string 	GroupName { get; set; }
}

[assembly: ApiExplorer(GroupName = "Foobar")]

针对ApiExplorerAttribute特性的解析以及基于该特性设置对应用模型的定制实现在如下这个ApiExplorerApplicationModelProvider类型中。如代码片段所示,该类型的构造函数中注入了代表承载环境的IHostEnvironment对象,我们利用它得到当前应用的名称,并将它作为程序集名称得到标注的ApiExplorerAttribute特性,进而得到基于ApiExplorer的设置。在实现的OnProvidersExecuting方法中,我们将相关设置应用到ApplicationModel对象上。

public class ApiExplorerApplicationModelProvider : IApplicationModelProvider
{
    private readonly bool? 	_isVisible;
    private readonly string 	_groupName;

    public int Order => -1000;

    public ApiExplorerApplicationModelProvider(IHostEnvironment hostEnvironment)
    {
        var assembly = Assembly.Load(new AssemblyName(hostEnvironment.ApplicationName));
        var attribute = assembly.GetCustomAttribute<ApiExplorerAttribute>();
        _isVisible = attribute?.IsVisible;
        _groupName = attribute?.GroupName;
    }
    public void OnProvidersExecuted(ApplicationModelProviderContext context) { }

    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    {
        context.Result.ApiExplorer.GroupName??= _groupName;
        context.Result.ApiExplorer.IsVisible ??= _isVisible;
    }
}

为了上面这个自定义的ApiExplorerApplicationModelProvider类型,我们对应用承载程序做了如下的改动。如代码片段所示,我们只需要调用IWebHostBuilder的ConfigureServices方法将该类型作为服务注册到依赖注入框架中即可。

class Program
{
    static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                .ConfigureServices(services => services
                    .AddSingleton<IApplicationModelProvider, ApiExplorerApplicationModelProvider>()
                    .AddSingleton<ApplicationModelProducer>()
                    .AddRouting()
                    .AddControllersWithViews(options=>options.Filters.Add(new FoobarAttribute())))
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(endpoints => endpoints.MapControllers())))
            .Build()
            .Run();
    }
}

改动后的演示程序启动后,我们利用浏览器访问应用的主页,可以得到如图2所示的输出结果。从浏览器上的输出结果可以看出,对于ApplicationModelFactory最终构建的ApplicationModel对象来说,它的
ApiExplorer
属性这次得到了相应的设置。

clip_image004

图2 注册自定义IApplicationModelProvider实现类型定制应用模型

四、自定义IApplicationModelConvention

除了利用自定义的IApplicationModelProvider实现类型对应用模型进行定制之外,我们还可以注册各种类型的约定达到相同的目的。上面演示的针对ApiExplorer相关设置的定制完全可以利用如下这个ApiExplorerConvention类型来完成。如代码片段所示,ApiExplorerConvention类型实现了IApplicationModelConvention接口,我们直接在构造函数中指定ApiExplorer相关的两个属性,并在实现的Apply方法中将其应用到表示应用模型的ApplicationModel对象上。

public class ApiExplorerConvention : IApplicationModelConvention
{
    private readonly bool? 	_isVisible;
    private readonly string 	_groupName;

    public ApiExplorerConvention(bool? isVisible, string groupName)
    {
        _isVisible = isVisible;
        _groupName = groupName;
    }

    public void Apply(ApplicationModel application)
    {
        application.ApiExplorer.IsVisible ??= _isVisible;
        application.ApiExplorer.GroupName ??= _groupName;
    }
}

用于定制应用模型的各种约定需要注册到代表MVC应用配置选项的MvcOptions对象上,所以我们需要对应用承载程序作相应的修改。如下面你代码片段所示,在调用IServiceCollection接口的AddControllersWithViews扩展方法是,我们创建了一个ApiExplorerConvention对象,并将其添加到作为配置选项的MvcOptions对象的Conventions属性上。改动后的演示程序启动后,我们利用浏览器访问应用的主页依然可以得到如图2所示的输出结果。

class Program
{
    static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                .ConfigureServices(services => services
                    .AddSingleton<ApplicationModelProducer>()
                    .AddRouting()
                    .AddControllersWithViews(options=>
                    {
                        options.Filters.Add(new GlobalFilter());
                        options.Conventions.Add(new ApiExplorerConvention(true, "Foobar"));
                    }))
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(endpoints => endpoints.MapControllers())))
            .Build()
            .Run();
    }
}

ASP.NET Core MVC应用模型的构建[1]: 应用的蓝图
ASP.NET Core MVC应用模型的构建[2]: 应用模型
ASP.NET Core MVC应用模型的构建[3]: Controller模型
ASP.NET Core MVC应用模型的构建[4]: Action模型

前言

在日常工作中,我们经常需要在Excel中使用公式对表中数据进行计算(求和、求差和求均值等)和分析,从而实现对数据的分类,通常情况下,当数据量较少或场景变化单一的情况下,使用公式可以满足用户的要求,但当数据量较大或者场景变化复杂的情况下,使用公式也无法满足用户的需求的情况。这个时候就可以用编码的方式来解决,以下面的背景需求为例,小编将为大家介绍如何使用葡萄城公司基于 .NET 和 .NET Core 平台的服务端高性能表格组件组件GrapeCity Documents for Excel (以下简称GcExcel)解析Excel中的现有公式并根据需求对其进行修改。

背景需求

下图是一张销售数据表,左侧显示原始销售数据,包括销售代表的姓名、地区、产品和销售数量,右侧显示了从原始数据中提取的特定的销售代表对应的销售分析结果,以及每个产品区域组合的月度销售目标进度。目标进度的标准如下:

  • 低于 2500:低于目标
  • 超过 3000:达到目标
  • 超过 5000:高于目标

一般情况下,我们使用Excel中的 IF、ISNUMBER 和 FILTER 函数就可以实现将左侧的销售原始数据转化为右侧的销售分析结果,如下所示:

=IF(ISNUMBER(FILTER(A2:D19,A2:A19="Fritz")),IFS(FILTER(A2:D19,A2:A19="Fritz")>5000,"Above Target",FILTER(A2:D19,A2:A19="Fritz")>3000,"On Target",FILTER(A2:D19,A2:A19="Fritz")<2500,"Below Target"),FILTER(A2:D19,A2:A19="Fritz"))

但是这样的话就会出现一个问题,对于不同的人名,小编需要将上面公式中销售代表的姓名进行替换,也就是需要不断地手动改变姓名执行操作,这一举动不仅枯燥,而且很容易出错。因此这个时候就可以使用GcExcel通过解析公式并使用解析的语法树轻松替换销售代表姓名,可以简化此任务。

使用 C# 解析和修改 Excel 公式

首先,创建一个新的 C#(.NET Core) 项目,并使用NuGet 包管理器安装 GcExcel 包,然后按照前面的步骤操作。

1、使用示例数据初始化工作簿

实例化 Workbook 类的实例并从 Excel 文件导入示例数据,如下所示。

//Create a new workbook
var workbook = new GrapeCity.Documents.Excel.Workbook();           
//Load sample data from excel file
workbook.Open("SampleData.xlsx");
//Enable dynamic array formula
workbook.AllowDynamicArray = true;

2、提取公式

在工作簿加载示例数据和预期公式后,我们从工作表中提取所需的公式,以便使用 Formula 属性进行解析和修改。

GcExcel API 提供的公式解析器希望传递的公式不带“=”(等于)运算符,以便成功进行公式解析。因此,请注意如何在不使用“=”运算符的情况下提取公式。

//Fetch worksheet
var worksheet = workbook.Worksheets[0];
//Fetch the original formula which needs to be parsed.
var originalFormula = worksheet.Range["H3"].Formula.Substring(1);

3、解析公式

调用 FormulaSynatxTree 类的 Parse 方法来解析公式并生成语法树,帮助您理解公式包含的所有不同类型的值、运算符和函数。

公式语法树的每个标记都由 GcExcel API 中的其他类表示,例如函数的 FunctionNode、运算符的 OperatorNode 等。

下面的代码解析了上一步中提取的销售分析公式。然后,它将生成的 FormulaSyntaxTree 中的值附加到工作簿,该工作簿随后保存为 Excel 文件,以帮助您了解公式的语法树。

//Method to parse a formula and print the syntax tree
public static void ParseAndPrint(IWorksheet worksheet, string formula)
{
   // Get syntax tree
   var syntaxTree = FormulaSyntaxTree.Parse(formula);

   // Flatten nodes
   var displayItems = new List<(string TypeName, int IndentLevel, string Content)>();

   void flatten(SyntaxNode node, int level)
   {
      displayItems.Add((node.GetType().Name, level, node.ToString()));
      foreach (var child in node.Children)
      {
         flatten(child, level + 1);
      }
   }

   flatten(syntaxTree.Root, 0);

   // Output          
   worksheet.ShowRowOutline = false;
   worksheet.OutlineColumn.ColumnIndex = 1;

   // Header
   worksheet.Range["A1"].Value = "Formula";
   worksheet.Range["A3"].Value = "Syntax node";
   worksheet.Range["B3"].Value = "Part";

   // Values
   worksheet.Range["B1"].Value = "'=" + formula;
   for (var i = 0; i < displayItems.Count; i++)
   {
      var item = displayItems[i];
      var text = "'" + item.TypeName;

      worksheet.Range[i + 4, 0].Value = text;
      worksheet.Range[i + 4, 0].IndentLevel = item.IndentLevel;
      worksheet.Range[i + 4, 1].Value = "'" + item.Content;
   }

   //Apply styling
   worksheet.Range["A1:B3"].Interior.Color = System.Drawing.Color.FromArgb(68, 114, 196);
   worksheet.Range["A1:B3"].Font.Color = System.Drawing.Color.White;
   worksheet.Range["A1:B3"].Borders.Color = System.Drawing.Color.FromArgb(91, 155, 213);
   worksheet.Range["A1:B3"].Borders.LineStyle = BorderLineStyle.Thin;
   worksheet.Range["A1,A3,B3"].Font.Size = 14;
   worksheet.Range["A1,A3,B3"].Font.Bold = true;
   worksheet.Range["A:C"].EntireColumn.AutoFit();           
}

下图是生成的 FormulaSyntaxTree 的效果图图。请注意,这只是完整语法树的一部分:

4、修改公式

从上一步生成的语法树中,您可以看到销售代表姓名以 TextNode 形式表示,并且在公式中多次出现。我们可以通过简单的查找和替换操作来替换所有这些出现的情况,如下面的代码所示:

  1. 了替换公式中的销售代表姓名,我们从他们的姓名列表开始。我们使用 UNIQUE 函数从原始数据中过滤掉唯一名称列表。然后使用这个 UNIQUE 函数的结果来解析和修改所有销售代表的销售分析公式。
  2. 我们使用 TextNode 类修改销售代表姓名。下面的代码初始化 TextNode 类的实例,并将要在公式中搜索的销售代表姓名作为参数传递。该实例可以称为查找节点。
  3. 接下来,我们初始化 TextNode 类的另一个实例,并将公式中要替换的销售代表姓名作为参数传递。该实例可以称为替换节点。
  4. 下面的代码中定义了一个递归函数 replaceNode,用于遍历语法树的所有子节点,并将每个出现的 Find 节点替换为 Replace 节点。每个销售代表都会重复此操作。
  5. 修改公式后,新公式将分配给工作表中的单元格以生成预期的销售报告。

下面的代码包含一些格式化代码来格式化销售报告内容。

//Method to parse and modify the formula
public static void ModifyFormula(IWorksheet worksheet, string originalFormula)
{
    //Apply UNIQUE formula to get unique sales representatives list
    worksheet.Range["F1"].Value = "Unique Rep";
    worksheet.Range["F2"].Formula = "=UNIQUE(A2:A19)";
    var uniqueRep = worksheet.Range["F2#"];
    // Apply Styling
    worksheet.Range["F:F"].EntireColumn.AutoFit();
    worksheet.Range["F1"].Interior.Color = System.Drawing.Color.FromArgb(68, 114, 196);
    worksheet.Range["F1"].Font.Color = System.Drawing.Color.White;
    worksheet.Range["F2#"].Borders.Color = System.Drawing.Color.FromArgb(91, 155, 213);
    worksheet.Range["F2#"].Borders.LineStyle = BorderLineStyle.Thin;

    //Get syntax tree
    var syntaxTree = FormulaSyntaxTree.Parse(originalFormula);

    //Find
    var findText = new TextNode("Fritz");

    //Replacement
    var replaceText = new TextNode("");

    //Loop through names list to modify the formula for each sales representative
    for (int r = 0, resultRow = 3; r < uniqueRep.Cells.Count; r++, resultRow = resultRow + 4)
    {
       //Get name to be replaced in the formula
       var cval = uniqueRep.Cells[r].Value.ToString();

       if (findText.Value != cval)
       {
          //Assign name to be replaced to Replace TextNode
          replaceText.Value = cval;

          //Invoke the recursive method to perform find and replace operation
          replaceNode(syntaxTree.Root, findText, replaceText);

          //Assign the modified formula to a cell in the worksheet
          var resultRange = "H" + resultRow.ToString();
          worksheet.Range[resultRange].Formula = "=" + syntaxTree.ToString();
          worksheet.Range[resultRange + "#"].Borders.Color = System.Drawing.Color.FromArgb(91, 155, 213);
          worksheet.Range[resultRange + "#"].Borders.LineStyle = BorderLineStyle.Thin;

          //Update the value of Find node to perform find and replace operation for next sales representative name
          findText = replaceText;
       }
    }

    //Find and replace
    void replaceNode(SyntaxNode lookIn, SyntaxNode find, SyntaxNode replacement)
    {
       var children = lookIn.Children;

       for (var i = 0; i < children.Count; i++)
       {
          var child = children[i];
          if (child.Equals(find))
          {
             children[i] = replacement;
          }
          else
          {
             replaceNode(child, find, replacement);
          }
       }
    }
 }

这是修改后的公式之一:

=IF(ISNUMBER(FILTER(A2:D19,A2:A19="Xi")),IFS(FILTER(A2:D19,A2:A19="Xi")>5000,"Above Target",FILTER(A2:D19,A2:A19="Xi")>3000,"On Target",FILTER(A2:D19,A2:A19="Xi")<2500,"Below Target"),FILTER(A2:D19,A2:A19="Xi"))

5、保存 Excel 文件

将所有修改的公式添加到工作表后,将调用 Workbook 类的 Save 方法来保存 Excel 文件,如下面的代码所示:

//Save modified Excel file
workbook.Save("ModifiedFormula.xlsx", SaveFileFormat.Xlsx);

打开保存的 Excel 文件可以看到下图:

总结

以上就是使用C#实现解析Excel的全过程,如果您想了解更多信息,欢迎
点击这里
查看更多资料。

扩展链接:

轻松构建低代码工作流程:简化繁琐任务的利器

优化预算管理流程:Web端实现预算编制的利器

如何在.NET电子表格应用程序中创建流程图

本文分享自华为云社区《
Python并发编程探秘:多线程与异步编程的深入解析
》,作者:柠檬味拥抱。

在Python编程中,多线程是一种常用的并发编程方式,它可以有效地提高程序的执行效率,特别是在处理I/O密集型任务时。Python提供了
threading
模块,使得多线程编程变得相对简单。本文将深入探讨
threading
模块的基础知识,并通过实例演示多线程的应用。

1. 多线程基础概念

在开始之前,让我们先了解一些多线程编程的基本概念:

  • 线程(Thread):是操作系统能够进行运算调度的最小单位,通常在一个进程内部。
  • 多线程(Multithreading):是指在同一程序中同时运行多个线程。
  • GIL(Global Interpreter Lock):Python解释器的全局解释器锁,限制同一时刻只能有一个线程执行Python字节码,因此在CPU密集型任务中,多线程并不能充分利用多核处理器。

2. threading模块基础

threading
模块提供了创建和管理线程的工具。以下是一些常用的
threading
模块中的类和函数:

  • Thread
    类:用于创建线程的类,通过继承
    Thread
    类并实现
    run
    方法来定义线程的执行逻辑。
  • start()
    方法:启动线程。
  • join()
    方法:等待线程执行结束。
  • active_count()
    函数:获取当前活动线程的数量。

3. 代码实战:多线程下载图片

下面通过一个实例来演示多线程的应用,我们将使用多线程来下载一系列图片。

import threading
import requests
fromqueue import QueueclassImageDownloader:
def __init__(self, urls):
self.urls
=urls
self.queue
=Queue()

def download_image(self, url):
response
= requests.get(url)if response.status_code == 200:
filename
= url.split("/")[-1]
with open(filename,
"wb") asf:
f.write(response.content)
print(f
"Downloaded: {filename}")

def worker(self):
whileTrue:
url
= self.queue.get()if url isNone:breakself.download_image(url)
self.queue.task_done()

def start_threads(self, num_threads
=5):
threads
=[]for _ inrange(num_threads):
thread
= threading.Thread(target=self.worker)
thread.start()
threads.append(thread)
for url inself.urls:
self.queue.put(url)

self.queue.join()
for _ inrange(num_threads):
self.queue.put(None)
for thread inthreads:
thread.join()
if __name__ == "__main__":
image_urls
= ["url1", "url2", "url3", ...] # 替换为实际图片的URL
downloader
=ImageDownloader(image_urls)
downloader.start_threads()

这个例子中,我们创建了一个
ImageDownloader
类,其中包含了一个
worker
方法,用于下载图片。通过多线程,我们能够并行地下载多张图片,提高下载效率。

4. 代码解析

  • download_image
    方法:负责下载图片的具体实现。
  • worker
    方法:作为线程的执行逻辑,不断从队列中取出待下载的图片URL,并调用
    download_image
    方法。
  • start_threads
    方法:启动指定数量的线程,将图片URL放入队列中,等待所有线程执行完毕。

6. 线程安全与锁机制

在多线程编程中,由于多个线程同时访问共享资源,可能引发竞态条件(Race Condition)。为了避免这种情况,可以使用锁机制来确保在某一时刻只有一个线程能够访问共享资源。

threading
模块中提供了
Lock
类,通过它可以创建一个锁,使用
acquire
方法获取锁,使用
release
方法释放锁。下面是一个简单的示例:

import threading

counter
= 0counter_lock=threading.Lock()

def increment_counter():
globalcounterfor _ in range(1000000):
with counter_lock:
counter
+= 1def main():
thread1
= threading.Thread(target=increment_counter)
thread2
= threading.Thread(target=increment_counter)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(
"Counter:", counter)if __name__ == "__main__":
main()

这个例子中,我们创建了一个全局变量
counter
,并使用锁确保在两个线程同时修改
counter
时不会发生竞态条件。

7. 多线程的适用场景

多线程适用于处理I/O密集型任务,如网络请求、文件读写等。在这些场景中,线程可以在等待I/O的过程中让出CPU,让其他线程有机会执行,提高程序整体效率。

然而,在处理CPU密集型任务时,由于Python的GIL,多线程并不能充分利用多核处理器,可能导致性能瓶颈。对于CPU密集型任务,考虑使用多进程编程或其他并发模型。

9. 异常处理与多线程

在多线程编程中,异常的处理可能变得更加复杂。由于每个线程都有自己的执行上下文,异常可能在一个线程中引发,但在另一个线程中被捕获。为了有效地处理异常,我们需要在每个线程中使用合适的异常处理机制。

import threading

def thread_function():
try:
# 一些可能引发异常的操作
result
= 10 / 0except ZeroDivisionErrorase:
print(f
"Exception in thread: {e}")if __name__ == "__main__":
thread
= threading.Thread(target=thread_function)
thread.start()
thread.join()

print(
"Main thread continues...")

在这个例子中,线程
thread_function
中的除法操作可能引发
ZeroDivisionError
异常。为了捕获并处理这个异常,我们在线程的代码块中使用了
try-except
语句。

10. 多线程的注意事项

在进行多线程编程时,有一些常见的注意事项需要特别关注:

  • 线程安全性:确保多个线程同时访问共享资源时不会引发数据竞争和不一致性。
  • 死锁:当多个线程相互等待对方释放锁时可能发生死锁,需要谨慎设计和使用锁。
  • GIL限制:Python的全局解释器锁可能限制多线程在CPU密集型任务中的性能提升。
  • 异常处理:需要在每个线程中适当处理异常,以防止异常在一个线程中引发但在其他线程中未被捕获。

11. 多线程的性能优化

在一些情况下,我们可以通过一些技巧来优化多线程程序的性能:

  • 线程池:使用
    concurrent.futures
    模块中的
    ThreadPoolExecutor
    来创建线程池,提高线程的重用性。
  • 队列:使用队列来协调多个线程之间的工作,实现生产者-消费者模型。
  • 避免GIL限制:对于CPU密集型任务,考虑使用多进程、
    asyncio
    等其他并发模型。

13. 面向对象的多线程设计

在实际应用中,我们通常会面对更复杂的问题,需要将多线程和面向对象设计结合起来。以下是一个简单的例子,演示如何使用面向对象的方式来设计多线程程序:

import threading
import time
classWorkerThread(threading.Thread):
def __init__(self, name, delay):
super().__init__()
self.name
=name
self.delay
=delay

def run(self):
print(f
"{self.name} started.")
time.sleep(self.delay)
print(f
"{self.name} completed.")if __name__ == "__main__":
thread1
= WorkerThread("Thread 1", 2)
thread2
= WorkerThread("Thread 2", 1)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(
"Main thread continues...")

在这个例子中,我们创建了一个
WorkerThread
类,继承自
Thread
类,并重写了
run
方法,定义了线程的执行逻辑。每个线程被赋予一个名字和一个延迟时间。

14. 多线程与资源管理器

考虑一个场景,我们需要创建一个资源管理器,负责管理某个资源的分配和释放。这时,我们可以使用多线程来实现资源的异步管理。以下是一个简单的资源管理器的示例:

import threading
import time
classResourceManager:
def __init__(self, total_resources):
self.total_resources
=total_resources
self.available_resources
=total_resources
self.
lock =threading.Lock()

def allocate(self, request):
with self.
lock:if self.available_resources >=request:
print(f
"Allocated {request} resources.")
self.available_resources
-=requestelse:
print(
"Insufficient resources.")

def release(self, release):
with self.
lock:
self.available_resources
+=release
print(f
"Released {release} resources.")classUserThread(threading.Thread):
def __init__(self, name, resource_manager, request, release):
super().__init__()
self.name
=name
self.resource_manager
=resource_manager
self.request
=request
self.release
=release

def run(self):
print(f
"{self.name} started.")
self.resource_manager.allocate(self.request)
time.sleep(
1) # Simulate some work with allocated resources
self.resource_manager.release(self.release)
print(f
"{self.name} completed.")if __name__ == "__main__":
manager
= ResourceManager(total_resources=5)

user1
= UserThread("User 1", manager, request=3, release=2)
user2
= UserThread("User 2", manager, request=2, release=1)

user1.start()
user2.start()

user1.join()
user2.join()

print(
"Main thread continues...")

在这个例子中,
ResourceManager
类负责管理资源的分配和释放,而
UserThread
类表示一个使用资源的用户线程。通过使用锁,确保资源的安全分配和释放。

16. 多线程的调试与性能分析

在进行多线程编程时,调试和性能分析是不可忽视的重要环节。Python提供了一些工具和技术,帮助我们更好地理解和调试多线程程序。

调试多线程程序

使用
print
语句:在适当的位置插入
print
语句输出关键信息,帮助跟踪程序执行流程。

日志模块:使用Python的
logging
模块记录程序运行时的信息,包括线程的启动、结束和关键操作。

pdb调试器:在代码中插入断点,使用Python的内置调试器
pdb
进行交互式调试。

import pdb

# 在代码中插入断点
pdb.set_trace()

性能分析多线程程序

使用
timeit
模块:通过在代码中嵌入计时代码,使用
timeit
模块来测量特定操作或函数的执行时间。

import timeit

def my_function():
# 要测试的代码

# 测试函数执行时间
execution_time
= timeit.timeit(my_function, number=1)
print(f
"Execution time: {execution_time} seconds")

使用
cProfile
模块:
cProfile
是Python的性能分析工具,可以帮助查看函数调用及执行时间。

import cProfile

def my_function():
# 要测试的代码

# 运行性能分析
cProfile.run(
"my_function()")

使用第三方工具:一些第三方工具,如
line_profiler

memory_profiler
等,可以提供更详细的性能分析信息,帮助发现性能瓶颈。

# 安装line_profiler
pip install line_profiler

# 使用line_profiler进行性能分析
kernprof
-l script.py
python
-m line_profiler script.py.lprof

17. 多线程的安全性与风险

尽管多线程编程可以提高程序性能,但同时也带来了一些潜在的安全性问题。以下是一些需要注意的方面:

  1. 线程安全性:确保共享资源的访问是线程安全的,可以通过锁机制、原子操作等手段进行控制。

  2. 死锁:在使用锁的过程中,小心死锁的产生,即多个线程相互等待对方释放资源,导致程序无法继续执行。

  3. 资源泄漏:在多线程编程中,容易出现资源未正确释放的情况,例如线程未正确关闭或锁未正确释放。

  4. GIL限制:在CPU密集型任务中,全局解释器锁(GIL)可能成为性能瓶颈,需谨慎选择多线程或其他并发模型。

18. 探索其他并发模型

虽然多线程是一种常用的并发编程模型,但并不是唯一的选择。Python还提供了其他一些并发模型,包括:

  1. 多进程编程:通过
    multiprocessing
    模块实现,每个进程都有独立的解释器和GIL,适用于CPU密集型任务。

  2. 异步编程:通过
    asyncio
    模块实现,基于事件循环和协程,适用于I/O密集型任务,能够提高程序的并发性。

  3. 并行计算:使用
    concurrent.futures
    模块中的
    ProcessPoolExecutor

    ThreadPoolExecutor
    ,将任务并行执行。

19. 持续学习与实践

多线程编程是一个广阔而复杂的领域,本文只是为你提供了一个入门的指南。持续学习和实践是深入掌握多线程编程的关键。

建议阅读Python官方文档和相关书籍,深入了解
threading
模块的各种特性和用法。参与开源项目、阅读其他人的源代码,也是提高技能的好方法。

21. 多线程的异步化与协程

在现代编程中,异步编程和协程成为处理高并发场景的重要工具。Python提供了
asyncio
模块,通过协程实现异步编程。相比于传统多线程,异步编程可以更高效地处理大量I/O密集型任务,而无需创建大量线程。

异步编程基础

异步编程通过使用
async

await
关键字来定义协程。协程是一种轻量级的线程,可以在运行时暂停和继续执行。

import asyncioasyncdef my_coroutine():
print(
"Start coroutine")await asyncio.sleep(1)
print(
"Coroutine completed")asyncdef main():awaitasyncio.gather(my_coroutine(), my_coroutine())if __name__ == "__main__":
asyncio.run(main())

在上述例子中,
my_coroutine
是一个协程,使用
asyncio.sleep
模拟异步操作。通过
asyncio.gather
同时运行多个协程。

异步与多线程的比较

性能: 异步编程相较于多线程,可以更高效地处理大量的I/O密集型任务,因为异步任务在等待I/O时能够让出控制权,不阻塞其他任务的执行。

复杂性: 异步编程相对于多线程来说,编写和理解的难度可能较大,需要熟悉协程的概念和异步编程的模型。

示例:异步下载图片

以下是一个使用异步编程实现图片下载的简单示例:

import asyncio
import aiohttp
asyncdef download_image(session, url):async with session.get(url) asresponse:if response.status == 200:
filename
= url.split("/")[-1]
with open(filename,
"wb") asf:
f.write(
awaitresponse.read())
print(f
"Downloaded: {filename}")asyncdef main():
image_urls
= ["url1", "url2", "url3", ...] # 替换为实际图片的URLasync with aiohttp.ClientSession() assession:
tasks
= [download_image(session, url) for url inimage_urls]await asyncio.gather(*tasks)if __name__ == "__main__":
asyncio.run(main())

在这个例子中,通过
aiohttp
库创建异步HTTP请求,
asyncio.gather
并发执行多个协程。

22. 异步编程的异常处理

在异步编程中,异常的处理方式也有所不同。在协程中,我们通常使用
try-except
块或者
asyncio.ensure_future
等方式来处理异常。

import asyncioasyncdef my_coroutine():try:
# 异步操作
await asyncio.sleep(1)
raise ValueError(
"An error occurred")
except ValueError
ase:
print(f
"Caught an exception: {e}")asyncdef main():
task
=asyncio.ensure_future(my_coroutine())awaitasyncio.gather(task)if __name__ == "__main__":
asyncio.run(main())

在这个例子中,
asyncio.ensure_future
将协程包装成一个
Task
对象,通过
await asyncio.gather
等待任务执行完毕,捕获异常。

23. 异步编程的优势与注意事项

优势

高并发性: 异步编程适用于大量I/O密集型任务,能够更高效地处理并发请求,提高系统的吞吐量。

资源效率: 相较于多线程,异步编程通常更节省资源,因为协程是轻量级的,可以在一个线程中运行多个协程。

注意事项

  • 阻塞操作: 异步编程中,阻塞操作会影响整个事件循环,应尽量避免使用阻塞调用。
  • 异常处理: 异步编程的异常处理可能更加复杂,需要仔细处理协程中的异常情况。
  • 适用场景: 异步编程更适用于I/O密集型任务,而不是CPU密集型任务。

24. 探索更多异步编程工具和库

除了
asyncio

aiohttp
之外,还有一些其他强大的异步编程工具和库:

  • asyncpg
    : 异步PostgreSQL数据库驱动。
  • aiofiles
    : 异步文件操作库。
  • aiohttp
    : 异步HTTP客户端和服务器框架。
  • aiomysql
    : 异步MySQL数据库驱动。
  • uvloop
    : 用于替代标准事件循环的高性能事件循环。

25. 持续学习与实践

异步编程是一个广泛且深入的主题,本文只是为你提供了一个简要的介绍。建议深入学习
asyncio
模块的文档,理解事件循环、协程、异步操作等概念。

同时,通过实际项目的实践,你将更好地理解和掌握异步编程的技术和最佳实践。

总结

本文深入探讨了Python中的多线程编程和异步编程,涵盖了多线程模块(
threading
)的基础知识、代码实战,以及异步编程模块(
asyncio
)的基本概念和使用。我们从多线程的基础,如
Thread
类、锁机制、线程安全等开始,逐步展示了多线程在实际应用中的应用场景和注意事项。通过一个实例展示了多线程下载图片的过程,强调了线程安全和异常处理的重要性。

随后,本文引入了异步编程的概念,通过协程、
async

await
关键字,以及
asyncio
模块的使用,向读者展示了异步编程的基础。通过一个异步下载图片的实例,强调了异步编程在处理I/O密集型任务中的高效性。

文章还对异步编程的异常处理、优势与注意事项进行了详细讨论,同时介绍了一些常用的异步编程工具和库。最后,鼓励读者通过不断学习、实践,深化对多线程和异步编程的理解,提高在并发编程方面的能力。

无论是多线程编程还是异步编程,都是提高程序并发性、性能和响应性的关键技术。通过深入理解这些概念,读者可以更好地应对现代编程中复杂的并发需求,提升自己的编程水平。祝愿读者在多线程和异步编程的学习过程中取得丰硕的成果!

点击关注,第一时间了解华为云新鲜技术~

热点随笔:

·
开年!5 款令人惊艳的开源项目「GitHub 热点速览」
(
削微寒
)
·
SQL中为什么不要使用1=1?
(
萤火架构
)
·
编写高效的代码,你应该了解Array、Memory、ReadOnlySequence . . .
(
Artech
)
·
都说了能不动就别动,非要去调整,出生产事故了吧
(
青石路
)
·
4.1k Star!全面的C#/.NET/.NET Core学习、工作、面试指南
(
追逐时光者
)
·
Sora文生视频模型深度剖析:全网独家指南,洞悉98%关键信息,纯干货
(
汀、人工智能
)
·
C# 12 中新增的八大功能你都知道吗?
(
追逐时光者
)
·
关于代码性能优化的总结
(
程序员不帅哥
)
·
使用 .NET 8.0 和 OpenGL 创建一个简易的渲染器
(
o王先生o
)
·
如何基于three.js(webgl)引擎架构,实现3D密集架库房,3D档案室
(
魂断蓝桥666
)
·
vue3的宏到底是什么东西?
(
欧阳码农
)
·
记一次 .NET某列控连锁系统 崩溃分析
(
一线码农
)

热点新闻:

·
给互联网人的反侦查手册 2.0
·
2024 年.NET 框架发展趋势预测
·
研究了纯血鸿蒙后,我感觉华为是在摸着苹果过河
·
平均每天27项!比亚迪全球累计申请专利超4.8万项
·
返乡潮退去后的县城:遍地是人情
·
淘宝可以微信支付了?客服:将全面覆盖
·
美院博士成AI名师?直播2小时卖课20万?AI课程乱象调查
·
狂热警告!机构担忧互联网泡沫再现,英伟达财报成为风口浪尖
·
7.98万,比亚迪锁死燃油车的反扑,底气何来
·
没有雷军的小米手机发布会,直接发了一台「相机」
·
帮人背债,躺赚百万?
·
中国四大航空公司居然干不过一条高铁线?

前言

我们在使用
ASP.NET Core WebApi
时它支持使用指定的输入和输出格式来交换数据。输入数据靠模型绑定的机制处理,输出数据则需要用格式化的方式进行处理。
ASP.NET Core
框架已经内置了处理
JSON

XML
的输入和输出方式,默认的情况我们提交
JSON
格式的内容,它可以自行进行模型绑定,也可以把对象类型的返回值输出成
JSON
格式,这都归功于内置的
JSON
格式化程序。本篇文章我们将通过自定义一个
YAML
格式的转换器开始,逐步了解它到底是如何工作的。以及通过自带的
JSON
格式化输入输出源码,加深对
Formatter
程序的了解。

自定义开始

要想先了解
Formatter
的工作原理,当然需要从自定义开始。因为一般自定义的时候我们一般会选用自己最简单最擅长的方式去扩展,然后逐步完善加深理解。格式化器分为两种,一种是用来处理输入数据格式的
InputFormatter
,另一种是用来处理返回数据格式的
OutputFormatter
。本篇文章示例,我们从自定义
YAML
格式的转换器开始。因为目前
YAML
格式确实比较流行,得益于它简单明了的格式,目前也有很多中间件都是用
YAML
格式。这里我们使用的是
YamlDotNet
这款组件,具体的引入信息如下所示

<PackageReference Include="YamlDotNet" Version="15.1.0" />

YamlInputFormatter

首先我们来看一下自定义请求数据的格式化也就是
InputFormatter
,它用来处理了请求数据的格式,也就是我们在
Http请求体
里的数据格式如何处理,手下我们需要定义个
YamlInputFormatter
类,继承自
TextInputFormatter
抽象类

public class YamlInputFormatter : TextInputFormatter
{
    private readonly IDeserializer _deserializer;

    public YamlInputFormatter(DeserializerBuilder deserializerBuilder)
    {
        _deserializer = deserializerBuilder.Build();

        //添加与之绑定的MediaType,这里其实绑定的提交的ContentType的值
        //如果请求ContentType:text/yaml或ContentType:text/yml才能命中该YamlInputFormatter
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yaml"));
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yml"));
        
        //添加编码类型比如application/json;charset=UTF-8后面的这种charset
        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(encoding);

        //获取请求Body
        var readStream = context.HttpContext.Request.Body;
        
        object? model;
        try
        {
            TextReader textReader = new StreamReader(readStream);
            //获取Action参数类型
            var type = context.ModelType;
            //把yaml字符串转换成具体的对象
            model = _deserializer.Deserialize(textReader, type);
        }
        catch (YamlException ex) 
        {
            context.ModelState.TryAddModelError(context.ModelName, ex.Message);
            throw new InputFormatterException("反序列化输入数据时出错\n\n", ex.InnerException!);
        }

        if (model == null && !context.TreatEmptyInputAsDefaultValue)
        {
            return InputFormatterResult.NoValue();
        }
        else
        {
            return InputFormatterResult.Success(model);
        }
    }
}

这里需要注意的是配置
SupportedMediaTypes
,也就是添加与
YamlInputFormatter
绑定的
MediaType
,也就是我们请求时设置的
Content-Type
的值,这个配置是必须要的,否则没办法判断当前
YamlInputFormatter
与哪种
Content-Type
进行绑定。接下来定义完了之后如何把它接入程序使用它呢?也很简单在
MvcOptions
中配置即可,如下所示

builder.Services.AddControllers(options => {
    options.InputFormatters.Add(new YamlInputFormatter(new DeserializerBuilder()));
});

接下来我们定义一个简单类型和Action来演示一下,类和代码不具备任何实际意义,只是为了演示

[HttpPost("AddAddress")]
public Address AddAddress(Address address)
{
    return address;
}

public class Address
{
    public string City { get; set; }
    public string Country { get; set; }
    public string Phone { get; set; }
    public string ZipCode { get; set; }
    public List<string> Tags { get; set; }
}

我们用
Postman
测试一下,提交一个
yaml
类型的格式,效果如下所示

这里需要注意的是我们需要在
Postman
中设置
Content-Type

text/yml

text/yaml

YamlOutputFormatter

上面我们演示了如何定义
InputFormatter
它的作用是将请求的数据格式化成具体类型。无独有偶,既然请求数据格式可以定义,那么输出的数据格式同样可以定义,这里就需要用到
OutputFormatter
。接下来我们定义一个
YamlOutputFormatter
继承自
TextOutputFormatter
抽象类,代码如下所示

public class YamlOutputFormatter : TextOutputFormatter
{
    private readonly ISerializer _serializer;

    public YamlOutputFormatter(SerializerBuilder serializerBuilder)
    {
        //添加与之绑定的MediaType,这里其实绑定的提交的Accept的值
        //如果请求Accept:text/yaml或Accept:text/yml才能命中该YamlOutputFormatter
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yaml"));
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yml"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);

        _serializer = serializerBuilder.Build();
    }

    public override bool CanWriteResult(OutputFormatterCanWriteContext context)
    {
        //什么条件可以使用yaml结果输出,至于为什么要重写CanWriteResult方法,我们在后面分析源码的时候会解释
        string accept = context.HttpContext.Request.Headers.Accept.ToString() ?? "";
        if (string.IsNullOrWhiteSpace(accept))
        {
            return false;
        }

        var parsedContentType = new MediaType(accept);
        for (var i = 0; i < SupportedMediaTypes.Count; i++)
        {
            var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
            if (parsedContentType.IsSubsetOf(supportedMediaType))
            {
                return true;
            }
        }
        return false;
    }

    public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(selectedEncoding);

        try
        {
            var httpContext = context.HttpContext;
            //获取输出的对象,转成成yaml字符串并输出
            string respContent = _serializer.Serialize(context.Object);
            await httpContext.Response.WriteAsync(respContent);
        }
        catch (YamlException ex)
        {
            throw new InputFormatterException("序列化输入数据时出错\n\n", ex.InnerException!);
        }
    }
}

同样的这里我们也添加了
SupportedMediaTypes
的值,它的作用是我们请求时设置的
Accept
的值,这个配置也是必须要的,也就是请求的头中为
Accept:text/yaml

Accept:text/yml
才能命中该
YamlOutputFormatter
。配置的时候同样也在
MvcOptions
中配置即可

builder.Services.AddControllers(options => {
    options.OutputFormatters.Add(new YamlOutputFormatter(new SerializerBuilder()));
});

接下来我们同样还是使用上面的代码进行演示,只是我们这里更换一下重新设置一下相关Header即可,这次我们直接提交
json
类型的数据,它会输出
yaml
格式,代码什么的完全不用变,结果如下所示

这里需要注意的请求头的设置发生了变化

小结

上面我们讲解了控制请求数据格式的
TextInputFormatter
和控制输出格式的
TextOutputFormatter
。其中
InputFormatter
负责给
ModelBinding
输送类型对象,
OutputFormatter
负责给
ObjectResult
输出值,这我们可以看到它们只能控制
WebAPI

Controller/Action
的且返回
ObjectResult
的这种情况才生效,其它的比如
MinimalApi

GRPC
是起不到效果的。通过上面的示例,有同学心里可能会存在疑问,上面在
AddControllers
方法中注册
TextInputFormatter

TextOutputFormatter
的时候,没办法完成注入的服务,比如如果
YamlInputFormatter

YamlOutputFormatter
构造实例的时候无法获取
DI容器
中的实例。确实,如果使用上面的方式我们确实没办法完成这个需求,不过我们可以通过其它方法实现,那就是去扩展
MvcOptions
选项,实现如下所示

public class YamlMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
    private readonly ILoggerFactory _loggerFactory;

    public YamlMvcOptionsSetup(ILoggerFactory loggerFactory)
    {
        _loggerFactory = loggerFactory;
    }

    public void Configure(MvcOptions options)
    {
        var yamlInputLogger = _loggerFactory.CreateLogger<YamlInputFormatter>();
        options.InputFormatters.Add(new YamlInputFormatter(new DeserializerBuilder()));

        var yamlOutputLogger = _loggerFactory.CreateLogger<YamlOutputFormatter>();
        options.OutputFormatters.Add(new YamlOutputFormatter(new SerializerBuilder()));
    }
}

我们定义了
YamlMvcOptionsSetup
去扩展
MvcOptions
选项,然后我们将
YamlMvcOptionsSetup
注册到容器即可

builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, YamlMvcOptionsSetup>());

探究工作方式

上面我们演示了如何自定义
InputFormatter

OutputFormatter
,也讲解了
InputFormatter
负责给
ModelBinding
输送类型对象,
OutputFormatter
负责给
ObjectResult
输出值。接下来我们就通过阅读其中的源码来看一下
InputFormatter

OutputFormatter
是如何工作来影响
模型绑定

ObjectResult
的结果。

需要注意的是!我们展示的源码是删减过的,只关注我们需要关注的地方,因为源码中涉及的内容太多,不方便观看,所以只保留我们关注的地方,还望谅解。

TextInputFormatter如何工作


上面我们看到了
YamlInputFormatter
是继承了
TextInputFormatter
抽象类,并重写了
ReadRequestBodyAsync
方法。接下来我们就从
TextInputFormatter

ReadRequestBodyAsync
方法来入手,我们来看一下源码定义[

点击查看TextInputFormatter源码