本次我们实现自定义键名映射功能,用户可以指定C#对象的属性和哪个Json键值对构成映射。
注意:示例代码使用了C#8.0的语法特性,如果要在你的机器上运行,请确保安装了.Net Core 3.x开发环境。
首先创建一个用于提示键名映射的特性:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class KeyAttribute : Attribute
{
public string KeyName { get; }
public KeyAttribute(string kname)
{
KeyName = kname;
}
}
KeyName属性表示的是Json中对应的键名,使用方法如下:
[Key("Horse")]
public string Deer { get; set; }
通过这样标记一个属性,我们就实现了传说中的“指鹿为马”。
我们创建一个AdvancedParser类来实现使用特性的高级功能,这个类的大多数方法都和PrimaryParser相同,目前只有ParseObject这个方法不同。为了增强可读性,AdvancedParser中还会引入一些私有方法。
首先是ParseObject方法,一切变动都起源于它:
private object ParseObject(Queue<Token> ctx, Type ttype)
{
...
while (true)
{
token = ctx.Dequeue();
CheckSyntax(ref token, TokenType.String);
key = token.value;
prop = GetPropertyInfo(ttype, key);
...
}
}
可以看到只有prop = GetPropertyInfo(ttype, key);这句发生了变化。在PrimaryParser中,这个值是直接通过获取对应名字的属性来获取的;而到了AdvancedParser中,这个过程就复杂了起来,因此将其单独提炼到一个方法中。
private PropertyInfo GetPropertyInfo(Type type, string key)
{
if (!keyAttributeBuffer.ContainsKey(type))
{
ScanKeyAttribute(type);
}
return keyAttributeBuffer[type].ContainsKey(key) ? keyAttributeBuffer[type][key] : type.GetProperty(key);
}
keyAttributeBuffer是一张缓存了键名映射关系的表:
private ConcurrentDictionary<Type, ConcurrentDictionary<string, PropertyInfo>> keyAttributeBuffer;
这里使用支持并发的字典,因为用户可能在多个线程中改写表的内容。
在GetPropertyInfo方法中,首先判断表中有没有注册对应的类型,如果没有,则进行注册。然后判断要获取的键是否在表中,如果在,说明用户使用了自定义键名映射,返回对应的属性;如果不在,说明用户没有自定义,则使用默认值。
下面来看一下ScanKeyAttribute方法:
private void ScanKeyAttribute(Type type)
{
keyAttributeBuffer[type] = new ConcurrentDictionary<string, PropertyInfo>();
KeyAttribute attr;
foreach (var item in type.GetProperties())
{
if ((attr = item.GetCustomAttribute<KeyAttribute>()) != null)
{
keyAttributeBuffer[type][attr.KeyName] = item;
}
}
}
这个方法对首次进行获取元数据操作的类进行注册,缓存Key特性的信息来提高以后的执行效率。它会遍历对应类型中的每个属性,如果这个属性有Key特性,则注册进表里。
自定义键名映射其实就是扩展了ParseObject过程中获取属性的逻辑,让程序能通过扫描特性来将不同名的属性和键值对关联起来,总体比较简单,容易理解。下次继续对语法分析器进行扩展,让它能够支持用户自定义的转换函数。