C#: 优雅的方法调用封装

15

抱歉标题可能有些模糊,但我想要实现的目标通过代码更好地表述。

我有一个WCF客户端,当我调用方法时,我希望每个调用都被包装在一些错误处理代码中。因此,我没有直接暴露方法,而是在客户端类上创建了以下辅助函数:

    public T HandleServiceCall<T>(Func<IApplicationService, T> serviceMethod)
    {
        try
        {
            return serviceMethod(decorator);
        }
        [...]
    }

客户端代码使用它如下:

service.HandleServiceCall(channel => channel.Ping("Hello"));

调用Ping方法的过程被封装在一些逻辑中,以便尝试处理任何错误。

这很好用,但我现在有一个需求,需要知道实际上在服务上调用了哪些方法。最初,我希望使用表达式树来检查Func<IApplicationService, T>,但进展不大。

最后,我采用了装饰器模式:

    public T HandleServiceCall<T>(Func<IApplicationService, T> serviceMethod)
    {
        var decorator = new ServiceCallDecorator(client.ServiceChannel);
        try
        {
            return serviceMethod(decorator);
        }
        [...]
        finally
        {
            if (decorator.PingWasCalled)
            {
                Console.Writeline("I know that Ping was called")
            }
        }
    }

装饰器本身:

    private class ServiceCallDecorator : IApplicationService
    {
        private readonly IApplicationService service;

        public ServiceCallDecorator(IApplicationService service)
        {
            this.service = service;
            this.PingWasCalled = new Nullable<bool>();
        }

        public bool? PingWasCalled
        {
            get;
            private set;
        }

        public ServiceResponse<bool> Ping(string message)
        {
            PingWasCalled = true;
            return service.Ping(message);
        }
    }

这段代码非常笨重,而且需要写很多行。 有没有更加优雅的方法可以实现同样的功能呢?


你在哪里创建装饰器? - smartcaveman
3
表达式树应该是解决问题的途径。能否展示代码并告诉我们问题出在哪里? - Daniel Hilgarth
听起来像是 PostSharp 的工作。 - geofftnz
@smartcaveman:它是一个私有类,与HandleServiceMethod在同一父类中。 - djskinner
@Daniel:那是我最初尝试的方法,但在第一个障碍上失败了——将Func转换为Expression - djskinner
@geofftnz:虽然我觉得我的装饰器解决方案代码太多了,但我认为像PostSharp这样的东西有点过头了。是否有C#本地的解决方案?或者有免费的替代品吗? - djskinner
4个回答

2
您可以使用一个表达式,然后检查其主体。
类似于:
public T HandleServiceCall<T>(Expression<Func<IApplicationService, T>> serviceMethod)     
{         
    try         
    {          
        var func = serviceMethod.Compile();
        string body = serviceMethod.Body.ToString();
        return func(new ConcreteAppService()); 
    }        
    catch(Exception ex)
    {
        ...     
              }
}

1
这个结合了@Enrico的检查方法调用的描述非常好。顺便解决了另一个问题,因为它防止客户端输入多个方法调用 - 这是一个不错的补充。 - djskinner
我的最终解决方案是由@Richard Friend和@Enrico Campidoglio提供的答案组合而成,我使用了Enrico的代码来识别被调用的方法。 - djskinner

2

您考虑过使用面向切面编程(AOP)的方法吗?它听起来似乎正是您需要的。

将异常包装和其他“元方法”功能编写为与您的服务方法“正交”的方面。

关于AOP的一些常规信息:维基百科中的AOP

以及一个带容器的潜在解决方案:Windsor Castle中的AOP


我同意,这很合适。但对我来说感觉有点过于繁琐了。有没有更简单、本地化的替代方案? - djskinner
一个更轻量级的版本可以是(如上所建议的)PostSharp。不过我没有使用过它。 - grzeg

1
这是一个使用表达式树的快速示例:
public T HandleServiceCall<T>(Expression<Func<T>> serviceMethod)
{
    try
    {
        return serviceMethod();
    }
    finally
    {
        var serviceMethodInfo = ((MethodCallExpression)serviceMethod.Body).Method;
        Console.WriteLine("The '{0}' service method was called", serviceMethodInfo.Name);
    }
}

请注意,此示例假定serviceMethod表达式始终包含方法调用。
相关资源:

只有一个方法调用吗? - djskinner
尝试测试这个,但是 Func 没有 Body 的定义。 - djskinner
1
你需要将 HandleServiceCall<T> 方法的参数类型更改为 Expression<Func<T>> - Enrico Campidoglio
1
你传递给 Expression<Func<T>> 参数的 lambda 表达式不能包含多个语句,否则会出现编译器错误,因为它无法转换为表达式树("带有语句体的 lambda 表达式无法转换为表达式树")。 - Enrico Campidoglio
我已将您的答案与@Richard的答案结合起来,并按照您的建议使用Expression<Func>作为参数。您关于多个语句的第二点实际上对我很有益,因为它限制了客户只能一次调用一个方法。 - djskinner

0

是的,我相信你的代码已经过度烹制了。

关于将您的代码包装为常见的安全代理处理程序,请在这里查看一个不错的实现。使用它很容易:

using (var client = new Proxy().Wrap()) {
 client.BaseObject.SomeMethod();
}

现在你还需要访问方法名称 - 只需使用Environment.StackTrace即可。您需要在Marc Gravell的Wrap中添加堆栈向上走。


这种方法与我设计代码的方式稍有不同。如果我进行大量重构,我一定会考虑这种方法。 - djskinner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接