.NET Core工程应用系列(2) 实现可配置Attribute的Json序列化方案
背景
在这篇文章中,我们实现了基于自定义Attribute的审计日志数据对象属性过滤,但是在实际项目的应用中遇到了一点麻烦。需要进行审计的对象属性中会包含其他类对象,而我们之前的实现是没办法处理这种类属性对象内部的Attribute的。另外,属性值为null的会抛异常。
但是Newtonsoft自带的JsonConverter.SerializeObject方法实际上是能够处理这些情况的,给类属性对象所属的类中某个属性添加的Attribute能够正常被处理。同时我们也希望这个Attribute仅仅在这种情况下被应用,项目中的其地方序列化忽略这个Attribute。
骚年,继续我们的填坑之旅。
解决方案
思路
首先既然原框架中的JsonConverter.SerializeObject能够做到序列化类对象属性时处理另一个类中的诸如JsonIgnore的Attribute,那这篇文章中我们重写AuditDataProvider中的Serialize方法时,就不能自定义序列化的操作,而是借用JsonConverter.SerializeObject方法,然后想办法通过配置项来实现定制化的Attribute处理。
核心代码
打开Newtonsoft.Json的源代码进行查看,跟踪JsonConverter.SerializeObject方法:
SerializeObject静态方法
public static string SerializeObject(object value, Type type, JsonSerializerSettings settings)
{
// 接收调用方法时传入的JsonSerializerSettings对象并构造JsonSerializer对象
JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(settings);
// 调用序列化对象操作
return SerializeObjectInternal(value, type, jsonSerializer);
}
先看CreateDefault方法:
public static JsonSerializer CreateDefault(JsonSerializerSettings settings)
{
JsonSerializer serializer = CreateDefault();
if (settings != null)
{
ApplySerializerSettings(serializer, settings);
}
return serializer;
}
private static void ApplySerializerSettings(JsonSerializer serializer, JsonSerializerSettings settings)
{
// ... 省略若干代码
if (settings.ContractResolver != null)
{
// 如果指定了ContractResolver,则使用我们指定的,否则使用默认的Resolver
serializer.ContractResolver = settings.ContractResolver;
}
// ... 省略若干代码
}
SerializeObjectInternal方法
追踪方法SerializeObjectInternal到深层,可以看到在内部调用的序列化逻辑是根据当前遇到的节点类型分别实现了不同的WriteJson方法,以KeyValueConverter的WriteJson方法为例:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
ReflectionObject reflectionObject = ReflectionObjectPerType.Get(value.GetType());
// 使用ContractResolver对象进行后面的序列化,其实看到这里就可以了,我们大致可以推断出来具体解析一个对象
// 的工作,是由这个ContractResolver对象来定义的。
DefaultContractResolver resolver = serializer.ContractResolver as DefaultContractResolver;
writer.WriteStartObject();
writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(KeyName) : KeyName);
serializer.Serialize(writer, reflectionObject.GetValue(value, KeyName), reflectionObject.GetType(KeyName));
writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(ValueName) : ValueName);
serializer.Serialize(writer, reflectionObject.GetValue(value, ValueName), reflectionObject.GetType(ValueName));
writer.WriteEndObject();
}
DefaultContractResolver
查看官方实现的一个CamelCasePropertyNamesContractResolver类,继承自DefaultContractResolver类。我们发现在基类中有这样一个虚方法:
protected virtual IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
这个方法说明了我们可以通过实现该方法来定义要获取当前对象中的哪些属性。解决方案已经很明显了,我们继承该基类,实现自己的CreateProperties方法,在CreateProperties方法中通过Attribute过滤需要序列化的属性集合返回即可。
代码实现
通过分析,我们推测使用自定义的ContractResolver,在内部判断属性上的Attribute值,来返回过滤后的对象属性集合就能实现我们想要的功能。
添加自定义ContractResolver,重写CreateProperties方法
public class MyContractResolver<T> : DefaultContractResolver where T : Attribute
{
private readonly Type _attributeToIgnore;
public MyContractResolver()
{
_attributeToIgnore = typeof(T);
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
// 过滤出那些没有Ignore掉的属性集合
var list = type.GetProperties()
.Where(x => x.GetCustomAttributes().All(a => a.GetType() != _attributeToIgnore))
.Select(p => new JsonProperty()
{
PropertyName = p.Name,
PropertyType = p.PropertyType,
Readable = true,
Writable = true,
ValueProvider = base.CreateMemberValueProvider(p)
}).ToList();
return list;
}
}
使用自定义ContractResolver
修改CustomFileDataProvider中的Serialize方法:
public override object Serialize<T>(T value)
{
if (value == null)
{
return null;
}
// 传入自定义的MyContractResolver对象并指定需要忽略的Attribute类型
var js = JsonConvert.SerializeObject(value, new JsonSerializerSettings
{
ContractResolver = new MyContractResolver<UnAuditableAttribute>()
});
return JToken.FromObject(js);
}
测试结果
首先我们修改对象Order,让它包含一个类对象属性:
public class OrderBase
{
[UnAuditable]
public string Name { get; set; }
}
public class Order : OrderBase
{
public Guid Id { get; set; }
[UnAuditable]
public string CustomerName { get; set; }
public int TotalAmount { get; set; }
public DateTime OrderTime { get; set; }
public Product Product { get; set; }
public Order(string name, Guid id, string customerName, int totalAmount, DateTime orderTime, Product product)
{
Id = id;
CustomerName = customerName;
TotalAmount = totalAmount;
OrderTime = orderTime;
Product = product;
Name = name;
}
public void UpdateOrderAmount(int newOrderAmount)
{
TotalAmount = newOrderAmount;
}
public void UpdateName(string name)
{
CustomerName = name;
}
}
public class Product
{
[UnAuditable]
public string ProductName { get; set; }
public int ProductPrice { get; set; }
public Guid ProductId { get; set; }
}
修改Main方法:
static void Main(string[] args)
{
ConfigureAudit();
var order = new Order("BaseName", Guid.NewGuid(), "Jone Doe", 100, DateTime.UtcNow, new Product
{
ProductId = Guid.NewGuid(),
ProductName = "Some Product Name",
ProductPrice = 30
});
using (var scope = AuditScope.Create("Order::Update", () => order))
{
order.UpdateOrderAmount(200);
order.UpdateName(null);
// optional
scope.Comment("this is a test for update order.");
}
}
运行程序,查看记录的审计日志:
$ cat Order::Update_637409019020799770.json
{
"EventType": "Order::Update",
"Environment": {
"UserName": "yu.li1",
"MachineName": "Yus-MacBook-Pro",
"DomainName": "Yus-MacBook-Pro",
"CallingMethodName": "TryCustomAuditNet.Program.Main()",
"AssemblyName": "TryCustomAuditNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Culture": ""
},
"Target": {
"Type": "Order",
"Old": "{\"Id\":\"7f5f26af-fc09-45cf-9f2f-349d6a2a962c\",\"TotalAmount\":100,\"OrderTime\":\"2020-11-13T14:05:01.684625Z\",\"Product\":{\"ProductPrice\":30,\"ProductId\":\"7f3e2c16-fe20-43cc-ae18-594ddcb77ca9\"}}",
"New": "{\"Id\":\"7f5f26af-fc09-45cf-9f2f-349d6a2a962c\",\"TotalAmount\":200,\"OrderTime\":\"2020-11-13T14:05:01.684625Z\",\"Product\":{\"ProductPrice\":30,\"ProductId\":\"7f3e2c16-fe20-43cc-ae18-594ddcb77ca9\"}}"
},
"Comments": [
"this is a test for update order."
],
"StartDate": "2020-11-13T14:05:01.694775Z",
"EndDate": "2020-11-13T14:05:02.075199Z",
"Duration": 380
}
那几个添加了UnAuditableAttribute的属性已经不在我们的日志中了,收工,回家过周末。
总结
本文对应的代码在这里。
.NET Core工程应用系列(2) 实现可配置Attribute的Json序列化方案的更多相关文章
- .NET Core工程应用系列(1) 定制化Audit.NET实现自定义AuditTarget
需求背景 最近在项目上需要增加对用户操作进行审计日志记录的功能,调研了一圈,在.net core生态里,用的最多的是Audit.NET.浏览完这个库的文档后,觉得大致能满足我们的诉求,于是建立一个控制 ...
- 小解系列-自关联对象.Net MVC中 json序列化循环引用问题
自关联对象在实际开发中用的还是比较多,例如常见的树形菜单.本文是自己实际的一个小测试,可以解决循环引用对象的json序列化问题,文笔不好请多见谅,如有错误请指出,希望有更好的解决方案,一起进步. 构造 ...
- 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之三 —— 配置
==== 目录 ==== 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之一 —— 开篇 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之二 —— ...
- 《ASP.NET Core 高性能系列》关于.NET Core的配置信息的若干事项
1.配置文件的相关闲话 Core自身对于配置文件不是必须品,但由上文分析可知ASP.NET Core默认采用appsettings.json作为配置文件,关于配置信息的优先等级 命令行>环境变量 ...
- Net core学习系列(九)——Net Core配置
一.简介 NET Core为我们提供了一套用于配置的API,它为程序提供了运行时从文件.命令行参数.环境变量等读取配置的方法.配置都是键值对的形式,并且支持嵌套,.NET Core还内建了从配置反序列 ...
- spring cloud+dotnet core搭建微服务架构:配置中心(四)
前言 我们项目中有很多需要配置的地方,最常见的就是各种服务URL地址,这些地址针对不同的运行环境还不一样,不管和打包还是部署都麻烦,需要非常的小心.一般配置都是存储到配置文件里面,不管多小的配置变动, ...
- spring cloud+dotnet core搭建微服务架构:配置中心续(五)
前言 上一章最后讲了,更新配置以后需要重启客户端才能生效,这在实际的场景中是不可取的.由于目前Steeltoe配置的重载只能由客户端发起,没有实现处理程序侦听服务器更改事件,所以还没办法实现彻底实现这 ...
- EntityFramework Core 学习系列(一)Creating Model
EntityFramework Core 学习系列(一)Creating Model Getting Started 使用Command Line 来添加 Package dotnet add pa ...
- 13.翻译系列:Code-First方式配置多对多关系【EF 6 Code-First系列】
原文链接:https://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code- ...
随机推荐
- [luogu5616]恶魔之树
记录$lcm$的质因子状态(包括大于$\sqrt 300$的质因子),设$f[s]$表示质因子状态为$s$的$lcm$之和,转移枚举当前的数$k$,转移到$lcm(s,k)$即可,时间复杂度为$o(n ...
- [nowcoder5668J]Operating on the Tree
考虑令$a_{i}$为i的位置,$p_{i}=0/1$表示第i个点的贡献,那么$p_{x}=0$当且仅当存在与其相邻的点$y$满足$a_{y}<a_{x}$且$p_{y}=1$ 树形dp,定义状 ...
- [hdu6761]Minimun Index
$lyndon\ word$(以下简写为Lw):对于一个字符串s,其为Lw当且仅当其的最小后缀为自身 性质:若$u<v$为LW,那么$uv$也为Lw(反证法即可证) $lyndon$分解:将一个 ...
- 接上篇:Git Worktree 高级使用,这样清爽多了
前言 上一篇文章 Git Worktree 大法真香 带大家了解了 git worktree 是如何帮助我同时在多个分支工作,并且互不影响的.但是创建 worktree 的目录位置不是在当前项目下,总 ...
- springboot项目中常遇到的问题-初学者最容易犯的错
1.在spring中有两个main方法 2.在idea中少提代码类了,或者某类中代码依赖关系没解决掉
- 【AGC052A】
题目 \(有T组询问\) \(每次给一个n\) \(3个01串\),\(这三个串每个都有n个0\),\(n个1\),\(求一个2*n + 1的字符串\),\(使他成为S1+S1,S2+S2,S3+S3 ...
- Codeforces 193E - Fibonacci Number(打表找规律+乱搞)
Codeforces 题目传送门 & 洛谷题目传送门 蠢蠢的我竟然第一眼想套通项公式?然鹅显然 \(5\) 在 \(\bmod 10^{13}\) 意义下并没有二次剩余--我真是活回去了... ...
- 【GS文献】测序时代植物复杂性状育种之基因组选择
综述:Genomic Selection in the Era of Next Generation Sequencing for Complex Traits in Plant Breeding 要 ...
- FASTA/Q序列处理神器---seqkit
该软件对于处理FASTA/Q十分方便,省去自己编写脚本 安装 1 conda install seqkit 使用 序列操作(seq) 1 ## 取方向序列 2 seqkit seq test.fa - ...
- php-fpm一个PHPFastCGI进程管理器
PHP-FPM(FastCGI Process Manager:FastCGI进程管理器)是一个PHPFastCGI管理器,对于PHP 5.3.3之前的php来说,是一个补丁包 [1] ,旨在将Fa ...