Cassandra - correct approach asp.net core application - c#

I creating new asp.net core project and I decided to use Cassandra as a Database but I never do it before. I don't know my approach is correct? Can you gave me some advice to use this DB in .net core. (I using autofac as a dependency injection container, cassandra provider: CassandraCSharpDriver)
My current solve:
Create Cluster:
cluster = Cluster.Builder()
.AddContactPoint(cassandraData.ContactPoint)
.WithPort(int.Parse(cassandraData.Port))
.WithCredentials(cassandraData.User, cassandraData.Password)
.Build();
After that i inject this by dependency injection to my Session Cache:
public CassandraSessionCache(Cluster cluster)
{
_cluster = cluster;
_sessions = new ConcurrentDictionary<string, Lazy<ISession>>();
}
public ISession GetSession(string keyspaceName)
{
if (!_sessions.ContainsKey(keyspaceName))
_sessions.GetOrAdd(keyspaceName, key => new Lazy<ISession>(() =>
_cluster.Connect(key)));
var result = _sessions[keyspaceName];
return result.Value;
}
I Create also Map to Cassandra and use it my startup class.
After this configuration my repository look like this:
public class TestRepository : ITestRepository
{
private readonly CassandraSessionCache _cassandra;
private Mapper _mapper;
public TestRepository(CassandraSessionCache cassandra)
{
_cassandra = cassandra;
}
public async Task DeleteAsync(Guid id, string keySpace)
{
SetSessionAndMapper(keySpace);
await _mapper.DeleteAsync<Test>("WHERE id = ?", id);
}
public async Task<Install> GetAsync(string id, string keySpace)
{
SetSessionAndMapper(keySpace);
return await _mapper.FirstOrDefaultAsync<Test>("SELECT * FROM \"Test\" WHERE id = ?", id);
}
public async Task PostAsync(Test data, string keySpace)
{
SetSessionAndMapper(keySpace);
await _mapper.InsertAsync(data);
}
private void SetSessionAndMapper(string keySpace)
{
var session = _cassandra.GetSession(keySpace);
_mapper = new Mapper(session);
}
}
All is correct or I do something wrong?

Related

ASP.NET Core Web API and MongoDB with multiple Collections

I have WebAPI which interacts with MongoDB.
I created all of my MongoDB collection classes as singleton using the lazy approach. I understood it was a "standard" approach, but since I am using .NET Core for my web API, I found this tutorial that at first glance described how to do it more efficiently for .NET Core apps.
Anyway, it explains how to do it with one database, containing one collection only, which is quite limited.
With for example 2 collections, it seems even deprecated. For example this part of code:
public BookService(IBookstoreDatabaseSettings settings)
{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);
_books = database.GetCollection<Book>(settings.BooksCollectionName);
}
If I have a second collection with the same kind of constructor, it will create a new instance of MongoClient which is not recommended by the MongoDB documentation:
It is recommended to store a MongoClient instance in a global place, either as a static variable or in an IoC container with a singleton lifetime.
My question then is: is there a better way of using MongoDB with multiple collections with .NET Core (kind of join this question), or is it better to use the "universal lazy" way?
I think, Retic's approach is not so bad.
Lets continue on his answer and extract the database configuration into a separate method ConfigureMongoDb:
public void ConfigureServices(IServiceCollection services)
{
ConfigureMongoDb(services);
services.AddControllers()
.AddNewtonsoftJson(options => options.UseMemberCasing());
}
private void ConfigureMongoDb(IServiceCollection services)
{
var settings = GetMongoDbSettings();
var db = CreateMongoDatabase(settings);
var collectionA = db.GetCollection<Author>(settings.AuthorsCollectionName);
services.AddSingleton(collectionA);
services.AddSingleton<AuthorService>();
var collectionB = db.GetCollection<Book>(settings.BooksCollectionName);
services.AddSingleton(collectionB);
services.AddSingleton<BookService>();
}
private BookstoreDatabaseSettings GetMongoDbSettings() =>
Configuration.GetSection(nameof(BookstoreDatabaseSettings)).Get<BookstoreDatabaseSettings>();
private IMongoDatabase CreateMongoDatabase(BookstoreDatabaseSettings settings)
{
var client = new MongoClient(settings.ConnectionString);
return client.GetDatabase(settings.DatabaseName);
}
or in a more compact form:
private void ConfigureMongoDb(IServiceCollection services)
{
var settings = GetMongoDbSettings();
var db = CreateMongoDatabase(settings);
AddMongoDbService<AuthorService, Author>(settings.AuthorsCollectionName);
AddMongoDbService<BookService, Book>(settings.BooksCollectionName);
void AddMongoDbService<TService, TModel>(string collectionName)
{
services.AddSingleton(db.GetCollection<TModel>(collectionName));
services.AddSingleton(typeof(TService));
}
}
The downside with this approach is, that all mongodb related instances (except the services) are created at startup.
This is not always bad, because in the case of wrong settings or other mistakes you get immediate response on startup.
If you want lazy initialization for this instances, you can register the database creation and the collection retrieval with a factory method:
public void ConfigureMongoDb(IServiceCollection services)
{
var settings = GetMongoDbSettings();
services.AddSingleton(_ => CreateMongoDatabase(settings));
AddMongoDbService<AuthorService, Author>(settings.AuthorsCollectionName);
AddMongoDbService<BookService, Book>(settings.BooksCollectionName);
void AddMongoDbService<TService, TModel>(string collectionName)
{
services.AddSingleton(sp => sp.GetRequiredService<IMongoDatabase>().GetCollection<TModel>(collectionName));
services.AddSingleton(typeof(TService));
}
}
In the service you have to inject just the registered collection.
public class BookService
{
private readonly IMongoCollection<Book> _books;
public BookService(IMongoCollection<Book> books)
{
_books = books;
}
}
public class AuthorService
{
private readonly IMongoCollection<Author> _authors;
public AuthorService(IMongoCollection<Author> authors)
{
_authors = authors;
}
}
You can register a single Instance of the IMongoDatabase in your Services container. then you can add Singleton Collections to your services container using the IMongoDatabase Instance.
var client = new MongoClient(connectionString);
var db = client.GetDatabase(dbName);
var collectionA = db.GetCollection<Model>(collectionName);
services.AddSingleton<IMongoDatabase, db>();
services.AddSingleton<IMongoCollection, collectionA>();
to use these you would expose your services to your controllers via the controllers constructor.
public class SomeController
{
private readonly IMongoCollection<SomeModel> _someCollection;
public SomeController(IMongoCollection<SomeModel> someCollection)
{
_someCollection = someCollection;
}
}
I am working on a project that will have multiple collection. I am not sure about how to structure the appSetting.json file:
"DatabaseSettings": {
"ConnectionString": "somestrings",
"DatabaseName": "somename",
"CollectionName": "One Collection"
},
is it fine to have to do an array of the collection name:
"DatabaseSettings": {
"ConnectionString": "somestrings",
"DatabaseName": "somename",
"CollectionName": ["One Collection", "Second Collection" "Third Collection"]
},
I register it as a singleton like this:
builder.Services.Configure<DatabaseSettings>(builder.Configuration.GetSection("DatabaseSettings"));
builder.Services.AddSingleton<IMongoDatabase>(sp => {
var databaseSettings = sp.GetRequiredService<IOptions<DatabaseSettings>>().Value;
var mongoDbClient = new MongoClient(databaseSettings.ConnectionString);
var mongoDb = mongoDbClient.GetDatabase(databaseSettings.DatabaseName);
return mongoDb;
});
builder.Services.AddScoped<IStudentRepository, StudentRepository>();
I get settings from appsettings.json like this:
{
"DatabaseSettings": {
"ConnectionString": "mongodb+srv://xwezi:MyPassword#school-cluster.65eoe.mongodb.net",
"DatabaseName": "school"
}
}
and I'm using in my repository like this:
public class StudentRepository : IStudentRepository
{
private readonly DatabaseSettings databaseSettings;
private readonly IMongoCollection<Student> students;
public StudentRepository(
IOptions<DatabaseSettings> databaseOptions,
IMongoDatabase mongoDatabase)
{
databaseSettings = databaseOptions.Value;
students = mongoDatabase.GetCollection<Student>("students");
}
public Student? GetStudent(string id)
{
return students?.Find(s => s.Id == id).FirstOrDefault();
}
public IList<Student>? GetStudents()
{
return students?.Find(s => true).ToList();
}
}
I return just a mongo client when i register it and get the database later.. i don't see any reason why not to?
builder.Services.AddSingleton<IMongoClient>(options => {
var settings = builder.Configuration.GetSection("MongoDBSettings").Get<MongoDBSettings>();
var client = new MongoClient(settings.ConnectionString);
return client;
});
builder.Services.AddSingleton<IvMyRepository, vMyRepository>();
Then i use like this:
public vapmRepository(IMongoClient mongoClient)
{
IMongoDatabase mongoDatabase = mongoClient.GetDatabase("mdb01");
_vprocesshealthCollection = mongoDatabase.GetCollection<vMsg>("vhealth");
}
Now you have a MongoClient and can connect to any database(s) you want freely for any n collections

use mediator cqrs in TDD for integration test .net core 3

i use Mediator in my project .
Demo Project on Github
i want to use TDD in my project and integration test with .Net core 3.0
i write this code int test class for use intergration test with mediator :
public class SubscribeTest : IClassFixture<TravelRequest<Startup>>, IClassFixture<DbContextFactory>, IDisposable
{
private readonly TravelRequest<Startup> request;
private readonly DbContextFactory contextFactory;
public SubscribeTest(TravelRequest<Startup> request , DbContextFactory contextFactory)
{
this.request = request;
this.contextFactory = contextFactory;
}
public void Dispose()
{
request.Dispose();
}
[Fact]
public async Task ListSubscribeAsync()
{
var add = await request.Get("/Subscribe/GetListSubscribe");
await add.BodyAs<SubscribListDto>();
}
}
and this is TravelRequest :
public class TravelRequest<TStartup> : IDisposable where TStartup : class
{
private readonly HttpClient client;
private readonly TestServer server;
public TravelRequest()
{
var webHostBuilder = new WebHostBuilder().UseStartup<TStartup>().UseConfiguration(ConfigorationSingltonConfigoration.GetConfiguration());
this.server = new TestServer(webHostBuilder);
this.client = server.CreateClient();
}
}
and this is ConfigorationSingltonConfigoration for use the appSetting-test.json :
public class ConfigorationSingltonConfigoration
{
private static IConfigurationRoot configuration;
private ConfigorationSingltonConfigoration() { }
public static IConfigurationRoot GetConfiguration()
{
if (configuration is null)
configuration = new ConfigurationBuilder()
.SetBasePath(Path.Combine(Path.GetFullPath("../../../")))
.AddJsonFile("appsettings-test.json")
.AddEnvironmentVariables()
.Build();
return configuration;
}
}
and finally this is for set DbContext :
public class DbContextFactory : IDisposable
{
public TravelContext Context { get; private set; }
public DbContextFactory()
{
var dbBuilder = GetContextBuilderOptions<TravelContext>("SqlServer");
Context = new TravelContext(dbBuilder.Options);
Context.Database.Migrate();
}
public void Dispose()
{
Context.Dispose();
}
public TravelContext GetRefreshContext()
{
var dbBuilder = GetContextBuilderOptions<TravelContext>("SqlServer");
Context = new TravelContext(dbBuilder.Options);
return Context;
}
private DbContextOptionsBuilder<TravelContext> GetContextBuilderOptions<T>(string connectionStringName)
{
var connectionString = ConfigorationSingltonConfigoration.GetConfiguration().GetConnectionString(connectionStringName);
var contextBuilder = new DbContextOptionsBuilder<TravelContext>();
var servicesCollection = new ServiceCollection().AddEntityFrameworkSqlServer().BuildServiceProvider();
contextBuilder.UseSqlServer(connectionString).UseInternalServiceProvider(servicesCollection);
return contextBuilder;
}
}
Now my problem is here , when i RunTest in result it show me this error :
---- System.InvalidOperationException : Unable to resolve service for type 'System.Collections.Generic.IList1[FluentValidation.IValidator1[Travel.Services.SubscribeServices.Query.SubscribeList.SubscribeListCommand]]' while attempting to activate 'Travel.Common.ValidateBehavior.ValidateCommandBehavior2[Travel.Services.SubscribeServices.Query.SubscribeList.SubscribeListCommand,Travel.Common.Operation.OperationResult1[System.Collections.Generic.IEnumerable`1[Travel.ViewModel.SubscibeDto.SubscribListDto]]]'. ---- System.NotImplementedException : The method or operation is not implemented.
whats the problem ? how can i solve ths problem ??? i put the source code of project in top of question
I suspect that the validators are not registered into the container.
You can use the FluentValidation in .NET Core with Dependency Injection, by installing the nuget and than passing the assembly.
More references at Using FluentValidation in .NET Core with Dependency Injection.
More reference of how you can do this at Fluent Validation on .NET Core 2.0 does not register.

How to create separate in memory database for each test?

I'm writing a test with xunit on .NET Core 3.0 and I have a problem with the in-memory database. I need a separate database for each test but now I create a single database that causes problems, but I have no idea how to create a new database for each test.
public class AccountAdminTest : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private IServiceScopeFactory scopeFactory;
private readonly CustomWebApplicationFactory<Startup> _factory;
private ApplicationDbContext _context;
public AccountAdminTest(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = true,
BaseAddress = new Uri("https://localhost:44444")
});
scopeFactory = _factory.Services.GetService<IServiceScopeFactory>();
var scope = scopeFactory.CreateScope();
_context = scope.ServiceProvider.GetService<ApplicationDbContext>();
}
}
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.AddDbContext<ApplicationDbContext>((options, context) =>
{
context.UseInMemoryDatabase("IdentityDatabase");
});
});
}
}
Now it's look like this but still dosen't work. When i change lifetime on AddDbContext it doesn't change anything.
public class AccountAdminTest : IDisposable
{
public AccountAdminTest(ITestOutputHelper output)
{
this.output = output;
_factory = new CustomWebApplicationFactory<Startup>();
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = true,
BaseAddress = new Uri("https://localhost:44444")
});
scopeFactory = _factory.Services.GetService<IServiceScopeFactory>();
_scope = scopeFactory.CreateScope();
_context = _scope.ServiceProvider.GetService<ApplicationDbContext>();
var _user = User.getAppAdmin();
_context.Add(_user);
_context.SaveChanges(); //Here i got error on secound test. It says "An item with the same key has already been added"
}
public void Dispose()
{
_scope.Dispose();
_factory.Dispose();
_context.Dispose();
_client.Dispose();
}
I can't get token when use Guid as db name. It says that username/password is not valid. I use IdentityServer for authentication
public async Task<string> GetAccessToken(string userName, string password, string clientId, string scope)
{
var disco = await _client.GetDiscoveryDocumentAsync("https://localhost:44444");
if (!String.IsNullOrEmpty(disco.Error))
{
throw new Exception(disco.Error);
}
var response = await _client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = clientId,
Scope = scope,
UserName = userName,
Password = password,
});
return response.AccessToken;
}
All you need to change is this code here:
services.AddDbContext<ApplicationDbContext>((options, context) =>
{
context.UseInMemoryDatabase("IdentityDatabase");
});
Instead of the constant value "IdentityDatabase", use something like Guid.NewGuid().ToString():
context.UseInMemoryDatabase(Guid.NewGuid().ToString());
Then, every time the context is fetched, it will be using a new in-memory database.
Edit: You must specify an unique name for every test run to avoid sharing the same InMemory database as slightly mentioned here and already answered here and here.
Nonetheless the suggestion below to switch to "Constructor and Dispose" still applies.
IClassFixture is not the right tool to use, the documentation says:
When to use: when you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished.
In your case you have to use "Constructor and Dispose", the documentation says:
When to use: when you want a clean test context for every test (sharing the setup and cleanup code, without sharing the object instance).
So your test will be:
public class AccountAdminTest
{
private readonly HttpClient _client;
private IServiceScopeFactory scopeFactory;
private readonly CustomWebApplicationFactory<Startup> _factory;
private ApplicationDbContext _context;
public AccountAdminTest(CustomWebApplicationFactory<Startup> factory)
{
//
_factory = new CustomWebApplicationFactory<Startup>();
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = true,
BaseAddress = new Uri("https://localhost:44444")
});
scopeFactory = _factory.Services.GetService<IServiceScopeFactory>();
_scope = scopeFactory.CreateScope();
_context = scope.ServiceProvider.GetService<ApplicationDbContext>();
}
//Tests...
public void Dispose() {
_scope.Dispose();
_factory.Dispose();
//Dispose and cleanup anything else...
}
}
Alternatively you can specify a lifetime of ServiceLifetime.Transient for your DbContext using this overload of .AddDbContext

IDistributedCache in .net core 2.0 is not shared between calls

I'm developing .NET Core 2.0 Web API and trying to use IDistributedCache to keep some data between different calls.
For this task I created wrapper and registered it as a singleton in Startup.cs file in order to use dependency injection in my controllers.
This is how the wrapper looks (simplified):
public interface ICacheWrapper
{
Task Set<T>(string name, T obj);
Task<T> TryGet<T>(string name);
}
public class CacheWrapper : ICacheWrapper
{
private readonly IDistributedCache _cache;
private readonly TimeSpan _defaultOffset = new TimeSpan(1, 0, 0, 0);
public CacheWrapper(IDistributedCache cache)
{
_cache = cache;
}
public async Task Set<T>(string name, T obj)
{
await DoSet(name, obj);
}
public async Task<T> TryGet<T>(string name)
{
var data = await Get<T>(name);
}
private async Task DoSet<T>(string name, T obj)
{
var binFormatter = new BinaryConverter();
var array = binFormatter.Serialize(obj);
var options = new DistributedCacheEntryOptions { SlidingExpiration = _defaultOffset };
await _cache.SetAsync(name, array, options);
}
private async Task<T> Get<T>(string name)
{
var binFormatter = new BinaryConverter();
var data = await _cache.GetAsync(name);
if (data == null)
{
return default(T);
}
var obj = binFormatter.Deserialize<T>(data);
return obj;
}
}
I also added this line to my Startup.cs file: services.AddDistributedMemoryCache();
The problem is: between each API call IDistributedCache object seems to reset - it does not contain any data. What should I do to keep the cache between calls?
Edit: I can confirm that adding to cache works in scope of one call, after doing SetAsync cache contains one item.
Edit2 This is how I register my wrapper : services.AddSingleton<ICacheWrapper, CacheWrapper>();

Hangfire dependency injection with .NET Core

How can I use .NET Core's default dependency injection in Hangfire?
I am new to Hangfire and searching for an example which works with ASP.NET Core.
See full example on GitHub https://github.com/gonzigonz/HangfireCore-Example.
Live site at http://hangfirecore.azurewebsites.net/
Make sure you have the Core version of Hangfire:
dotnet add package Hangfire.AspNetCore
Configure your IoC by defining a JobActivator. Below is the config for use with the default asp.net core container service:
public class HangfireActivator : Hangfire.JobActivator
{
private readonly IServiceProvider _serviceProvider;
public HangfireActivator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override object ActivateJob(Type type)
{
return _serviceProvider.GetService(type);
}
}
Next register hangfire as a service in the Startup.ConfigureServices method:
services.AddHangfire(opt =>
opt.UseSqlServerStorage("Your Hangfire Connection string"));
Configure hangfire in the Startup.Configure method. In relationship to your question, the key is to configure hangfire to use the new HangfireActivator we just defined above. To do so you will have to provide hangfire with the IServiceProvider and this can be achieved by just adding it to the list of parameters for the Configure method. At runtime, DI will providing this service for you:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IServiceProvider serviceProvider)
{
...
// Configure hangfire to use the new JobActivator we defined.
GlobalConfiguration.Configuration
.UseActivator(new HangfireActivator(serviceProvider));
// The rest of the hangfire config as usual.
app.UseHangfireServer();
app.UseHangfireDashboard();
}
When you enqueue a job, use the registered type which usually is your interface. Don't use a concrete type unless you registered it that way. You must use the type registered with your IoC else Hangfire won't find it.
For Example say you've registered the following services:
services.AddScoped<DbManager>();
services.AddScoped<IMyService, MyService>();
Then you could enqueue DbManager with an instantiated version of the class:
BackgroundJob.Enqueue(() => dbManager.DoSomething());
However you could not do the same with MyService. Enqueuing with an instantiated version would fail because DI would fail as only the interface is registered. In this case you would enqueue like this:
BackgroundJob.Enqueue<IMyService>( ms => ms.DoSomething());
DoritoBandito's answer is incomplete or deprecated.
public class EmailSender {
public EmailSender(IDbContext dbContext, IEmailService emailService)
{
_dbContext = dbContext;
_emailService = emailService;
}
}
Register services:
services.AddTransient<IDbContext, TestDbContext>();
services.AddTransient<IEmailService, EmailService>();
Enqueue:
BackgroundJob.Enqueue<EmailSender>(x => x.Send(13, "Hello!"));
Source:
http://docs.hangfire.io/en/latest/background-methods/passing-dependencies.html
Note: if you want a full sample, see my blog post on this.
All of the answers in this thread are wrong/incomplete/outdated. Here's an example with ASP.NET Core 3.1 and Hangfire.AspnetCore 1.7.
Client:
//...
using Hangfire;
// ...
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddHangfire(config =>
{
// configure hangfire per your requirements
});
}
}
public class SomeController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobClient;
public SomeController(IBackgroundJobClient backgroundJobClient)
{
_backgroundJobClient = backgroundJobClient;
}
[HttpPost("some-route")]
public IActionResult Schedule([FromBody] SomeModel model)
{
_backgroundJobClient.Schedule<SomeClass>(s => s.Execute(model));
}
}
Server (same or different application):
{
//...
services.AddScoped<ISomeDependency, SomeDependency>();
services.AddHangfire(hangfireConfiguration =>
{
// configure hangfire with the same backing storage as your client
});
services.AddHangfireServer();
}
public interface ISomeDependency { }
public class SomeDependency : ISomeDependency { }
public class SomeClass
{
private readonly ISomeDependency _someDependency;
public SomeClass(ISomeDependency someDependency)
{
_someDependency = someDependency;
}
// the function scheduled in SomeController
public void Execute(SomeModel someModel)
{
}
}
As far as I am aware, you can use .net cores dependency injection the same as you would for any other service.
You can use a service which contains the jobs to be executed, which can be executed like so
var jobId = BackgroundJob.Enqueue(x => x.SomeTask(passParamIfYouWish));
Here is an example of the Job Service class
public class JobService : IJobService
{
private IClientService _clientService;
private INodeServices _nodeServices;
//Constructor
public JobService(IClientService clientService, INodeServices nodeServices)
{
_clientService = clientService;
_nodeServices = nodeServices;
}
//Some task to execute
public async Task SomeTask(Guid subject)
{
// Do some job here
Client client = _clientService.FindUserBySubject(subject);
}
}
And in your projects Startup.cs you can add a dependency as normal
services.AddTransient< IClientService, ClientService>();
Not sure this answers your question or not
Currently, Hangfire is deeply integrated with Asp.Net Core. Install Hangfire.AspNetCore to set up the dashboard and DI integration automatically. Then, you just need to define your dependencies using ASP.NET core as always.
If you are trying to quickly set up Hangfire with ASP.NET Core (tested in ASP.NET Core 2.2) you can also use Hangfire.MemoryStorage. All the configuration can be performed in Startup.cs:
using Hangfire;
using Hangfire.MemoryStorage;
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(opt => opt.UseMemoryStorage());
JobStorage.Current = new MemoryStorage();
}
protected void StartHangFireJobs(IApplicationBuilder app, IServiceProvider serviceProvider)
{
app.UseHangfireServer();
app.UseHangfireDashboard();
//TODO: move cron expressions to appsettings.json
RecurringJob.AddOrUpdate<SomeJobService>(
x => x.DoWork(),
"* * * * *");
RecurringJob.AddOrUpdate<OtherJobService>(
x => x.DoWork(),
"0 */2 * * *");
}
public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider)
{
StartHangFireJobs(app, serviceProvider)
}
Of course, everything is store in memory and it is lost once the application pool is recycled, but it is a quick way to see that everything works as expected with minimal configuration.
To switch to SQL Server database persistence, you should install Hangfire.SqlServer package and simply configure it instead of the memory storage:
services.AddHangfire(opt => opt.UseSqlServerStorage(Configuration.GetConnectionString("Default")));
I had to start HangFire in main function. This is how I solved it:
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var serviceScope = host.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
try
{
var liveDataHelper = services.GetRequiredService<ILiveDataHelper>();
var justInitHangfire = services.GetRequiredService<IBackgroundJobClient>();
//This was causing an exception (HangFire is not initialized)
RecurringJob.AddOrUpdate(() => liveDataHelper.RePopulateAllConfigDataAsync(), Cron.Daily());
// Use the context here
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Can't start " + nameof(LiveDataHelper));
}
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Actually there is an easy way for dependency injection based job registration.
You just need to use the following code in your Startup:
public class Startup {
public void Configure(IApplicationBuilder app)
{
var factory = app.ApplicationServices
.GetService<IServiceScopeFactory>();
GlobalConfiguration.Configuration.UseActivator(
new Hangfire.AspNetCore.AspNetCoreJobActivator(factory));
}
}
However i personally wanted a job self registration including on demand jobs (recurring jobs which are never executed, except by manual trigger on hangfire dashboard), which was a little more complex then just that. I was (for example) facing issues with the job service activation, which is why i decided to share most of my implementation code.
//I wanted an interface to declare my jobs, including the job Id.
public interface IBackgroundJob {
string Id { get; set; }
void Invoke();
}
//I wanted to retrieve the jobs by id. Heres my extension method for that:
public static IBackgroundJob GetJob(
this IServiceProvider provider,
string jobId) => provider
.GetServices<IBackgroundJob>()
.SingleOrDefault(j => j.Id == jobId);
//Now i needed an invoker for these jobs.
//The invoker is basically an example of a dependency injected hangfire job.
internal class JobInvoker {
public JobInvoker(IServiceScopeFactory factory) {
Factory = factory;
}
public IServiceScopeFactory Factory { get; }
public void Invoke(string jobId)
{
//hangfire jobs should always be executed within their own scope.
//The default AspNetCoreJobActivator should technically already do that.
//Lets just say i have trust issues.
using (var scope = Factory.CreateScope())
{
scope.ServiceProvider
.GetJob(jobId)?
.Invoke();
}
}
//Now i needed to tell hangfire to use these jobs.
//Reminder: The serviceProvider is in IApplicationBuilder.ApplicationServices
public static void RegisterJobs(IServiceProvider serviceProvider) {
var factory = serviceProvider.GetService();
GlobalConfiguration.Configuration.UseActivator(new Hangfire.AspNetCore.AspNetCoreJobActivator(factory));
var manager = serviceProvider.GetService<IRecurringJobManager>();
var config = serviceProvider.GetService<IConfiguration>();
var jobs = serviceProvider.GetServices<IBackgroundJob>();
foreach (var job in jobs) {
var jobConfig = config.GetJobConfig(job.Id);
var schedule = jobConfig?.Schedule; //this is a cron expression
if (String.IsNullOrWhiteSpace(schedule))
schedule = Cron.Never(); //this is an on demand job only!
manager.AddOrUpdate(
recurringJobId: job.Id,
job: GetJob(job.Id),
cronExpression: schedule);
}
//and last but not least...
//My Method for creating the hangfire job with injected job id
private static Job GetJob(string jobId)
{
var type = typeof(JobInvoker);
var method = type.GetMethod("Invoke");
return new Job(
type: type,
method: method,
args: jobId);
}
Using the above code i was able to create hangfire job services with full dependency injection support. Hope it helps someone.
Use the below code for Hangfire configuration
using eForms.Core;
using Hangfire;
using Hangfire.SqlServer;
using System;
using System.ComponentModel;
using System.Web.Hosting;
namespace eForms.AdminPanel.Jobs
{
public class JobManager : IJobManager, IRegisteredObject
{
public static readonly JobManager Instance = new JobManager();
//private static readonly TimeSpan ZeroTimespan = new TimeSpan(0, 0, 10);
private static readonly object _lockObject = new Object();
private bool _started;
private BackgroundJobServer _backgroundJobServer;
private JobManager()
{
}
public int Schedule(JobInfo whatToDo)
{
int result = 0;
if (!whatToDo.IsRecurring)
{
if (whatToDo.Delay == TimeSpan.Zero)
int.TryParse(BackgroundJob.Enqueue(() => Run(whatToDo.JobId, whatToDo.JobType.AssemblyQualifiedName)), out result);
else
int.TryParse(BackgroundJob.Schedule(() => Run(whatToDo.JobId, whatToDo.JobType.AssemblyQualifiedName), whatToDo.Delay), out result);
}
else
{
RecurringJob.AddOrUpdate(whatToDo.JobType.Name, () => RunRecurring(whatToDo.JobType.AssemblyQualifiedName), Cron.MinuteInterval(whatToDo.Delay.TotalMinutes.AsInt()));
}
return result;
}
[DisplayName("Id: {0}, Type: {1}")]
[HangFireYearlyExpirationTime]
public static void Run(int jobId, string jobType)
{
try
{
Type runnerType;
if (!jobType.ToType(out runnerType)) throw new Exception("Provided job has undefined type");
var runner = runnerType.CreateInstance<JobRunner>();
runner.Run(jobId);
}
catch (Exception ex)
{
throw new JobException($"Error while executing Job Id: {jobId}, Type: {jobType}", ex);
}
}
[DisplayName("{0}")]
[HangFireMinutelyExpirationTime]
public static void RunRecurring(string jobType)
{
try
{
Type runnerType;
if (!jobType.ToType(out runnerType)) throw new Exception("Provided job has undefined type");
var runner = runnerType.CreateInstance<JobRunner>();
runner.Run(0);
}
catch (Exception ex)
{
throw new JobException($"Error while executing Recurring Type: {jobType}", ex);
}
}
public void Start()
{
lock (_lockObject)
{
if (_started) return;
if (!AppConfigSettings.EnableHangFire) return;
_started = true;
HostingEnvironment.RegisterObject(this);
GlobalConfiguration.Configuration
.UseSqlServerStorage("SqlDbConnection", new SqlServerStorageOptions { PrepareSchemaIfNecessary = false })
//.UseFilter(new HangFireLogFailureAttribute())
.UseLog4NetLogProvider();
//Add infinity Expiration job filter
//GlobalJobFilters.Filters.Add(new HangFireProlongExpirationTimeAttribute());
//Hangfire comes with a retry policy that is automatically set to 10 retry and backs off over several mins
//We in the following remove this attribute and add our own custom one which adds significant backoff time
//custom logic to determine how much to back off and what to to in the case of fails
// The trick here is we can't just remove the filter as you'd expect using remove
// we first have to find it then save the Instance then remove it
try
{
object automaticRetryAttribute = null;
//Search hangfire automatic retry
foreach (var filter in GlobalJobFilters.Filters)
{
if (filter.Instance is Hangfire.AutomaticRetryAttribute)
{
// found it
automaticRetryAttribute = filter.Instance;
System.Diagnostics.Trace.TraceError("Found hangfire automatic retry");
}
}
//Remove default hangefire automaticRetryAttribute
if (automaticRetryAttribute != null)
GlobalJobFilters.Filters.Remove(automaticRetryAttribute);
//Add custom retry job filter
GlobalJobFilters.Filters.Add(new HangFireCustomAutoRetryJobFilterAttribute());
}
catch (Exception) { }
_backgroundJobServer = new BackgroundJobServer(new BackgroundJobServerOptions
{
HeartbeatInterval = new System.TimeSpan(0, 1, 0),
ServerCheckInterval = new System.TimeSpan(0, 1, 0),
SchedulePollingInterval = new System.TimeSpan(0, 1, 0)
});
}
}
public void Stop()
{
lock (_lockObject)
{
if (_backgroundJobServer != null)
{
_backgroundJobServer.Dispose();
}
HostingEnvironment.UnregisterObject(this);
}
}
void IRegisteredObject.Stop(bool immediate)
{
Stop();
}
}
}
Admin Job Manager
public class Global : System.Web.HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
if (Core.AppConfigSettings.EnableHangFire)
{
JobManager.Instance.Start();
new SchedulePendingSmsNotifications().Schedule(new Core.JobInfo() { JobId = 0, JobType = typeof(SchedulePendingSmsNotifications), Delay = TimeSpan.FromMinutes(1), IsRecurring = true });
}
}
protected void Application_End(object sender, EventArgs e)
{
if (Core.AppConfigSettings.EnableHangFire)
{
JobManager.Instance.Stop();
}
}
}

Categories

Resources