使用 NamedScope 扩展 Ninject 的 InRequestScope

背景

C#,Ninject,定期执行某计划任务。首先想到的是使用 Quartz 来安排计划任务,于是看是否有相应的集成。果然有:https://github.com/dtinteractive/Ninject.Extensions.Quartz/。该项目提供了一个 NinjectJobFactory,用来创建 Job,代码很简单,就是从 Ninject 的 Kernel 里去 Resolve Job:

Code
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:

Code
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 了。

原文地址:https://www.cnblogs.com/xushuo/p/Ninject_NamedScope_RequestScope_NinjectHttpApplicationPlugin.html