背景
在这篇文章中,我们实现了基于自定义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,则使用我们指定的,否则使用默认的Resolverserializer.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);// optionalscope.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}
那几个添加了
UnAuditable
Attribute的属性已经不在我们的日志中了,收工,回家过周末。
总结
本文对应的代码在这里。