2023年2月

PostSharp是一种Aspect Oriented Programming 面向切面(或面向方面)的组件框架,适用在.NET开发中,本篇主要介绍Postsharp在.NET开发中的相关知识,以及一些如日志、缓存、事务处理、异常处理等常用的切面处理操作。

AOP(Aspect-Oriented Programming)是一种将函数的辅助性功能与业务逻辑相分离的编程泛型(programming paradigm),其目的是将横切关注点(cross-cutting concerns)分离出来,使得程序具有更高的模块化特性。AOP是面向方面软件开发(Aspect-Oriented Software Development)在编码实现层面上的具体表现。

我们知道,解耦是程序员编码开发过程中一直追求的,AOP也是为了解耦所诞生。引入AOP技术,能很大程度上简化我们编码,减少复制的代码量,也便于统一维护统一的部分代码,如日志、缓存、事务处理、异常处理等常用的处理。

1、AOP框架的介绍

1)AOP技术介绍

AOP技术利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”

2)AOP使用场景

AOP用来封装横切关注点,具体可以在下面的场景中使用:

Authentication 权限

Caching 缓存

Context passing 内容传递

Error handling 错误处理

Lazy loading 懒加载

Debugging  调试

logging, tracing, profiling and monitoring 记录跟踪 优化 校准

Performance optimization 性能优化

Persistence  持久化

Resource pooling 资源池

Synchronization 同步

Transactions 事务

3)PostSharp框架

PostSharp是一个用于在.NET平台上实现AOP的框架,是比较常用的一个AOP框架,官方网站为
http://www.sharpcrafters.com
。目前最新版本为4.X,但是是收费的AOP软件。

PostSharp使用静态织入方式实现AOP,其连接点非常丰富,使用简单,而且相对其它一些.NET平台上的AOP框架来说,PostSharp较为轻量级,但是功能却一点也不逊色。

总体来说,使用PostSharp,将会带来如下优点:

  • 横切关注点单独分离出来,提高了代码的清晰性和可维护性。
  • 只要在Aspect中编写辅助性功能代码,在一定程度上减少了工作量和冗余代码。

当然,使用PostSharp也会存在一些缺点,主要缺点有如下两方面:

  • 增加了调试的难度。
  • 相比于不用AOP的代码,运行效率有所降低。

不过瑕不掩瑜,相对于这些缺点问题,使用PostSharp可以极大提高开发效率,减少重复代码,从而提高代码的可读性、可维护性。

另外在GitHub上还有一些开源的AOP组件,例如排头位的是KingAOP(https://github.com/AntyaDev/KingAOP),不过由于它采用了Dynamic的方式来实现,如它的构造对象如下所示。

dynamic helloWorld = newHelloWorld();
helloWorld.HelloWorldCall();

因此虽然比较方便,而且号称和PostSharp使用习惯类似,但是改变了对象的创建方式,对一般项目的类对象处理并不太适合。因此我还是比较倾向于使用PostSharp来进行AOP的编程开发。

2、PostSharp框架的使用

1)准备PostSharp的编译环境

PostSharp目前版本是4.x,我在官网下载了进行使用,不过经常发生"Error connecting to the pipe server. See previous warnings for details.",后来干脆使用了3.x版本的,反而能够正常使用,非常不错,呵呵。

PostSharp是一个可以安装在VS上的插件,安装后在VS的菜单栏目里面增加了一个PostSharp的菜单项,如下所示。

一般项目如果需要使用PostSharp特性的,在项目属性的【PostSharp】选项页中,使用【Add PostSharp to this project】把PostSharp加入到项目里面进行使用。

添加后,会弹出PostSharp的插件提示对话框,提示将加入相应的PostSharp包等内容,如下所示。

完成后就可以在项目中使用PostSharp的相关类了。

2)增加PostSharp的AOP切面处理

一般约定每个Aspect类的命名必须为“XXXAttribute”的形式。其中“XXX”就是这个Aspect的名字。PostSharp中提供了丰富的内置“Base Aspect”以便我们继承,其中这里我们继承“OnMethodBoundaryAspect ”,这个Aspect提供了进入、退出函数等连接点方法。另外,Aspect上必须设置“[Serializable] ”,这与PostSharp内部对Aspect的生命周期管理有关。

日志的Aspect类的代码如下所示。

[Serializable]public classLogAttribute : OnMethodBoundaryAspect
{
public override voidOnEntry(MethodExecutionArgs args)
{
Console.WriteLine(Environment.NewLine);

Console.WriteLine(
"Entering [ {0} ] ...", args.Method);base.OnEntry(args);
}
public override voidOnExit(MethodExecutionArgs args)
{
Console.WriteLine(
"Leaving [ {0} ] ...", args.Method);base.OnExit(args);
}
}

异常处理的类代码如下所示。

[Serializable]public classExceptionAttribute : OnExceptionAspect
{
public override voidOnException(MethodExecutionArgs args)
{
Console.WriteLine(String.Format(
"Exception in :[{0}] , Message:[{1}]", args.Method, args.Exception.Message));
args.FlowBehavior
=FlowBehavior.Continue;base.OnException(args);
}
}

计时处理的Aspect类代码如下所示。

[Serializable]
[MulticastAttributeUsage(MulticastTargets.Method)]
public classTimingAttribute : PostSharp.Aspects.OnMethodBoundaryAspect
{
[NonSerialized]
Stopwatch _StopWatch;
public override voidOnEntry(PostSharp.Aspects.MethodExecutionArgs args)
{
_StopWatch
=Stopwatch.StartNew();base.OnEntry(args);
}
public override voidOnExit(PostSharp.Aspects.MethodExecutionArgs args)
{
Console.WriteLine(
string.Format("[{0}] took {1}ms to execute",new StackTrace().GetFrame(1).GetMethod().Name,
_StopWatch.ElapsedMilliseconds));
base.OnExit(args);
}
}

事务处理的Aspect类代码如下所示。

[Serializable]
[AspectTypeDependency(AspectDependencyAction.Order, AspectDependencyPosition.After,
typeof(LogAttribute))]public classRunInTransactionAttribute : OnMethodBoundaryAspect
{
[NonSerialized]
TransactionScope TransactionScope;
public override voidOnEntry(MethodExecutionArgs args)
{
this.TransactionScope = newTransactionScope(TransactionScopeOption.RequiresNew);
}
public override voidOnSuccess(MethodExecutionArgs args)
{
this.TransactionScope.Complete();
}
public override voidOnException(MethodExecutionArgs args)
{
args.FlowBehavior
=FlowBehavior.Continue;
Transaction.Current.Rollback();
Console.WriteLine(
"Transaction Was Unsuccessful!");
}
public override voidOnExit(MethodExecutionArgs args)
{
this.TransactionScope.Dispose();
}
}

下面是几个Aspect类的切面处理代码,如下所示。

[Exception]
[Log]
static voidCalc()
{
throw new DivideByZeroException("A Math Error Occured...");
}

[Log, Timing]
static voidLongRunningCalc()
{
//wait for 1000 miliseconds Thread.Sleep(1000);
}

从上面我们可以看到,常规的异常处理、日志处理都已经通过Attribute的方式进行处理了,在函数体里面都只是剩下具体的业务逻辑代码了,这样极大提高了代码的可读性,简洁明了。

运行上面的代码函数的调用,我们可以在输出日志里面看到具体的结果内容。

Entering [ Void Calc() ] ...
“System.DivideByZeroException”类型的第一次机会异常在 PostSharpExample.exe 中发生
Exception
in:[Void Calc()] , Message:[A Math Error Occured...]
Leaving [ Void Calc() ] ...


Entering [ Void LongRunningCalc() ] ...
Leaving [ Void LongRunningCalc() ] ...
[LongRunningCalc] took 1002ms to execute

这样,通过声明的方式,就实现了常规日志 、异常的处理,当然实际项目上使用日志、异常处理的这些代码肯定会更加复杂一些,不过小例子已经实现了切面逻辑的分离处理了,尘归尘、土归土,一切都是那么的简洁安静了。

在之前一篇随笔《
在.NET项目中使用PostSharp,实现AOP面向切面编程处理
》介绍了PostSharp框架的使用,试用PostSharp能给我带来很多便利和优势,减少代码冗余,提高可读性,并且可以更加优雅的实现常规的日志、异常、缓存、事务等业务场景的处理。本篇主要介绍使用MemoryCache实现缓存的处理。

1、MemoryCache的介绍回顾

上篇没有提及缓存的处理,一般情况下,缓存的处理我们可以利用微软的分布式缓存组件MemoryCache进行缓存的处理操作。MemoryCache的使用网上介绍的不多,不过这个是.NET4.0新引入的缓存对象,主要是替换原来企业库的缓存模块,使得.NET的缓存可以无处不在,而不用基于特定的Windows版本上使用。

缓存在很多情况下需要用到,合理利用缓存可以一方面可以提高程序的响应速度,同时可以减少对特定资源访问的压力。本文主要针对自己在Winform方面的缓存使用做一个引导性的介绍,希望大家能够从中了解一些缓存的使用场景和使用方法。缓存是一个中大型系统所必须考虑的问题。为了避免每次请求都去访问后台的资源(例如数据库),我们一般会考虑将一些更新不是很频繁的,可以重用的数据,通过一定的方式临时地保存起来,后续的请求根据情况可以直接访问这些保存起来的数据。这种机制就是所谓的缓存机制。

.NET 4.0的缓存功能主要由三部分组成:System.Runtime.Caching,System.Web.Caching.Cache和Output Cache。

System.Runtime.Caching这是在.NET 4.0中新增的缓存框架,主要是使用MemoryCache对象,该对象存在于程序集System.Runtime.Caching.dll。

System.Web.Caching.Cache这个则是在.NET2.0开始就一直存在的缓存对象,一般主要用在Web中,当然也可以用于Winform里面,不过要引用System.Web.dll。

Output Cache则是Asp.NET里面使用的,在ASP.NET 4.0之前的版本都是直接使用System.Web.Caching.Cache来缓存HTML片段。在ASP.NET 4.0中对它进行了重新设计,提供了一个OutputCacheProvider供开发人员进行扩展,但是它默认情况下,仍然使用System.Web.Caching.Cache来做做缓存

我在之前的一篇随笔《
Winform里面的缓存使用
》曾经介绍了MemoryCache辅助类的处理,用来方便实现缓存的数据操作。它的辅助类主要代码如下所示。

    /// <summary>
    ///基于MemoryCache的缓存辅助类/// </summary>
    public static classMemoryCacheHelper
{
private static readonly Object locker = new object();/// <summary> ///创建一个缓存的键值,并指定响应的时间范围,如果失效,则自动获取对应的值/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="key">对象的键</param> /// <param name="cachePopulate">获取缓存值的操作</param> /// <param name="slidingExpiration">失效的时间范围</param> /// <param name="absoluteExpiration">失效的绝对时间</param> /// <returns></returns> public static T GetCacheItem<T>(String key, Func<T> cachePopulate, TimeSpan? slidingExpiration = null, DateTime? absoluteExpiration = null)
{
if(String.IsNullOrWhiteSpace(key)) throw new ArgumentException("Invalid cache key");if(cachePopulate == null) throw new ArgumentNullException("cachePopulate");if(slidingExpiration == null && absoluteExpiration == null) throw new ArgumentException("Either a sliding expiration or absolute must be provided");if(MemoryCache.Default[key] == null)
{
lock(locker)
{
if(MemoryCache.Default[key] == null)
{
var item = newCacheItem(key, cachePopulate());var policy =CreatePolicy(slidingExpiration, absoluteExpiration);

MemoryCache.Default.Add(item, policy);
}
}
}
return(T)MemoryCache.Default[key];
}
private static CacheItemPolicy CreatePolicy(TimeSpan? slidingExpiration, DateTime?absoluteExpiration)
{
var policy = newCacheItemPolicy();if(absoluteExpiration.HasValue)
{
policy.AbsoluteExpiration
=absoluteExpiration.Value;
}
else if(slidingExpiration.HasValue)
{
policy.SlidingExpiration
=slidingExpiration.Value;
}

policy.Priority
=CacheItemPriority.Default;returnpolicy;
}
/// <summary> ///清空缓存/// </summary> public static voidClearCache()
{
List
<string> cacheKeys = MemoryCache.Default.Select(kvp =>kvp.Key).ToList();foreach (string cacheKey incacheKeys)
{
MemoryCache.Default.Remove(cacheKey);
}
}

...//省略部分代码

}

而我们在程序中,如果需要使用缓存,那么调用这个辅助类来解决,也算是比较方便的,实现缓存的代码如下所示。

    public static classUserCacheService
{
/// <summary> ///获取用户全部简单对象信息,并放到缓存里面/// </summary> /// <returns></returns> public static List<SimpleUserInfo>GetSimpleUsers()
{
System.Reflection.MethodBase method
=System.Reflection.MethodBase.GetCurrentMethod();string key = string.Format("{0}-{1}", method.DeclaringType.FullName, method.Name);return MemoryCacheHelper.GetCacheItem<List<SimpleUserInfo>>(key,delegate() {//return CallerFactory<IUserService>.Instance.GetSimpleUsers();//模拟从数据库获取数据 List<SimpleUserInfo> list = new List<SimpleUserInfo>();for(int i = 0; i< 10; i++)
{
var info = newSimpleUserInfo();
info.ID
=i;
info.Name
= string.Concat("Name:", i);
info.FullName
= string.Concat("姓名:", i);
list.Add(info);
}
returnlist;
},
new TimeSpan(0, 10, 0));//10分钟过期 }/// <summary> ///根据用户的ID,获取用户的登陆名称,并放到缓存里面/// </summary> /// <param name="userId">用户的ID</param> /// <returns></returns> public static string GetNameByID(stringuserId)
{
string result = "";if (!string.IsNullOrEmpty(userId))
{
System.Reflection.MethodBase method
=System.Reflection.MethodBase.GetCurrentMethod();string key = string.Format("{0}-{1}-{2}", method.DeclaringType.FullName, method.Name, userId);

result
= MemoryCacheHelper.GetCacheItem<string>(key,delegate() {//return CallerFactory<IUserService>.Instance.GetNameByID(userId.ToInt32()); return string.Concat("Name:", userId);
},
new TimeSpan(0, 30, 0));//30分钟过期 }returnresult;
}

上面案例我模拟构造数据库数据返回,否则一般使用BLLFactory<T>、或者混合框架客户端里面使用CallerFactory<T>进行调用接口了,相当于需要对它们进行进一步的函数封装处理才能达到目的。

案例中可以设置失效缓存时间,并且失效后,自动通过Func<T> cachePopulate的函数重新获取缓存内容,在实际情况下,也是非常智能的一种处理方式。

2、结合PostSharp和MemoryCache实现缓存

上面的案例使用MemoryCache辅助类来实现缓存的处理,能够解决实际的问题,不过同时问题也来了,每次缓存处理,都需要写一段额外的代码进行处理,代码的冗余就非常多了,而且一旦很多地方采用缓存,那么维护这些代码就很成问题。

我们希望引入PostSharp技术,来减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。这种AOP的代码织入技术能够很好分离横切面和业务处理,从而实现简化代码的目的。

就上面的代码问题,我们来看看,引入PostSharp后,我们的代码是如何实现缓存处理的。

    /// <summary>
    ///使用PostSharp,结合MemoryCache实现缓存的处理类/// </summary>
    public classCacheService
{
/// <summary> ///获取用户全部简单对象信息,并放到缓存里面/// </summary> /// <returns></returns> [Cache(ExpirationPeriod = 30)]public static List<SimpleUserInfo> GetSimpleUsers(intuserid)
{
//return CallerFactory<IUserService>.Instance.GetSimpleUsers();//模拟从数据库获取数据 List<SimpleUserInfo> list = new List<SimpleUserInfo>();for (int i = 0; i < 10; i++)
{
var info = newSimpleUserInfo();
info.ID
=i;
info.Name
= string.Concat("Name:", i);
info.FullName
= string.Concat("姓名:", i);
list.Add(info);
}
returnlist;
}
/// <summary> ///根据用户的ID,获取用户的登陆名称,并放到缓存里面/// </summary> /// <param name="userId">用户的ID</param> /// <returns></returns> [Cache]public static string GetNameByID(stringuserId)
{
//return CallerFactory<IUserService>.Instance.GetNameByID(userId.ToInt32()); return string.Concat("Name:", userId);
}
}

我们注意到了上面的函数代码,除了调用业务逻辑(这里构造数据演示)外,其实是没有多余的其他代码的。不过我们是在函数开始进行了一个特性的标识:

[Cache(ExpirationPeriod = 30)]

或者

[Cache]

这个就是我们声明使用缓存处理的函数,如此而已,是不是非常简单了。

我们来看看生成后的代码反编译得到的结果,如下所示。

这个和我们实际的代码是不太一样的,这里整合了PostSharp的织入代码,从而能够实现缓存的处理操作了,但是我们在开发过程中是透明的,只需要维护好自己编写的代码即可。

这个里面需要使用了CacheAttribute来进行标识,这个类的代码就是使用了PostSharp的基类进行处理了

    /// <summary>
    ///方法实现缓存的标识/// </summary>
[Serializable]public classCacheAttribute : MethodInterceptionAspect
{
/// <summary> ///缓存的失效时间设置,默认采用30分钟/// </summary> public int ExpirationPeriod = 30;/// <summary> ///PostSharp的调用处理,实现数据的缓存处理/// </summary> public override voidOnInvoke(MethodInterceptionArgs args)
{
//默认30分钟失效,如果设置过期时间,那么采用设置值 TimeSpan timeSpan = new TimeSpan(0, 0, ExpirationPeriod, 0);var cache =MethodResultCache.GetCache(args.Method, timeSpan);var arguments = args.Arguments.ToList(); var result =cache.GetCachedResult(arguments);if (result != null)
{
args.ReturnValue
=result;return;
}
else{base.OnInvoke(args);//调用后重新更新缓存 cache.CacheCallResult(args.ReturnValue, arguments);
}
}
}

这个CacheAttribute特性类包含一个设置失效的时间间隔(分钟),来指定函数返回结果的失效时间的,通过继承MethodInterceptionAspect基类,我们重写了void OnInvoke(MethodInterceptionArgs args)函数,从而对调用过程的横切面进行介入:

如果调用过程中获得缓存结果,则直接返回,不需要调用函数业务逻辑;否则调用函数获得返回值,并重新设置缓存结果值。

在函数代码里面,通过传入参数(包括方法对象、超时时间等)实现方法缓存对象的构建。

MethodResultCache.GetCache(args.Method, timeSpan);

在MethodResultCache里面,我们就是对方法的缓存进行处理的,首先需要声明一个MemoryCache的对象用于管理缓存(分布式缓存)。

        /// <summary>
        ///初始化缓存管理器/// </summary>
        private voidInitCacheManager()
{
_cache
= newMemoryCache(_methodName);
}

其中通过函数获取方法和参数的键,也就是唯一的键。

        /// <summary>
        ///根据调用方法名称和参数获取缓存键/// </summary>
        /// <param name="arguments">方法的参数列表</param>
        /// <returns></returns>
        private string GetCacheKey(IEnumerable<object>arguments)
{
var key = string.Format("{0}({1})", _methodName,string.Join(",", arguments.Select(x => x != null ? x.ToString() : "<Null>")));returnkey;
}

设置缓存的操作,我们就是调用MemoryCache缓存管理类来实现的键值设置的,如下代码所示。

        /// <summary>
        ///缓存结果内容/// </summary>
        /// <param name="result">待加入缓存的结果</param>
        /// <param name="arguments">方法的参数集合</param>
        public void CacheCallResult(object result, IEnumerable<object>arguments)
{
_cache.Set(GetCacheKey(arguments), result, DateTimeOffset.Now.Add(_expirationPeriod));
}

这样我们就设置了一个键值的缓存,并指定了缓存的失效时间,在这个时间段内,我们每次获取的数据,不需要再次调用外部接口,直接从缓存里面获取,速度提高很多,同时也减轻了分布式构架中的服务器承载的IO压力。

我们可以编写一小段代码进行测试出来的效率,如下代码所示。

            //First test
            DateTime start =DateTime.Now;var list = CacheService.GetSimpleUsers(1);int end = (int)DateTime.Now.Subtract(start).TotalMilliseconds;

Console.WriteLine(
"first:" +end);//Second test start =DateTime.Now;
list
= CacheService.GetSimpleUsers(2);
end
= (int)DateTime.Now.Subtract(start).TotalMilliseconds;
Console.WriteLine(
"Second:" + end);

获得的结果如下所示(分别介绍获得结果的时间)。

 first: 519Second:501first:0Second:0first:0Second:0

从上面代码可以看出,第一次请求数据的有一定的时间差,后面请求毫秒数则是直接0了。

通过上面的 PostSharp和MemoryCache的整合,我们可以极大简化了缓存的处理代码,并且能够利用较为不错的MemoryCache缓存管理类来实现缓存的处理,非常方便和高效了。

在前面几篇随笔中,介绍了PostSharp的使用,以及整合MemoryCache,《
在.NET项目中使用PostSharp,实现AOP面向切面编程处理
》、《
在.NET项目中使用PostSharp,使用MemoryCache实现缓存的处理
》参数了对PostSharp的使用,并介绍了MemoryCache的缓存使用,但是缓存框架的世界里面,有很多成熟的缓存框架,如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等,这时候我们如果有一个大内总管或者一个吸星大法的武功,把它们融合起来,那么就真的是非常完美的一件事情,这个就是我们CacheManager缓存框架了,这样的灵活性缓存框架并结合了PostSharp横切面对常规代码的简化功能,简直就是好鞍配好马、宝剑赠英雄,整合起来处理缓存真的是如虎添翼。

1、CacheManager缓存框架的回顾

关于这个缓存框架,我在随笔《
.NET缓存框架CacheManager在混合式开发框架中的应用(1)-CacheManager的介绍和使用
》中进行了介绍,读者可以从中了解一下CacheManager缓存框架究竟是一个什么样的东西。

CacheManager是一个以C#语言开发的开源.Net缓存框架抽象层。它不是具体的缓存实现,但它支持多种缓存提供者(如Redis、Memcached等)并提供很多高级特性。
CacheManager 主要的目的使开发者更容易处理各种复杂的缓存场景,使用CacheManager可以实现多层的缓存,让进程内缓存在分布式缓存之前,且仅需几行代码来处理。
CacheManager 不仅仅是一个接口去统一不同缓存提供者的编程模型,它使我们在一个项目里面改变缓存策略变得非常容易,同时也提供更多的特性:如缓存同步、并发更新、序列号、事件处理、性能计算等等,开发人员可以在需要的时候选择这些特性。

CacheManager缓存框架支持Winform和Web等应用开发,以及支持多种流行的缓存实现,如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等。

纵观整个缓存框架,它的特定很明显,在支持多种缓存实现外,本身主要是以内存缓存(进程内)为主,其他分布式缓存为辅的多层缓存架构方式,以达到快速命中和处理的机制,它们内部有相关的消息处理,使得即使是分布式缓存,也能够及时实现并发同步的缓存处理。

CacheManager缓存框架在配置方面,支持代码方式的配置、XML配置,以及JSON格式的配置处理,非常方便。

CacheManager缓存框架默认对缓存数据的序列化是采用二进制方式,同时也支持多种自定义序列化的方式,如基于JOSN.NET的JSON序列化或者自定义序列化方式。

CacheManager缓存框架可以对缓存记录的增加、删除、更新等相关事件进行记录。

CacheManager缓存框架的缓存数据是强类型的,可以支持各种常规类型的处理,如Int、String、List类型等各种基础类型,以及可序列号的各种对象及列表对象。

CacheManager缓存框架支持多层的缓存实现,内部良好的机制可以高效、及时的同步好各层缓存的数据。

CacheManager缓存框架支持对各种操作的日志记录。

CacheManager缓存框架在分布式缓存实现中支持对更新的锁定和事务处理,让缓存保持更好的同步处理,内部机制实现版本冲突处理。

CacheManager缓存框架支持两种缓存过期的处理,如绝对时间的过期处理,以及固定时段的过期处理,是我们处理缓存过期更加方便。

....

很多特性基本上覆盖了缓存的常规特性,而且提供的接口基本上也是我们所经常用的Add、Put、Update、Remove等接口,使用起来也非常方便。

CacheManager的GitHub源码地址为:
https://github.com/MichaCo/CacheManager
,如果需要具体的Demo及说明,可以访问其官网:
http://cachemanager.net/

一般来说,对于单机版本的应用场景,基本上是无需引入这种缓存框架的,因为客户端的并发量很少,而且数据请求也是寥寥可数的,性能方便不会有任何问题。

如果对于分布式的应用系统,如我在很多随笔中介绍到我的《混合式开发框架》、《Web开发框架》,由于数据请求是并发量随着用户增长而增长的,特别对于一些互联网的应用系统,极端情况下某个时间点一下可能就会达到了整个应用并发的峰值。那么这种分布式的系统架构,引入数据缓存来降低IO的并发数,把耗时请求转换为内存的高速请求,可以极大程度的降低系统宕机的风险。

我们以基于常规的Web API层来构建应用框架为例,整个数据缓存层,应该是在Web API层之下、业务实现层之上的一个层,如下所示。

2、整合PostSharp和CacheManager实现多种缓存框架的处理

由于MemoryCache是在单个机器上进行缓存的处理,而且无法进行序列号,电脑宕机后就会全部丢掉缓存内容,由于这个缺点,我们对《
在.NET项目中使用PostSharp,使用MemoryCache实现缓存的处理
》基础上进行进一步的调整,整合CacheManager进行使,从而可以利用缓存弹性化处理以及可序列号的特点。

我们在正常情况下,还是需要使用Redis这个强大的分布式缓存的,关于Redis的安装和使用,请参考我的随笔《
基于C#的MongoDB数据库开发应用(4)--Redis的安装及使用
》。

我们首先定义一个CacheAttribute的Aspect类,用来对缓存的切面处理。

    /// <summary>
    ///方法实现缓存的标识/// </summary>
[Serializable]public classCacheAttribute : MethodInterceptionAspect
{
/// <summary> ///缓存的失效时间设置,默认采用30分钟/// </summary> public int ExpirationPeriod = 30;/// <summary> ///PostSharp的调用处理,实现数据的缓存处理/// </summary> public override voidOnInvoke(MethodInterceptionArgs args)
{
//默认30分钟失效,如果设置过期时间,那么采用设置值 TimeSpan timeSpan = new TimeSpan(0, 0, ExpirationPeriod, 0);var cache =MethodResultCache.GetCache(args.Method, timeSpan);var arguments = args.Arguments.ToList();//args.Arguments.Union(new[] {WindowsIdentity.GetCurrent().Name}).ToList(); var result =cache.GetCachedResult(arguments);if (result != null)
{
args.ReturnValue
=result;return;
}
else{base.OnInvoke(args);//调用后更新缓存 cache.CacheCallResult(args.ReturnValue, arguments);
}
}
}

然后就是进一步处理完善类 MethodResultCache来对缓存数据进行处理了。该类负责构造一个CacheManager管理类来对缓存进行处理,如下代码所示。

初始化缓存管理器的代码如下所示,这里利用了MemoryCache作为快速的内存缓存(主缓存),以及Redis作为序列化存储的缓存容器(从缓存),它们有内在机制进行同步处理。

        /// <summary>
        ///初始化缓存管理器/// </summary>
        private voidInitCacheManager()
{
_cache
= CacheFactory.Build("getStartedCache", settings =>{
settings
.WithSystemRuntimeCacheHandle(
"handleName")
.And
.WithRedisConfiguration(
"redis", config =>{
config.WithAllowAdmin()
.WithDatabase(
0)
.WithEndpoint(
"localhost", 6379);
})
.WithMaxRetries(
100)
.WithRetryTimeout(
50)
.WithRedisBackplane(
"redis")
.WithRedisCacheHandle(
"redis", true)
;
});
}

对缓存结果进行处理的函数如下所示。

        /// <summary>
        ///缓存结果内容/// </summary>
        /// <param name="result">待加入缓存的结果</param>
        /// <param name="arguments">方法的参数集合</param>
        public void CacheCallResult(object result, IEnumerable<object>arguments)
{
var key =GetCacheKey(arguments);
_cache.Remove(key);
var item = new CacheItem<object>(key, result, ExpirationMode.Sliding, _expirationPeriod);
_cache.Add(item);
}

首先就是获取方法参数的键,然后移除对应的缓存,加入新的缓存,并设定缓存的失效时间段即可。

清空缓存的时候,直接调用管理类的Clear方法即可达到目的。

        /// <summary>
        ///清空方法的缓存/// </summary>
        public voidClearCachedResults()
{
_cache.Clear();
}

这样,我们处理好后,在一个业务调用类里面进行设置缓存标志即可,如下代码所示。

        /// <summary>
        ///获取用户全部简单对象信息,并放到缓存里面/// </summary>
        /// <returns></returns>
        [Cache(ExpirationPeriod = 1)]public static List<SimpleUserInfo> GetSimpleUsers(intuserid)
{
Thread.Sleep(
500);//return CallerFactory<IUserService>.Instance.GetSimpleUsers();//模拟从数据库获取数据 List<SimpleUserInfo> list = new List<SimpleUserInfo>();for (int i = 0; i < 10; i++)
{
var info = newSimpleUserInfo();
info.ID
=i;
info.Name
= string.Concat("Name:", i);
info.FullName
= string.Concat("姓名:", i);
list.Add(info);
}
returnlist;
}

为了测试缓存的处理,以及对Redis的支持情况,我编写了一个简单的案例,功能如下所示。

测试代码如下所示。

        //测试缓存
        private void button1_Click(objectsender, EventArgs e)
{
Console.WriteLine(
"测试缓存:");//测试反复调用获取数值的耗时 DateTime start =DateTime.Now;var list = CacheService.GetSimpleUsers(1);int end = (int)DateTime.Now.Subtract(start).TotalMilliseconds;

Console.WriteLine(
"first:" +end);
Console.WriteLine(
"List:" +list.Count);//Second test//检查不同的方法参数,对缓存值的影响 start =DateTime.Now;
list
= CacheService.GetSimpleUsers(2);
end
= (int)DateTime.Now.Subtract(start).TotalMilliseconds;
Console.WriteLine(
"Second:" +end);
Console.WriteLine(
"List2:" +list.Count);
}
//更新缓存 private void button2_Click(objectsender, EventArgs e)
{
Console.WriteLine(
"更新缓存:");//首先获取对应键的缓存值//然后对缓存进行修改//最后重新加入缓存 var key = "CacheManagerAndPostSharp.CacheService.GetSimpleUsers";var item =MethodResultCache.GetCache(key);var argument = new List<object>(){1};var result =item.GetCachedResult(argument);
Console.WriteLine(
"OldResult:" +result.ToJson());

List
<SimpleUserInfo> newList = result as List<SimpleUserInfo>;if(newList != null)
{
newList.Add(
new SimpleUserInfo() { ID = new Random().Next(), Name = RandomChinese.GetRandomChars(2) });
}
item.CacheCallResult(newList, argument);
}
//清空缓存 private void button3_Click(objectsender, EventArgs e)
{
Console.WriteLine(
"清空缓存:");//首先获取对应键的缓存值 var key = "CacheManagerAndPostSharp.CacheService.GetSimpleUsers";var item =MethodResultCache.GetCache(key);var argument = new List<object>(){1};//然后清空方法的所有缓存 item.ClearCachedResults();//最后重新检验缓存值为空 var result =item.GetCachedResult(argument);
Console.WriteLine(
"Result:" + result !=null ? result.ToJson() : "null");
}

测试运行结果如下所示。

测试缓存: 
first:
870List:10Second:502List2:10更新缓存:
OldResult:[
{
"ID":0,"HandNo":null,"Name":"Name:0","Password":null,"FullName":"姓名:0","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":1,"HandNo":null,"Name":"Name:1","Password":null,"FullName":"姓名:1","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":2,"HandNo":null,"Name":"Name:2","Password":null,"FullName":"姓名:2","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":3,"HandNo":null,"Name":"Name:3","Password":null,"FullName":"姓名:3","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":4,"HandNo":null,"Name":"Name:4","Password":null,"FullName":"姓名:4","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":5,"HandNo":null,"Name":"Name:5","Password":null,"FullName":"姓名:5","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":6,"HandNo":null,"Name":"Name:6","Password":null,"FullName":"姓名:6","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":7,"HandNo":null,"Name":"Name:7","Password":null,"FullName":"姓名:7","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":8,"HandNo":null,"Name":"Name:8","Password":null,"FullName":"姓名:8","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":9,"HandNo":null,"Name":"Name:9","Password":null,"FullName":"姓名:9","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null}]

测试缓存:
first:
0List:11Second:0List2:10清空缓存:null

同时我们看到在Redis里面,有相关的记录如下所示。

结合PostSharp和CacheManager,使得我们在使用缓存方面更具有弹性化,可以根据情况通过配置实现使用不同的缓存处理,但是在代码中使用缓存就是只需要声明一下即可,非常方便简洁了。

在前面的记录保存和显示里面,都是采用一个视图的方式进行数据的展示和录入的,如果对于主从表的记录同时录入,那么就需要主从表两个GridView来进行展示和数据录入的了,对于主从表录入相对复杂一些,具体如何操作呢?

这里我们依旧采用分页控件来进行数据的分页及直接录入数据操作,而且增加了主从表数据同时在一个GridControl界面上进行处理。

这样主表记录为字典类型,从表为字典明细项目,得到的数据展示界面效果如下所示。

当然我们可直接在底部进行数据的录入,包括主表记录和从表的明细记录,都可以一气呵成的录入并进行保存处理的,界面效果如下所示。

GridView的主从关系需要设置好集合的映射关系,我们需要通过设置GridLevelNode集合实现主从表关系的处理的。

初始化从表的GridView2和主从表关系的代码如下所示

通过上面的初始化代码,指定了主从表的关系后,我们还需要对绑定的数据源进行一定的处理,才能够在GridControl控件上显示主从表关系的记录。

首先需要定义一个业务对象,用来存储主从关系的记录对象。

然后在BindData绑定数据的时候,代码处理如下即可。

这样就可以得到开始介绍的主从表界面效果了。

数据保存的代码和前面的操作类似,我们需要分别对GridView1和GridView2的数据保存操作进行处理,如下代码所示。

GridView2的字典项目明细保存操作如下所示。

主从表的记录删除这里需要顺带介绍一下,由于主从表公用一个右键菜单的删除操作。

那么处理的时候,我们需要判断是操作从表还是主表记录,对它们要分开处理,然后提示是否操作成功,如果成功,我们可以移除这行即可,避免重新更新数据导致的焦点丢失。

在本篇介绍的Winform界面样式改变及存储操作中,是指基于DevExpress进行界面样式的变化。一般情况下,默认我们会为客户提供多种DevExpress的界面皮肤以供个人喜好选择,默认DevExpress提供40余种皮肤样式,用户可以根据自己的喜好,选择较为美观、得体的皮肤,为了方便,我们对用户的皮肤选择进行记录,并可以动态改变。

1、界面皮肤的选择

Winform开发框架(包括混合式Winform开发框架)皮肤如下界面所示。

在皮肤集合中打开,可以看到很多界面皮肤可供选择

上面初始化的皮肤代码,是通过调用SkinHelper的DevExpress内在辅助类实现的,通过添加一个RibbonGalleryBarItem控件对象,然后添加如下代码实现的

DevExpress.XtraBars.Helpers.SkinHelper.InitSkinGallery(rgbiSkins, true);

以及在Ribbon控件上添加一个小的皮肤下拉按钮,如下代码所示

this.ribbonControl.Toolbar.ItemLinks.Clear();this.ribbonControl.Toolbar.ItemLinks.Add(rgbiSkins);

如果我们固定设置界面一种皮肤,那么调用下面的代码直接设置即可,如下所示。

UserLookAndFeel.Default.SetSkinStyle("Office 2010 Blue");

2、界面皮肤的保存和显示

不过,我们如果希望皮肤可以随意更改并可以存储到配置文件中,那么我们就需要对它进行一定的加工处理。

首先我们编写一个设置皮肤名称的函数,如下所示。

        /// <summary>
        ///设置用户界面的皮肤/// </summary>
        private voidSetSkinStyle()
{
//从配置文件读取SkinName的值,如无则使用标准样式 string skinName = config.AppConfigGet("SkinName");
skinName
= string.IsNullOrEmpty(skinName) ? "Office 2010 Blue": skinName;foreach (GalleryItemGroup group inrgbiSkins.Gallery.Groups)
{
foreach (GalleryItem item ingroup.Items)
{
if (string.Concat(item.Tag) ==skinName)
{
item.Checked
= true;
rgbiSkins.Gallery.MakeVisible(item);
UserLookAndFeel.Default.SetSkinStyle(skinName);
return;
}
item.Checked
= false;
}
}
}

这样我们在皮肤改变后,重新读取配置,并重新设置界面的皮肤即可。

那么我们需要实现RibbonGalleryBarItem控件对象的单击事件GalleryItemClick,如下代码所示。

            rgbiSkins.GalleryItemClick += (object sender, GalleryItemClickEventArgs e) =>{
config.AppConfigSet(
"SkinName", string.Concat(e.Item.Tag));
SetSkinStyle();
};

这样综合起来就是两个函数代码实现了,如下所示

        /// <summary>
        ///初始化皮肤设置/// </summary>
        private voidInitSkinStyle()
{
//初始化皮肤样式名称 DevExpress.XtraBars.Helpers.SkinHelper.InitSkinGallery(rgbiSkins, true);
rgbiSkins.GalleryItemClick
+= (object sender, GalleryItemClickEventArgs e) =>{
config.AppConfigSet(
"SkinName", string.Concat(e.Item.Tag));
SetSkinStyle();
};
this.ribbonControl.Toolbar.ItemLinks.Clear();this.ribbonControl.Toolbar.ItemLinks.Add(rgbiSkins);this.ribbonControl.Minimized = true;//默认最小化//UserLookAndFeel.Default.SetSkinStyle("Office 2010 Blue"); SetSkinStyle();
}
/// <summary> ///设置用户界面的皮肤/// </summary> private voidSetSkinStyle()
{
//从配置文件读取SkinName的值,如无则使用标准样式 string skinName = config.AppConfigGet("SkinName");
skinName
= string.IsNullOrEmpty(skinName) ? "Office 2010 Blue": skinName;foreach (GalleryItemGroup group inrgbiSkins.Gallery.Groups)
{
foreach (GalleryItem item ingroup.Items)
{
if (string.Concat(item.Tag) ==skinName)
{
item.Checked
= true;
rgbiSkins.Gallery.MakeVisible(item);
UserLookAndFeel.Default.SetSkinStyle(skinName);
return;
}
item.Checked
= false;
}
}
}

然后在主界面的初始化代码中进行调用即可,同时记得在App.Config文件中增加一个SkinName的配置项,方便程序进行存储皮肤名称。

这样就可以实现我们皮肤的变化及存储功能了,修改皮肤,并重启程序,系统依旧使用保存好的皮肤,功能符合预期。

这个功能不算复杂,提供代码方便大家进行复制使用。