实现 EF Core 6 自定义查询标记

前言

在《EF Core使用Simple Logging输出日志》中,我们介绍了查询标记 TagWith,它可以帮助我们快速定位到需要的日志:

而在 .NET 6 中,新增了另外一个查询标记 TagWithCallSite,它可以标记出代码的位置:.

var user = await new DefaultDbContext().User
    .Where(p => p.Name == "My IO")
    .TagWith("Find My IO")
    .TagWithCallSite()
    .FirstOrDefaultAsync();

实现 EF Core 6 自定义查询标记

那它是怎么做到的呢?

原理探究

查看 TagWithCallSite 的源代码:

public static IQueryable<T> TagWithCallSite<T>(
        this IQueryable<T> source,
        [NotParameterized] [CallerFilePath] string? filePath = null,
        [NotParameterized] [CallerLineNumber] int lineNumber = 0)

原来,它使用了 CallerFilePathAttribute 和 CallerLineNumberAttribute 来获取包含调用方的源文件完整路径和调用方法的行号。

原理利用

这让我们想到了,在《.NET 6新特性试用 | ArgumentNullException卫语句》中发现的 CallerArgumentExpressionAttribute,它可以获取执行的表达式。

通过添加此 Attribute,我们可以创建自己的自定义查询标记。实现代码如下:

public static IQueryable<T> TagWithCallInfo<T>(this IQueryable<T> source,
            string? tag = null,
            [NotParameterized][CallerArgumentExpression("source")] string? argument = null,
            [NotParameterized][CallerMemberName] string? memberName = null,
            [NotParameterized][CallerFilePath] string? filePath = null,
            [NotParameterized][CallerLineNumber] int lineNumber = 0)
{
    var stringBuilder = new StringBuilder();

    stringBuilder.AppendLine(tag);

    foreach (var str in argument.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries))
    {
        stringBuilder.AppendLine(str);
    }
    stringBuilder.AppendLine($@"at {memberName}");
    stringBuilder.AppendLine($@"File: {filePath}:{lineNumber}");

    return source.TagWith(stringBuilder.ToString());
}

该方法不仅包含自定义标记文本,还自动包括了被调用的 LINQ 查询表达式、方法名称、文件路径、行号。

Demo

运行下列代码进行验证,完全满足了我们的要求:

var user = await new DefaultDbContext().User
    .Where(p => p.Name == "My IO")    
    .TagWithCallInfo("Find My IO")
    .FirstOrDefaultAsync();

实现 EF Core 6 自定义查询标记

结论

今天,通过扩展 TagWithCallSite ,我们实现了自定义查询标记。