首页 > 解决方案 > 创建一个将装饰方法视为委托的属性

问题描述

序言/上下文

所以目前我有一个 Log 方法。

public T Log<T>(Func<T> func)
{
    string methodName = func.Method.Name;
    bool success = false;
    T product = default(T);

    Debug($"Entering {methodName}");

    try
    {
        product = func.Invoke();
        success = true;
    }
    catch (Exception e)
    {
        Info($"FAILURE: {methodName}: {e.Message}");
    }

    Debug($"{success.ToString().ToUpper()}: Exiting {methodName}");
    return product;
}

对我来说效果很好,这样称呼

Log(() => AddTwoNumbers(1, 2));

我一直在使用装饰器模式,并且基本上说接口的所有方法都调用它们的基(或包含的对象,或者您想要实现基方法),以便记录这些方法。

免责声明 在我的情况下,我无法轻松访问 AOP,因此生成处理此问题的代理并非易事。我主要是对遗留代码进行大修,并从 Visual Basic 6 EXE 中调用我的 .NET 库,据我所知,这些 AOP 框架中的大多数都需要直接合并到应用程序中。

我要解决的问题

很多时候我发现自己这样做:

public int AddFirstFourNumbers(params int[] numbahs) 
{
  Log(() => 
  {
    int product;
    product += numbahs[0];
    product += numbahs[1];
    product += numbahs[2];
    product += numbahs[3];

    return product;
  }
}

或者像这样装饰扩展类的所有成员

public override int AddTwoNumbers(int num1, int num2)
{
  return Log(() => base.AddTwoNumbers(num1, num2));
}

我想把它变成这样:

[Log]
public int AddFirstFourNumbers(params int[] numbahs) 
{
    int product;
    product += numbahs[0];
    product += numbahs[1];
    product += numbahs[2];
    product += numbahs[3];

    return product;
}

和这个

[Log]
public override int AddTwoNumbers(int num1, int num2)
{
  return base.AddTwoNumbers(num1, num2);
}

我对创建自定义属性不是很熟悉,但是有没有办法将此日志属性修饰的方法视为委托并将其传递给第一个块中描述的 Log 方法?我在 Microsoft Docs 上看到的示例似乎没有做任何接近我希望的事情。

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/creating-custom-attributes

同时我们有 MVC 验证属性,它似乎提供了大量有用的功能,所以这可能是可能的

我是 Action 和 Func Wrappers 的忠实粉丝,它们允许我装饰我的基本方法并分离出我的关注点。我只是在寻找一种更清洁的方法。如果能够仅添加[WithImpersonation]到方法的顶部或[UndoOnFailure]仅在实际方法块中显示核心实现,那就太好了。

标签: c#reflection

解决方案


首先,我想谈谈使用属性方法会遇到的问题。假设您有一个AddFirstFourNumbers用该属性装饰的方法Log,如您的问题中所述。

该属性作为方法上的元数据存在。而已。它包含的任何方法只有在您专门调用它们时才会被调用。换句话说:

var attrib = typeof(ClassWithAddFirstFourNumbers)  // get the attribute
    .GetMethod(nameof(AddFirstFourNumbers))
    .GetAttributes<LogAttribute>()
    .FirstOrDefault();

if (attrib != null)  attrib.BeforeLog();
AddFirstFourNumbers(somenumbers);
if (attrib != null)  attrib.AfterLog();

现在,您可以使用您的 lambda 方法来创建一个获取此属性的方法,但您必须使用表达式树方法(链接如下)从 lambda 调用中获取属性。现在你有一个没有真正原因的属性。不好玩。

我将此作为替代方案。我相信你可能想多了。

首先,您的Log方法在实现时不会获得您传递给 lambda 的方法的名称。您可以使用表达式更改获取方法名称的方式,但这意味着您的 lambda 调用只能是单个方法调用,而不是方法组或属性访问。

我认为您过度使用异常处理。我建议您在最高级别捕获异常,您可以决定如何处理它们。在那里,您将拥有调用堆栈,它将指示异常发生的位置。您通常不需要记录异常发生的位置,除非您需要记录变量以了解异常的原因。(KeyNotFoundException不会告诉您未找到哪个密钥!)正如您在问题中提出的那样,通用日志记录系统不会 - 不能 - 获取这些变量。

这是更主观的意见,但我觉得你有点想多了。这是我的建议。保持简单,只需在您的方法中添加一些日志记录:

public int AddFirstFourNumbers(params int[] numbahs) 
{
    int product;
    product += numbahs[0];
    product += numbahs[1];

    Debug("Still adding numbers but we'll require additional pylons soon");

    product += numbahs[2];
    product += numbahs[3];
    return product;
}

和:

var sum = AddSomeNumbers(somenumbers);
Debug($"{nameof(AddSomeNumbers)} on {somenumbers.Length} numbers -> {sum}");

这使您的日志消息具有更大的灵活性和实用性,并且不会比 lambda 方法更多地乱扔代码。这意味着您需要编写一条日志消息,但没有规定它们都需要保持一致并且完全包含多汁的调试提示。一些简单的东西Debug($"sum={sum}");仍然有用并且不需要太多时间来输入。

好的。最后一个想法可能会为您提供最大的好处来实现您想要实现的目标。我是否有以下规定?

  • 你想知道你从哪里登录。类和方法。甚至是行号。

  • 您想知道线程或用户。

  • 您希望能够在全球范围内添加额外的日志信息,而无需更改您的代码。

  • 您希望能够将额外的日志信息添加到特定的类或命名空间,而无需更改您的代码。

使用 NLog、log4net、Serilog 等库。他们都很好。所有这些都可以让你为你的日志消息定义一个全局模式:类名、方法名、线程 id、用户名、时间戳、环境变量等。我最熟悉 NLog,它提供这些作为布局渲染器。我将向您展示使用 NLog 是如何工作的。

public class YourExampleClass
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    public void ExampleMethod()
    {
        try
        {
           Logger.Info("Hello world");
        }
        catch (Exception ex)
        {
           Logger.Error(ex, "Goodbye cruel world");
        }
    }
}  

使用该代码,我指定的其他信息(例如位置和时间戳)将使用布局渲染器添加到配置中。整齐吧?这不需要对您的程序进行大规模更改。您可以从需要登录的类中登录,其他类不会被更改。

为了您的方便,这里是官方的 NLog 教程


推荐阅读