[.NET领域驱动设计实战系列]专题九:DDD案例:网上书店AOP和站点地图的实现
一、引言
在前面一专题介绍到,要让缓存生效还需要实现对AOP(面向切面编程)的支持。所以本专题将介绍了网上书店案例中AOP的实现。关于AOP的概念,大家可以参考文章:http://www.cnblogs.com/jin-yuan/p/3811077.html。这里我简单介绍下AOP:AOP可以理解为对方法进行截获,这样就可以在方法调用前或调用后插入需要的逻辑。例如可以在方法调用前,加入缓存查找逻辑等。这里缓存查找逻辑就在方法调用前被执行。通过对AOP的支持,每个方法就可以分为3部分了,方法调用前逻辑->具体需要调用的方法->方法调用后的逻辑。也就是在方法调用的时候“切了一刀”。
二、网上书店AOP的实现
你可以从零开始去实现AOP,但是目前已经存在很多AOP框架了,所以在本案例中将直接通过Unity的AOP框架(Unity.Interception)来实现网上书店对AOP的支持。通常AOP的实现放在基础设施层进行实现,因为可能其他所有层都需要加入对AOP的支持。本案例中将对两个方面的AOP进行实现,一个是方法调用前缓存的记录或查找,另一个是方法调用后异常信息的记录。在实现具体代码之前,我们需要在基础设施层通过Nuget来引入Unity.Interception包。添加成功之后,我们需要定义两个类分别去实现AOP框架中IInterceptionBehavior接口。由于本案例中需要对缓存和异常日志功能进行AOP实现,自然就需要定义CachingBehavior和ExceptionLoggingBehavior两个类去实现IInterceptionBehavior接口。首先让我们看看CachingBehavior类的实现,具体实现代码如下所示:
// 缓存AOP的实现 public class CachingBehavior : IInterceptionBehavior { private readonly ICacheProvider _cacheProvider; public CachingBehavior() { _cacheProvider = ServiceLocator.Instance.GetService<ICacheProvider>(); } // 生成缓存值的键值 private string GetValueKey(CacheAttribute cachingAttribute, IMethodInvocation input) { switch (cachingAttribute.Method) { // 如果是Remove,则不存在特定值键名,所有的以该方法名称相关的缓存都需要清除 case CachingMethod.Remove: return null; // 如果是Get或者Update,则需要产生一个针对特定参数值的键名 case CachingMethod.Get: case CachingMethod.Update: if (input.Arguments != null && input.Arguments.Count > 0) { var sb = new StringBuilder(); for (var i = 0; i < input.Arguments.Count; i++) { sb.Append(input.Arguments[i]); if (i != input.Arguments.Count - 1) sb.Append("_"); } return sb.ToString(); } else return "NULL"; default: throw new InvalidOperationException("无效的缓存方式。"); } } #region IInterceptionBehavior Members public IEnumerable<Type> GetRequiredInterfaces() { return Type.EmptyTypes; } public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext) { // 获得被拦截的方法 var method = input.MethodBase; var key = method.Name; // 获得拦截的方法名 // 如果拦截的方法定义了Cache属性,说明需要对该方法的结果需要进行缓存 if (!method.IsDefined(typeof (CacheAttribute), false)) return getNext().Invoke(input, getNext); var cachingAttribute = (CacheAttribute)method.GetCustomAttributes(typeof (CacheAttribute), false)[0]; var valueKey = GetValueKey(cachingAttribute, input); switch (cachingAttribute.Method) { case CachingMethod.Get: try { // 如果缓存中存在该键值的缓存,则直接返回缓存中的结果退出 if (_cacheProvider.Exists(key, valueKey)) { var value = _cacheProvider.Get(key, valueKey); var arguments = new object[input.Arguments.Count]; input.Arguments.CopyTo(arguments, 0); return new VirtualMethodReturn(input, value, arguments); } else // 否则先调用方法,再把返回结果进行缓存 { var methodReturn = getNext().Invoke(input, getNext); _cacheProvider.Add(key, valueKey, methodReturn.ReturnValue); return methodReturn; } } catch (Exception ex) { return new VirtualMethodReturn(input, ex); } case CachingMethod.Update: try { var methodReturn = getNext().Invoke(input, getNext); if (_cacheProvider.Exists(key)) { if (cachingAttribute.IsForce) { _cacheProvider.Remove(key); _cacheProvider.Add(key, valueKey, methodReturn.ReturnValue); } else _cacheProvider.Update(key, valueKey, methodReturn); } else _cacheProvider.Add(key, valueKey, methodReturn.ReturnValue); return methodReturn; } catch (Exception ex) { return new VirtualMethodReturn(input, ex); } case CachingMethod.Remove: try { var removeKeys = cachingAttribute.CorrespondingMethodNames; foreach (var removeKey in removeKeys) { if (_cacheProvider.Exists(removeKey)) _cacheProvider.Remove(removeKey); } // 执行具体截获的方法 var methodReturn = getNext().Invoke(input, getNext); return methodReturn; } catch (Exception ex) { return new VirtualMethodReturn(input, ex); } default: break; } return getNext().Invoke(input, getNext); } public bool WillExecute { get { return true; } } #endregion }
从上面代码可以看出,通过Unity.Interception框架来实现AOP变得非常简单了,我们只需要实现IInterceptionBehavior接口中的Invoke方法和WillExecute属性即可。并且从上面代码可以看出,AOP的支持最核心代码实现在于Invoke方法的实现。既然我们需要在方法调用前查找缓存,如果缓存不存在再调用方法从数据库中进行查找,如果存在则直接从缓存中进行读取数据即可。自然需要在 getNext().Invoke(input, getNext)代码执行前进缓存进行查找,然而上面CachingBehavior类正式这样实现的。
介绍完缓存功能AOP的实现之后,下面具体看看异常日志的AOP实现。具体实现代码如下所示:
// 用于异常日志记录的拦截行为 public class ExceptionLoggingBehavior :IInterceptionBehavior { /// <summary> /// 需要拦截的对象类型的接口 /// </summary> /// <returns></returns> public IEnumerable<Type> GetRequiredInterfaces() { return Type.EmptyTypes; } /// <summary> /// 通过该方法来拦截调用并执行所需要的拦截行为 /// </summary> /// <param name="input">调用拦截目标时的输入信息</param> /// <param name="getNext">通过行为链来获取下一个拦截行为的委托</param> /// <returns>从拦截目标获得的返回信息</returns> public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext) { // 执行目标方法 var methodReturn = getNext().Invoke(input, getNext); // 方法执行后的处理 if (methodReturn.Exception != null) { Utils.Log(methodReturn.Exception); } return methodReturn; } // 表示当拦截行为被调用时,是否需要执行某些操作 public bool WillExecute { get { return true; } } }
异常日志功能的AOP实现与缓存功能的AOP实现类似,只是一个需要在方法执行前注入,而一个是在方法执行后进行注入罢了,其实现原理都是在截获的方法前后进行。方法截获功能AOP框架已经帮我们实现了。
到此,我们网上书店AOP的实现就已经完成了,但要正式生效还需要通过配置文件把AOP的实现注入到需要截获的方法当中去,这样执行这些方法才会执行注入的行为。对应的配置文件如下标红部分所示:
<!--Unity的配置信息--> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" /> <container> <extension type="Interception" /> <!--Cache Provider--> <register type="OnlineStore.Infrastructure.Caching.ICacheProvider, OnlineStore.Infrastructure" mapTo="OnlineStore.Infrastructure.Caching.EntLibCacheProvider, OnlineStore.Infrastructure" /> <!--仓储接口的注册--> <register type="OnlineStore.Domain.Repositories.IRepositoryContext, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.EntityFrameworkRepositoryContext, OnlineStore.Repositories"> <lifetime type="singleton" /> </register> <register type="OnlineStore.Domain.Repositories.IProductRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ProductRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.ICategoryRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.CategoryRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IProductCategorizationRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ProductCategorizationRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IUserRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IShoppingCartRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ShoppingCartRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IShoppingCartItemRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ShoppingCartItemRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IOrderRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.OrderRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IUserRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IUserRoleRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRoleRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IRoleRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.RoleRepository, OnlineStore.Repositories" /> <!--Domain Services--> <register type="OnlineStore.Domain.Services.IDomainService, OnlineStore.Domain" mapTo="OnlineStore.Domain.Services.DomainService, OnlineStore.Domain" /> <!--应用服务的注册--> <register type="OnlineStore.ServiceContracts.IProductService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.ProductServiceImp, OnlineStore.Application"> <!--注入AOP功能的实现--> <interceptor type="InterfaceInterceptor" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" /> </register> <register type="OnlineStore.ServiceContracts.IOrderService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.OrderServiceImp, OnlineStore.Application"> <!--注入AOP功能的实现--> <interceptor type="InterfaceInterceptor" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" /> </register> <register type="OnlineStore.ServiceContracts.IUserService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.UserServiceImp, OnlineStore.Application"> <!--注入AOP功能的实现--> <interceptor type="InterfaceInterceptor" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" /> </register> <!--Domain Event Handlers--> <register type="OnlineStore.Domain.Events.IDomainEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Domain" mapTo="OnlineStore.Domain.Events.EventHandlers.OrderDispatchedEventHandler, OnlineStore.Domain" name="OrderDispatchedEventHandler" /> <register type="OnlineStore.Domain.Events.IDomainEventHandler`1[[OnlineStore.Domain.Events.OrderConfirmedEvent, OnlineStore.Domain]], OnlineStore.Domain" mapTo="OnlineStore.Domain.Events.EventHandlers.OrderConfirmedEventHandler, OnlineStore.Domain" name="OrderConfirmedEventHandler" /> <!--Event Handlers--> <register name="orderSendEmailHandler" type="OnlineStore.Events.IEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Events" mapTo="OnlineStore.Events.Handlers.SendEmailHandler, OnlineStore.Events.Handlers" /> <!--Event Aggregator--> <register type="OnlineStore.Events.IEventAggregator, OnlineStore.Events" mapTo="OnlineStore.Events.EventAggregator, OnlineStore.Events"> <constructor> <param name="handlers"> <array> <dependency name="orderSendEmailHandler" type="OnlineStore.Events.IEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Events" /> </array> </param> </constructor> </register> <!--Event Bus--> <!--<register type="OnlineStore.Events.Bus.IEventBus, OnlineStore.Events" mapTo="OnlineStore.Events.Bus.EventBus, OnlineStore.Events"> <lifetime type="singleton" /> </register>--> <!--注入MsmqEventBus--> <register type="OnlineStore.Events.Bus.IEventBus, OnlineStore.Events" mapTo="OnlineStore.Events.Bus.MsmqEventBus, OnlineStore.Events"> <lifetime type="singleton" /> <constructor> <param name="path" value=".\Private$\OnlineStoreQueue" /> </constructor> </register> </container> </unity> <!--END: Unity-->
到此,网上书店案例中AOP的实现就完成了。通过上面的配置可以看出,客户端在调用应用服务方法前后会调用我们注入的行为,即缓存行为和异常日志行为。通过对AOP功能的支持,就不需要为每个需要进行缓存或需要异常日志行为的方法来重复写这些相同的逻辑了。从而避免了重复代码的重复实现,提高了代码的重用性和降低了模块之间的依赖性。
三、网上书店案例中站点地图的实现
在大部分网站中都实现了站点地图的功能,在Asp.net中,我们可以通过SiteMap模块来实现站点地图的功能,在Asp.net MVC也可以通过MvcSiteMapProvider第三方开源框架来实现站点地图。所以针对网上书店案例,站点地图的支持也是必不可少的。下面让我们具体看看站点地图在本案例中是如何去实现的呢?
在看实现代码之前,让我们先来理清下实现思路。
本案例中站点地图的实现,并没有借助MvcSiteMapProvider第三方框架来实现。其实现原理首先获得用户的路由请求,然后根据用户请求根据站点地图的配置获得对应的配置节点,接着根据站点地图的节点信息生成类似">首页>"这样带标签的字符串;如果获得的节点是配置文件中某个父节点的子节点,此时会通过递归的方式找到其父节点,然后递归地生成对应带标签的字符串,从而完成站点地图的功能。分析完实现思路之后,下面让我们再对照下具体的实现代码来加深理解。具体的实现代码如下所示:
public class MvcSiteMap { private static readonly MvcSiteMap _instance = new MvcSiteMap(); private static readonly XDocument Doc = XDocument.Load(HttpContext.Current.Server.MapPath(@"~/SiteMap.xml")); private UrlHelper _url = null; private string _currentUrl; public static MvcSiteMap Instance { get { return _instance;} } private MvcSiteMap() { } public MvcHtmlString Navigator() { // 获得当前请求的路由信息 _url = new UrlHelper(HttpContext.Current.Request.RequestContext); var routeUrl = _url.RouteUrl(HttpContext.Current.Request.RequestContext.RouteData.Values); if (routeUrl != null) _currentUrl = routeUrl.ToLower(); // 从配置的站点Xml文件中找到当前请求的Url相同的节点 var c = FindNode(Doc.Root); var temp = GetPath(c); return MvcHtmlString.Create(BuildPathString(temp)); } // 从SitMap配置文件中找到当前请求匹配的节点 private XElement FindNode(XElement node) { // 如果xml节点对应的url是否与当前请求的节点相同,如果相同则直接返回xml对应的节点 // 如果不同开始递归子节点 return IsUrlEqual(node) == true ? node : RecursiveNode(node); } // 判断xml节点对应的url是否与当前请求的url一样 private bool IsUrlEqual(XElement c) { var a = GetNodeUrl(c).ToLower(); return a == _currentUrl; } // 递归Xml节点 private XElement RecursiveNode(XElement node) { foreach (var c in node.Elements()) { if (IsUrlEqual(c) == true) { return c; } else { var x = RecursiveNode(c); if (x != null) { return x; } } } return null; } // 获得xml节点对应的请求url private string GetNodeUrl(XElement c) { return _url.Action(c.Attribute("action").Value, c.Attribute("controller").Value, new {area = c.Attribute("area").Value}); } // 根据对应请求url对应的Xml节点获得其在Xml中的路径,即获得其父节点有什么 // SiteMap.xml 中节点的父子节点一定要配置对 private Stack<XElement> GetPath(XElement c) { var temp = new Stack<XElement>(); while (c != null) { temp.Push(c); c = c.Parent; } return temp; } // 根据节点的路径来拼接带标签的字符串 private string BuildPathString(Stack<XElement> m) { var sb = new StringBuilder(); var tc = new TagBuilder("span"); tc.SetInnerText(">"); var sp = tc.ToString(); var count = m.Count; for (var x = 1; x <= count; x++) { var c = m.Pop(); TagBuilder tb; if (x == count) { tb = new TagBuilder("span"); } else { tb = new TagBuilder("a"); tb.MergeAttribute("href", GetNodeUrl(c)); } tb.SetInnerText(c.Attribute("title").Value); sb.Append(tb); sb.Append(sp); } return sb.ToString(); } }
对应的站点地图配置信息如下所示:
<?xml version="1.0" encoding="utf-8" ?> <node title="首页" area="" action="Index" controller="Home"> <node title="我的" area="UserCenter" action="Manage" controller="Account"> <node title="订单" area="" action="Orders" controller="Home" /> <node title="账户" area="" action="Manage" controller="Account" /> <node title="购物车" area="" action="ShoppingCart" controller="Home" /> </node> <node title="关于" area="AboutCenter" action="About" controller="Home" > <node title="Online Store 项目" area="" action="About" controller="Home" /> <node title="联系方式" area="" action="Contact" controller="Home" /> </node> </node>
实现完成之后,下面让我们具体看看本案例中站点地图的实现效果看看,具体运行效果如下图所示:
四、小结
到这里,本专题的内容就结束了。本专题主要借助Unity.Interception框架在网上书店中引入了AOP功能,并且最后简单介绍了站点地图的实现。在下一专题将对CQRS模式做一个全面的介绍。
本案例所有源码:https://github.com/lizhi5753186/OnlineStore_Second/