I'm trying to setup Hangfire to run a recurring job within Startup.cs with a standalone method. For this to work, I need to grab some ApplicationServices I have injected already. But the job execution fails with this error:
Recurring job 'Job: Dispatch Email from Queue' can't be scheduled due to an error and will be retried in 00:00:15.
System.InvalidOperationException: Recurring job can't be scheduled, see inner exception for details.
---> Hangfire.Common.JobLoadException: Could not load the job. See inner exception for the details.
---> Newtonsoft.Json.JsonSerializationException: Could not create an instance of type DA.BrrMs.Application.Interfaces.ServiceInterfaces.IBudgetReleaseRequestService. Type is an interface or abstract class and cannot be instantiated.
Here is what I have:
public class Startup
{
private IConfiguration Configuration { get; }
private RecurringJobManager _jobManager;
public Startup(IConfiguration configuration) => Configuration = configuration;
public void ConfigureServices(IServiceCollection services)
{
...
if (Configuration["SystemSettings:UseHangfire"] == "1")
{
services.AddHangfire(c => c.UseMemoryStorage());
JobStorage.Current = new MemoryStorage();
services.AddHangfireServer();
GlobalConfiguration.Configuration.UseMemoryStorage();
}
RegisterServices(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
var requestService = app.ApplicationServices.GetService<IBudgetReleaseRequestService>();
var mailService = app.ApplicationServices.GetService<IMailer>();
_jobManager = new RecurringJobManager();
_jobManager.AddOrUpdate("Job: Dispatch Email from Queue", () => StartupMailJob(requestService, mailService), Cron.Minutely);
}
private static void RegisterServices(IServiceCollection services) => DependencyContainer.RegisterServices(services);
public static async Task StartupMailJob(IBudgetReleaseRequestService requestService, IMailer mailService)
{
try
{
var emailsToSend = await requestService.DispatchEmails();
// Send emails
foreach (var emailToSend in emailsToSend)
{
}
// Update status of sent
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
// In a project, far far away...
public static void RegisterServices(IServiceCollection services)
{
...
services.AddScoped<IBudgetReleaseRequestService, BudgetReleaseRequestService>();
...
}
How do I satisfy Hangfire's requirment of a class instance instead of an interface?
Here is the implementation that I found best worked for me:
1 Register the service I wanted to schedule in Startup.cs
services.AddScoped<IMyJob, MYJob>();
2 Created an activator class that is used to define the scope for the execution of the job.
public class HangFireActivatorMyJob : IHangFireActivatorMyJob
{
private readonly IServiceProvider _serviceProvider;
public HangFireActivatorMyJob (IServiceProvider serviceProvider)
{
this._serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public async Task Run(IJobCancellationToken token)
{
token.ThrowIfCancellationRequested();
await RunAtTimeOf(DateTime.Now);
}
public async Task RunAtTimeOf(DateTime now)
{
using IServiceScope scope = this._serviceProvider.CreateScope();
var myJobService= scope.ServiceProvider.GetRequiredService<IMyJob>();
await myJobService.RunSomeTaskOnJob();
}
}
3 Register the activator
services.AddTransient<IHangFireActivatorMyJob, HangFireActivatorMyJob >();
4 Add the IRecurringJobManager interface to the configure method within Startup.cs
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IRecurringJobManager recurringJobManager)
5 Add the job to the provided IRecurringJobManager
recurringJobManager.AddOrUpdate<HangFireActivatorMyJob >(nameof(HangFireActivatorMyJob ),
job => serviceProvider.GetRequiredService<IHangFireActivatorMyJob>()
.Run(JobCancellationToken.Null)
, Cron.Hourly(3), TimeZoneInfo.Utc);
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 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();
}
}
I'm able to access my IHubContext<MyHub> fine and dandy in my .NET Core WebAPI's Controller through DI in the constructor, but I want to access it elsewhere too.
Specifically, when I consume a message from RabbitMQ, sometimes I want to update clients through _myHubContext.Clients.All.InvokeAsync(), but I just can't figure out how to get it.
I'm also having issues finding documentation for doing this kind of thing outside of the controller.
Any help would be appreciated.
EDIT:
To add some detail, and where the cause of my problem may originate, I'm trying to access the IHubContext (and some of my own services registered in ConfigureServices) within my Startup class, specifically during IApplicationLifetime ApplicationStarted and ApplicationStopped which call a RabbitMQ consumer's methods to connect and disconnect.
I'm I correct in guessing that maybe I'm unable to access registered services in the Startup class? If so, how would I go about starting these services?
Update:
Moving services.AddSignalR() and some of the services that are called at startup one level up to the WebHost.ConfigureServices in Program.cs solved some of my problems, but of course there are more.
I wasn't getting any messages on my JS client when I received a message from RabbitMQ, but my client was connecting successfully. "Weird..." I thought. To get more info, I wired up a an GET action in my controller to sent some content through the SignalR Hub. Whenever I called that GET, it works... the IHubContext<MyHub>. I get the hubContext through the constructor in my RabbitMQ listener, just like I do with the controller.
The new question: are things injected differently in the controller than they are into services that I register myself at startup? How so, and how do I overcome that?
Some code to go with it...
Excerpt from Program.cs
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel()
.UseIISIntegration()
.ConfigureServices(services => {
services.AddSignalR();
services.AddTransient<ISubscriber, Subscriber>();
services.AddTransient<IDataService, DataService>();
services.AddTransient<IHealthCheckProcessor, HealthCheckProcessor>();
services.AddTransient<INodeProcessor, NodeProcessor>();
})
.UseStartup<Startup>()
.Build();
From Startup.cs
public class Startup
{
public Startup(IConfiguration _configuration, ISubscriber _subscriber)
{
configuration = _configuration;
subscriber = _subscriber;
}
public IConfiguration configuration { get; }
public ISubscriber subscriber { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors(builder => builder
// CORS stuff);
app.UseSignalR(routes =>
{
routes.MapHub<StatusHub>("Status");
});
app.UseMvc();
applicationLifetime.ApplicationStarted.Register(OnStartup);
applicationLifetime.ApplicationStopping.Register(OnShutdown);
}
private void OnStartup() {
// MessageBroker stuff
subscriber.Start(messageBroker);
}
private void OnShutdown() {
subscriber.Stop();
}
}
From Subscriber.cs
public class Subscriber : ISubscriber {
public static IConnection connection;
public static IModel channel;
public IHubContext<StatusHub> hubContext;
public static IHealthCheckProcessor healthCheckProcessor;
public static INodeProcessor nodeProcessor;
public Subscriber(IHubContext<StatusHub> _hubContext, INodeProcessor _nodeProcessor, IHealthCheckProcessor _healthCheckProcessor)
{
connection = new ConnectionFactory().CreateConnection();
channel = connection.CreateModel();
hubContext = _hubContext;
nodeProcessor = _nodeProcessor;
healthCheckProcessor = _healthCheckProcessor;
}
public void Start(MessageBroker messageBroker)
{
var factory = new ConnectionFactory() { HostName = messageBroker.URL }.CreateConnection();
foreach (Queue queue in messageBroker.Queues)
{
channel.QueueDeclare(
queue: queue.Name,
durable: queue.Durable,
exclusive: queue.Exclusive,
autoDelete: queue.AutoDelete,
arguments: null
);
EventingBasicConsumer consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
byte[] body = ea.Body;
string message = Encoding.UTF8.GetString(body);
RouteMessage(queue, message);
};
channel.BasicConsume(
queue: queue.Name,
autoAck: queue.AutoAck,
consumer: consumer
);
hubContext.Clients.All.InvokeAsync("Send", "It worked - from the subscriber");
}
}
public void RouteMessage(Queue queue, string message) {
if(queue.Name == "discovery") {
nodeProcessor.Process(message);
}
if(queue.Name == "health") {
healthCheckProcessor.Process(message);
}
}
public void Stop()
{
Console.WriteLine("Terminating connection to RabbitMQ instance.");
channel.Close(200, "Goodbye");
connection.Close();
}
}
From HealthCheckProcessor.cs
public class HealthCheckProcessor : IHealthCheckProcessor {
private IDataService dataService;
private IHubContext<StatusHub> hubContext;
public HealthCheckProcessor(IDataService _dataService, IHubContext<StatusHub> _hubContext)
{
dataService = _dataService;
hubContext = _hubContext;
}
public void Process(string message) {
HealthCheck health = JsonConvert.DeserializeObject<HealthCheck>(message);
Node node = dataService.GetSingle(health.NodeId);
node.Health = health;
dataService.Update(node);
Console.WriteLine("It's sending.");
hubContext.Clients.All.InvokeAsync("Send", "It worked - from the processor");
}
}
From the Controller:
[Route("api/[controller]")]
public class MyController: Controller
{
private IDataService _dataService;
private readonly IConfiguration configuration;
private static IHubContext<StatusHub> hubContext;
public NodesController(IConfiguration config, IDataService dataService, IHubContext<StatusHub> _hubContext)
{
_dataService = dataService;
configuration = config;
hubContext = _hubContext;
}
[HttpGet]
public string Get()
{
hubContext.Clients.All.InvokeAsync("Send", "Blarg!");
return "Well, I tried.";
}
}
You are trying to access services that are not available at the time you request them.
Configure is called after ConfigureServices specifically so that any services registered can be accessible.
public class Startup {
public Startup(IConfiguration _configuration) {
configuration = _configuration;
}
public IConfiguration configuration { get; }
public void ConfigureServices(IServiceCollection services) {
services.AddCors();
services.AddMvc();
services.AddSignalR();
services.AddTransient<ISubscriber, Subscriber>();
services.AddTransient<IDataService, DataService>();
services.AddTransient<IHealthCheckProcessor, HealthCheckProcessor>();
services.AddTransient<INodeProcessor, NodeProcessor>();
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IApplicationLifetime applicationLifetime,
IServiceProvider sp
) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
app.UseCors(builder => builder
// CORS stuff);
app.UseMvc();
app.UseSignalR(routes => {
routes.MapHub<StatusHub>("Status");
});
//At this point all the necessary dependencies have been registered and configured
var subscriber = sp.GetService<ISubscriber>();
applicationLifetime.ApplicationStarted.Register(() => OnStartup(subscriber));
applicationLifetime.ApplicationStopping.Register(() => OnShutdown(subscriber));
}
private void OnStartup(ISubscriber subscriber) {
// MessageBroker stuff
subscriber.Start(messageBroker);
}
private void OnShutdown(ISubscriber subscriber) {
subscriber.Stop();
}
}
You should be able to now remove the convenience ConfigureServices when building the host.
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel()
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
I have troubles to get my ASP.NET Core SignalR app working.
I have this server-side code :
public class PopcornHub : Hub
{
private int Users;
public async Task BroadcastNumberOfUsers(int nbUser)
{
await Clients.All.InvokeAsync("OnUserConnected", nbUser);
}
public override async Task OnConnectedAsync()
{
Users++;
await BroadcastNumberOfUsers(Users);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
Users--;
await BroadcastNumberOfUsers(Users);
await base.OnDisconnectedAsync(exception);
}
}
whose SignalR Hub service is configured as :
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSignalR();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
app.UseSignalR(routes =>
{
routes.MapHub<PopcornHub>("popcorn");
});
...
}
In my client-side (WPF app), I have a service :
public class PopcornHubService : IPopcornHubService
{
private readonly HubConnection _connection;
public PopcornHubService()
{
_connection = new HubConnectionBuilder()
.WithUrl($"{Utils.Constants.PopcornApi.Replace("/api", "/popcorn")}")
.Build();
_connection.On<int>("OnUserConnected", (message) =>
{
});
}
public async Task Start()
{
await _connection.StartAsync();
}
}
My issue is that, when I call Start() method, I get the exception "Cannot start a connection that is not in the Initial state".
The issue occurs either locally or in Azure.
The SignalR endpoint is fine but no connection can be established. What am I missing?
You seem to be trying to start a connection that has already been started. Also note that as of alpha2 the connection is not restartable - i.e. if it is stopped you cannot restart it - you need to create a new connection.
EDIT
In post alpha2 versions the connection will be restartable.
I have ApplicationDbContext, which dynamically change connection string to database, which depends on user's library name. So, for each library, I have it's own database. When I'm creating migrations and apply them, they are related only to default database with default connection string, where no library name defined.
How can I make and apply migrations to all this databases, that dynamically created, that depends on library name (they exist after creation, they are fully defined and working databases)?
If I understood you properly, you should implement the following:
1) Implement Service that will migrate your schema and seed data. (despite what they did in the latest version, I still prefer my implementation)
Helper
public static async Task EnsureSeedData(IServiceProvider serviceProvider)
{
using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
// here could be several seed services if necessary
var seedService = scope.ServiceProvider.GetService<ISeedService>();
await seedService.MigrateAsync();
await seedService.SeedAsync();
}
}
ISeedService Interface
internal interface ISeedService
{
Task MigrateAsync();
Task SeedAsync();
}
SeedService Implementation
internal sealed class SeedService : ISeedService
{
private readonly InitializeContext _identityContext;
public SeedService(InitializeContext identityContext)
{
_identityContext = identityContext;
}
public async Task MigrateAsync()
{
await _identityContext.Database.MigrateAsync();
}
public async Task SeedAsync()
{
if (_identityContext.AllMigrationsApplied())
{
var strategy = _identityContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using (var transaction = await _identityContext.Database.BeginTransactionAsync())
{
// seed data if necessary
transaction.Commit();
}
});
}
}
}
Program.cs file
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
Task.Run(async () =>
{
await SeedData.EnsureSeedData(host.Services);
}).Wait();
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
2) Register context and related services in DI, which will allow dynamically retrieve connection string. There are several ways to do it, my implementation is not the best one, so it's up to you.
Startup.cs file
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISeedService, SeedService>();
services.AddScoped<IDbConfiguration, FakeDbConfiguration>();
services.AddDbContext<InitializeContext>((provider, builder) =>
{
var dbConfiguration = provider.GetService<IDbConfiguration>();
builder.UseSqlServer(dbConfiguration.Connection);
});
// omitted
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// omitted
}
}
DbConfiguration file.
internal sealed class FakeDbConfiguration : IDbConfiguration
{
private readonly IConfiguration _configuration;
public FakeDbConfiguration(IConfiguration configuration)
{
_configuration = configuration;
}
// REPLACE THIS PART WITH YOURS IMPLEMENTATION
public string Connection => _configuration["Database:Connection"];
I hope it help you.