Marten中自定义值类型ReferenceNumber的查询问题及IMemberSource实现报错求助
我最近在使用Marten做文档数据库查询时,碰到了自定义值类型的查询支持问题,折腾了好一阵还是没搞定,来求助大家。
先说说我的场景:
我定义了一个只读记录结构体ReferenceNumber用来封装字符串,作为Root类的一个属性:
public class Root { public ReferenceNumber Ref { get; set; } } public readonly record struct ReferenceNumber { private readonly string _referenceNumber; public ReferenceNumber(in string referenceNumber) { _referenceNumber = referenceNumber.Trim(); } public static implicit operator ReferenceNumber(in string value) => new(value); public static implicit operator string(in ReferenceNumber reference) => reference._referenceNumber; public override string ToString() => _referenceNumber; }
然后我尝试用LINQ查询Root实体,匹配ReferenceNumber:
await using IDocumentSession session = _readDocumentStore.LightweightSession(); return await session.Query<Root>() .FirstOrDefaultAsync(x => x.Ref == referenceNumber);
不出意外报错了,Marten提示不支持LINQ表达式里的自定义值类型:
Marten cannot support custom value types in LINQ expression. Please query on either simple properties of the value type, or register a custom IMemberSource for this value type.
按照提示,我需要要么查值类型的简单属性,要么实现自定义的IMemberSource。但我找了一圈,Google上关于IMemberSource的例子只有3条结果,官方文档也没详细说明,只说去Discord讨论。无奈之下我借助Copilot生成了一个实现:
public class QueryableMember : IQueryableMember { public string JsonPathSegment { get; set; } public string MemberName { get; set; } public Type MemberType { get; set; } public Type DeclaringType { get; set; } public string Name { get; set; } public bool IsAsync { get; set; } public MemberInfo MemberInfo { get; set; } public IQueryableMember Parent { get; set; } public string TypedLocator => $"CAST({RawLocator} as text)"; public string RawLocator => $"d.data -> {SqlFragment()}"; public string JSONBLocator => $"d.data ->> {SqlFragment()}"; public IQueryableMember[] Ancestors => Parent == null ? [] : [.. Parent.Ancestors, Parent]; public string LocatorForIncludedDocumentId => $"d.data ->> {SqlFragment()}"; public string NullTestLocator => $"d.data -> {SqlFragment()} IS NULL"; private string SqlFragment() { return $"'{JsonPathSegment}'"; } public void Apply(ICommandBuilder builder) { builder.Append(JSONBLocator); } public string BuildOrderingExpression(Ordering ordering, CasingRule casingRule) { string direction = ordering.Direction == OrderingDirection.Asc ? "asc" : "desc"; return $"{JSONBLocator} {direction}"; } public Dictionary<string, object> FindOrPlaceChildDictionaryForContainment(Dictionary<string, object> dict) { if (!dict.TryGetValue(JsonPathSegment, out object? value)) { value = new Dictionary<string, object>(); dict[JsonPathSegment] = value; } return value as Dictionary<string, object>; } public void PlaceValueInDictionaryForContainment(Dictionary<string, object> dict, ConstantExpression constant) { // For ReferenceNumber, convert to string using ToString() string value = constant.Value?.ToString() ?? string.Empty; dict[JsonPathSegment] = value; } public string SelectorForDuplication(string pgType) { return $"{RawLocator} {pgType}"; } } public class ReferenceNumberMemberSource : IMemberSource { public bool TryResolve( IQueryableMember parent, StoreOptions options, MemberInfo memberInfo, Type memberType, [NotNullWhen(true)] out IQueryableMember? member) { member = new QueryableMember { Parent = parent, DeclaringType = memberInfo.DeclaringType!, MemberType = memberType, Name = memberInfo.Name, MemberName = memberInfo.Name, MemberInfo = memberInfo, IsAsync = false, JsonPathSegment = $"$.{memberInfo.Name}", }; return true; } }
然后我把这个ReferenceNumberMemberSource注册到Marten的配置里,结果启动应用又报了新的错误:
Id of type System.Guid cannot be used as a Duplicated Field by Marten (Parameter 'members')
我完全搞不懂这个错误是怎么来的,明明我只处理ReferenceNumber类型啊。
其实我的需求特别简单:ReferenceNumber本质就是封装了一个字符串,我已经通过JsonConverter把它在数据库里存成了普通字符串(不是嵌套对象),我只是希望在LINQ查询里能把x.Ref == referenceNumber当成普通的字符串比较来处理,就像直接用string类型的属性一样。
有没有大佬能指点一下:
- 有没有比实现
IMemberSource更简单的方案来满足我的需求? - 如果必须用
IMemberSource,我当前的实现哪里有问题,怎么解决那个Guid相关的报错?
内容来源于stack exchange

