.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方法,以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的属性已经不在我们的日志中了,收工,回家过周末。

总结

本文对应的代码在这里


原文作者:Asinta
原文链接:https://www.asinta.cn/2020/11/13/Configuable-Json-Serialization-Using-Attribute-With-Newtonsoft-Json/
版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容