使用Source Generators修改属性的Get和Set的值
前言
博主最近遇到了一个需求, 需要对返回实体中的部分字段数据做脱敏处理, 对于这个需求,基本上有三个方案可以实现:
- 在每个需要脱敏属性的Set方法中调用脱敏方法, 该方法的缺点是在很多地方都会出现脱敏方法的调用, 也会让属性的实现变的复杂, 所以一开始就被Pass了。
- 通过AOP的方式实现
- 通过Source Generators的方式实现
方案一、每个Set中调用脱敏方法
public class ExampleModel
{
private string _fullName;
public string FullName
{
get
{
return _fullName;
}
set
{
_fullName = 脱敏方法(value)
}
}
}
由上述示例代码可以看到, 整体的代码相对冗余, 不够简洁。
方案二、通过AOP的方式实现
public class ExampleModel
{
[脱敏Attribute]
public string FullName { get; set; }
}
public class 脱敏Attribute : Attribute
{
public void OnSetValue(object value)
{
value = 脱敏方法(value);
}
}
注意: 以上内容只是类似于伪代码的实现
通过对比方案二和方案一, 可以发现方案二的实现比方案一简洁的多, 同时使用上也方便的多, 对于需脱敏的属性, 只要增加特性即可。
这个方案的缺点是, .Net框架中本身并没有自带AOP框架的实现,需要引入第三方的AOP框架。
方案三、通过Source Generators的方式实现
1. 实体类
public partial class ExampleModel
{
[脱敏Attribute]
private string _fullName;
}
注意1: 实体类加上了partial修饰符,这是一个部分类
注意2: 实体类中包含的是一个私有属性
2. Source Generators类:
Source Generators的使用方式,请参照:Source Generators 探索
namespace SourceGeneratorSamples
{
[Generator]
public class DesensitizationGenerator : ISourceGenerator
{
/// <summary>
/// 特性类代码
/// </summary>
private const string attributeText = @"
using System;
namespace Desensitization
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
sealed class DesensitizationAttribute : Attribute
{
public DesensitizationAttribute()
{
}
}
}
";
public void Initialize(GeneratorInitializationContext context)
{
//语法树
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
//生成特性类代码
context.AddSource("DesensitizationAttribute", attributeText);
if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
return;
CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));
INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("Desensitization.DesensitizationAttribute");
//循环所有属性
List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
{
SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
{
IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
//获取包含了指定特性的属性
if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
{
fieldSymbols.Add(fieldSymbol);
}
}
}
//根据包含属性的类, 生成对应的部分类
foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
{
string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, context);
context.AddSource($"{group.Key.Name}_desensitization.cs", classSource);
}
}
/// <summary>
/// 生成实体类代码
/// </summary>
/// <param name="classSymbol"></param>
/// <param name="fields"></param>
/// <param name="attributeSymbol"></param>
/// <param name="context"></param>
/// <returns></returns>
private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, GeneratorExecutionContext context)
{
if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
{
return null;
}
string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
//开始生成部分类
StringBuilder source = new StringBuilder($@"
namespace {namespaceName}
{(请删除){
public partial class {classSymbol.Name}
{(请删除){
");
//生成部分类的每一个字段
foreach (IFieldSymbol fieldSymbol in fields)
{
ProcessField(source, fieldSymbol, attributeSymbol);
}
source.Append("} }");
return source.ToString();
}
/// <summary>
/// 生成属性代码
/// </summary>
/// <param name="source"></param>
/// <param name="fieldSymbol"></param>
/// <param name="attributeSymbol"></param>
private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
{
string fieldName = fieldSymbol.Name;
ITypeSymbol fieldType = fieldSymbol.Type;
AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;
string propertyName = chooseName(fieldName, overridenNameOpt);
if (propertyName.Length == 0 || propertyName == fieldName)
{
return;
}
source.Append($@"
public {fieldType} {propertyName}
{(请删除){
get
{(请删除){
return this.{fieldName};
}(请删除)}
set
{(请删除){
this.{fieldName} = 脱敏方法(value);
}(请删除)}
}}
");
//根据私有属性名(如:_fullName),转换为公有属性名(如:FullName)
string chooseName(string fieldName, TypedConstant overridenNameOpt)
{
if (!overridenNameOpt.IsNull)
{
return overridenNameOpt.Value.ToString();
}
fieldName = fieldName.TrimStart('_');
if (fieldName.Length == 0)
return string.Empty;
if (fieldName.Length == 1)
return fieldName.ToUpper();
return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
}
}
/// <summary>
///
/// </summary>
class SyntaxReceiver : ISyntaxReceiver
{
public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
/// <summary>
///
/// </summary>
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// any field with at least one attribute is a candidate for property generation
if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
&& fieldDeclarationSyntax.AttributeLists.Count > 0)
{
CandidateFields.Add(fieldDeclarationSyntax);
}
}
}
}
}
3. 生成的代码1-脱敏的特性类
using System;
namespace Desensitization
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
sealed class DesensitizationAttribute : Attribute
{
public DesensitizationAttribute()
{
}
}
}
4. 生成的代码2-部分实体类
public partial class ExampleViewModel
{
public string FullName
{
get
{
return this._fullName;
}
set
{
this._fullName = 脱敏方法(value);
}
}
}
结合刚刚我们自定义的部分实体类, 我们可以发现实际上方法三的最终代码和方法一是一致的,只不过我们通过SourceGenerator实现了对冗余代码的生成, 而且这部分冗余代码在我们实际开发过程中是不可见的, 是在编译的过程中,由编译器生成代码并将这部分代码打包到DLL文件中,这样做的好处是:
- 代码和方案二一样简洁
- 无须引入第三方的框架
- 性能相对会比方案二更好