20 DATA SHAPING
20 数据整形
In this chapter, we are going to talk about a neat concept called data shaping and how to implement it in ASP.NET Core Web API. To achieve that, we are going to use similar tools to the previous section. Data shaping is not something that every API needs, but it can be very useful in some cases.
在本章中,我们将讨论一个称为数据整形的简洁概念,以及如何在 ASP.NET Core Web API 中实现它。为此,我们将使用与上一节类似的工具。数据整形不是每个 API 都需要的,但在某些情况下它可能非常有用。
Let’s start by learning what data shaping is exactly.
让我们首先了解一下数据调整到底是什么。
20.1 What is Data Shaping?
20.1 什么是数据整形?
Data shaping is a great way to reduce the amount of traffic sent from the API to the client. It enables the consumer of the API to select (shape) the data by choosing the fields through the query string.
数据调整是减少从 API 发送到客户端的流量的好方法。它使 API 的使用者能够通过查询字符串选择字段来选择(调整)数据。
What this means is something like:
这意味着:
https://localhost:5001/api/companies/companyId/employees?fi elds=name,age
By giving the consumer a way to select just the fields it needs, we can potentially reduce the stress on the API. On the other hand, this is not something every API needs, so we need to think carefully and decide whether we should implement its implementation because it has a bit of reflection in it.
通过为消费者提供一种只选择它需要的字段的方法,我们有可能减轻 API 的压力。另一方面,这不是每个 API 都需要的,所以我们需要仔细考虑并决定是否应该实现它的实现,因为它有一点反射。
And we know for a fact that reflection takes its toll and slows our application down.
我们知道一个事实,反射会造成损失并减慢我们的应用程序速度。
Finally, as always, data shaping should work well together with the concepts we’ve covered so far – paging, filtering, searching, and sorting.
最后,与往常一样,数据调整应该与我们到目前为止介绍的概念(分页、筛选、搜索和排序)很好地协同工作。
First, we are going to implement an employee-specific solution to data shaping. Then we are going to make it more generic, so it can be used by any entity or any API.
首先,我们将实施一个特定于员工的数据整形解决方案。然后,我们将使其更加通用,以便任何实体或任何 API 都可以使用它。
Let’s get to work.
让我们开始工作吧。
20.2 How to Implement Data Shaping
20.2 如何实现数据调整
First things first, we need to extend our RequestParameters class since we are going to add a new feature to our query string and we want it to be available for any entity:
首先,我们需要扩展 RequestParameters 类,因为我们要向查询字符串添加新功能,并且我们希望它可用于任何实体:
public string? Fields { get; set; }
We’ve added the Fields property and now we can use fields as a query string parameter.
我们添加了 Fields 属性,现在可以将 fields 用作查询字符串参数。
Let’s continue by creating a new interface in the Contracts project:
让我们继续在 Contracts 项目中创建一个新界面:
public interface IDataShaper<T> { IEnumerable<ExpandoObject> ShapeData(IEnumerable<T> entities, string fieldsString); ExpandoObject ShapeData(T entity, string fieldsString); }
The IDataShaper defines two methods that should be implemented — one for the single entity and one for the collection of entities. Both are named ShapeData, but they have different signatures.
IDataShaper 定义了两个应该实现的方法 — 一个用于单个实体,一个用于实体集合。两者都名为 ShapeData,但它们具有不同的签名。
Notice how we use the ExpandoObject from System.Dynamic namespace as a return type. We need to do that to shape our data the way we want it.
请注意我们如何使用 System.Dynamic 命名空间中的 ExpandoObject 作为返回类型。我们需要这样做,以我们想要的方式塑造我们的数据。
To implement this interface, we are going to create a new DataShaping folder in the Service project and add a new DataShaper class:
为了实现这个接口,我们将在 Service 项目中创建一个新的 DataShaping 文件夹,并添加新的 DataShaper 类:
public class DataShaper<T> : IDataShaper<T> where T : class { public PropertyInfo[] Properties { get; set; } public DataShaper() { Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); } public IEnumerable<ExpandoObject> ShapeData(IEnumerable<T> entities, string fieldsString) {var requiredProperties = GetRequiredProperties(fieldsString); return FetchData(entities, requiredProperties); } public ExpandoObject ShapeData(T entity, string fieldsString) { var requiredProperties = GetRequiredProperties(fieldsString); return FetchDataForEntity(entity, requiredProperties); } private IEnumerable<PropertyInfo> GetRequiredProperties(string fieldsString) { var requiredProperties = new List<PropertyInfo>(); if (!string.IsNullOrWhiteSpace(fieldsString)) { var fields = fieldsString.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (var field in fields) { var property = Properties .FirstOrDefault(pi => pi.Name.Equals(field.Trim(), StringComparison.InvariantCultureIgnoreCase)); if (property == null) continue; requiredProperties.Add(property); } } else { requiredProperties = Properties.ToList(); } return requiredProperties; }private IEnumerable<ExpandoObject> FetchData(IEnumerable<T> entities, IEnumerable<PropertyInfo> requiredProperties) { var shapedData = new List<ExpandoObject>(); foreach (var entity in entities) { var shapedObject = FetchDataForEntity(entity, requiredProperties); shapedData.Add(shapedObject); } return shapedData; } private ExpandoObject FetchDataForEntity(T entity, IEnumerable<PropertyInfo> requiredProperties) { var shapedObject = new ExpandoObject();foreach (var property in requiredProperties) { var objectPropertyValue = property.GetValue(entity); shapedObject.TryAdd(property.Name, objectPropertyValue); } return shapedObject; } }
We need these namespaces to be included as well:
我们还需要包含这些命名空间:
using Contracts;
using System.Dynamic;
using System.Reflection;
There is quite a lot of code in our class, so let’s break it down.
我们的类中有相当多的代码,所以让我们分解一下。
20.3 Step-by-Step Implementation
20.3 分步实施
We have one public property in this class – Properties. It’s an array of PropertyInfo’s that we’re going to pull out of the input type, whatever it is — Company or Employee in our case:
这个类中有一个公共属性 – Properties。这是一个 PropertyInfo 数组,我们将从输入类型中提取出来,无论它是什么 — 在我们的例子中是 Company 或 Employee:
public PropertyInfo[] Properties { get; set; } public DataShaper() { Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); }
So, here it is. In the constructor, we get all the properties of an input class.
所以,就在这里。在构造函数中,我们获取 input 类的所有属性。
Next, we have the implementation of our two public ShapeData methods:
接下来,我们实现了两个公共 ShapeData 方法:
public IEnumerable<ExpandoObject> ShapeData(IEnumerable<T> entities, string fieldsString) { var requiredProperties = GetRequiredProperties(fieldsString); return FetchData(entities, requiredProperties); } public ExpandoObject ShapeData(T entity, string fieldsString) { var requiredProperties = GetRequiredProperties(fieldsString); return FetchDataForEntity(entity, requiredProperties); }
Both methods rely on the GetRequiredProperties method to parse the input string that contains the fields we want to fetch.
这两种方法都依赖于 GetRequiredProperties 方法来解析包含我们要获取的字段的输入字符串。
The GetRequiredProperties method does the magic. It parses the input string and returns just the properties we need to return to the controller:
GetRequiredProperties 方法可以执行神奇的作。它解析输入字符串并仅返回我们需要返回给控制器的属性:
private IEnumerable<PropertyInfo> GetRequiredProperties(string fieldsString) { var requiredProperties = new List<PropertyInfo>(); if (!string.IsNullOrWhiteSpace(fieldsString)) { var fields = fieldsString.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (var field in fields) { var property = Properties .FirstOrDefault(pi => pi.Name.Equals(field.Trim(), StringComparison.InvariantCultureIgnoreCase)); if (property == null) continue; requiredProperties.Add(property); } } else { requiredProperties = Properties.ToList(); } return requiredProperties; }
There’s nothing special about it. If the fieldsString is not empty, we split it and check if the fields match the properties in our entity. If they do, we add them to the list of required properties.
它没有什么特别之处。如果 fieldsString 不为空,我们将其拆分并检查字段是否与实体中的属性匹配。如果出现,我们会将它们添加到必需属性列表中。
On the other hand, if the fieldsString is empty, all properties are required.
另一方面,如果 fieldsString 为空,则所有属性都是必需的。
Now, FetchData and FetchDataForEntity are the private methods to extract the values from these required properties we’ve prepared.
现在,FetchData 和 FetchDataForEntity 是从我们准备的这些必需属性中提取值的私有方法。
The FetchDataForEntity method does it for a single entity:
FetchDataForEntity 方法对单个实体执行此作:
private ExpandoObject FetchDataForEntity(T entity, IEnumerable<PropertyInfo> requiredProperties) { var shapedObject = new ExpandoObject();foreach (var property in requiredProperties) { var objectPropertyValue = property.GetValue(entity); shapedObject.TryAdd(property.Name, objectPropertyValue); } return shapedObject; }
Here, we loop through the requiredProperties parameter. Then, using a bit of reflection, we extract the values and add them to our ExpandoObject. ExpandoObject implements IDictionary<string,object>
, so we can use the TryAdd method to add our property using its name as a key and the value as a value for the dictionary.
在这里,我们遍历 requiredProperties 参数。然后,使用一些反射,我们提取值并将它们添加到我们的 ExpandoObject 中。ExpandoObject 实现 IDictionary<string,object>
,因此我们可以使用 TryAdd 方法添加属性,使用其名称作为键,将值用作字典的值。
This way, we dynamically add just the properties we need to our dynamic object.
这样,我们就可以动态地将所需的属性添加到动态对象中。
The FetchData method is just an implementation for multiple objects. It utilizes the FetchDataForEntity method we’ve just implemented:
FetchData 方法只是多个对象的实现。它利用了我们刚刚实现的 FetchDataForEntity 方法:
private IEnumerable<ExpandoObject> FetchData(IEnumerable<T> entities, IEnumerable<PropertyInfo> requiredProperties) { var shapedData = new List<ExpandoObject>(); foreach (var entity in entities) { var shapedObject = FetchDataForEntity(entity, requiredProperties); shapedData.Add(shapedObject); } return shapedData; }
To continue, let’s register the DataShaper class in the IServiceCollection in the Program class:
若要继续,让我们在 Program 类的 IServiceCollection 中注册 DataShaper 类:
builder.Services.AddScoped<IDataShaper<EmployeeDto>, DataShaper<EmployeeDto>>();
During the service registration, we provide the type to work with.
在服务注册期间,我们会提供要使用的类型。
Because we want to use the DataShaper class inside the service classes, we have to modify the constructor of the ServiceManager class first:
因为我们想在服务类中使用 DataShaper 类,所以我们必须先修改 ServiceManager 类的构造函数:
public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger, IMapper mapper, IDataShaper<EmployeeDto> dataShaper) { _companyService = new Lazy<ICompanyService>(() => new CompanyService(repositoryManager, logger, mapper)); _employeeService = new Lazy<IEmployeeService>(() => new EmployeeService(repositoryManager, logger, mapper, dataShaper)); }
We are going to use it only in the EmployeeService class.
我们只在 EmployeeService 类中使用它。
Next, let’s add one more field and modify the constructor in the EmployeeService class:
接下来,让我们再添加一个字段并修改 EmployeeService 类中的构造函数:
...
private readonly IDataShaper<EmployeeDto> _dataShaper; public EmployeeService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper, IDataShaper<EmployeeDto> dataShaper) { _repository = repository; _logger = logger; _mapper = mapper; _dataShaper = dataShaper; }
Let’s also modify the GetEmployeesAsync method of the same class:
我们还修改同一类的 GetEmployeesAsync 方法:
public async Task<(IEnumerable<ExpandoObject> employees, MetaData metaData)> GetEmployeesAsync (Guid companyId, EmployeeParameters employeeParameters, bool trackChanges) { if (!employeeParameters.ValidAgeRange) throw new MaxAgeRangeBadRequestException(); await CheckIfCompanyExists(companyId, trackChanges); var employeesWithMetaData = await _repository.Employee .GetEmployeesAsync(companyId, employeeParameters, trackChanges); var employeesDto = _mapper.Map<IEnumerable<EmployeeDto>>(employeesWithMetaData); var shapedData = _dataShaper.ShapeData(employeesDto, employeeParameters.Fields); return (employees: shapedData, metaData: employeesWithMetaData.MetaData); }
We have changed the method signature so, we have to modify the interface as well:
我们已经更改了方法签名,因此,我们还必须修改接口:
Task<(IEnumerable<ExpandoObject> employees, MetaData metaData)> GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges);
Now, we can test our solution:
现在,我们可以测试我们的解决方案:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees?fields=name,age
It works great.
它效果很好。
Let’s also test this solution by combining all the functionalities that we’ve implemented in the previous chapters:
我们还通过组合我们在前几章中实现的所有功能来测试此解决方案:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees?pageNumber=1&pageSize=4&minAge=26&maxAge=32&searchTerm=A&orderBy=name desc&fields=name,age
Excellent. Everything is working like a charm.
非常好。一切都像魅力一样运作。
20.4 Resolving XML Serialization Problems
20.4 解决 XML 序列化问题
Let’s send the same request one more time, but this time with the different accept header (text/xml):
让我们再发送一次相同的请求,但这次使用不同的 accept 标头 (text/xml):
It works — but it looks pretty ugly and unreadable. But that’s how the XmlDataContractSerializerOutputFormatter serializes our ExpandoObject by default.
它有效 — 但它看起来非常丑陋且难以阅读。但默认情况下,这就是 XmlDataContractSerializerOutputFormatter 序列化 ExpandoObject 的方式。
We can fix that, but the logic is out of the scope of this book. Of course, we have implemented the solution in our source code. So, if you want, you can use it in your project.
我们可以解决这个问题,但逻辑超出了本书的范围。当然,我们已经在源代码中实现了解决方案。因此,如果您愿意,您可以在您的项目中使用它。
All you have to do is to create the Entity class and copy the content from our Entity class that resides in the Entities/Models folder.
您所要做的就是创建 Entity 类并从位于 Entities/Models 文件夹中的 Entity 类中复制内容。
After that, just modify the IDataShaper interface and the DataShaper class by using the Entity type instead of the ExpandoObject type. Also, you have to do the same thing for the IEmployeeService interface and the EmployeeService class. Again, you can check our implementation if you have any problems.
之后,只需使用 Entity 类型而不是 ExpandoObject 类型来修改 IDataShaper 接口和 DataShaper 类。此外,还必须对 IEmployeeService 接口和 EmployeeService 类执行相同的作。同样,如果您有任何问题,可以检查我们的实现。
After all those changes, once we send the same request, we are going to see a much better result:
在所有这些更改之后,一旦我们发送相同的请求,我们将看到更好的结果:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees?pageNumber=1&pageSize=4&minAge=26&maxAge=32&searchTerm=A&orderBy=name desc&fields=name,age
If XML serialization is not important to you, you can keep using ExpandoObject — but if you want a nicely formatted XML response, this is the way to go.
如果 XML 序列化对您来说并不重要,您可以继续使用 ExpandoObject — 但如果您想要格式良好的 XML 响应,这就是要走的路。
To sum up, data shaping is an exciting and neat little feature that can make our APIs flexible and reduce our network traffic. If we have a high- volume traffic API, data shaping should work just fine. On the other hand, it’s not a feature that we should use lightly because it utilizes reflection and dynamic typing to get things done.
综上所述,数据整形是一个令人兴奋且简洁的小功能,它可以使我们的 API 变得灵活,减少我们的网络流量。如果我们有一个大容量流量 API,数据整形应该可以正常工作。另一方面,它不是一个我们应该轻易使用的功能,因为它利用反射和动态类型来完成工作。
As with all other functionalities, we need to be careful when and if we should implement data shaping. Performance tests might come in handy even if we do implement it.
与所有其他功能一样,我们需要小心何时以及是否应该实现数据整形。即使我们确实实施了性能测试,它也可能会派上用场。