MVC5 Ninject 绑定和 HttpContext
MVC5 Ninject binding and HttpContext
我正在尝试建立一个新项目,我添加了一个新的 class MembershipService,它需要 HttpContext 在它的构造函数中传递。
在之前的项目中我使用了代码
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IMembershipService>()
.To<MembershipService>()
.InRequestScope()
.WithConstructorArgument("context", HttpContext.Current);
....
}
但是在新项目中我使用的是 Ninject 模块,在 Whosebug 和 Google 上进行了一些搜索之后,我得出了以下代码:
public class ServiceHandlerModule : Ninject 模块
{
public override void Load()
{
Bind<IMembershipService>()
.To<MembershipService>()
.WithConstructorArgument("context", ninjectContext=> HttpContext.Current);
this.Kernel.Bind(x =>
{
x.FromAssemblyContaining(typeof(NinjectWebCommon))
.SelectAllClasses()
.Where(t => t != typeof(MembershipService))
.BindDefaultInterface();
});
this.Kernel.Bind(x =>
{
x.FromAssemblyContaining<BrandServiceHandler>()
.SelectAllClasses()
.Where(t => t != typeof(MembershipService))
.BindDefaultInterface();
});
}
}
但是,我收到以下错误:
Description: An unhandled exception occurred during the execution of
the current web request. Please review the stack trace for more
information about the error and where it originated in the code.
Exception Details: Ninject.ActivationException: Error activating
string No matching bindings are available, and the type is not
self-bindable. Activation path:
5) Injection of dependency string into parameter filename of
constructor of type HttpRequest
4) Injection of dependency HttpRequest into parameter request of
constructor of type HttpContext
3) Injection of dependency HttpContext into parameter httpContext of
constructor of type MembershipService
2) Injection of dependency IMembershipService into parameter
membershipService of constructor of type HomeController
1) Request for HomeController
有人能指出我哪里错了吗?
谢谢,
约翰
I've added a new class MembershipService that requires the HttpContext
to be passed in it's constructor.
这就是你出错的地方。 HttpContext 是一个 运行 时间值,但您的对象图应该只包含编译时或配置时依赖项。其他任何东西,运行时间值,应该通过方法调用传递,或者应该作为注入服务的属性公开。
不遵循此准则,将使编写和测试对象图变得更加困难。测试你的组合根是一个很好的例子,因为在测试框架内 运行 时 HttpContext.Current
不可用。
因此,请防止此 MembershipService
对 HttpContext
产生构造函数依赖。相反,注入一个将 HttpContext
公开为 属性 的服务,因为这允许您在对象图是构造函数后请求此上下文。
但也许更好的方法是将 HttpContext
隐藏在特定于应用程序的抽象之后。 HttpContext
不是抽象;它又大又丑 API 使您的代码更难测试和更难理解。相反,创建非常 narrow/focused 的接口,例如这样的接口:
public interface IUserContext
{
User CurrentUser { get; }
}
现在您的 MembershipService
可以依赖 IUserContext
,它通过 属性 公开 User
对象。现在您可以创建一个 AspNetUserContext
实现,它在调用 CurrentUser
属性 时在内部使用 HttpContext.Current
。这会产生更简洁、更易于维护的代码。
这是一个可能的实现:
public class AspNetUserContext : IUserContext
{
public User CurrentUser
{
// Do not inject HttpContext in the ctor, but use it
// here in this property
get { return new User(HttpContext.Current.User); }
}
}
我同意 Steven 的观点,但是,您也可以:
kernel.Bind<HttpContext>().ToMethod(c => HttpContext.Current);
Steven 关于 HttpContext
是运行时值的说法是正确的。在编写应用程序时甚至没有填充它的值。
如果您考虑一下,这是有道理的,因为应用程序应该在任何单个用户上下文之外进行初始化。
然而,Steven 的解决方案只是将问题转移到了不同的服务上。毕竟实现了IUserContext
的class还是需要把HttpContext
作为依赖的
解决方案是使用 Abstract Factory 以允许在运行时而不是在工厂连接时访问 HttpContext
实例。
Important: HttpContext is not an abstraction, so it cannot be swapped or mocked. To ensure we are dealing with an abstraction, Microsoft has provided the HttpContextBase abstract class and the default concrete type HttpContextWrapper. HttpContextBase has exactly the same interface as HttpContext. You should always use HttpContextBase as the abstract reference type within your services, not HttpContext.
考虑到这两点,您可以为您的 HttpContext
创建一个工厂,如下所示:
public interface IHttpContextFactory
{
HttpContextBase Create();
}
public class HttpContextFactory
: IHttpContextFactory
{
public HttpContextBase Create()
{
return new HttpContextWrapper(HttpContext.Current);
}
}
然后可以修改您的 MembershipService
以在其构造函数中接受 IHttpContextFactory
:
public class MembershipService : IMembershipService
{
private readonly IHttpContextFactory httpContextFactory;
// This is called at application startup, but note that it
// does nothing except get our service(s) ready for runtime.
// It does not actually use the service.
public MembershipService(IHttpContextFactory httpContextFactory)
{
if (httpContextFactory == null)
throw new ArgumentNullException("httpContextFactory");
this.httpContextFactory = httpContextFactory;
}
// Make sure this is not called from any service constructor
// that is called at application startup.
public void DoSomething()
{
HttpContextBase httpContext = this.httpContextFactory.Create();
// Do something with HttpContext (at runtime)
}
}
你只需要在组合时注入 HttpContextFactory
。
kernel.Bind<IHttpContextFactory>()
.To<HttpContextFactory>();
kernel.Bind<IMembershipService>()
.To<MembershipService>();
不过,仅此一项可能无法解决全部问题。您需要确保您的应用程序的其余部分在准备就绪之前不会尝试使用 HttpContext
。就 DI 而言,这意味着您不能在应用程序启动中组成的任何类型的构造函数或其中一个构造函数调用的任何服务成员中使用 HttpContext
。要解决这个问题,您可能需要创建额外的抽象工厂以确保这些服务在 HttpContext
准备就绪之前不会调用 IMembershipService
的成员。
有关如何完成此操作的详细信息,请参阅 this answer。
Steven 的解决方案还需要围绕 HttpContext
创建一个 Facade。虽然这并不能真正帮助解决手头的问题,但我同意,如果您的 MembershipService
(可能还有其他服务)仅使用 HttpContext
的少数成员,这可能是个好主意。通常,此模式旨在使复杂对象更易于使用(例如将其扁平化为可能嵌套在其层次结构深处的几个成员)。但是您确实需要权衡添加另一种类型的额外维护与在您的应用程序中使用 HttpContext
的复杂性(或换出其中一部分的价值)来做出该决定。
我正在尝试建立一个新项目,我添加了一个新的 class MembershipService,它需要 HttpContext 在它的构造函数中传递。
在之前的项目中我使用了代码
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IMembershipService>()
.To<MembershipService>()
.InRequestScope()
.WithConstructorArgument("context", HttpContext.Current);
....
}
但是在新项目中我使用的是 Ninject 模块,在 Whosebug 和 Google 上进行了一些搜索之后,我得出了以下代码: public class ServiceHandlerModule : Ninject 模块 {
public override void Load()
{
Bind<IMembershipService>()
.To<MembershipService>()
.WithConstructorArgument("context", ninjectContext=> HttpContext.Current);
this.Kernel.Bind(x =>
{
x.FromAssemblyContaining(typeof(NinjectWebCommon))
.SelectAllClasses()
.Where(t => t != typeof(MembershipService))
.BindDefaultInterface();
});
this.Kernel.Bind(x =>
{
x.FromAssemblyContaining<BrandServiceHandler>()
.SelectAllClasses()
.Where(t => t != typeof(MembershipService))
.BindDefaultInterface();
});
}
}
但是,我收到以下错误:
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: Ninject.ActivationException: Error activating string No matching bindings are available, and the type is not self-bindable. Activation path:
5) Injection of dependency string into parameter filename of constructor of type HttpRequest
4) Injection of dependency HttpRequest into parameter request of constructor of type HttpContext
3) Injection of dependency HttpContext into parameter httpContext of constructor of type MembershipService
2) Injection of dependency IMembershipService into parameter membershipService of constructor of type HomeController
1) Request for HomeController
有人能指出我哪里错了吗?
谢谢, 约翰
I've added a new class MembershipService that requires the HttpContext to be passed in it's constructor.
这就是你出错的地方。 HttpContext 是一个 运行 时间值,但您的对象图应该只包含编译时或配置时依赖项。其他任何东西,运行时间值,应该通过方法调用传递,或者应该作为注入服务的属性公开。
不遵循此准则,将使编写和测试对象图变得更加困难。测试你的组合根是一个很好的例子,因为在测试框架内 运行 时 HttpContext.Current
不可用。
因此,请防止此 MembershipService
对 HttpContext
产生构造函数依赖。相反,注入一个将 HttpContext
公开为 属性 的服务,因为这允许您在对象图是构造函数后请求此上下文。
但也许更好的方法是将 HttpContext
隐藏在特定于应用程序的抽象之后。 HttpContext
不是抽象;它又大又丑 API 使您的代码更难测试和更难理解。相反,创建非常 narrow/focused 的接口,例如这样的接口:
public interface IUserContext
{
User CurrentUser { get; }
}
现在您的 MembershipService
可以依赖 IUserContext
,它通过 属性 公开 User
对象。现在您可以创建一个 AspNetUserContext
实现,它在调用 CurrentUser
属性 时在内部使用 HttpContext.Current
。这会产生更简洁、更易于维护的代码。
这是一个可能的实现:
public class AspNetUserContext : IUserContext
{
public User CurrentUser
{
// Do not inject HttpContext in the ctor, but use it
// here in this property
get { return new User(HttpContext.Current.User); }
}
}
我同意 Steven 的观点,但是,您也可以:
kernel.Bind<HttpContext>().ToMethod(c => HttpContext.Current);
Steven 关于 HttpContext
是运行时值的说法是正确的。在编写应用程序时甚至没有填充它的值。
如果您考虑一下,这是有道理的,因为应用程序应该在任何单个用户上下文之外进行初始化。
然而,Steven 的解决方案只是将问题转移到了不同的服务上。毕竟实现了IUserContext
的class还是需要把HttpContext
作为依赖的
解决方案是使用 Abstract Factory 以允许在运行时而不是在工厂连接时访问 HttpContext
实例。
Important: HttpContext is not an abstraction, so it cannot be swapped or mocked. To ensure we are dealing with an abstraction, Microsoft has provided the HttpContextBase abstract class and the default concrete type HttpContextWrapper. HttpContextBase has exactly the same interface as HttpContext. You should always use HttpContextBase as the abstract reference type within your services, not HttpContext.
考虑到这两点,您可以为您的 HttpContext
创建一个工厂,如下所示:
public interface IHttpContextFactory
{
HttpContextBase Create();
}
public class HttpContextFactory
: IHttpContextFactory
{
public HttpContextBase Create()
{
return new HttpContextWrapper(HttpContext.Current);
}
}
然后可以修改您的 MembershipService
以在其构造函数中接受 IHttpContextFactory
:
public class MembershipService : IMembershipService
{
private readonly IHttpContextFactory httpContextFactory;
// This is called at application startup, but note that it
// does nothing except get our service(s) ready for runtime.
// It does not actually use the service.
public MembershipService(IHttpContextFactory httpContextFactory)
{
if (httpContextFactory == null)
throw new ArgumentNullException("httpContextFactory");
this.httpContextFactory = httpContextFactory;
}
// Make sure this is not called from any service constructor
// that is called at application startup.
public void DoSomething()
{
HttpContextBase httpContext = this.httpContextFactory.Create();
// Do something with HttpContext (at runtime)
}
}
你只需要在组合时注入 HttpContextFactory
。
kernel.Bind<IHttpContextFactory>()
.To<HttpContextFactory>();
kernel.Bind<IMembershipService>()
.To<MembershipService>();
不过,仅此一项可能无法解决全部问题。您需要确保您的应用程序的其余部分在准备就绪之前不会尝试使用 HttpContext
。就 DI 而言,这意味着您不能在应用程序启动中组成的任何类型的构造函数或其中一个构造函数调用的任何服务成员中使用 HttpContext
。要解决这个问题,您可能需要创建额外的抽象工厂以确保这些服务在 HttpContext
准备就绪之前不会调用 IMembershipService
的成员。
有关如何完成此操作的详细信息,请参阅 this answer。
Steven 的解决方案还需要围绕 HttpContext
创建一个 Facade。虽然这并不能真正帮助解决手头的问题,但我同意,如果您的 MembershipService
(可能还有其他服务)仅使用 HttpContext
的少数成员,这可能是个好主意。通常,此模式旨在使复杂对象更易于使用(例如将其扁平化为可能嵌套在其层次结构深处的几个成员)。但是您确实需要权衡添加另一种类型的额外维护与在您的应用程序中使用 HttpContext
的复杂性(或换出其中一部分的价值)来做出该决定。