【问题标题】:Create an Attribute which will treat the decorated method as a delegate创建一个将装饰方法视为委托的属性
【发布时间】:2020-01-09 03:22:31
【问题描述】:

序言/上下文

所以目前我有一个 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] 并仅在实际方法块中显示核心实现,那就太好了。

【问题讨论】:

  • MVC 属性的工作方式是,框架中有代码查找这些属性并激活它们。你需要在这里做同样的事情,所以使用属性并不能真正给你带来太多好处。
  • 是的,它看起来像。我可以[添加反射] (docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…),但随后我会在每个方法开始时调用 Log 和 Lambda-ing 我的方法主体来调用属性。这几乎是 AOP 提供的没有代码混乱的东西。
  • 我正在写一个答案,你可以考虑一个替代方案。
  • 嘿,我已经为这个(以及更广泛的)目的创建了这个库。如果您的应用程序至少与 netstandard2.0 兼容,您可以使用 2.x,否则使用 1.x 版本。 github.com/pamidur/aspect-injector

标签: c# reflection


【解决方案1】:

首先,我想谈谈使用属性方法会遇到的问题。假设您有一个方法 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 的方法的名称。您可以更改获取方法名称 by using expressions 的方式,但这意味着您的 lambda 调用只能是单个方法调用,而不是方法组或属性访问。

我认为您过度使用异常处理。我建议你捕获异常at the highest level where you can make a decision about how to handle them。在那里,您将拥有调用堆栈,它将指示异常发生的位置。您通常不需要记录异常发生的位置,除非您需要记录变量以了解异常的原因。 (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,它以layout renderers 提供这些。我将向您展示使用 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");
        }
    }
}  

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

为了您的方便,这里是the official NLog tutorial

【讨论】:

  • 感谢您解释为什么这不起作用。似乎如果无法访问 AOP,我将无法实现我所要求的,因为我所要求的内容是 AOP 专门涵盖的,没有其他解决方法。至少根据您提供的信息,我可以理解为什么这不会是我宝贵的时间。
  • 虽然指向 NLog 和 Log4Net 的指针很有帮助,但这并不是本文的重点。我希望能够通过属性包含许多委托包装方法,而不仅仅是日志记录。
  • @Imbaker1234 我明白了。如果你真的想要使用属性,你可以使用 DynamicProxy 之类的东西,但它有很多限制,比如只能拦截虚拟方法。对您的应用程序的更改将更加实质性。或者如果你有钱,PostSharp。
  • 是否可以在 DLL 中使用 DynamicProxy?我编写的大部分代码都是用于替换或扩展遗留应用程序的补充 DLL。
  • @Imbaker1234 你会遇到一些限制。您熟悉适配器模式吗?它的工作原理类似:您获取某个对象的实例,用代理拦截器包装它,然后在您使用该类的地方使用拦截器。在代理对象上调用方法将改为调用拦截器。您可以随意包装 actual 方法调用。但是,您只能拦截虚拟方法和属性。不过,拦截器将能够扫描属性并按照指示使用它们,因此您的属性使用会更加透明。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-03
  • 1970-01-01
  • 1970-01-01
  • 2012-06-04
相关资源
最近更新 更多