C# 3.0 中的“穷人代码注入器”(异常处理范围、策略、*.* 注入)






4.91/5 (15投票s)
一种在 C# 3.0 中实现代码注入的简单方法
引言
最近我一直在编写一些 WCF 服务,现在已经到了需要统一异常处理的地步。当然,我首先想到了 Enterprise Library。它的组成部分是 Exception Handling Application Block,这是一个非常好的解决方案;然而,我同样不喜欢它,原因与我不喜欢 Validation Application Block 一样(太复杂,难以使用)。或者,还有 Policy Injection Application Block。这也是一个不错的选择,但你需要付出很多努力才能取得任何成果。
如果我们仔细考虑一下,这些库只不过是代码,这些代码有一个范围(有效性范围),并且在此之前和之后,我们希望将代码注入到相同的有效性范围内。
EHAB 在我们的代码之后根据特定规则执行统一的异常代码,这样我们就不必在每个方法中编写处理程序。这些可以放在一个地方,EHAB 会处理其余的事情。
PIAB 将在执行我们的代码之前和之后执行某些处理程序。例如,之前进行验证和授权,之后进行异常处理和日志记录。
为了完成这项任务,面向方面编程也得到了发展。这方面也有各种各样的库,其中许多是开源的。
这些都是针对同一个问题的复杂解决方案。根据奥卡姆剃刀原则,最简单的解决方案才是正确的;这就是为什么我想象如何以最简单的方式解决这个问题,目前以及将来都用 C# 3.0。
想法
C# 中有一个非常有用的构造;很少有人知道它在程序员手中有多么有用。这就是匿名方法。从 C# 3.0 开始,这个机会的效率有所提高,并且也可以使用 lambda 语法进行输入。例如:
Func< int, bool > isPositive = v => v > 0;
这是一个 lambda 表达式,编译器将生成匿名方法,其类型为 delegate
。但是也有机会给出代码块
Func< int, string > listOf = v =>
{
string result = null;
for (int i = 0; i < v; i++)
{
result = i == 0 ? i.ToString() : string.Format("{0}, {1}", result, i.ToString();
}
return result;
}
所以,我们有一个比长语法更短、更简单的交叉方式。
delegate (string arg1, int arg2, ..., FooNs.FooClass argn) { ... }
为什么这个匿名方法如此有趣?让我们看看它是如何工作的
class MyClass
{
// 1. object context from the class
Func< int, bool > MyMethod(int arg1, int arg2)
{
// 2. object context from the class, together with the given arguments
Func< int, bool > anonim = (/*implicitly typed int*/ argOfAninom) =>
{
// 3. context from the anonym method, together with the
// given arguments for this anonim method
return true.
}
return anonim;
}
}
在任何地方、任何上下文中,示例中的匿名方法都将访问创建点 1、2 和 3 的上下文,特别是**它将保留在 MyMethod 的调用有效范围之内!**
如果我们给它一些递归,我们就会得到一个作用域列表,这些列表相互构建并完全由 CLR 管理。我们也可以使用它来捕获异常,并且代码堆栈在整个调用列表中都有效!
解决方案
了解了这些,实现简单的代码注入并不是一件困难的任务。我们只需要一个结构
public struct Scope
{
public delegate Chain Chain(Chain code);
public delegate void Block();
public Scope(params Chain[] codes)
{
this.code = null;
if (codes != null)
{
foreach (Chain code in codes)
{
AddCode(code);
}
}
}
public Scope(params Scope[] others)
{
this.code = null;
if (others != null)
{
foreach (Scope other in others)
{
AddCode(other.code);
}
}
}
Chain code;
private static Chain BlockToChain(Block code)
{
return c =>
{
code();
return null;
};
}
private void AddCode(Chain otherCode)
{
if (otherCode != null)
{
Chain thisCode = this.code;
if (thisCode == null)
{
this.code = otherCode;
}
else
{
this.code = (c) => otherCode(thisCode(c)); // Chain of anonym delegates.
}
}
}
public Block Begin
{
set
{
if (value == null) throw new ArgumentNullException("value");
if (code != null)
{
code(BlockToChain(value))(null); // Calling chain of delegates
}
}
}
public static Scope operator +(Scope scope1, Scope scope2)
{
Scope result = new Scope(scope1);
result.AddCode(scope2.code);
return result;
}
public static Scope operator +(Scope scope, Chain code)
{
Scope result = new Scope(scope);
result.AddCode(code);
return result;
}
public static implicit operator Scope(Chain code)
{
return new Scope(code);
}
public static implicit operator Chain(Scope scope)
{
return scope.code;
}
}
它的核心是一个指向自身的 delegate
构造。在此基础上,我们能够创建调用匿名方法的链接。这个小结构可以被视为一个编译器,它可以与其他 Scope
结构或 Chain
处理程序方法组合(参见构造函数和运算符)。
Using the Code
这很简单。让我们看看如何定义异常处理 scope
。将异常按功能分类、将它们划分为类并为这些类编写处理程序是有用的。
例如,参数异常处理
static Scope.Chain ArgumentExceptionScope(Scope.Chain upperNode)
{
return (chainNode) =>
{
try
{
upperNode(null);
}
catch (ArgumentNullException ex)
{
if (echo) Console.WriteLine("Argument '{0}' is null.", ex.ParamName);
}
catch (ArgumentException ex)
{
if (echo) Console.WriteLine("Argument '{0}' is invalid.", ex.ParamName);
}
return null;
};
}
操作错误的异常处理程序
static Scope.Chain InvalidOpertaionExceptionScope(Scope.Chain upperNode)
{
return (chainNode) =>
{
try
{
upperNode(null);
}
catch (InvalidOperationException)
{
if (echo) Console.WriteLine("Invalid operation.");
}
return null;
};
}
一般异常处理程序
static Scope.Chain ExceptionScope(Scope.Chain upperNode)
{
return (chainNode) =>
{
try
{
upperNode(null);
}
catch (Exception ex)
{
if (echo) Console.WriteLine("Unhandled '{0}' exception: {1}",
ex.GetType().Name, ex.Message);
}
return null;
};
}
等等。这些是 static
方法,但这并不是必须的,它们也可以属于一个类实例。这个想法的本质是 upperNode(null);
代表要修饰的代码,我们可以在处理程序中为此编写任何代码(例如 TransactionScope
),事物将在此处注入。upperNode()
调用参数当然是 null
,匿名方法的返回值也是 null
,因为 chainNode
参数和返回值只是负担,本质是调用匿名方法。
处理程序可以看到我们定义的那个环境,这就是为什么创建基于它的日志记录器很简单
public class Logger
{
public Logger(string name)
{
Enabled = true;
Name = name;
logScope = new Scope(Log);
}
public bool Enabled { get; set; }
public string Name { get; private set; }
Scope logScope;
public Scope LogScope
{
get { return logScope; }
}
Scope.Chain Log(Scope.Chain upperNode)
{
if (Enabled)
return DoLog(upperNode);
else
return ln => { upperNode(null); return null; };
}
private Scope.Chain DoLog(Scope.Chain upperNode)
{
return node =>
{
Console.WriteLine(">LOG --- Entering: {0} ---", Name);
try
{
upperNode(null);
}
catch (Exception ex)
{
Console.WriteLine(">LOG --- Exception caught: {0} ---",
ex.GetType().Name);
throw ex;
}
Console.WriteLine(">LOG --- Leaving: {0} ---", Name);
return null;
};
}
}
好了,我们有很多处理程序可以创建并简单地运行;如何使用它们?很简单:让我们用运算符构建一个(或多个) Scope
结构
Scope scope = new Scope();
scope += logger.LogScope;
scope += ArgumentExceptionScope;
scope += InvalidOpertaionExceptionScope;
scope += ExceptionScope;
或者使用构造函数
scope = new Scope(logger.LogScope,
new Scope(ArgumentExceptionScope, InvalidOpertaionExceptionScope),
ExceptionScope);
scope
是从内到外构建的,因此最后给出的处理程序将用其代码环绕整个范围。无论我们是从现有的或复杂的范围还是从处理程序构建它,它都将运行,并且“气泡”规则从内到外将是有效的。
它非常灵活,因为它的某些部分可以由我们创建它的上下文驱动。在此示例中,可以通过 scope
中的 logger 对象打开或关闭日志记录,其中存在此实例(logger Enabled)。但是我们可以用任何复杂的业务规则来驱动处理程序。它比 AOP 和 EntLib 灵活得多,因为它们是独立的系统,不需要实现任何接口,事先不会有任何连接,CLR 会为我们安排一切。
只剩下在给定的 scope
中运行我们的代码(注入)。没有什么比这更容易的了
myCoolScope.Begin = () =>
{
// Do stuff
// throw errors
throw new Exception("Something went really wrong out there!");
};
我们可以在任何方法中这样做,代码将被注入到 myCoolScope
定义的环境中,可以记录日志,可以处理异常,可以定义策略处理,等等。从 Scope
的代码可以看出,Begin
之后的代码会立即执行(属性设置):这个 scope
在 Begin
构造之后应该被想象成一个普通的 using 块!
与 using 相比有一个区别:在 Scope Begin
代码中,你不能简单地通过“return”从方法中返回,但这也并非大问题。为此保留一个局部变量就足够了
private static void GetSomeValueTest()
{
Console.WriteLine("\nGetSomeValueTest()");
int v = 0;
Console.WriteLine("Value is {0}.", v);
v = GetSomeValueFromInjectedCode(v);
Console.WriteLine("Value is {0}.", v);
}
private static int GetSomeValueFromInjectedCode(int fromValue)
{
fullScope.Begin = () =>
{
fromValue = fromValue + 1978;
};
return fromValue;
}
HandlerBase
我还绘制了一个处理程序 abstract
基类,以简化生活
public sealed class ScopeExceptionEventArg : EventArgs
{
public Exception Exception { get; internal set; }
public bool Handled { get; set; }
}
public abstract class HandlerBase
{
public HandlerBase()
{
scope = new Scope(ScopeMethod);
Enabled = true;
}
Scope scope;
// The Scope structure for compiling scopes:
public Scope Scope
{
get { return scope; }
}
public static implicit operator Scope(HandlerBase handler)
{
if (handler == null) return new Scope();
return handler.scope;
}
bool inFlag;
public bool Enabled { get; set; } // Can be enabled or disabled
// There are events also:
public event EventHandler EnterScope;
public event EventHandler LeaveScope;
public event EventHandler< ScopeExceptionEventArg > ScopeException;
Scope.Chain ScopeMethod(Scope.Chain code)
{
if (inFlag || !Enabled) return (p) => { code(null); return null; };
return (p) =>
{
inFlag = true;
try
{
OnEnterScope(EventArgs.Empty);
this.code = code;
Call();
}
finally
{
OnLeaveScope(EventArgs.Empty);
inFlag = false;
}
return null;
};
}
Scope.Chain code;
// Injected code called as a virtual method
protected virtual void Call()
{
if (code != null)
{
try
{
code(null);
}
catch (Exception ex)
{
ScopeExceptionEventArg e = new ScopeExceptionEventArg { Exception = ex };
OnScopeException(e);
if (!e.Handled) throw ex;
}
finally
{
code = null;
}
}
}
// Event handler methods are virtual also
protected virtual void OnEnterScope(EventArgs e)
{
EventHandler handler = EnterScope;
if (handler != null) handler(this, e);
}
protected virtual void OnLeaveScope(EventArgs e)
{
EventHandler handler = LeaveScope;
if (handler != null) handler(this, e);
}
protected virtual void OnScopeException(ScopeExceptionEventArg e)
{
EventHandler< ScopeExceptionEventArg > handler = ScopeException;
if (handler != null) handler(this, e);
}
}
通过这种方式,我们可以更容易地创建一个日志记录器(或任何其他东西)
public class LoggerScope : HandlerBase
{
public LoggerScope() : base() { }
public string Message { get; set; }
protected override void OnEnterScope(EventArgs e)
{
base.OnEnterScope(e);
if (!string.IsNullOrEmpty(Message))
{
Console.WriteLine(">LOG --- Entering: {0} ---", Message);
}
}
protected override void OnLeaveScope(EventArgs e)
{
base.OnLeaveScope(e);
if (!string.IsNullOrEmpty(Message))
{
Console.WriteLine(">LOG --- Leaving: {0} ---", Message);
Message = null;
}
}
protected override void OnScopeException(ScopeExceptionEventArg e)
{
base.OnScopeException(e);
if (!string.IsNullOrEmpty(Message))
{
Console.WriteLine(">LOG --- Exception caught: {0},
Type: {1} ---", Message, e.Exception.GetType().Name);
}
}
}
或者一个性能分析器
public class ProfilerScope : HandlerBase
{
DateTime start;
public TimeSpan EllapsedTime
{
get;
private set;
}
protected override void OnEnterScope(EventArgs e)
{
base.OnEnterScope(e);
start = DateTime.Now;
EllapsedTime = TimeSpan.Zero;
}
protected override void OnLeaveScope(EventArgs e)
{
EllapsedTime = DateTime.Now - start;
base.OnLeaveScope(e);
}
}
您可以从这些构建一个 scope
LoggerScope logger = new LoggerScope();
ProfilerScope profiler = new ProfilerScope();
Scope scope = new Scope(profiler.Scope, logger.Scope);
// ...
scope.Begin = () => // profiled and logged
{
// Do stuff.
}
// ...
// Turn off logger for this call:
logger.Enabled = false;
scope.Begin = () =>
{
// Do stuff.
}
// And so on. The scope can be handled with proper handler classes.
我附上了一个测试项目,这样您就可以看到它的简单和高效。在测试程序中,使用此方法进行异常处理的速度并不比临时编写处理程序慢(大约是 delegate
调用),但比 EHAB 快得多,也简单得多。
一个实际示例
举个例子:我有一个实体管理器服务。在其业务逻辑中,可能会发生异常。例如,我们想将一个城市移动到一个属于另一个国家(而不是该城市所在国家)的地区——这会产生异常,因为它错了,不能这样做。这些异常由业务逻辑定义,它们是标准的 CLR 异常。但是,这些异常不会跨 SOAP 通信,它们必须转换为兼容的 SOAP 错误消息。“伐木工方法”将是如果我将捕获和转换异常的代码添加到外观(facade)的代码中。这不好,这应该是业务逻辑的任务——这是它的职责。也许我可以编写一个 WCF 处理程序来自动执行这项工作,但问题是,我将无法在没有 WCF 的情况下静态地测试这些库。
所以我为业务逻辑编写了一个 Scope
处理程序,外观可以从中创建适当的 scope
public class DataExceptionConvertHandler : HandlerBase
{
protected override void Call()
{
try
{
base.Call();
}
catch (NameStoreItemNotFoundExcepton ex)
{
throw new FaultException< NameStoreItemNotFoundFault >
(new NameStoreItemNotFoundFault { NameStoreItemID = ex.ID }, ex.Message);
}
catch (NameStoreItemIsUnremovableExcepton ex)
{
throw new FaultException< NameStoreItemIsUnremovableFault >
(new NameStoreItemIsUnremovableFault
{ NameStoreItemID = ex.ID }, ex.Message);
}
catch (RegionNotFoundExcepton ex)
{
throw new FaultException< RegionNotFoundFault >
(new RegionNotFoundFault { RegionUID = ex.EnitityUID,
CountryCode = GeoUtils.GetCountryCodeOfLCID(ex.LCID) }, ex.Message);
}
catch (RegionIsUnremovableExcepton ex)
{
throw new FaultException< RegionIsUnremovableFault >
(new RegionIsUnremovableFault { RegionUID = ex.EnitityUID });
}
catch (SettlementNotFoundException ex)
{
throw new FaultException< SettlementNotFoundFault >
(new SettlementNotFoundFault { SettlementUID = ex.EnitityUID });
}
catch (SettlementIsUnremovableException ex)
{
throw new FaultException< SettlementIsUnremovableFault >
(new SettlementIsUnremovableFault { SettlementUID = ex.EnitityUID });
}
}
}
一切都在正确的地方,它透明、快速且易于维护。服务外观(service-façade)现在看起来像这样(在此示例中,目前只有这个转换 scope
可用)
public partial class PartnerManagementService : IPartnerManagement
{
public PartnerManagementService()
{
facadeScope = new DataExceptionConvertHandler();
}
#region Scopes
Scope facadeScope;
#endregion
#region Validation
private static void ValidateWithNeutralLocale(object request)
{
// I have a C 3.0 lambda based validator also. This will be my second post. ;)
}
private static void Validate(object request)
{
// ...
}
#endregion
}
public partial class PartnerManagementService
{
public NACENamesResponse GetNACENames(NACENamesRequest request)
{
NACENamesResponse response = null;
facadeScope.Begin = () =>
{
ValidateWithNeutralLocale(request);
INACEManager man = DataManagerFactory.CreateNACEManager();
List< NACENameModel > list = man.GetByLCID
(request.International.GetNeutralCulture().LCID);
response = new NACENamesResponse
{
SectorNames = list.ToArray()
};
};
return response;
}
public NACENameResponse GetNACEName(NACENameRequest request)
{
NACENameResponse response = null;
facadeScope.Begin = () =>
{
ValidateWithNeutralLocale(request);
INACEManager man = DataManagerFactory.CreateNACEManager();
response = new NACENameResponse
{
NACENameValue = man.GetName(request.NACECode,
request.International.GetNeutralCulture().LCID)
};
};
return response;
}
public void DefineNACEName(DefineNACENameRequest request)
{
facadeScope.Begin = () =>
{
ValidateWithNeutralLocale(request);
INACEManager man = DataManagerFactory.CreateNACEManager();
man.DefineName(request.NACEName,
request.International.GetNeutralCulture().LCID);
};
}
public void UndefineNACEName(UndefineNACENameRequest request)
{
facadeScope.Begin = () =>
{
ValidateWithNeutralLocale(request);
INACEManager man = DataManagerFactory.CreateNACEManager();
man.UndefineName(request.NACECode,
request.International.GetNeutralCulture().LCID);
};
}
}
这就是 C# 3.0 中的“穷人代码注入器”。请好好使用它!
历史
- 2007 年 12 月 18 日 - 第一个版本