背景

这篇文章中,我们实现了基于自定义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方法,以KeyValueConverterWriteJson方法为例:

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序列化方案的更多相关文章

  1. .NET Core工程应用系列(1) 定制化Audit.NET实现自定义AuditTarget

    需求背景 最近在项目上需要增加对用户操作进行审计日志记录的功能,调研了一圈,在.net core生态里,用的最多的是Audit.NET.浏览完这个库的文档后,觉得大致能满足我们的诉求,于是建立一个控制 ...

  2. 小解系列-自关联对象.Net MVC中 json序列化循环引用问题

    自关联对象在实际开发中用的还是比较多,例如常见的树形菜单.本文是自己实际的一个小测试,可以解决循环引用对象的json序列化问题,文笔不好请多见谅,如有错误请指出,希望有更好的解决方案,一起进步. 构造 ...

  3. 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之三 —— 配置

    ==== 目录 ==== 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之一 —— 开篇 跟我学: 使用 fireasy 搭建 asp.net core 项目系列之二 —— ...

  4. 《ASP.NET Core 高性能系列》关于.NET Core的配置信息的若干事项

    1.配置文件的相关闲话 Core自身对于配置文件不是必须品,但由上文分析可知ASP.NET Core默认采用appsettings.json作为配置文件,关于配置信息的优先等级 命令行>环境变量 ...

  5. Net core学习系列(九)——Net Core配置

    一.简介 NET Core为我们提供了一套用于配置的API,它为程序提供了运行时从文件.命令行参数.环境变量等读取配置的方法.配置都是键值对的形式,并且支持嵌套,.NET Core还内建了从配置反序列 ...

  6. spring cloud+dotnet core搭建微服务架构:配置中心(四)

    前言 我们项目中有很多需要配置的地方,最常见的就是各种服务URL地址,这些地址针对不同的运行环境还不一样,不管和打包还是部署都麻烦,需要非常的小心.一般配置都是存储到配置文件里面,不管多小的配置变动, ...

  7. spring cloud+dotnet core搭建微服务架构:配置中心续(五)

    前言 上一章最后讲了,更新配置以后需要重启客户端才能生效,这在实际的场景中是不可取的.由于目前Steeltoe配置的重载只能由客户端发起,没有实现处理程序侦听服务器更改事件,所以还没办法实现彻底实现这 ...

  8. EntityFramework Core 学习系列(一)Creating Model

    EntityFramework Core 学习系列(一)Creating Model Getting Started 使用Command Line 来添加 Package  dotnet add pa ...

  9. 13.翻译系列:Code-First方式配置多对多关系【EF 6 Code-First系列】

    原文链接:https://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code- ...

随机推荐

  1. 【Golang】基于beego/orm实现相同表结构不同表名的分表方法实现

    一.背景 在业务场景开发的过程中, 随着数据量的增加,相同表结构不同表名的分表策略是常用的方案选择之一.如下以golang做为后端业务开发,尝试修改beego的orm库做一个相同表结构不同表名的分表实 ...

  2. ound interface org.elasticsearch.common.bytes.BytesReference, but class was expected

    es得版本和本地项目不一致.. 配置es版本,现在使用得是5.2得版本,可是 maven上看到 elasticsearch-rest-high-level-client 最低也得6版本.下载安装高版本 ...

  3. Codeforces 1225G - To Make 1(bitset+状压 dp+找性质)

    Codeforces 题目传送门 & 洛谷题目传送门 还是做题做太少了啊--碰到这种题一点感觉都没有-- 首先我们来证明一件事情,那就是存在一种合并方式 \(\Leftrightarrow\) ...

  4. 生物信息Linux用户创建与配额设置

    创建一个新用户,并配置使用. create_usr.sh: #/usr/bin/bash user=$1 password="123" useradd ${user} -g met ...

  5. 数据分析体系 — 用户粘性的两个计算指标(DAU/MAU和月人均活跃天数)

    很多运营都了解DAU(日活跃用户数)和MAU(月活跃用户数)的重要性,但在某些情况下这两个数值本身并不能反映出太多问题,这个时候就要引用到[DAU/MAU]的概念,即[日活/月活] 用户粘性的两个计算 ...

  6. mysql 实现某年单季度内的品牌TOPn销量在此年此单季度内销量占比

    数据表:       结果表: mysql语句:  

  7. 非标准的xml解析器的C++实现:一、思考基本数据结构的设计

    前言: 我在C++项目中使用xml作为本地简易数据管理,到目前为止有5年时间了,从最初的全文搜索标签首尾,直到目前项目中实际运用的类库细致到已经基本符合w3c标准,我一共写过3次解析器,我自己并没有多 ...

  8. RTSP, RTP, RTCP, RTMP傻傻分不清?

    RTSP基于TCP传输请求和响应报文,RTP基于UDP传输流媒体数据,RTCP基于UDP传送传输质量信息(如丢包和延迟). 比如喀什一个局域网内10个人同时点播广州的同一个源,喀什和广州之间就要传10 ...

  9. Scala【需求二:求各省市的各个指标】

    需求处理步骤 原始数据->json->过滤->列裁剪 需求二:求各省市的各个指标 原始数据 文本pmt.json,每一行都是一个json字符串.里面包含ip等信息 {"se ...

  10. 从面试官的角度,聊聊java面试流程

    在这篇回答里,就讲以我常规的面试流程为例,说下java方面大致会问什么问题,以及如何确认候选人达到招聘要求. 先说面试前准备,可能有些面试官是拿到简历直接问,而且是在候选人自我介绍时再草草浏览简历,但 ...