如何为record类统一配置validateString的Serilog泛型日志上下文?
我有多个类似下面的Record类型,都继承自MavApiRecordBase:
internal record Carrier : MavApiRecordBase{ /// <summary> /// Identificativo Carrier /// </summary> [JsonPropertyName("id")] public Int64 Id { get; set; } /// <summary> /// Tag (nome) del Carrier /// </summary> [JsonPropertyName("tag")] public string Name { get => Name; set => Name = validateString<Carrier>(value, 64); } /// <summary> /// Tag (nome) del Payload a cui il carrier risulta associato. null se non associato ad alcun payload. /// </summary> [JsonPropertyName("payload")] public string? PayloadTag { get => Name; set => Name = validateString<Carrier>(value, 64); } }
其中validateString是多个Record共享的静态方法,它的泛型参数仅用于Serilog的Log.ForContext<TLogContext>(),用来标记日志的上下文类型。
现在我每次要修改某个Record里所有validateString<>的泛型参数,只能用Visual Studio的查找替换,操作很麻烦。我想把这个泛型类型绑定到某个可统一修改的静态变量/属性,比如在抽象基类MavApiRecordBase里定义一个可重写的protected成员:
protected override Type defLogCtx {get; set;} = typeof(Carrier); // 期望能这样调用 validateString<defLogCtx>(..) // 在Carrier类内部,这样改一次就能统一所有调用 // 或者干脆去掉泛型参数
当前validateString的实现如下:
public static string validateString<TLogContext>(string? str, int maxLen) { if (str is null) Log.ForContext<TLogContext>().Error("rogue null reference. used empty string instead."); else if (str.Length > maxLen) Log.ForContext<TLogContext>().Warning("max string length is {l}. Got {val}.", maxLen, str); return str.Truncate(maxLen) ?? ""; }
我刚接触C#,想知道这种思路是否可行,有没有更合理的实现方式。
解决方案说明
首先明确:直接用变量作为泛型参数不可行
C#的泛型参数必须是编译期确定的常量类型,不能用运行时的Type变量(比如你定义的defLogCtx)来作为泛型参数,所以validateString<defLogCtx>(..)这种写法是编译不通过的。需要调整实现方式:
方案1:将validateString改为非泛型,接收Type参数
修改validateString,把泛型参数换成Type类型的参数,同时使用Serilog的非泛型ForContext方法:
public static string ValidateString(string? str, int maxLen, Type logContextType) { if (str is null) Log.ForContext(logContextType).Error("rogue null reference. used empty string instead."); else if (str.Length > maxLen) Log.ForContext(logContextType).Warning("max string length is {l}. Got {val}.", maxLen, str); return str.Truncate(maxLen) ?? ""; }
然后在基类里定义一个可重写的属性,默认返回当前子类的类型:
public abstract class MavApiRecordBase { protected virtual Type LogContextType => GetType(); }
最后在子类的属性里调用时,直接传入这个属性值即可:
internal record Carrier : MavApiRecordBase { [JsonPropertyName("id")] public long Id { get; set; } // 注意:你原来的get/set写法会导致无限递归,必须修正! private string _name = ""; [JsonPropertyName("tag")] public string Name { get => _name; set => _name = ValidateString(value, 64, LogContextType); } private string? _payloadTag; [JsonPropertyName("payload")] public string? PayloadTag { get => _payloadTag; set => _payloadTag = ValidateString(value, 64, LogContextType); } }
方案2:使用泛型基类封装调用
如果不想修改validateString的泛型实现,可以用泛型基类来统一指定日志上下文类型:
public abstract class MavApiRecordBase<T> where T : MavApiRecordBase<T> { protected string ValidateString(string? str, int maxLen) { return YourStaticClass.validateString<T>(str, maxLen); } }
子类继承时只需指定自身作为泛型参数,之后所有属性里直接调用封装好的实例方法即可:
internal record Carrier : MavApiRecordBase<Carrier> { [JsonPropertyName("id")] public long Id { get; set; } private string _name = ""; [JsonPropertyName("tag")] public string Name { get => _name; set => _name = ValidateString(value, 64); } private string? _payloadTag; [JsonPropertyName("payload")] public string? PayloadTag { get => _payloadTag; set => _payloadTag = ValidateString(value, 64); } }
额外注意:修正原代码中的无限递归问题
你原来的属性写法get => Name; set => Name = ...会导致无限递归:get访问Name属性本身,set又赋值给Name属性,运行时会直接栈溢出。必须改成用私有字段存储值,或者使用自动属性结合初始化逻辑。
内容的提问来源于stack exchange,提问作者Andrea Bardelli

