Secure ASP controllers/methods programmatically - c#

I'm after the API that allows to apply [Authorize] attribute from API rather than having it on controller's class/method.
Consider third party controller classes that come from dependency nuget package and I want to host them in my application and implement security for them.
I know I can use
app.UseEndpoints(endpoints => { endpoints.MapControllers().RequireAuthorization(new AuthorizeAttribute("policy")); });
but this will configure ALL controllers and methods to be protected by policy and I need to have control per class/method.
Mapping manually with
endpoints.MapPut("***").RequireAuthorization(new AuthorizeAttribute("someOtherPolicy"))
is also not an option since the mapping is already defined with [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("api/someRoute")] attributes on imported controllers.
Please advise

In case you can determinate on which endpoint should be specific authorization policy applied, you can try following:
public class MyAuthorizationFilter: AuthorizeAttribute, IAsyncAuthorizationFilter
{
public MyAuthorizationFilter() : base()
{
//this.AuthenticationSchemes = "";
//this.Policy = "";
//this.Roles = "";
}
public Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
context.Result = new OkObjectResult("my custom policy was checked");
return Task.CompletedTask;
}
}
public static class DynamicAuthorizationPolicies
{
public static void RegisterMyFilters(this IServiceProvider serviceProvider)
{
var allEndpoints = serviceProvider
.GetService<IActionDescriptorCollectionProvider>()
?.ActionDescriptors
?.Items;
if (allEndpoints is null)
{
return;
}
foreach (var endpoint in allEndpoints)
{
// If you debug hier you will see that you can register
// differnt filters or attribute in different collections
// for the endpoint
if (/*endpoint match some requirement*/true)
{
var authorizeFilter = serviceProvider.GetService<MyAuthorizationFilter>();
var descriptor = new FilterDescriptor(authorizeFilter, FilterScope.Action);
endpoint.FilterDescriptors.Add(descriptor);
}
}
}
}
... and here is the usage:
public void ConfigureServices(IServiceCollection services)
{
// services.Add...
services.AddTransient<MyAuthorizationFilter>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// app.Use....
app.ApplicationServices.RegisterMyFilters();
}

Related

Is there a way to have multiple connection to the database with the same context but with different connection strings?

In my current project I have an ASP .NET Core Web application and I'm using Entity Framework with PostgreSQL.
The connection to the DB is established in startup.cs file
var connectionString = Configuration.GetConnectionString("DeliveryDb");
services.AddDbContext<DeliveryDbContext>(options =>
options.UseNpgsql(connectionString, x => x.MigrationsAssembly("DeliveryWebService")));
Connection string looks like
"Host=localhost;Port=5433;Database=delivery-db;Username=postgres;Password=qwerty"
The problem is that I have different roles, created by CREATE ROLE ..., in my database with different rights, etc. My app logic is pretty simple:
User signs in with his login and password
This data is transfered to a controller
Then by this data in Users table I determine the role of this user
And what I want to do afterward is to close the current connection to Postgres DB and reconnect to it with a proper user role like a manager or admin, meaning to use different connection strings:
"Host=localhost;Port=5433;Database=delivery-db;Username=admin;Password=12345"
"Host=localhost;Port=5433;Database=delivery-db;Username=manager;Password=67890"
And this is where I'm stuck. I don't know how to close a connection in EF and then reconnect. I was looking towards AddDbContextPool instead of using AddDbContext to use a pool of connections to kinda connect to the database with all possible roles. But it didn't work out for me (or maybe I am missing something)...
Any ideas? Thanks in advance
I can think of two options.
Option 1 Create two Different dbContext that inherit from one base dbContext. One for admin and one for users. Get user's role by adminDbContext and put user's role into HttpContext.Items then get userDbContext from serviceProvider.
public abstract class AppDBContext<T> : DbContext where T : DbContext
{
public AppDBContext(DbContextOptions<T> options)
: base(options)
{
}
public virtual DbSet<User> User { get; set; }
...
}
public class AdminDbContext : AppDBContext<AdminDbContext>
{
public AdminDbContext(DbContextOptions<AdminDbContext> options) : base(options)
{
}
}
public class UserDbContext : AppDBContext<UserDbContext>
{
public UserDbContext(DbContextOptions<UserDbContext> options) : base(options)
{
}
}
Your appsettings.json
{
"ConnectionStrings": {
"Admin": "Host=localhost;Port=5433;Database=delivery-db;Username=admin;Password=12345",
"Role1": "Host=localhost;Port=5433;Database=delivery-db;Username=role1;Password=role1_password",
"Role2": "Host=localhost;Port=5433;Database=delivery-db;Username=role2;Password=role2_password",
"DefaultRole": "Host=localhost;Port=5433;Database=delivery-db;Username=defaultRole;Password=default_role_password;"
}
}
Your Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//Need for get IHttpContextAccessor
services.AddHttpContextAccessor();
services.AddDbContext<AdminDbContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("Admin"))
);
services.AddDbContext<UserDbContext>((services, optionsBuilder) =>
{
var httpContextAccessor = services.GetService<IHttpContextAccessor>();
var hasRole = httpContextAccessor.HttpContext.Items.TryGetValue("Role", out object role);
string connectionStringName = "DefaultRole";
if (hasRole)
connectionStringName = role.ToString();
var connStr = Configuration.GetConnectionString(connectionStringName);
optionsBuilder.UseSqlServer(connStr);
});
}
Your Controller
private readonly AdminDbContext adminDbContext;
private readonly IServiceProvider serviceProvider;
public HomeController(AdminDbContext adminDbContext, IServiceProvider serviceProvider)
{
this.adminDbContext = adminDbContext;
this.serviceProvider = serviceProvider;
}
public IActionResult SomeApi(Model model)
{
var user = adminDbContext.User.FirstOrDefault(...); // Get user
var userRole = user.RoleName; // Get user's role
HttpContext.Items.Add("Role", userRole); // Put user's role in items and we get it's value in startup.cs when we creating UserDbContext's Options
var userDbContext = serviceProvider.GetService<UserDbContext>();
//Now you have userDbContext
...
}
Option 2 Create custom authentication scheme. This way you authenticate user and detect user's role in authentication process and you don't need to inject ServiceProvider to your service/controller. For more infomation you can read this https://joonasw.net/view/creating-auth-scheme-in-aspnet-core-2
Like option 1 create two dbContexts and appsettings.json.
Create these classes
using Microsoft.AspNetCore.Authentication;
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
public class CustomAuthOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "CustomDetectRole";
}
public static class CustomAuthenticationBuilderExtensions
{
// Custom authentication extension method
public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<CustomAuthOptions> configureOptions)
{
// Add custom authentication scheme with custom options and custom handler
return builder.AddScheme<CustomAuthOptions, CustomAuthHandler>(CustomAuthOptions.DefaultScheme, configureOptions);
}
}
public class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
private readonly AdminDbContext adminDbContext;
public CustomAuthHandler(IOptionsMonitor<CustomAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, AdminDbContext adminDbContext)
: base(options, logger, encoder, clock)
{
this.adminDbContext = adminDbContext;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//You can use Request object here if you need
var user = await adminDbContext.User.FirstOrDefaultAsync(...); // Get user
var userRole = user.RoleName; // Get user's role here
if (string.IsNullOrEmpty(userRole))
return await NeedLoginAgain();
else
return await Success(userRole);
}
private Task<AuthenticateResult> Success(string role)
{
var claims = new ClaimsIdentity(CustomAuthOptions.DefaultScheme);
claims.AddClaim(new Claim(ClaimTypes.Role, role));
_ = claims.IsAuthenticated;
var ticket = new AuthenticationTicket(new ClaimsPrincipal(claims), CustomAuthOptions.DefaultScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
private Task<AuthenticateResult> NeedLoginAgain()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
}
Your Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddHttpContextAccessor();
services.AddAuthentication(CustomAuthOptions.DefaultScheme)
.AddCustomAuth(options => { });
services.AddDbContext<AdminDbContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("Admin"))
);
services.AddDbContext<UserDbContext>((services, optionsBuilder) =>
{
var httpContextAccessor = services.GetService<IHttpContextAccessor>();
var role = httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(item => item.Type.Equals(ClaimTypes.Role))?.Value;
string connectionStringName = "DefaultUser";
if (string.IsNullOrEmpty(role) == false)
connectionStringName = role.ToString();
var connStr = Configuration.GetConnectionString(connectionStringName);
optionsBuilder.UseSqlServer(connStr);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
app.UseEndpoints(endpoints =>
{
...
});
}
and finally your controller
private readonly UserDbContext userDbContext;
public HomeController(UserDbContext userDbContext)
{
this.userDbContext = userDbContext;
}
[Authorize(AuthenticationSchemes = CustomAuthOptions.DefaultScheme)]
public IActionResult SomeApi()
{
//use userDbContext here
...
}

Force reload of ResponseCache in .NET Core 2.1 when request data changes

I am using the following attribute [ResponseCache(Duration = 60)] to cache a specific GET Request which is called a lot on my backend in .NET Core.
Everything is working fine except the cache isn't reloaded when some data in database has changed within the 60 seconds.
Is there a specific directive I have to set to reload/update the cache? link
Example Code Snippet from my Controller:
[HttpGet]
[ResponseCache(Duration = 60)]
public ActionResult<SomeTyp[]> SendDtos()
{
var dtos = _repository.QueryAll();
return Ok(dtos);
}
There is a solution with a usage of "ETag", "If-None-Match" HTTP headers. The idea is using a code which can give us an answer to the question: "Did action response changed?".
This can be done if a controller completely owns particular data lifetime.
Create ITagProvider:
public interface ITagProvider
{
string GetETag(string tagKey);
void InvalidateETag(string tagKey);
}
Create an action filter:
public class ETagActionFilter : IActionFilter
{
private readonly ITagProvider _tagProvider;
public ETagActionFilter(ITagProvider tagProvider)
{
_tagProvider = tagProvider ?? throw new ArgumentNullException(nameof(tagProvider));
}
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception != null)
{
return;
}
var uri = GetActionName(context.ActionDescriptor);
var currentEtag = _tagProvider.GetETag(uri);
if (!string.IsNullOrEmpty(currentEtag))
{
context.HttpContext.Response.Headers.Add("ETag", currentEtag);
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
var uri = GetActionName(context.ActionDescriptor);
var requestedEtag = context.HttpContext.Request.Headers["If-None-Match"];
var currentEtag = _tagProvider.GetETag(uri);
if (requestedEtag.Contains(currentEtag))
{
context.HttpContext.Response.Headers.Add("ETag", currentEtag);
context.Result = new StatusCodeResult(StatusCodes.Status304NotModified);
}
}
private string GetActionName(ActionDescriptor actionDescriptor)
{
return $"{actionDescriptor.RouteValues["controller"]}.{actionDescriptor.RouteValues["action"]}";
}
}
Initialize filter in Startup class:
public void ConfigureServices(IServiceCollection services)
{
// code above
services.AddMvc(options =>
{
options.Filters.Add(typeof(ETagActionFilter));
});
services.AddScoped<ETagActionFilter>();
services.AddSingleton<ITagProvider, TagProvider>();
// code below
}
Use InvalidateETag method somewhere in controllers (in the place where you modifing data):
[HttpPost]
public async Task<ActionResult> Post([FromBody] SomeType data)
{
// TODO: Modify data
// Invalidate tag
var tag = $"{controllerName}.{methodName}"
_tagProvider.InvalidateETag(tag);
return NoContent();
}
This solution may require a change of a client side. If you are using fetch, you can use, for example, the following library: https://github.com/export-mike/f-etag.
P.S. I didn't specify an implementation of the ITagProvider interface, you will need to write your own.
P.P.S. Articles about ETag and caching: https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag

Asp .net Core using service instance to call method in another method which runs in Startup.cs

I have class with method which is event and its gets data which i need to store to database.
public class DataParser
{
SmsService smsService = new SmsService(..require context...);
public void ReceiveSms()
{
//ParserLogic
smsService.SaveMessage(...Values...);
}
}
As service saves data with help of context I need to pass it and initialize in constructor. After i do that when I'm creating my parser object to run on Startup ir requires to pass context there.
public class Startup
{
DataParser data = new DataParser(...requires db context...)
public void ConfigureServices(IServiceCollection services)
{
//Opens port for runtime
InnerComPortSettings.OpenPort();
//Runtime sms receiver
data.ReceiveSms();
}
}
So how can I properly save data to db?
you need to refactor your code.
1) You dont have to create service inside parser. Pass it as dependency
public class DataParser
{
public DataParser(SmsService smsService)
{
SmsService _smsService = smsService;
}
public void ReceiveSms( )
{
//ParserLogic
smsService.SaveMessage(...Values...);
}
}
2) Now you need to register your context and parser and service
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options =>... your options here); // register your context
services.AddSingleton<SmsService, SmsService>(); // register your sms servcice which is required data context
services.AddSingleton<DataParser, DataParser>(); // register your parser
}
5) now its time to refactor your sms service
public class SmsService
{
private readonly IServiceScopeFactory _scopeFactory;
public SmsService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task SaveMessage(....)
{
using (var scope = _scopeFactory.CreateScope())
{
using (var ctx = scope.ServiceProvider.GetService<MyDbContext>())
{
... make changes
await ctx.SaveChangesAsync();
}
}
}
}
4) When everything registered, you can resolve what you need in configure method of Startup class
public void Configure(IApplicationBuilder app, DataParser data) // resolving your data perser and using it
{
//Opens port for runtime
InnerComPortSettings.OpenPort();
//Runtime sms receiver
data.ReceiveSms();
}
Or you can resolve your parser in controllers, services, everuwhere you want.

How do I configure Owin to use a custom AuthenticationHandler?

I have read that it's possible to create a custom Owin authentication handler, but I can't figure out how to configure Owin to use my handler instead of the default one.
How do I tell Owin to use this class instead of the default?
public class XDOpenIdAuthHandler: OpenIdConnectAuthenticationHandler
{
public XDOpenIdAuthHandler(ILogger logger)
: base(logger)
{
}
protected override void RememberNonce(OpenIdConnectMessage message, string nonce)
{
//Clean up after itself, otherwise cookies keep building up until we've got over 100 and
// the browser starts throwing errors. Bad OpenId provider.
var oldNonces = Request.Cookies.Where(kvp => kvp.Key.StartsWith(OpenIdConnectAuthenticationDefaults.CookiePrefix + "nonce")).ToArray();
if (oldNonces.Any())
{
CookieOptions cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsSecure
};
foreach (KeyValuePair<string, string> oldNonce in oldNonces)
{
Response.Cookies.Delete(oldNonce.Key, cookieOptions);
}
}
base.RememberNonce(message, nonce);
}
}
You must add it as a part of a custom AuthenticationMiddleware.
public class CustomAuthMiddleware : AuthenticationMiddleware<OpenIdConnectAuthenticationOptions>
{
public CustomAuthMiddleware(OwinMiddleware nextMiddleware, OpenIdConnectAuthenticationOptions authOptions)
: base(nextMiddleware, authOptions)
{ }
protected override AuthenticationHandler<OpenIdConnectAuthenticationOptions> CreateHandler()
{
return new XDOpenIdAuthHandler(yourLogger);
}
}
Then using it in the Startup.Auth for example:
public partial class Startup
{
// For more information on configuring authentication, please visit https://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
app.Use<CustomAuthMiddleware>(new OpenIdConnectAuthenticationOptions());
}
}
Be aware however that the Owin pipeline must not contain the default OpenIdConnectAuthenticationMiddleware, otherwise it will still get called as part of the request pipe.

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