使用 NamedScope 扩展 Ninject 的 InRequestScope
背景
C#,Ninject,定期执行某计划任务。首先想到的是使用 Quartz 来安排计划任务,于是看是否有相应的集成。果然有:https://github.com/dtinteractive/Ninject.Extensions.Quartz/。该项目提供了一个 NinjectJobFactory,用来创建 Job,代码很简单,就是从 Ninject 的 Kernel 里去 Resolve Job:
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { IJobDetail jobDetail = bundle.JobDetail; Type jobType = jobDetail.JobType; try { if (log.IsDebugEnabled) { log.Debug(string.Format(CultureInfo.InvariantCulture, "Producing instance of Job '{0}', class={1}", jobDetail.Key, jobType.FullName)); } return _kernel.Get(jobType) as IJob; } catch (Exception e) { SchedulerException se = new SchedulerException(string.Format(CultureInfo.InvariantCulture, "Problem instantiating class '{0}'", jobDetail.JobType.FullName), e); throw se; } }
该项目还提供了一个可选的 IScheduler 的默认单例实现:
Bind<IScheduler>().ToMethod(ctx => ctx.Kernel.Get<ISchedulerfactory>().GetScheduler()).InSingletonScope();
挑战
看起来很顺利,使用这个默认的 IScheduler 的实现去 Trigger Job 就 OK 了。但是在实际使用发现一个问题,那就是创建出来的 Job 如果实现了 IDisposable 接口是不会被显式 Dispose 的。解决方案有两个,一个是在 NinjectJobFactory 里实现 ReturnJob 方法,在该方法里尝试 Dispose Job。第二个办法是使用 JobListener,在 JobWasExecuted 里 Dispose Job。
之所以 Job 需要实现 IDisposable 接口,是因为其依赖项实现了 IDisposable 接口,需要在 Job 执行结束之后 Dispose 。在 Job 的 Dispose 方法里 Dispose 依赖项,看起来是再正确不过的做法了。但是考虑一下 ASP.NET MVC 的 Controller ,我们并没有在 Controller 里重写 Dispose 方法,去 Dispose 依赖项,那么,这些依赖项是如何被及时 Dispose 的呢?
答案在 Ninject.Web.Common 里,这里有一个 OnePerRequestHttpModule,这个 HttpModule 会注册一个 EndRequest 事件,并在该事件里 Clear 掉所有 Scope 为 HttpContext.Current 的实例。
var context = HttpContext.Current; this.MapKernels(kernel => kernel.Components.Get().Clear(context));
凡是定义为InRequestScope的依赖项,都会在这里被Dispose。
这个跟我们 Quartz Job 的 case 很像,但是 Quartz 没有一个 CurrentJob 的静态变量。怎么来定义一个 Job Scope 呢?
方案
通过阅读 Ninject 文档,我发现 Ninject.Web.Common 预留了一个 INinjectHttpApplicationPlugin 接口,并可以与 Ninject.Extension.NamedScope 整合,实现自定义的 InRequestScope。
INinjectHttpApplicationPlugin 接口主要定义了三个方法:Start,Stop 和 GetRequestScope。Start,Stop 分别对应启动和停止插件,而 GetRequestScope 则用来获取对应 InRequestScope 的依赖项的 Scope。
NamedScope extension 可以 Define 一个命名的、自定义的 Scope;而依赖项的生命周期可以定义为该 Scope 内。
这两者结合刚好符合我们的需求。
首先实现 INinjectHttpApplicationPlugin 接口,在 Start 的时候,绑定所有 IJob 的实现,并定义 NamedScope。在 GetRequestScope 的时候,尝试拿到此 NamedScope:
class QuartzPlugin : INinjectHttpApplicationPlugin { private readonly IKernel kernel; private IScheduler scheduler; public QuartzPlugin(IKernel kernel) { this.kernel = kernel; } public override void Start() { this.kernel.Bind(x => x.FromThisAssembly() .IncludingNonePublicTypes() .SelectAllClasses() .InheritedFrom() .BindToSelf() .Configure(c => c.DefinesNamedScope("QuartzJobScope"))); this.scheduler = this.kernel.Get<IScheduler>(); this.scheduler.ListenerManager.AddJobListener(this.kernel.Get<ReleaseJobListener>()); this.scheduler.Start(); // schedule jobs to the scheduler. } public override void Stop() { this.scheduler.Shutdown(false); } public override object GetRequestScope(IContext context) { return context.TryGetNamedScope("QuartzJobScope"); } }
然后把这个 Plugin 注册到 Ninject 的 Components 里,并声明依赖项的 Scope 为 InRequestScope:
kernel.Components.Add<INinjectHttpApplicationPlugin, QuartzPlugin>(); kernel.Bind<SomeDbContext>().ToSelf().InRequestScope();
最后,启动 Ninject.Web.Common 里的 Bootstrapper 就 OK 了。