I'm building a simulator with Asp.Net application where simulation is done as a separate task. I'm having problem with accessing data which is processed in this task.
I've tried to access task or threat I created, but don't see a way to track created task.
I've also tried to use Session, but after request is completed I have no access to Session anymore, so background task stops with error.
LatheController:
public class LatheController : Controller
{
private readonly ApiDbContext _dbContext;
private static ILatheService _latheService;
public LatheController(ApiDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpGet]
public LatheCell GetLatheCell()
{
if (_latheService == null)
{
_latheService = new LatheService();
_latheService.Start(lc);
}
return _latheService.GetLatheCell();
}
[HttpGet("{id}")]
public LatheCell GetLatheCell([FromRoute]int id)
{
return AppHttpContext.Current.Session.GetObjectFromJson<LatheCell>("latheCell");
}
}
LatheService:
public class LatheService : ILatheService
{
private LatheSimulator _latheSim;
public void Start(LatheCell lc)
{
_latheSim = new LatheSimulator(lc);
Task task = new Task( () => { _latheSim.Start(); });
task.Start();
}
public LatheCell GetLatheCell()
{
return _latheSim.GetLatheCell();
}
}
LatheSimulator:
public class LatheSimulator
{
private LatheCell _latheCell;
private bool _keepRunning;
public LatheSimulator(LatheCell latheCell)
{
_latheCell = latheCell;
}
public void Start()
{
_keepRunning = true;
Simulation();
}
public void Stop()
{
_keepRunning = false;
}
public LatheCell GetLatheCell()
{
return _latheCell;
}
private void Simulation()
{
while (_keepRunning)
{
_latheCell.RunningCycle++;
//// The simuation ///
AppHttpContext.Current.Session.SetObjectAsJson("latheCell", _latheCell);
//Sleep operation to simulate speed of the conveyor
System.Threading.Thread.Sleep(_latheCell.ConveyorIn.Speed);
}
}
}
Startup:
public void ConfigureServices(IServiceCollection services)
{
/// ... some other configurations
services.AddMvc();
services.AddDistributedMemoryCache();
services.AddSession();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
///....
app.UseSession();
AppHttpContext.Services = app.ApplicationServices;
/// .....
}
AppHttpContext:
public static class AppHttpContext
{
static IServiceProvider services = null;
public static IServiceProvider Services
{
get { return services; }
set
{
if (services != null)
{
throw new Exception("Can't set once a value has already been set.");
}
services = value;
}
}
public static HttpContext Current
{
get
{
IHttpContextAccessor httpContextAccessor = services.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor;
return httpContextAccessor?.HttpContext;
}
}
}
I know I could use database for this, but because I will be calling database every 5 sec in the simulation, I would like to keep simulator in the memory for the performance.
If anyone could put me in the right direction I will be very appreciated. I'm scratching my head for several days now.
I would like to keep simulator in the memory for the performance.
The problem is if App Pool recycles and App Domain restarts, you will loose everything that task is running.
Ideally, we need to storage data in persistent storage like SQL Server. I'll say calling database every 5 sec is not a big deal, unless you are querying a lot of data to process.
We normally use background task such as hangfire. You could read more about other background tasks at Scott Hanselman's How to run Background Tasks in ASP.NET.
Related
I am using Readiness healthchecks for my project and want to add a warmup period to it.
Dependency Injection is being used to get the warmup task from the Kernel but I am not able to get it because the Readiness Healthcheck is being initialized before the IKernel it seems.
I am getting the follow error:
Unable to resolve service for type 'IKernel' while attempting to activate 'Project.Ranking.API.HealthCheck.RankingReadinessHealthCheck'.
How does one use a class to warm up the pod before it is being used.
I have not been able to find a working example where someone warms up before the endpoints are available.
UPDATE:
Core.Library Startup.CS
public void CoreConfigureServices(IServiceCollection services)
{
... other code
services.AddHealthChecks()
.AddIdentityServer("https://identity.example.com")
.AddCheck<IReadinessHealthCheck>("Readiness", failureStatus: null)
.AddCheck<ILivenessHealthCheck>("Liveness", failureStatus: null);
services.AddSingleton<ILivenessHealthCheck, LivenessHealthCheck>();
}
public void CoreConfigure(IApplicationBuilder app, IHostEnvironment env)
{
... other code
app.UseHealthChecks("/healthcheck/live", new HealthCheckOptions()
{
Predicate = check => check.Name == "Liveness"
});
app.UseHealthChecks("/healthcheck/ready", new HealthCheckOptions()
{
Predicate = check => check.Name == "Readiness",
});
}
API Startup.CS
public void ConfigureServices(IServiceCollection services)
{
CoreConfigureServices(services);
... other code
services.AddSingleton<Core.Library.IReadinessHealthCheck, ReadinessHealthCheck>();
}
public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
CoreConfigure(app, env);
... other code
//Here used to be the warm up, but this is used in the liveness probe and i want to warm up in the readiness probe
//Kernel.Get<IWarmupTask>().Initialize();
Kernel.Bind<IReadinessHealthCheck>().To<ReadinessHealthCheck>();
}
Core.Library BaseReadinessHealthCheck.cs
public abstract class BaseReadinessHealthCheck : IReadinessHealthCheck
{
public BaseReadinessHealthCheck()
{
}
private bool StartupTaskCompleted { get; set; } = false;
public abstract void WarmUp();
public void CompleteTask()
{
StartupTaskCompleted = true;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
//start tasks
if (!StartupTaskCompleted)
{
Task.Run(() => WarmUp());
}
if (StartupTaskCompleted)
{
return Task.FromResult(HealthCheckResult.Healthy("The startup task is finished."));
}
return Task.FromResult(HealthCheckResult.Unhealthy("The startup task is still running."));
}
}
API ReadinessHealthCheck.CS
public class ReadinessHealthCheck : ReadinessHealthCheck
{
public ReadinessHealthCheck(IKernel kernel) : base(kernel)
{
}
public override void WarmUp()
{
// I want to do a warmup here, where it calls IWarmupTask
CompleteTask();
}
}
I know this is a repeated question , went through answers and dont know whats happening here. In this problem we need to transfer the values from appsettings.json to another class other than Controllers here its ServiceSettings.cs.
This is a sample 'hello world' like program, here we need transfer values from appsettings.json to plugins.
This is folder architecture
appsettings.json
"Application": {
"TimerInterval": 10000,
"LogLevel": "Debug"
}
I created a class based upon this app setting in class library-
ApplicationSettings.cs
public class ApplicationSettings
{
public int TimerInterval { get; set; }
public string LogLevel { get; set; }
}
I tried push data from appsettings via the last line code
services.Configure<ApplicationSettings>(hostContext.Configuration.GetSection("Application"));
Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices((hostContext, services) =>
{
services.AddSingleton<IConfiguration>(hostContext.Configuration);
// Service Settings Injected here
services.AddOptions<ServiceSettings>();
services.AddHostedService<Worker>();
services.Configure<ApplicationSettings>(hostContext.Configuration.GetSection("Application"));
// for configure application
});
}
Here during start method of the worker class i need to get values from ServiceSettings() which always returns null value.
Worker.cs(Re Edited)
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IConfiguration _configuration;
private ServiceSettings _settings;
public Worker(ILogger<Worker> logger, IConfiguration config)
{
_logger = logger;
_configuration = config;
}
public override Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Start Asynch Method");
// Load Settings From Configuration Files
_settings = new ServiceSettings();
_settings.Load();
_logger.LogInformation("Settings: {setting}", _settings.TimerInterval);
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var values = _configuration.GetSection("DataSources").Get<List<DataSource>>();
while (!stoppingToken.IsCancellationRequested) {
await Task.Delay(Convert.ToInt32(_configuration["Application:TimerInterval"]), stoppingToken);
}
}
}
The service settings values are provided below which receives the null value
ServiceSettings.cs
public class ServiceSettings
{
private readonly IOptions<ApplicationSettings> _appSettings;
public ServiceSettings(IOptions<ApplicationSettings> appSettings)
{
_appSettings = appSettings;
}
public int TimerInterval { get; set; }
public string LogLevel { get; set; }
public void Load()
{
// Error is shown here
try { TimerInterval = Convert.ToInt32(_appSettings.Value.TimerInterval); }
catch { TimerInterval = 60; }
try
// Here too
{ LogLevel = Convert.ToString(_appSettings.Value.LogLevel).ToLower(); }
catch { LogLevel = "info"; }
}
}
I am pretty new to worker service, What i miss here? kindly guide me with the resources Thank you all.
This appears to be a design issue.
First lets fix the composition root. Avoid injecting IConfiguration. It can be seen as a code smell as IConfiguration should ideally be used in startup.
public class Program {
public static void Main(string[] args) {
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices((hostContext, services) => {
IConfiguration config = hostContext.Configuration;
// parse settings
ApplicationSettings appSettings = config
.GetSection("Application").Get<ApplicationSettings>();
//set defaults.
if(appSettings.TimerInterval == 0)
appSettings.TimerInterval = 60;
if(string.IsNullOrWhiteSpace(appSettings.LogLevel))
appSettings.LogLevel = "Debug";
services.AddSingleton(appSettings); //<-- register settings run-time data
services.AddHostedService<Worker>();
});
}
Note how the settings are extracted from configuration and added to the service collection.
Since there is already a strongly defined type (ApplicationSettings) There really is no need for the ServiceSettings based on what was shown in the original question.
Update the worker to explicitly depend on the actual object required.
public class Worker : BackgroundService {
private readonly ILogger<Worker> _logger;
private readonly ApplicationSettings settings;
public Worker(ILogger<Worker> logger, ApplicationSettings settings) {
_logger = logger;
this.settings = settings; //<-- settings injected.
}
public override Task StartAsync(CancellationToken cancellationToken) {
Console.WriteLine("Start Asynch Method");
_logger.LogInformation("Settings: {setting}", settings.TimerInterval);
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
await Task.Delay(settings.TimerInterval), stoppingToken);
}
}
}
You always have to get instances from your service collection. You typically do this by injecting them in class constructor.
// WRONG
// Worker.cs
_settings = new ServiceSettings();
This code does not compile because your ServiceSettings class has a constructor that requires one parameter but no parameter is given.
how should your class know anything about the options stored in service collection without any reference?
Then it seems to make no sense to have two classes with the same data ServiceSettings and ApplicationSettings are the same. If you need the application settings in a service inject IOptions<ApplicationSettings> that's all. If you need separate settings classes, provide them as IOption<MyOtherSectionSettings>.
In the end, it could look like so:
public class Worker {
private readonly ApplicationSettings _settings;
private readonly ILogger<Worker> _logger;
public Worker(IOptions<ApplicationSettings> settingsAccessor, ILogger<Worker> logger) {
_settings = settingsAccessor.Value;
_logger = logger;
}
public override Task StartAsync(CancellationToken cancellationToken) {
Console.WriteLine("Start Asynch Method");
_logger.LogInformation("Settings: {setting}", _settings.TimerInterval);
return base.StartAsync(cancellationToken);
}
}
Note that reading settingsAccessor.Value is the place where the framework really tries to access the configuration and so here we should think about error conditions (if not validated before).
I am trying to send a message to a client in the server using SignalR
I am trying to do that in a class that is not a Controller. I have made the Startup like so:
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.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.Configure<ConfigurationModel>(Configuration.GetSection("configurationModel"));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
app.UseSignalR(routes => { routes.MapHub<MoveViewHub>("/movehub"); });
}
}
In my Program, this one:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
This is in my Hub:
public class MoveViewHub : Hub
{
private async void ReceiveTagNumber(object sender, EventArgs e)
{
await Clients.All.SendAsync("ReceivedFromServer", sender.ToString());
}
public async Task MoveViewFromServer(float newX, float newY)
{
Console.WriteLine(#"Receive position from Server app: " + newX + "/" + newY);
await Clients.Others.SendAsync("ReceivedNewPosition", newX, newY);
//await Clients.All.SendAsync("ReceivedNewPosition", newX, newY);
}
public async Task WriteThisMessage(string message)
{
Console.WriteLine(message);
await Clients.Others.SendAsync("ReceivedStatus", "Message was received. Thank you.");
}
public override Task OnConnectedAsync()
{
Console.WriteLine("Client has connected");
RfidClass rfidClass = new RfidClass("THE HUB CONTEXT SHOULD BE HERE"); ====>> I NEED TO PASS MY HUBCONTEXT
rfidClass.sas();
RfidClass.SendTagNumber += ReceiveTagNumber;
System.Diagnostics.Process.Start(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Notepad++", #"notepad++.exe"));
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
Console.Write("Client has disconnected");
return base.OnDisconnectedAsync(exception);
}
}
This is the RfidClass:
private IHubContext<MoveViewHub> hubContext;
public RfidClass(IHubContext<MoveViewHub> hubContext)
{
this.hubContext = hubContext;
}
public void sas()
{
Start();
}
private void Start()
{
try
{
hubContext.Clients.Others.SendAsync("ReceivedFromServer", "You are connected");
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
How can I make it right?
You need to inject IServiceProvider into your Hub by .NET Core DI (like into standard Controller, injecting by constructor):
public class MoveViewHub : Hub
{
private readonly IServiceProvider provider
public MovieViewHub(IServiceProvider provider)
{
this.provider = provider
}
}
Then you can do something like this:
public override Task OnConnectedAsync()
{
Console.WriteLine("Client has connected");
// you need to inject service provider to your hub, then get hub context from
// registered services
using (var scope = this.provider.CreateScope())
{
// get instance of hub from service provider
var scopedServices = scope.ServiceProvider;
var hub = scopedServices.GetRequiredService<IHubContext<MoveViewHub>>
// pass hub to class constructor
RfidClass rfidClass = new RfidClass(hub)
rfidClass.sas();
RfidClass.SendTagNumber += ReceiveTagNumber;
}
System.Diagnostics.Process.Start(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Notepad++", #"notepad++.exe"));
return base.OnConnectedAsync();
}
EDIT:
If you just want to SignalR work, you dont need to work on Hub. Instead make service. In this service inject HubContext<> of your Hub:
// you need to make your own class and interface and inject hub context
public interface ISignalRService
{
Task SendMessageToAll(string message);
}
public class SignalRService : ISignalRService
{
private readonly IHubContext<YourHub> hubContext;
public SignalRService (IHubContext<NotificationHub> hubContext)
{
this.hubContext = hubContext;
}
public async Task SendMessageToAll(string message)
{
await this.hubContext.Clients.All.SendAsync("ReciveMessage", message);
}
}
Then register that service in your Startup class:
services.AddScoped<ISignalRService, SignalRService>();
After that you can call SignalRService wherever you want to like normal service from .NetCore DI container:
private readonly ISignalRService notificationService;
public SomeController(ISignalRService notificationService)
{
this.notificationService = notificationService;
}
[HttpGet]
public async Task<IActionResult> Send()
{
await this.notificationService.SendMessageToAll("message");
return Ok();
}
You dont need to make some work around like RfidClass.
I have created a basic DatabaseContext which handles the communication with a SQLite-Database:
public class DatabaseContext : DbContext
{
public DbSet<GearComponent> GearComponents{ get; set; }
public DatabaseContext() { }
public DatabaseContext(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Filename = database.db");
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<GearComponent>().HasKey(m => m.Id);
base.OnModelCreating(builder);
}
}
I registered this DatabaseContext in Startup.cs like this:
services.AddDbContext<DatabaseContext>(options => options.UseSqlite("Filename=database.db"));
I created a database with this command:
dotnet ef migrations add testMigration
I also auto-created a controller to access the database via HTTP-GET/POST/PUT.
This controller gets an instance of the DatabaseContext:
public class GearComponentsController : ControllerBase
{
private readonly DatabaseContext _context;
public GearComponentsController(DatabaseContext context)
{
_context = context;
}
//...
}
This GearComponentsController mainly is for the frontend to receive the database entries. For the backend I don't want to go with HTTP-POST/GET etc. but instead I want to directly access the DatabaseContext - but how?
I tried this in Program.cs:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
using (var db = new DatabaseContext())
{
db.GearComponents.Add(new GearComponent("Text", "Descr.", "08.12.2018"));
db.SaveChanges();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
But my database.db never receives this entry.
Edit: For everyone who is interested in how I got around this, look here.
Since you asked how to work with DatabaseContext outside your controller to perform some business logic. You can use straightforward approach with repository pattern. Will simply demonstrate for inserting data. Assuming you have model GearComponent created and EntityFramework already seted up.
Create file which contains interface and class which implements this interface:
public interface IGearComponentRepository
{
void Create(GearComponent obj)
}
public class GearComponentRepository : IGearComponentRepository
{
private readonly DatabaseContext _context;
public GearComponentRepository (DatabaseContext context)
{
_context = context;
}
public void Create(GearComponent data)
{
_context.Add(data);
_context.SaveChanges();
}
}
You need to register this service via IoC container in you Startup.cs file
public void ConfigureServices(IServiceCollection services)
{
..
services.AddMvc();
services.AddTransient<IGearComponentRepository, GearComponentRepository>();
..
}
Then you can use Repositories from your Controller
public class GearComponentsController : ControllerBase
{
private readonly IGearComponentRepository _gearComponentRepository;
public GearComponentsController(IGearComponentRepository
_gearComponentRepository)
{
_gearComponentRepository = gearComponentRepository;
}
[HttpPost]
public IActionResult Create(GearComponent data)
{
_dataRepository.Create(data);
return Ok();
}
}
in program.cs:
public static void Main(string[] args)
{
var hostBuilder = CreateWebHostBuilder(args);
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (!string.IsNullOrEmpty(env) && env.Equals("Production"))
{
hostBuilder.ConfigureLogging((context, builder) =>
{
// Read GelfLoggerOptions from appsettings.json
builder.Services.Configure<GelfLoggerOptions>(context.Configuration.GetSection("Graylog"));
// Optionally configure GelfLoggerOptions further.
builder.Services.PostConfigure<GelfLoggerOptions>(options =>
options.AdditionalFields["machine_name"] = Environment.MachineName);
// Read Logging settings from appsettings.json and add providers.
builder.AddConfiguration(context.Configuration.GetSection("Logging"))
.AddConsole()
//.AddDebug()
.AddGelf();
});
}
var host = hostBuilder.Build();
using (var scope = host.Services.CreateScope())
{
try
{
// Retrieve your DbContext isntance here
var dbContext = scope.ServiceProvider.GetRequiredService<NozomiDbContext>();
if (env != null && !env.Equals("Production"))
{
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
}
else
{
dbContext.Database.SetCommandTimeout((int)TimeSpan.FromMinutes(10).TotalSeconds);
dbContext.Database.Migrate();
}
// place your DB seeding code here
//DbSeeder.Seed(dbContext);
}
catch (Exception ex)
{
Console.WriteLine(ex);
// Continue
}
}
host.Run();
}
Focusing on:
using (var scope = host.Services.CreateScope())
and
var dbContext = scope.ServiceProvider.GetRequiredService<NozomiDbContext>();
You will be able to access your dbContext just like that.
As in the docs described, the method CreateWebhostbuilder is used to differ between build host and run host.
To run the host means, the code after is as reachable as after a throw statement.
Try this:
public class Program
{
public static void Main(string[] args)
{
//use var host to build the host
var host = CreateWebHostBuilder(args).Build();
using (var db = new DatabaseContext())
{
db.GearComponents.Add(new GearComponent("Text", "Descr.", "08.12.2018"));
db.SaveChanges();
}
// Run host after seeding
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Edit:
As far as I understand your issue now, you have a frontend, which should only consume the content of the database, if the refresh-button is clicked.
Along with that, your backend should consume other webservices and insert the consumed gearcomponents into the database.
In cause of the fact, that you don’t want another application do that job, which could be a windows-service (easy to handle intervals for updating the database),
the only way to trigger the Updates is to start them from the GearComponentsController or in an actionFilter. This way you can update your database and provide the data to the frontend. I hope, performance doesn’t matter.
I am following the official MS documentation for integration testing .Net Core (https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1).
I was able to get the first part of the integration test done where I was not overriding the startup class of the application I am testing (i.e. I was using a web application factorythat did not override any services).
I want to override the database setup to use an in-memory database for the integration test. The problem I am running into is that the configuration continues to try and use the sql server for services.AddHangfire().
How do I override only above specific item in my integration test? I only want to override the AddHangfire setup and not services.AddScoped<ISendEmail, SendEmail>(). Any help would be appreciated.
Test Class with the custom web application factory
public class HomeControllerShouldCustomFactory : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public HomeControllerShouldCustomFactory(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task IndexRendersCorrectTitle()
{
var response = await _client.GetAsync("/Home/Index");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Send Email", responseString);
}
}
Custom Web Application Factory
public class CustomWebApplicationFactory<TStartup>: WebApplicationFactory<SendGridExample.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
services.AddHangfire(x => x.UseStorage(inMemory));
// Build the service provider.
var sp = services.BuildServiceProvider();
});
}
}
My startup.cs in my application that I am testing
public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<ISendEmail, SendEmail>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHangfireServer();
app.UseHangfireDashboard();
RecurringJob.AddOrUpdate<ISendEmail>((email) => email.SendReminder(), Cron.Daily);
app.UseMvc();
Update
I don't see this issue in my other example project where I am using only entity framework. I have a simple application with an application db context which uses SQL server. In my test class, I override it with an in-memory database and everything works. I am at a loss at to why it will work in my example application but not work in my main application. Is this something to do with how HangFire works?
In my test application (example code below), I can delete my sql database, run my test, and the test passes because the application DB context does not go looking for the sql server instance but uses the in-memory database. In my application, the HangFire service keeps trying to use the sql server database (if I delete the database and try to use an in-memory database for the test - it fails because it can't find the instance its trying to connect to). How come there is such a drastic difference in how the two projects work when a similar path is used for both?
I ran through the debugger for my integration test which calls the index method on the home controller above (using the CustomWebApplicationFactory). As I am initializing a test server, it goes through my startup class which calls below in ConfigureServices:
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
After that, the Configure method tries to call below statement:
app.UseHangfireServer();
At this point the test fails as It cannot find the DB. The DB is hosted on Azure so I am trying to replace it with an in-memory server for some of the integration test. Is the approach I am taking incorrect?
My example application where its working
Application DB Context in my example application
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public virtual DbSet<Message> Messages { get; set; }
public async Task<List<Message>> GetMessagesAsync()
{
return await Messages
.OrderBy(message => message.Text)
.AsNoTracking()
.ToListAsync();
}
public void Initialize()
{
Messages.AddRange(GetSeedingMessages());
SaveChanges();
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "You're standing on my scarf." },
new Message(){ Text = "Would you like a jelly baby?" },
new Message(){ Text = "To the rational mind, nothing is inexplicable; only unexplained." }
};
}
}
Startup.cs in my example application
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
CustomWebApplicationFactory - in my unit test project
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
});
}
}
My unit test in my unit test project
public class UnitTest1 : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public UnitTest1(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async System.Threading.Tasks.Task Test1Async()
{
var response = await _client.GetAsync("/");
//response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Home", responseString);
}
Update 2
I think I found an alternate to trying to override all my configuration in my integration test class. Since it's a lot more complicated to override HangFire as opposed to an ApplicationDBContext, I came up with below approach:
Startup.cs
if (Environment.IsDevelopment())
{
var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
services.AddHangfire(x => x.UseStorage(inMemory));
}
else
{
services.AddHangfire(x => x.UseSqlServerStorage(Configuration["DBConnection"]));
}
Then in my CustomWebApplicationBuilder, I override the environment type for testing:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<SendGridExample.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development"); //change to Production for alternate test
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
});
}
}
With that approach, I don't need to worry about having to do extra logic to satisfy hangfire's check for an active DB. It works but I am not 100% convinced its the best approach as I'm introducing branching in my production startup class.
There are two different scenarios you need to check.
Create a job by class BackgroundJob
Create a job by interface IBackgroundJobClient
For the first option, you could not replace the SqlServerStorage with MemoryStorage.
For UseSqlServerStorage, it will reset JobStorage by SqlServerStorage.
public static IGlobalConfiguration<SqlServerStorage> UseSqlServerStorage(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] string nameOrConnectionString)
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
if (nameOrConnectionString == null) throw new ArgumentNullException(nameof(nameOrConnectionString));
var storage = new SqlServerStorage(nameOrConnectionString);
return configuration.UseStorage(storage);
}
UseStorage
public static class GlobalConfigurationExtensions
{
public static IGlobalConfiguration<TStorage> UseStorage<TStorage>(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] TStorage storage)
where TStorage : JobStorage
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
if (storage == null) throw new ArgumentNullException(nameof(storage));
return configuration.Use(storage, x => JobStorage.Current = x);
}
Which means, no matter what you set in CustomWebApplicationFactory, UseSqlServerStorage will reset BackgroundJob with SqlServerStorage.
For second option, it could replace IBackgroundJobClient with MemoryStorage by
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<JobStorage>(x =>
{
return GlobalConfiguration.Configuration.UseMemoryStorage();
});
});
}
}
In conclusion, I suggest you register IBackgroundJobClient and try the second option to achieve your requirement.
Update1
For DB is not available, it could not be resolved by configuring the Dependency Injection. This error is caused by calling services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));.
For resolving this error, you need to overriding this code in Startup.cs.
Try steps below:
Change Startup to below:
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)
{
//Rest Code
ConfigureHangfire(services);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//Rest Code
app.UseHangfireServer();
RecurringJob.AddOrUpdate(() => Console.WriteLine("RecurringJob!"), Cron.Minutely);
}
protected virtual void ConfigureHangfire(IServiceCollection services)
{
services.AddHangfire(config =>
config.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"))
);
}
}
Create StartupTest in test project.
public class StartupTest : Startup
{
public StartupTest(IConfiguration configuration) :base(configuration)
{
}
protected override void ConfigureHangfire(IServiceCollection services)
{
services.AddHangfire(x => x.UseMemoryStorage());
}
}
CustomWebApplicationFactory
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint: class
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost.CreateDefaultBuilder(null)
.UseStartup<TEntryPoint>();
}
}
Test
public class HangfireStorageStartupTest : IClassFixture<CustomWebApplicationFactory<StartupTest>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<StartupTest> _factory;
public HangfireStorageStartupTest(CustomWebApplicationFactory<StartupTest> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
}