背景

这篇文章中,我们实现了基于自定义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. [bzoj4553]序列

    记第i个位置有三个属性:1.ai表示原来的值:2.bi表示变成最大的值:3.ci表示变成最小的值.那么对于如果i在j的前面,那么必然有:$ai\le cj$且$bi\le aj$,那么令f[i]表示以 ...

  2. 【k8s】在AWS EKS部署并通过ALB访问k8s Dashboard保姆级教程

    本教程适用范围 在AWS上使用EKS服务部署k8s Dashboard,并通过ALB访问 EKS集群计算节点采用托管EC2,并使用启动模板. 使用AWS海外账号,us-west-2区域 使用账号默认v ...

  3. IDEA:Git stash 暂存分支修改的代码

    IDEA:Git stash 暂存分支修改的代码 场景:当我们正在master分支开发新功能的时候,突然接到一个任务发现线上出现了一个紧急的BUG需要修复,由于没有打新分支做这部分新需求,这时正做到半 ...

  4. javaweb监听

    监听项目启动 package com.java7115.quartz; import javax.servlet.ServletContextEvent; import javax.servlet.S ...

  5. 洛谷 P5233 - [JSOI2012]爱之项链(Polya 定理+递推)

    洛谷题面传送门 首先很明显题目暗示我们先求出符合条件的戒指数量,再计算出由这些戒指能够构成的项链的个数,因此考虑分别计算它们.首先是计算符合条件的戒指数量,题目中"可以通过旋转重合的戒指视作 ...

  6. Codeforces 643F - Bears and Juice(思维题)

    Codeforces 题目传送门 & 洛谷题目传送门 首先直接暴力枚举显然是不现实的,我们不妨换个角度来处理这个问题,考虑这 \(R_i\) 个瓶子中每一瓶被哪些熊在哪一天喝过. 我们考虑对这 ...

  7. 洛谷 P4749 - [CERC2017]Kitchen Knobs(差分转换+dp,思维题)

    题面传送门 一道挺有意思的思维题. 首先有一个 obvious 的结论,就是对于每个炉子,要么转到哪里都符合条件,要么存在唯一的最大值.对于转到哪儿都符合条件的炉子我们 duck 不必考虑它,故我们只 ...

  8. 【Perl】如何安装Bioperl模块?

    目录 失败尝试一:使用cpanm 失败尝试二:使用CPAN 成功尝试:直接conda安装bioperl 没有尝试:源码安装bioperl 生信软件绕不过Perl,Perl绕不过Bioperl.而Bio ...

  9. 半主机模式和_MICROLIB 库

    半主机是这么一种机制,它使得在ARM目标上跑的代码,如果主机电脑运行了调试器,那么该代码可以使用该主机电脑的输入输出设备.   这点非常重要,因为开发初期,可能开发者根本不知道该 ARM 器件上有什么 ...

  10. C语言中的main函数的参数解析

    main()函数既可以是无参函数,也可以是有参的函数.对于有参的形式来说,就需要向其传递参数.但是其它任何函数均不能调用main()函数.当然也同样无法向main()函数传递,只能由程序之外传递而来. ...