HttpContext null while testing controller - c#

I am trying to setup tests for my controller endpoints and I came up with this design after some research.
Controller interface
public interface IAuthenticationController
{
Task<IActionResult> Login(AuthenticateUserDTO loginDto);
}
The interface is implemented in the controller class, that inherits ControllerBase and has [ApiController] attribute.
BaseFixture.cs
public class BaseFixture : WebApplicationFactory<TestStartup>
{
private Lazy<HttpClient> client;
public BaseFixture()
{
client = new Lazy<HttpClient>(() => Server.CreateClient());
var repo = Server.Host.Services.GetRequiredService<IUserGestairRepository>();
var task = repo.UserManager.CreateAsync(new User_Gestair()
{
UserName = "dev#dev.com.br",
Email = "dev#dev.com.br"
}, "123456");
task.Wait();
var result = task.Result;
}
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost.CreateDefaultBuilder()
.UseStartup<TestStartup>();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<GestairDbContext>));
services.Remove(descriptor);
services.AddDbContext<GestairDbContext>(options =>
{
options.UseInMemoryDatabase(Guid.NewGuid().ToString());
});
services.AddDefaultIdentity<User_Gestair>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.User.RequireUniqueEmail = true;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
})
.AddRoles<IdentityRole>()
.AddErrorDescriber<IdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<GestairDbContext>();
services.AddHttpContextAccessor();
services.AddHttpClient();
services.AddTransient<IAspNetUser, AspNetUser>();
services.AddMediatorConfiguration(typeof(CreateCompanyCommandHandler).Assembly);
services.AddAutoMapper(typeof(Startup));
services.AddScoped<ICompanyController, CompanyController>();
services.AddScoped<IAuthenticationController, AuthenticationController>();
services.AddScoped<ICompanyRepository, CompanyRepository>();
services.AddScoped<IUserGestairRepository, UserGestairRepository>();
services.AddControllers()
.AddApplicationPart(Assembly.Load("Gestair.API"))
.AddControllersAsServices()
.AddFluentValidation(options =>
{
options.RegisterValidatorsFromAssemblyContaining<ApplicationValidations>(lifetime: ServiceLifetime.Scoped);
options.ValidatorFactoryType = typeof(HttpContextServiceProviderValidatorFactory);
});
});
builder.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
});
}
}
Then, I created the fixture to thest the Authentication controller, for instance.
public class AuthenticationControllerFixture : BaseFixture
{
public IAuthenticationController AuthenticationControllerService;
public readonly HttpClient _client;
public readonly BaseFixture _fixture;
public AuthenticationControllerFixture()
{
AuthenticationControllerService = Server.Host.Services.GetRequiredService<IAuthenticationController>();
}
public AuthenticateUserDTO CriarUsuarioLogin()
{
return new AuthenticateUserDTO()
{
Email = "dev#dev.com.br",
Senha = "123456"
};
}
}
And then I call the controller function in the test itself:
public class AuthenticationControllerTest : IClassFixture<AuthenticationControllerFixture>
{
private readonly AuthenticationControllerFixture _fixture;
public AuthenticationControllerTest(AuthenticationControllerFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task ShoudLogin()
{
var input = _fixture.CriarUsuarioLogin();
var response = await _fixture.AuthenticationControllerService.Login(input);
}
}
While debugging, the call is made, it calls the controller but HttpContext of the controller that implements that interface, is null which is causing error to correctly process the request.
How can I fix this, what am I missing?

Related

Integration tests do not recognize actions of controllers

I am trying integration tests with XUnit. I can access the endpoints that have been assigned on MapGet but to not actions inside controllers. I get a NotFound error. I am running the application on an API project with ApplicationFactory : WebApplicationFactory but all requests that I make with Factory.Client return NotFound. Is there any definition needed in integration tests in order to be able to send requests to controllers?
Test projects code:
private ApplicationFactory Factory { get; }
public AccountsControllerTests(ITestOutputHelper output)
{
Factory = new ApplicationFactory(output);
}
[Fact]
public async Task ListBalancesTest()
{
var client = Factory.CreateClient(new WebApplicationFactoryClientOptions(){AllowAutoRedirect = false});;
var resp = await client.GetAsync($"api/v1/test/get");
//!!!!!!! resp.StatusCode is HttpStatusCode.NotFound !!!!!!!
var mapgetResp= await client.GetAsync($"/test");
//!!!!!!! mapgetResp is Working !!!!!!!
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
API Code:
[ApiController]
[Route("api/v1/test")]
public class TestController : ControllerBase
{
[HttpGet("get")]
public async Task<ActionResult> Get()
{
return await Task.FromResult(Ok("Response from test"));
}
}
ApplicationFactory Code:
public class ApplicationFactory : WebApplicationFactory<TestStartup>
{
public ApplicationFactory(ITestOutputHelper testOutput = null)
{
_testOutput = testOutput;
}
protected override IWebHostBuilder CreateWebHostBuilder()
{
var builder = WebHost
.CreateDefaultBuilder()
.UseEnvironment("Development")
.UseStartup<TestStartup>()
.UseSerilog();
if (_testOutput != null)
{
builder = builder.ConfigureLogging(logging =>
{
logging.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<ILoggerProvider>(new TestOutputHelperLoggerProvider(_testOutput)));
});
}
return builder;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseContentRoot(".");
builder.ConfigureServices(services =>
{/...../}
}
}
Startup:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGet("/test", async context =>
{
await context.Response.WriteAsync(#$"Test value from MapGet");
});
/..../
});
Add code in API project in ConfigureWebHost(IWebHostBuilder builder)
builder.UseEnvironment("Test");
builder.ConfigureAppConfiguration((ctx, b) =>
{
ctx.HostingEnvironment.ApplicationName = typeof(Program).Assembly.GetName().Name;
});
builder.ConfigureServices(services =>
{
services.AddMvc()
.AddApplicationPart(typeof(TestStartup).Assembly); //TestStartup is Test project's startup!!
}
And works now thanks!

Cannot access a disposed object.\r\nObject name: 'UserManager`1 - while calling CreateAsync(user, model.password)

I am working on .net core porject.
My Project structure has 4 projects.
Student_Database - (Contains Database table model and ApplicatinDBContext)
Student_Entities - (Contains All the View side models)
Student_Service - (All the Database operation handling from here. It is connected to Database.
ex: IUserService and UserService)
Student_Web - ( Controllers and all the methods, logic along with all the views are in this project)
I have implemented Entity Framework core. And try to using Usermanager for insert data.
Now when I am calling method "CreateAsync" from controller(Student_Web) it works fine and user inserted.
But I want to implement database operation in Student_Service. So when I am calling "CreateAsync" from UserService it gives me error "Cannot access a disposed object.\r\nObject name: 'UserManager`1"
I am calling this interface IUserService from controller. So here is my code in UserService.
Please help me to solve this.
public class UserService : IUserService
{
#region Properties
private readonly IDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole<int>> _roleManager;
#endregion
#region Consturctor
public UserService(
IDbContext context
, UserManager<ApplicationUser> userManager
, RoleManager<IdentityRole<int>> roleManager
{
_context = context;
_userManager = userManager;
_roleManager = roleManager;
}
#endregion
#region Methods
public async Task<bool> Create(NewUsers model)
{
bool result = false;
try
{
var user = await _userManager.FindByNameAsync(model.UserName);
if (user == null)
{
model.Password = GeneratePassword();
user = new ApplicationUser
{
//Id = 10,
UserName = model.UserName,
Email = model.UserName,
AccessFailedCount = 0,
FirstName = model.FirstName,
LastName = model.LastName,
CreatedBy = 2,
CreatedDate = DateTime.UtcNow,
Active = false
};
var returnResult = await _userManager.CreateAsync(user, model.Password);
if (returnResult.Succeeded)
{
returnResult = await _userManager.AddToRoleAsync(user, _roleManager.Roles.Where(x=>x.Id == model.RoleId).Select(x => x.Name).FirstOrDefault());
}
if (model.CompanyId!= null)
{
foreach (var item in model.CompanyId)
{
var userMap = new UserCompanyMapping();
userMap.UserId = user.Id;
userMap.CompanyId = item;
_userCompanyMappingRepository.Insert(userMap);
}
}
result = returnResult.Succeeded;
}
}
catch (Exception ex)
{
return false;
}
return result;
}
#endregion
}
//startup class
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(option =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
option.Filters.Add(new AuthorizeFilter(policy));
});
services.AddDbContextPool<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentityCore<ApplicationUser>();
// Register Dependencies extra service
services.AddAppServices();
services.AddIdentity<ApplicationUser, IdentityRole<int>>(options =>
{
options.User.RequireUniqueEmail = true;
options.Password.RequireNonAlphanumeric = false;
})
.AddRoles<IdentityRole<int>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(option =>
{
option.LoginPath = "/login";
option.AccessDeniedPath = "/Login/AccessDenied";
});
// Register dependancy
RegisterAutoMapper(services);
RegisterServices(services);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.ConfigureRequestPipeline();
app.UseStaticFiles();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
//register all routes
EngineContext.Current.Resolve<IRoutePublisher>().RegisterRoutes(endpoints);
});
//app.UseEndpoints(endpoints =>
//{
// endpoints.MapControllerRoute(
// name: "default",
// pattern: "{controller=Login}/{action=Index}/{id?}");
//});
}
private void RegisterServices(IServiceCollection services)
{
// Get class libraryGetAssembly(ty)
var serviceLibraries = Assembly.Load("Student.Services")
.GetTypes()
.Where(x => x.IsClass && x.GetInterfaces().Any() && x.Namespace.Contains(".Services.Services"))
.ToList();
if (serviceLibraries != null && serviceLibraries.Count > 0)
{
foreach (var service in serviceLibraries)
{
var interfaceType = service.GetInterfaces().FirstOrDefault();
services.AddScoped(interfaceType, service);
}
}
}
private void RegisterAutoMapper(IServiceCollection services)
{
// Auto Mapper Configurations
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
}
}
//Action controller method
namespace Student.Web.Controllers
{
[Authorize]
public class UserController : Controller
{
private readonly IUserService userService;
private readonly ICommonService commonService;
public UserController(
IUserService userService,
ICommonService commonService)
{
this.userService = userService;
this.commonService = commonService;
}
public IActionResult Index()
{
return View();
}
[HttpGet]
public IActionResult Create()
{
ViewBag.RoleList = commonService.GetRoles().Result;
ViewBag.CompanyList = commonService.GetCompanies().Result;
ViewBag.CityList = commonService.GetCities().Result;
ViewBag.CompanyAccessList = commonService.GetCompanyAccessListMultiCheck().Result;
return View();
}
[HttpPost]
public IActionResult Create(UserAddModel model)
{
if (ModelState.IsValid)
{
var response = userService.Create(model);
}
return RedirectToAction("Index");
}
}
}
The call to your service is never awaited, so it is kind of became fire-and-forget which means the request might ends before the service finishes its job which would cause the requested services to get disposed.
To fix that you need to alter your Create Action a little bit by doing the followings:
Make your action async and let it return Task<IActionResult>.
Await the service.
[HttpPost]
public async Task<IActionResult> Create(UserAddModel model)
{
if (ModelState.IsValid)
{
var response = await userService.Create(model);
}
return RedirectToAction("Index");
}
Then it should work just fine.

asp.net core - Integration test and view components

I'm facing a strange issue since I created a view component in my app.
All my integration tests using an HttpClient start failing with response code "Internal Server Error".
Here the test configuration :
public class BaseTest<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
private readonly bool _hasUser;
private readonly HttpClient _client;
protected BaseTest(bool hasUser = false)
{
_hasUser = hasUser;
_client = CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
}
protected async Task<HttpResponseMessage> GetPageByPath(string path)
{
return await _client.GetAsync(path);
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
if (_hasUser)
{
services.AddScoped<IAuthenticationService, AuthenticationServiceStub>();
services.AddSingleton<IStartupFilter, FakeUserFilter>();
services.AddMvc(options =>
{
options.Filters.Add(new AllowAnonymousFilter());
options.Filters.Add(new AuthenticatedAttribute());
});
}
});
builder.ConfigureServices(services =>
{
// Create a new service provider.
ServiceProvider serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var logger = scopedServices
.GetRequiredService<ILogger<BaseTest<TStartup>>>();
}
});
}
}
}
And a usage example :
public class BasicTest : BaseTest<Startup>
{
public BasicTest() : base()
{
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/Users/SignOut")]
[Trait("Category", "Integration")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Act
var response = await GetPageByPath(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
If you need the component code let me know.
I already rollback code to check when this start happening, and it's start after the commit with the new Component called in several pages.

Make Identity core options configurable with database

I am new to aspnet core. we are using identity core 2.1. Now made a page from where admin can set the different configuration like idle-time lockout-time password-retries. Those settings are being saved into the database table. Now I want that my identity option will set from those values. I made a repository to get setting from database. but I am unable to call that repository function from startup.cs.
Can some please guide me? and Also tell me the best way to make identity options configurable from database.
I have made a service
public class SecuritySettingService : ISecuritySettingService
{
private readonly ISecuritySettingRepository _securitySettingRepository;
public SecuritySettingService(ISecuritySettingRepository securitySettingRepository)
{
_securitySettingRepository = securitySettingRepository;
}
public SecuritySetting GetSecuritySetting()
{
return _securitySettingRepository.GetSecuritySetting();
}
}
A repository to connect to database
public class SecuritySettingRepository : ISecuritySettingRepository
{
private readonly IDbRepository _dapperWrapper;
public SecuritySettingRepository(IDbRepository dapperWrapper)
{
_dapperWrapper = dapperWrapper;
}
public SecuritySetting GetSecuritySetting()
{
var response = _dapperWrapper.QuerySingleOrDefault<SecuritySetting>("security_setting_get", null, CommandType.StoredProcedure);
return response;
}
}
Made identity config class to clean up startup.cs
public static class IdentityConfig
{
public static void ConfigureIdentity(IServiceCollection services, ISecuritySettingService securitySettingService)
{
var securitySetting = securitySettingService.GetSecuritySetting();
services.AddIdentity<ApplicationUser, ApplicationRole>(options => {
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
}).AddUserManager<CustomUserManager>().AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Default User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+";
options.User.RequireUniqueEmail = true;
});
services.Configure<DataProtectionTokenProviderOptions>(options =>
{
options.TokenLifespan = TimeSpan.FromDays(30);
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.Cookie = new CookieBuilder
{
IsEssential = true // required for auth to work without explicit user consent; adjust to suit your privacy policy
};
});
}
}
startup.cs file is like
public class Startup
{
public ISecuritySettingService _securitySettingService;
public Startup(IConfiguration configuration, ISecuritySettingService securitySettingService)
{
Configuration = configuration;
_securitySettingService = securitySettingService;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
IdentityConfig.ConfigureIdentity(services, _securitySettingService);
services.AddOptions();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
services.AddHttpContextAccessor();
}
// 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)
{
}
}
in startup.cs when I call IdentityConfig.ConfigureIdentity(services, _securitySettingService); the object _securitySettingService is not present so my code throws exception invalid operation
For your current error, you did you register ISecuritySettingService and used it in Startup. For referencing ISecuritySettingService, you need to register it first.
For services.Configure<IdentityOptions>, it will not change automatically while changing the database. You need to update IdentityOptions by yourself.
Follow Steps below and modify it as needed.
ISecuritySettingRepository
public interface ISecuritySettingRepository
{
LockoutOption GetSecuritySetting();
LockoutOption UpdateSecuritySetting(LockoutOption lockoutOption);
}
SecuritySettingRepository
public class SecuritySettingRepository : ISecuritySettingRepository
{
private readonly DbConnection _dapperWrapper;
private readonly IConfiguration _configuration;
public SecuritySettingRepository(DbConnection dapperWrapper
, IConfiguration configuration)
{
_dapperWrapper = dapperWrapper;
_configuration = configuration;
}
public LockoutOption GetSecuritySetting()
{
using (var connection = new SqlConnection(_configuration.GetConnectionString("DefaultConnection")))
{
string sQuery = "SELECT top 1 * From LockoutOption Where Id = 1";
var response = connection.QueryFirstOrDefault<LockoutOption>(sQuery);
return response;
}
}
public LockoutOption UpdateSecuritySetting(LockoutOption lockoutOption)
{
using (var connection = new SqlConnection(_configuration.GetConnectionString("DefaultConnection")))
{
string sQuery = $"Update LockoutOption Set MaxFailedAccessAttempts = {lockoutOption.MaxFailedAccessAttempts} Where Id = {lockoutOption.Id}";
var result = connection.Execute(sQuery);
string sQuery1 = "SELECT top 1 * From LockoutOption Where Id = 1";
var response = connection.QueryFirstOrDefault<LockoutOption>(sQuery1);
return response;
}
}
}
ISecuritySettingService
public interface ISecuritySettingService
{
LockoutOption GetSecuritySetting();
LockoutOption UpdateSecuritySetting(LockoutOption lockoutOption);
}
SecuritySettingService
public class SecuritySettingService : ISecuritySettingService
{
private readonly ISecuritySettingRepository _securitySettingRepository;
private readonly IdentityOptions _identityOptions;
public SecuritySettingService(ISecuritySettingRepository securitySettingRepository
, IOptions<IdentityOptions> identityOptions)
{
_securitySettingRepository = securitySettingRepository;
_identityOptions = identityOptions.Value;
}
public LockoutOption GetSecuritySetting()
{
return _securitySettingRepository.GetSecuritySetting();
}
public LockoutOption UpdateSecuritySetting(LockoutOption lockoutOption)
{
var option = _securitySettingRepository.UpdateSecuritySetting(lockoutOption);
//update identity options
_identityOptions.Lockout.MaxFailedAccessAttempts = option.MaxFailedAccessAttempts;
return option;
}
}
Register in Startup
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<DbConnection>(serviceProvider => new DbConnection(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
// your rest configure services
services.AddTransient<ISecuritySettingService, SecuritySettingService>();
services.AddTransient<ISecuritySettingRepository, SecuritySettingRepository>();
var _ecuritySettingService = services.BuildServiceProvider().GetRequiredService<ISecuritySettingService>();
services.Configure<IdentityOptions>(options =>
{
options.Lockout.MaxFailedAccessAttempts = _ecuritySettingService.GetSecuritySetting()?.MaxFailedAccessAttempts ?? 3;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//your configure
}
}
Useage
namespace DapperPro.Controllers
{
public class LockoutOptionsController : Controller
{
private readonly ApplicationDbContext _context;
private readonly IdentityOptions _identityOptions;
private readonly ISecuritySettingService _securitySettingService;
public LockoutOptionsController(ApplicationDbContext context
, IOptions<IdentityOptions> identityOptions
, ISecuritySettingService securitySettingService)
{
_context = context;
_identityOptions = identityOptions.Value;
_securitySettingService = securitySettingService;
}
// POST: LockoutOptions/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,AllowedForNewUsers,MaxFailedAccessAttempts,DefaultLockoutTimeSpan")] LockoutOption lockoutOption)
{
_securitySettingService.UpdateSecuritySetting(lockoutOption);
return View(lockoutOption);
}
}
}

Get the HubContext using SimpleInjector

This guide does not appear to work for SimpleInjector.
My OWIN startup looks like this:
container = new Container();
container.Options.DefaultScopedLifestyle = new ExecutionContextScopeLifestyle();
container.RegisterSingleton(() => new SimpleInjectorSignalRDependencyResolver(_container));
container.RegisterSingleton(() =>
new HubConfiguration()
{
EnableDetailedErrors = true,
Resolver = _container.GetInstance<SimpleInjectorSignalRDependencyResolver>()
});
container.RegisterSingleton<IHubActivator, SimpleInjectorHubActivator>();
container.RegisterSingleton<IStockTicker,StockTicker>();
container.RegisterSingleton<HubContextAdapter<StockTickerHub, IStockTickerHubClient>>();
container.RegisterSingleton(() => GlobalHost.ConnectionManager);
container.Verify();
GlobalHost.DependencyResolver = container.GetInstance<SimpleInjectorSignalRDependencyResolver>();
app.Use(async (context, next) =>
{
using (container.BeginExecutionContextScope())
{
await next();
}
});
app.MapSignalR(container.GetInstance<HubConfiguration>());
And The HubContextAdapter looks like this:
public class HubContextAdapter<THub, TClient>
where THub : Hub
where TClient : class
{
private readonly IConnectionManager _manager;
public HubContextAdapter(IConnectionManager manager)
{
_manager = manager;
}
public IHubContext<TClient> GetHubContext()
{
return _manager.GetHubContext<THub, TClient>();
}
}
And SimpleInjectorSignalRDependencyResolver looks like:
public class SimpleInjectorSignalRDependencyResolver : DefaultDependencyResolver
{
public SimpleInjectorSignalRDependencyResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override object GetService(Type serviceType)
{
return _serviceProvider.GetService(serviceType) ?? base.GetService(serviceType);
}
public override IEnumerable<object> GetServices(Type serviceType)
{
var #this = (IEnumerable<object>)_serviceProvider.GetService(
typeof(IEnumerable<>).MakeGenericType(serviceType));
var #base = base.GetServices(serviceType);
return #this == null ? #base : #base == null ? #this : #this.Concat(#base);
}
private readonly IServiceProvider _serviceProvider;
}
And StockTicker looks like:
public class StockTicker : IStockTicker
{
private readonly HubContextAdapter<StockTickerHub, IStockTickerHubClient> _context;
public StockTicker(HubContextAdapter<StockTickerHub, IStockTickerHubClient> context)
{
_context = context;
}
}
When the StockTicker ticks and calls all clients to update the client method is not invoked and there is no network traffic.
SimpleInjector wants to instantiate the singletons after verification or after the first GetInstance call. This is too early for SignalR and the StockTicker and it will take an instance of GlobalHost.ConnectionManager before SimpleInjectorSignalRDependencyResolver is the resolver.
I chose to change the dependency on IConnectionManager to be Lazy<IConnectionManager> and the dependency on IStockTicker to be Lazy<IStockTicker> so that registration became like the following:
container = new Container();
container.Options.DefaultScopedLifestyle = new ExecutionContextScopeLifestyle();
container.RegisterSingleton(() => new SimpleInjectorSignalRDependencyResolver(_container));
container.RegisterSingleton(() =>
new HubConfiguration()
{
EnableDetailedErrors = true,
Resolver = _container.GetInstance<SimpleInjectorSignalRDependencyResolver>()
});
container.RegisterSingleton<IHubActivator, SimpleInjectorHubActivator>();
container.RegisterSingleton<IStockTicker,StockTicker>();
container.RegisterSingleton<Lazy<IStockTicker>>(() => new Lazy<IStockTicker>(() => container.GetInstace<IStockTicker>()) );
container.RegisterSingleton<HubContextAdapter<StockTickerHub, IStockTickerHubClient>>();
container.RegisterSingleton(() => new Lazy<IConnectionManager>(() => GlobalHost.ConnectionManager));
container.Verify();

Categories