I am struggling to simulate the required HttpContext for my unit tests.
I have abstracted control of the session away from my Mvc controller with a SessionManager interface and implemented this with a class called CookieSessionManager. (early development stages).
CookieSessionManager uses the HttpContext by using the injected singleton HttpContextAccessor (in Startup.cs ConfigureServices).
I'm using Cookie Authentication which is setup in Startup.cs with app.UseCookieAuthentication.
Testing this manually in debug mode works as expected
The MSUnit tests I have written for my AccountController class work with a MockSessionManager class injected.
The real issue I have is with the unit tests I have written for my CookieSessionManager class. I have tried to setup the HttpContext as shown below;
[TestClass]
public class CookieSessionManagerTest
{
private IHttpContextAccessor contextAccessor;
private HttpContext context;
private SessionManager sessionManager;
[TestInitialize]
public void Setup_CookieSessionManagerTest()
{
context = new DefaultHttpContext();
contextAccessor = new HttpContextAccessor();
contextAccessor.HttpContext = context;
sessionManager = new CookieSessionManager(contextAccessor);
}
The Error
But the call to sessionManager.Login(CreateValidApplicationUser()); doesn't appear to set the IsAuthenticated flag and the test CookieSessionManager_Login_ValidUser_Authenticated_isTrue fails.
[TestMethod]
public void CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
{
sessionManager.Login(CreateValidApplicationUser());
Assert.IsTrue(sessionManager.isAuthenticated());
}
public ApplicationUser CreateValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "ValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
Test Name: CookieSessionManager_Login_ValidUser_Authenticated_isTrue
: line 43 Test Outcome: Failed Test Duration: 0:00:00.0433169
Result StackTrace: at ClaimsWebAppTests.Identity.CookieSessionManagerTest.CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
CookieSessionManagerTest.cs:line 46 Result Message: Assert.IsTrue failed.
MY CODE
SessionManager
using ClaimsWebApp.Models;
namespace ClaimsWebApp.Identity
{
public interface SessionManager
{
bool isAuthenticated();
void Login(ApplicationUser applicationUser);
void Logout();
}
}
CookieSessionManager
using ClaimsWebApp.Identity;
using ClaimsWebApp.Models;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Security.Claims;
namespace ClaimsWebApp
{
public class CookieSessionManager : SessionManager
{
private List<ApplicationUser> applicationUsers;
private IHttpContextAccessor ContextAccessor;
private bool IsAuthenticated;
public CookieSessionManager(IHttpContextAccessor contextAccessor)
{
this.IsAuthenticated = false;
this.ContextAccessor = contextAccessor;
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
applicationUsers = new List<ApplicationUser>();
applicationUsers.Add(new ApplicationUser { UserName = "ValidUser" });
}
public bool isAuthenticated()
{
return IsAuthenticated;
}
public void Login(ApplicationUser applicationUser)
{
if (applicationUsers.Find(m => m.UserName.Equals(applicationUser.UserName)) != null)
{
var identity = new ClaimsIdentity(new[] {
new Claim(ClaimTypes.Name, applicationUser.UserName)
},
"MyCookieMiddlewareInstance");
var principal = new ClaimsPrincipal(identity);
ContextAccessor.HttpContext.Authentication.SignInAsync("MyCookieMiddlewareInstance", principal);
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
}
else
{
throw new Exception("User not found");
}
}
public void Logout()
{
ContextAccessor.HttpContext.Authentication.SignOutAsync("MyCookieMiddlewareInstance");
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
}
}
}
Startup.cs
using ClaimsWebApp.Identity;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace ClaimsWebApp
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<SessionManager, CookieSessionManager>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "MyCookieMiddlewareInstance",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=Login}/{id?}");
});
}
}
}
CookieSessionManagerTest.cs
using ClaimsWebApp;
using ClaimsWebApp.Identity;
using ClaimsWebApp.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ClaimsWebAppTests.Identity
{
[TestClass]
public class CookieSessionManagerTest
{
private IHttpContextAccessor contextAccessor;
private HttpContext context;
private SessionManager sessionManager;
[TestInitialize]
public void Setup_CookieSessionManagerTest()
{
context = new DefaultHttpContext();
contextAccessor = new HttpContextAccessor();
contextAccessor.HttpContext = context;
sessionManager = new CookieSessionManager(contextAccessor);
}
[TestMethod]
public void CookieSessionManager_Can_Be_Implemented()
{
Assert.IsInstanceOfType(sessionManager, typeof(SessionManager));
}
[TestMethod]
public void CookieSessionManager_Default_Authenticated_isFalse()
{
Assert.IsFalse(sessionManager.isAuthenticated());
}
[TestMethod]
public void CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
{
sessionManager.Login(CreateValidApplicationUser());
Assert.IsTrue(sessionManager.isAuthenticated());
}
public ApplicationUser CreateValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "ValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
public ApplicationUser CreateInValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "InValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
}
}
Unfortunately, it's pretty much impossible to test with HttpContext. It's a sealed class that doesn't utilize any interfaces, so you cannot mock it. Usually, your best bet is to abstract away the code that works with HttpContext, and then test just your other, more application-specific code.
It looks like you've sort of already done this via HttpContextAccessor, but you're utilizing it incorrectly. First, you're exposing the HttpContext instance, which pretty much defeats the entire purpose. This class should be able to return something like User.Identity.IsAuthenticated on its own, like: httpContextAccessor.IsAuthenticated. Internally, the property would access the private HttpContext instance and just return the result.
Once you're utilizing it in this way, you can then mock HttpContextAccessor to simply return what you need for your tests, and you don't have to worry about supplying it with an HttpContext instance.
Granted, this means there's still some untested code, namely, the accessor methods that work with HttpContext, but these are generally very straight-forward. For example, the code for IsAuthenticated would just be something like return httpContext.User.Identity.IsAuthenticated. The only way you're going to screw that up is if you fat-finger something, but the compiler will warn you about that.
I created this helper functionality for my unit tests, this allowed me to test those specific methods that required portions of the httpRequest.
public static IHttpContextAccessor GetHttpContext(string incomingRequestUrl, string host)
{
var context = new DefaultHttpContext();
context.Request.Path = incomingRequestUrl;
context.Request.Host = new HostString(host);
//Do your thing here...
var obj = new HttpContextAccessor();
obj.HttpContext = context;
return obj;
}
This doesn't directly answer the context of the question but it provides an alternative method to testing and when you start using it makes life so much easier.
There is an integration testing package available for ASP.NET Core and documentation about it can be found here:
https://docs.asp.net/en/latest/testing/integration-testing.html
Enjoy!
You can create a Test class Like below which inherit the HttpContext. And use the test class where you need it. You can add the missing implementation on the code.
public class TestHttpContext : HttpContext
{
[Obsolete]
public override AuthenticationManager Authentication
{
get { throw new NotImplementedException(); }
}
public override ConnectionInfo Connection
{
get { throw new NotImplementedException(); }
}
public override IFeatureCollection Features
{
get { throw new NotImplementedException(); }
}
public override IDictionary<object, object> Items
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override HttpRequest Request
{
get { throw new NotImplementedException(); }
}
public override CancellationToken RequestAborted
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override IServiceProvider RequestServices
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
HttpResponse _response;
public override HttpResponse Response
{
get
{
if (this._response == null)
{
this._response = new TestHttpResponse();
this._response.StatusCode = 999;
}
return this._response;
}
}
public override ISession Session
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override string TraceIdentifier
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override ClaimsPrincipal User
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override WebSocketManager WebSockets
{
get { throw new NotImplementedException(); }
}
public override void Abort()
{
throw new NotImplementedException();
}
}
Related
I've created a service that's used throughout my aspnet project that retrieves and validates a header among other things. Issue is that the Exception Filter is not able to catch the errors that are thrown by the service as it's not in the scope of the Exception Filter thus giving the user an ugly internal server error. Is there any way to gracefully return an argument error with description to the user with the use of the services?
The Startup:
services.AddScoped<UserService>();
services
.AddMvc(x =>
{
x.Filters.Add(typeof(Filters.MyExceptionFilter));
})
The Exception Filter:
public class MyExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is ArgumentException argumentException)
{
var response = context.HttpContext.Response;
context.ExceptionHandled = true;
response.StatusCode = 400;
context.Result = new ObjectResult(argumentException.Message);
return;
}
}
}
The Service:
public class UserService
{
public readonly string UserId;
public UserService(IHttpContextAccessor context)
{
if (!context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
throw new ArgumentException($"x-test header is required.");
}
UserId = user;
//do other stuff
}
}
Controller Action Method:
public async Task<IActionResult> Delete(string id,
[FromServices] UserService userService)
{
//do stuff
}
A rule-of-thumb in C# is do as little work in the constructor as possible. There's a few good reasons for this (e.g. you can't dispose a class that threw an exception in it's constructor). Another good reason is, as you have found out, construction might happen in a different place (e.g. a DI container) than the place you actually use the class.
The fix should be quite straightforward - just move the logic out of the constructor. You could use a Lazy<T> to do this for example:
public class UserService
{
public readonly Lazy<string> _userId ;
public UserService(IHttpContextAccessor context)
{
_userId = new Lazy<string>(() =>
{
if (!context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
throw new ArgumentException($"x-test header is required.");
}
return user;
});
//do other stuff
}
public string UserId => _userId.Value;
}
Or you could just get the value when you needed it:
public class UserService
{
public readonly IHttpContextAccessor _context;
public UserService(IHttpContextAccessor context)
{
_context = context;
//do other stuff
}
public string UserId
{
get
{
if (_context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
return user;
}
else
{
throw new ArgumentException($"x-test header is required.");
}
}
}
}
My Application: .Net Core 3.1 Web application using Microservice architecture; Identity for Authorization & Authentication as separate Microservice API.
I have extended the standard AspNetUsers and AspNetRoles table with custom fields.
Getting the following error when I am trying to create a new Role using Identity RoleManager.
Cannot access a disposed context instance. A common cause of this
error is disposing a context instance that was resolved from
dependency injection and then later trying to use the same context
instance elsewhere in your application. This may occur if you are
calling 'Dispose' on the context instance, or wrapping it in a using
statement. If you are using dependency injection, you should let the
dependency injection container take care of disposing context
instances. Object name: 'MembershipDBContext'.
Find my Code below
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var idenConnectionString = Configuration["DbContextSettings:IdentityConnectionString"];
var userConnectionString = Configuration["DbContextSettings:UserConnectionString"];
var dbPassword = Configuration["DbContextSettings:DbPassword"];
var builder = new NpgsqlConnectionStringBuilder(idenConnectionString)
{
Password = dbPassword
};
var userBuilder = new NpgsqlConnectionStringBuilder(userConnectionString)
{
Password = dbPassword
};
services.AddDbContext<MembershipDBContext>(opts => opts.UseNpgsql(builder.ConnectionString));
services.AddDbContext<UserDBContext>(opts => opts.UseNpgsql(userBuilder.ConnectionString));
services.AddIdentity<MembershipUser, MembershipRole>(options =>
{
options.Password.RequiredLength = 8;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+ ";
options.SignIn.RequireConfirmedEmail = false;
}).AddRoles<MembershipRole>().AddEntityFrameworkStores<MembershipDBContext>()
.AddDefaultTokenProviders();
services.AddTransient<IIdentityMSService, IdentityMSService>();//IdentityMS
services.AddTransient<IAdministrationService, AdministrationService>();//IdentityMS
services.AddTransient<IIdentityMSRepository, IdentityMSRepository>();//IdentityMS
services.AddTransient<IAdministrationRepository, AdministrationRepository>();//IdentityMS
services.AddTransient<UserDBContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddMediatR(typeof(Startup));
RegisterServices(services);
}
MembershipDBContext.cs
public class MembershipDBContext : IdentityDbContext<MembershipUser,MembershipRole,string>
{
public MembershipDBContext(DbContextOptions<MembershipDBContext> options) : base(options)
{
}
}
AdministrationController
public class AdministrationController : Controller
{
private readonly IAdministrationMVCService _adminService;
public AdministrationController(IAdministrationMVCService adminService)
{
_adminService = adminService;
}
// GET: AdministrationController/Create
public ActionResult Create()
{
return View();
}
// POST: AdministrationController/Create
[HttpPost]
//[ValidateAntiForgeryToken]
public async Task<IActionResult> CreateAsync(MembershipRole rm)
{
if (ModelState.IsValid)
{
try
{
rm.CompanyId = 1;
await _adminService.AddRoles(rm);
//return RedirectToAction(nameof(Index));
return View();
}
catch
{
return View();
}
}
return View();
}
}
AdministrationApiController
[HttpPost]
public void Post([FromBody] MembershipRole role)
{
_adminMSService.AddRoles(role);
}
AdministrationRepository
public class AdministrationRepository : IAdministrationRepository
{
private readonly RoleManager<MembershipRole> _roleManager;
private readonly UserManager<MembershipUser> _userManager;
public AdministrationRepository(RoleManager<MembershipRole> roleManager, UserManager<MembershipUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
public async Task AddRolesAsync(MembershipRole rvm)
{
try
{
IdentityResult result = await _roleManager.CreateAsync(rvm);
if (result.Succeeded) {
}
}
catch (Exception ex)
{
throw;
}
}
}
I tried making the services and repositories as singleton but it didn't work either. What am I missing here? Any insights?
Solved the issue! As DavidG pointed out in his comment, I missed the 'await' keyword at certain places like AdministrationApiController.
Posting as an 'Answer' here, as I am unable to mark the comment as 'Answer'.
First off, let me say that I don't think that is is an issue with ReactiveUI per se, which is why I've not created an issue on its github repo, and second, I realise that I'm using a beta version of ReactiveUI.
I want to use Structuremap because I'm going to have a plugin scenario in my WPF app, and the DI container in Splat isn't cut out for that sort of thing.
Observe these unit tests:
[Fact]
public void ShouldBeAbleToOverrideDefaultDependencyResolver()
{
Locator.Current = new ApplicationDependencyResolver(StructureMapBootstrapper.Instance.Container);
Locator.CurrentMutable.InitializeSplat();
Locator.CurrentMutable.InitializeReactiveUI();
var view = Locator.Current.GetService<SplashScreenView>();
view.Should().NotBeNull().And.BeOfType<SplashScreenView>();
}
[Fact]
public void ShouldBeAbleToLocateTheViewForAViewModel()
{
Locator.Current = new ApplicationDependencyResolver(StructureMapBootstrapper.Instance.Container);
Locator.CurrentMutable.InitializeSplat();
Locator.CurrentMutable.InitializeReactiveUI();
var viewLocator = Locator.Current.GetService<IViewLocator>();
var view = viewLocator.ResolveView(typeof (SplashScreenViewModel));
view.Should().NotBeNull().And.BeOfType<SplashScreenView>();
}
The first test passes. The second test does not, and provides this stacktrace:
StructureMap.StructureMapConfigurationExceptionNo default Instance is registered and cannot be automatically determined for type 'IViewFor<RuntimeType>'
There is no configuration specified for IViewFor<RuntimeType>
1.) Container.GetInstance(IViewFor<RuntimeType>)
at StructureMap.SessionCache.GetDefault(Type pluginType, IPipelineGraph pipelineGraph) in c:\BuildAgent\work\996e173a8ceccdca\src\StructureMap\SessionCache.cs: line 63
at StructureMap.Container.GetInstance(Type pluginType) in c:\BuildAgent\work\996e173a8ceccdca\src\StructureMap\Container.cs: line 325
at Redacted.ApplicationDependencyResolver.GetService(Type serviceType, String contract) in ApplicationDependencyResolver.cs: line 27
at ReactiveUI.DefaultViewLocator.attemptToResolveView(Type type, String contract)
at ReactiveUI.DefaultViewLocator.ResolveView(T viewModel, String contract)
at Redacted.BootstrapAndDependencyResolutionTests.ShouldBeAbleToLocateTheViewForAViewModel() in BootstrapAndDependencyResolutionTests.cs: line 39
I obviously do not, and can not, have any views which implement IViewFor<RuntimeType>. Anyone got any ideas as to why this is happening, and what I can do to get around this? I can't exclude it using the normal Structuremap configuration.
For full clarity here are the implementations of the resolver and the structuremap bootstrapper:
public class ApplicationDependencyResolver : IMutableDependencyResolver
{
private readonly IContainer _container;
public ApplicationDependencyResolver(IContainer container)
{
_container = container;
}
public void Dispose()
{
_container.Dispose();
}
public object GetService(Type serviceType, string contract = null)
{
return string.IsNullOrEmpty(contract)
? _container.GetInstance(serviceType)
: _container.GetInstance(serviceType, contract);
}
public IEnumerable<object> GetServices(Type serviceType, string contract = null)
{
return _container.GetAllInstances(serviceType).Cast<object>();
}
public void Register(Func<object> factory, Type serviceType, string contract = null)
{
var o = factory();
_container.Configure(configure => configure.For(serviceType).Use(o));
}
}
public sealed class StructureMapBootstrapper
{
private static readonly StructureMapBootstrapper InternalInstance = new StructureMapBootstrapper();
static StructureMapBootstrapper() { }
private StructureMapBootstrapper()
{
Configure();
}
public static StructureMapBootstrapper Instance { get { return InternalInstance; } }
public IContainer Container { get; private set; }
private void Configure()
{
Container = new Container(configure =>
{
configure.Scan(with =>
{
with.TheCallingAssembly();
with.LookForRegistries();
with.WithDefaultConventions();
});
});
}
}
After some quality time with the ReactiveUI unit tests, it turns out that the unit test which was failing was actually not implemented correctly, and should look like this:
[Fact]
public void ShouldBeAbleToLocateTheViewForAViewModel()
{
var container = StructureMapBootstrapper.Instance.Container;
var ihas = container.WhatDoIHave();
Locator.Current = new ApplicationDependencyResolver(container);
Locator.CurrentMutable.InitializeSplat();
Locator.CurrentMutable.InitializeReactiveUI();
var vm = new SplashScreenViewModel();
var viewLocator = Locator.Current.GetService<IViewLocator>();
var view = viewLocator.ResolveView(vm);
view.Should().NotBeNull().And.BeOfType<SplashScreenView>();
}
Specifically, it was the fact I was passing typeof(SplashScreenViewMode), and not an instance, that was causing the test to fail.
Edit: I also had to add with.AddAllTypesOf(typeof (IViewFor<>)); to the Structuremap configuration.
I am using Ninject together with ASP.NET MVC 4. I am using repositories and want to do constructor injection to pass in the repository to one of the controllers.
Here is my context object (EntityFramework) that implements my StatTracker interface:
public class StatTrackerRepository : IStatTrackerRepository
{
private GolfStatTrackerEntities _ctx;
public StatTrackerRepository(GolfStatTrackerEntities ctx)
{
_ctx = ctx;
}
public IQueryable<Facility> GetFacilites()
{
return _ctx.Facilities;
}
}
This is my Repository interface:
public interface IStatTrackerRepository
{
IQueryable<Facility> GetFacilites();
}
Which then calls my Home Controller:
public class HomeController : Controller
{
public IStatTrackerRepository _repo { get; set; }
public HomeController(IStatTrackerRepository repo)
{
_repo = repo;
}
public ActionResult Index()
{
var facilities = _repo.GetFacilites().ToList();
return View(facilities);
}
}
The page loads properly, however, once the page is loaded, it immedately uses an angularjs Controller which calls the $http method:
function facilityIndexController($scope, $http) {
$scope.data = [];
$http({ method: 'GET', url: '/api/facility' }).
success(function(result) {
angular.copy(result.data, $scope.data);
}).error(function() {
alert("Could not load facilities");
});
}
...which calls the following API controller:
public class FacilityController : ApiController
{
public IStatTrackerRepository _repo { get; set; }
public GolfStatTrackerEntities ctx { get; set; }
//public FacilityController()
//{
// _repo = new StatTrackerRepository(ctx);
//}
public FacilityController(IStatTrackerRepository repo)
{
_repo = repo;
}
public IEnumerable<Facility> Get()
{
var facilities = _repo.GetFacilites().ToList();
return facilities;
}
}
....where it falls into the error function of the angular $http call because the FacilityController(IStatTrackerRepository repo) is never ran.
I have tried using a parameterless contstructor that instantiates a StatTrackerRepository(ctx) for FacilityController(), however, I get a NullReferenceException when I do so.
My Ninject config is as follows:
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
try
{
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
//GlobalConfiguration.Configuration.DependencyResolver =
// new NinjectResolver(kernel);
return kernel;
}
catch
{
kernel.Dispose();
throw;
}
}
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<GolfStatTrackerEntities>().To<GolfStatTrackerEntities>().InRequestScope();
kernel.Bind<IStatTrackerRepository>().To<StatTrackerRepository>().InRequestScope();
}
I'm not sure if this is something wrong with Ninject or if there is an issue with how I am implementing Ninject. The injection seems to be working on the initial load of the Home view, however, when it uses angular to call the API, there is a disconnect with Ninject.
Please help.
We ended up using a similar configuration on one of our older projects and realized that we needed to add a little more infrastructure code to our MVC/WebApi App:
NinjectDependencyScope.cs
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Web.Http.Dependencies;
using Ninject;
using Ninject.Syntax;
namespace YourAppNameSpace
{
public class NinjectDependencyScope : IDependencyScope
{
private IResolutionRoot _resolver;
internal NinjectDependencyScope(IResolutionRoot resolver)
{
Contract.Assert(resolver != null);
_resolver = resolver;
}
public void Dispose()
{
var disposable = _resolver as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
_resolver = null;
}
public object GetService(Type serviceType)
{
if (_resolver == null)
{
throw new ObjectDisposedException("this", "This scope has already been disposed");
}
return _resolver.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
if (_resolver == null)
{
throw new ObjectDisposedException("this", "This scope has already been disposed");
}
return _resolver.GetAll(serviceType);
}
}
}
NinjectDependencyResolver.cs
using System.Web.Http.Dependencies;
using Ninject;
namespace YourAppNameSpace
{
public class NinjectDependencyResolver : NinjectDependencyScope, IDependencyResolver
{
private readonly IKernel _kernel;
public NinjectDependencyResolver(IKernel kernel)
: base(kernel)
{
_kernel = kernel;
}
public IDependencyScope BeginScope()
{
return new NinjectDependencyScope(_kernel.BeginBlock());
}
}
}
Then we added the following to the NinjectWebCommon.cs file:
public static void RegisterNinject(HttpConfiguration configuration)
{
// Set Web API Resolver
configuration.DependencyResolver = new NinjectDependencyResolver(Bootstrapper.Kernel);
}
And then the following to Global.asax.cs file, Application_Start method:
NinjectWebCommon.RegisterNinject(GlobalConfiguration.Configuration);
This is my controller
public class SuggestionController : ApiController
{
public ISuggestionRepository Repository { get; private set; }
public SuggestionController(ISuggestionRepository repository)
{
this.Repository = repository;
}
// to post suggestion
[HttpPost]
[ActionName("PostSuggestion")]
public HttpResponseMessage PostSuggestion(Suggestion suggestion)
{
var answerCorrect = this.Repository.CreateSuggestion(suggestion);
if (answerCorrect == true)
return Request.CreateResponse(HttpStatusCode.OK);
else
return Request.CreateResponse(HttpStatusCode.Conflict);
}
}
and this is my RegisterServices method in NinjectWebCommon.cs
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<ICompetitionRepository>().To(typeof(CompetitionRepository))
.WithConstructorArgument("serviceContext", new InMemoryDataContext<Competition>());
kernel.Bind<ISubmissionRepository>().To(typeof(SubmissionRepository))
.WithConstructorArgument("serviceContext", new InMemoryDataContext<Submission>());
kernel.Bind<IUserRepository>().To(typeof(UserRepository))
.WithConstructorArgument("serviceContext", new InMemoryDataContext<User>());
kernel.Bind<ISuggestionRepository>().To(typeof(SuggestionRepository))
.WithConstructorArgument("serviceContext", new InMemoryDataContext<Suggestion>());
}
But am getting an exception that my suggestion controller does not have a default constructor and its showing a 500 internal server when am hitting the controller from a client app
I know that we get the exception of controller not having default constructor if the ninject dependency is not working properly but the below is another controller i have implemeneted similar to suggestion controller and its working absolutely fine.
public IUserRepository Repository { get; private set; }
public SSOController(IUserRepository repository)
{
this.Repository = repository;
}
[HttpPost]
[ActionName("PostUser")]
public HttpResponseMessage PostUser([FromBody]string id)
{
var accessToken = id;
var client = new FacebookClient(accessToken);
dynamic result = client.Get("me", new { fields = "name,email" });
string name = result.name;
string email = result.email;
var existingUser = this.Repository.FindByUserIdentity(name);
if (existingUser == null)
{
var newUser = new User
{
Username = name,
Email = email,
};
var success = this.Repository.CreateAccount(newUser);
if (!success)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError);
}
//return created status code as we created the user
return Request.CreateResponse<User>(HttpStatusCode.Created, newUser);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
}
I have no idea where am going wrong. Please let me know if u have any suggestions.
EDIT:
my Global.asax
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
AuthConfig.RegisterAuth();
GlobalConfiguration.Configuration.IncludeErrorDetailPolicy =
IncludeErrorDetailPolicy.Always;
}
Dependency resolver am using
// Provides a Ninject implementation of IDependencyScope
// which resolves services using the Ninject container.
public class NinjectDependencyScope : IDependencyScope
{
IResolutionRoot resolver;
public NinjectDependencyScope(IResolutionRoot resolver)
{
this.resolver = resolver;
}
public object GetService(Type serviceType)
{
if (resolver == null)
throw new ObjectDisposedException("this", "This scope has been disposed");
return resolver.TryGet(serviceType);
}
public System.Collections.Generic.IEnumerable<object> GetServices(Type serviceType)
{
if (resolver == null)
throw new ObjectDisposedException("this", "This scope has been disposed");
return resolver.GetAll(serviceType);
}
public void Dispose()
{
IDisposable disposable = resolver as IDisposable;
if (disposable != null)
disposable.Dispose();
resolver = null;
}
}
// This class is the resolver, but it is also the global scope
// so we derive from NinjectScope.
public class NinjectDependencyResolver : NinjectDependencyScope, IDependencyResolver
{
IKernel kernel;
public NinjectDependencyResolver(IKernel kernel)
: base(kernel)
{
this.kernel = kernel;
}
public IDependencyScope BeginScope()
{
return new NinjectDependencyScope(kernel.BeginBlock());
}
}
and calling it in CreateKernel() method in NinjectWebCommon
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
// Install our Ninject-based IDependencyResolver into the Web API config
GlobalConfiguration.Configuration.DependencyResolver = new NinjectDependencyResolver(kernel);
return kernel;
}
Suggestion Repository
public class SuggestionRepository : Repository<Suggestion>, ISuggestionRepository
{
public SuggestionRepository(IServiceContext<Suggestion> servicecontext)
: base(servicecontext)
{ }
public bool CreateSuggestion(Suggestion suggestion)
{
this.ServiceContext.Create(suggestion);
this.ServiceContext.Save();
return true;
}
}
ISuggestionRepository
public interface ISuggestionRepository
{
bool CreateSuggestion(Suggestion suggestion);
}
Repository
public abstract class Repository<T>
{
public IServiceContext<T> ServiceContext { get; private set; }
public Repository(IServiceContext<T> serviceContext)
{
this.ServiceContext = serviceContext;
}
}
IserviceContext
public interface IServiceContext<T>
{
IQueryable<T> QueryableEntities { get; }
void Create(T entity);
void Update(T entity);
void Delete(T entity);
void Save();
}
Since you're using WebApi, you will need to use the WebApi extension for Ninject. Unfortunately, the current Ninject.WebApi nuget package is out of date, and doesn't work with the released version of WebApi.
Temporarily, until Remo gets around to updating Ninject.WebApi to the release version, you can use Ninject.WebApi-RC http://nuget.org/packages/Ninject.Web.WebApi-RC
http://www.eyecatch.no/blog/2012/06/using-ninject-with-webapi-rc/
EDIT:
To recap the information discussed in comments, Here are the recommendations:
1) Use Ninject.MVC3 and Ninject.Web.WebApi (but use Ninject.Web.WebApi-RC until the official is updated) as discussed above. Do not use a custom DependencyResolver, and let Ninject.Web.Mvc and .WebApi do their job.
2) Change your bindings to this:
kernel.Bind<ICompetitionRepository>().To<CompetitionRepository>();
... similar bindings
3) Add a generic binding for your ServiceContext
kernel.Bind(typeof(IServiceContext<>)).To(typeof(InMemoryDataContext<>));
I think the problem is you're using the ApiController.
Controllers and apiControllers are using a different dependancy injection container.
Both of them however expose the same methods.
If the working controller is inheriting the Controller class then that's your cause.
For a work around take a look at
this topic
I have faced the same issue.
This is how I rectified:
I created a WebContainerManager which is just a static wrapper around container.
Static container wrappers useful when you don't control instantiation and can't rely on injection - e.g. action filter attributes
public static class WebContainerManager
{
public static IKernel GetContainer()
{
var resolver = GlobalConfiguration.Configuration.DependencyResolver as NinjectDependencyResolver;
if (resolver != null)
{
return resolver.Container;
}
throw new InvalidOperationException("NinjectDependencyResolver not being used as the MVC dependency resolver");
}
public static T Get<T>()
{
return GetContainer().Get<T>();
}
}
Inside your controller, call your empty constructor like this with no parameters:
public SuggestionController() : this(WebContainerManager.Get<ISuggestionRepository>())
{
}
This should work.
This technique i got from the book on MVC4 by Jamie Kurtz #jakurtz.
You probably need to do some dependency injection so you can inject the ISuggestionRepository parameter on your SuggestionController constructor. To do that you need to override methods in the DefaultControllerFactory class to customize the creation of controllers. Since you are using NInject, you can have something like:
public class NInjectControllerFactory : DefaultControllerFactory
{
private IKernel kernel = new StandardKernel(new CustomModule());
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
return controllerType == null ? null : (IController)kernel.Get(controllerType);
}
public class CustomModule : NinjectModule
{
public override void Load()
{
this.Bind<ICompetitionRepository>().To(typeof(CompetitionRepository))
.WithConstructorArgument("serviceContext", new InMemoryDataContext<Competition>());
this.Bind<ISubmissionRepository>().To(typeof(SubmissionRepository))
.WithConstructorArgument("serviceContext", new InMemoryDataContext<Submission>());
this.Bind<IUserRepository>().To(typeof(UserRepository))
.WithConstructorArgument("serviceContext", new InMemoryDataContext<User>());
this.Bind<ISuggestionRepository>().To(typeof(SuggestionRepository))
.WithConstructorArgument("serviceContext", new InMemoryDataContext<Suggestion>());
}
}
}
Then in your Global.asax.cs, you can add a line to swap out the controller factory
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(new NInjectControllerFactory());
}