Hangfire job filter configuration in appsettings.json - hangfire

I have created a job filter attribute for Hangfire that will do some work before and after a job executes. The attribute looks similar to this:
public class MyJobAttribute : JobFilterAttribute, IServerFilter
{
public MyJobAttribute(string someConfig)
{
// ...
}
public void OnPerforming(PerformingContext context)
{
// ...
}
public void OnPerformed(PerformedContext context)
{
// ...
}
}
The attribute is added to a job method like this:
[MyJob("very important string")]
public void Test()
{
// Do the job
}
Everything works fine. The job filter is called before and after the job. Now I want to move the very important string to the appsettings.json file. I read somewhere that I can register the job filter globally and then inject an IConfiguration. But I would like to include this attribute on a specific job and do something similar to this:
[MyJob("%thekey%")]
public void Test()
{
// Do the job
}
And then in the appsettings.json file do like this:
{
"thekey": "very important string"
}
Is this possible in any way or perhaps even built-in?

Related

Dependency injection in Hotchocolate GraphQL, ASP.NET Core

Is it possible to do something like this?
Query.cs
class Query<T> : ObjectType<MyQuery<T>> where T : class
{
protected override void configure(IObjectTypeDescriptor<MyQuery<T>> descriptor)
{
descriptor
.Field(f => f.GetItems)
.Description("Return List");
}
}
public partial class MyQuery<T> where T : class
{
private readonly IGenericRepositorty _repo
public MyQuery(IGenericRepositorty repo)
{
_repo = repo;
}
public IEnumerable<T> GetItems()
{
return _repo.GetAll(); // GetAll in generic repo
}
}
Now if I am adding my service in Startup.cs as
services.AddQueryType<MyQuery<Entity>>();
It works.
But I want to add it as
services.AddQueryType<MyQuery<>>(); or kind of services.AddQueryType(typeOf(MyQuery<>));
The way we inject generic repo like this
services.AddScoped(typef(IGenericRepository<>),typeofGenericRepository<>)
So, here at run time it creates an instance.
The same way for query at run time I am trying whether it will be possible to create instance

.NET Core Middleware - access IApplicationBuilder in a controller?

I need to access IApplicationBuilder inside a controller.
What I have tried :-
I have written middleware (app.UseMyMiddleware) as follows
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMyMiddleware();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
///TODO - Pass IApplicationBuilder to HttpContext
await _next(context);
}
}
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
but I can't figure out how I can pass IApplicationBuilder to HttpContext in Invoke method. so, that I can use it in a controller.
I have also referred following stackoverflow question-answer
how to access IApplicationBuilder in a controller?
.Net Core Middleware - Getting Form Data from Request
Question(s) :-
How can pass IApplicationBuilder to HttpContext in Invoke method to use it in controller?
Is there any better way to access IApplicationBuilder inside controller apart from middleware?
IApplicationBuilder was not designed to work the way you want it to. Instead, if you have some data created at build time that you want to be available to middleware add a Singleton to the services and inject the singleton into the middleware.
You cannot access IApplicationBuilder anywhere later after completing the application building phase (after running Configure method). It's not available for injection at all.
However for the purpose of plugging-in or configuring middlewares at runtime based on request data (from HttpContext), you can use .UseWhen. Another one for terminal middleware is .MapWhen but I think that's not for your case. Here is an example of .UseWhen:
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
var allOptions = new [] {"option 1","option 2"};
foreach(var option in allOptions){
var currentOption = option;
builder.UseWhen(context => {
//suppose you can get the user's selected option from query string
var selectedOption = context.Request.Query["option_key"];
return selectedOption == currentOption;
}, app => {
//your MyMiddleware is supposed to accept one argument
app.UseMiddleware<MyMiddleware>(currentOption);
});
}
return builder;
}
}
To simplify it I suppose your options are just strings, you must know beforehand all possible options that the user can select via UI. Each one will be an exact match for the condition to plug-in a middleware and they must be all exclusive (so just one of them can enable one corresponding middleware), otherwise there will be duplicate middlewares, which may cause some issue.
By expressing the foreach above more clearly, it may represent something as follows:
//kind of pseudo code
if(selectedOption1){
app.UseMiddleware<MyMiddleware>("option 1");
} else if(selectedOption2){
app.UseMiddleware<MyMiddleware>("option 2");
}
...
You must decide how you get the selected option from the user (in the example above I get it from query string). You can get it from Cookie as well (to remember the user's selection) or from other sources such as route data, headers, form, request body. I think that's another issue, so if you have problem with that, please ask in another question.
First up all thanks to #Kingking and #GlennSills for there solution and valuable comments.
I have solved this problem as
Created one class which inherit from Hangfire.JobStorage as follows
public class HangfireSqlServerStorageExtension : Hangfire.JobStorage
{
private readonly HangfireSqlServerStorage _hangfireSqlServerStorage = new HangfireSqlServerStorage();
public HangfireSqlServerStorageExtension(string nameOrConnectionString)
{
_hangfireSqlServerStorage.SqlServerStorageOptions = new SqlServerStorageOptions();
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public HangfireSqlServerStorageExtension(string nameOrConnectionString, SqlServerStorageOptions options)
{
_hangfireSqlServerStorage.SqlServerStorageOptions = options;
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public void UpdateConnectionString(string nameOrConnectionString)
{
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public override IStorageConnection GetConnection()
{
return _hangfireSqlServerStorage.SqlServerStorage.GetConnection();
}
public override IMonitoringApi GetMonitoringApi()
{
return _hangfireSqlServerStorage.SqlServerStorage.GetMonitoringApi();
}
}
HangfireSqlServerStorage.cs
Used in HangfireSqlServerStorageExtension class above
public class HangfireSqlServerStorage
{
public SqlServerStorage SqlServerStorage { get; set; }
public SqlServerStorageOptions SqlServerStorageOptions { get; set; }
}
Startup.cs
In Startup file add singleton service for HangfireSqlServerStorageExtension instance and configure hangfire dashboard as follows
public class Startup
{
///Other necessary code here
public static HangfireSqlServerStorageExtension HangfireSqlServerStorageExtension { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
///Other necessary code here
HangfireSqlServerStorageExtension = new HangfireSqlServerStorageExtension("DBConnecttionString"));
services.AddSingleton<HangfireSqlServerStorageExtension>(HangfireSqlServerStorageExtension);
services.AddHangfire(configuration => configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_170));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
//Other necessary code here
app.UseHangfireDashboard("/Dashboard", new DashboardOptions(), HangfireSqlServerStorageExtension);
//Other necessary code here
}
}
Inside controller I have used it as follows
HangfireController.cs
public class HangfireController : Controller
{
protected readonly HangfireSqlServerStorageExtension
hangfireSqlServerStorageExtension;
public HangfireController(HangfireSqlServerStorageExtension hangfireSqlServerStorageExtension)
{
this.hangfireSqlServerStorageExtension = hangfireSqlServerStorageExtension;
}
public IActionResult DisplayHangfireDashboard()
{
// Update connString as follows
hangfireSqlServerStorageExtension.UpdateConnectionString(connString);
var hangfireDashboardUrl = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}" + "/Dashboard";
return Json(new { url = hangfireDashboardUrl });
}
}

Autofac: ITenantIdentificationStrategy with RouteValues

I'm having issues making multitenancy work. I've tried to follow the sample here and can't see what my implementation is doing differently.
The tenants are identified by a routing parameter in the address field. This seems to work without issues (calling TryIdentifyTenant returns the correct one). I am using ASP.NET Core 3.1, together with Autofac.AspNetCore-Multitenant v3.0.1 and Autofac.Extensions.DependencyInjection v6.0.0.
I have made a simplification of the code (which is tested and still doesn't work). Two tenants are configured, "terminal1" and "terminal2". The output should differ depending on the tenant. However, it always returns the base implementation. In the example below, inputing "https://localhost/app/terminal1" returns "base : terminal1" and "https://localhost/app/terminal2" returns "base : terminal2". It should return "userhandler1 : terminal1" and "userhandler2 : terminal2".
HomeController:
public class HomeController : Controller
{
private readonly IUserHandler userHandler;
private readonly TerminalResolverStrategy terminalResolverStrategy;
public HomeController(IUserHandler userHandler, TerminalResolverStrategy terminalResolverStrategy)
{
this.userHandler = userHandler;
this.terminalResolverStrategy = terminalResolverStrategy;
}
public string Index()
{
terminalResolverStrategy.TryIdentifyTenant(out object tenant);
return userHandler.ControllingVncUser + " : " + (string)tenant;
}
}
UserHandler:
public interface IUserHandler
{
public string ControllingVncUser { get; set; }
}
public class UserHandler : IUserHandler
{
public UserHandler()
{
ControllingVncUser = "base";
}
public string ControllingVncUser { get; set; }
}
public class UserHandler1 : IUserHandler
{
public UserHandler1()
{
ControllingVncUser = "userhandler1";
}
public string ControllingVncUser { get; set; }
}
public class UserHandler2 : IUserHandler
{
public UserHandler2()
{
ControllingVncUser = "userhandler2";
}
public string ControllingVncUser { get; set; }
}
Startup:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddControllersWithViews();
services.AddAutofacMultitenantRequestServices();
}
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterType<TerminalResolverStrategy>();
builder.RegisterType<UserHandler>().As<IUserHandler>();
}
public static MultitenantContainer ConfigureMultitenantContainer(IContainer container)
{
var strategy = new TerminalResolverStrategy(
container.Resolve<IOptions<TerminalAppSettings>>(),
container.Resolve<IHttpContextAccessor>());
var mtc = new MultitenantContainer(strategy, container);
mtc.ConfigureTenant("terminal1", b => b.RegisterType<UserHandler1>().As<IUserHandler>());
mtc.ConfigureTenant("terminal2", b => b.RegisterType<UserHandler2>().As<IUserHandler>());
return mtc;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
loggerFactory.AddLog4Net();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{terminal}/{controller=Home}/{action=Index}/{id?}");
});
}
}
Program:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacMultitenantServiceProviderFactory(Startup.ConfigureMultitenantContainer))
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
ITenantIdentificationStrategy:
public class TerminalResolverStrategy : ITenantIdentificationStrategy
{
public IHttpContextAccessor Accessor { get; private set; }
private readonly TerminalAppSettings settings;
public TerminalResolverStrategy(
IOptions<TerminalAppSettings> options,
IHttpContextAccessor httpContextAccessor
)
{
Accessor = httpContextAccessor;
settings = options.Value;
}
public bool TryIdentifyTenant(out object terminal)
{
HttpContext httpCtx = Accessor.HttpContext;//
terminal = null;
try
{
if (httpCtx != null &&
httpCtx.Request != null &&
httpCtx.Request.RouteValues != null &&
httpCtx.Request.RouteValues.ContainsKey("terminal"))
{
string requestedTerminal = httpCtx.Request.RouteValues["terminal"].ToString();
bool terminalExists = settings.Terminals.ContainsKey(requestedTerminal);
if (terminalExists)
{
terminal = requestedTerminal;
}
}
}
catch (Exception) {}
return terminal != null;
}
}
}
What am i doing wrong? Thanks in advance.
"Multitenancy doesn't seem to work at all" is a somewhat ambiguous statement that's hard to address. Unfortunately, I don't personally have the time to download all of your example code and try to repro the whole thing and debug into it and see exactly what's wrong. Perhaps someone else does. However, I can offer some tips as to places I'd look and things I'd try to see what's up.
Tenant ID strategy set up twice. I see in Startup.ConfigureContainer that there's a builder.RegisterType<TerminalResolverStrategy>(); line - that's going to register your strategy type as instance-per-dependency, so every time it's needed it'll be resolved fresh. I also see in Startup.ConfigureMultitenantContainer that you're manually instantiating the strategy that gets used by the multitenant container. There's a non-zero possibility that something is getting messed up there. I would pick one way to get that done - either register the strategy or manually create it - and I'd make sure that thing is a stateless singleton. (It's not registered in the example.)
Route pattern possibly questionable. I see the route pattern you have registered looks like this: {terminal}/{controller=Home}/{action=Index}/{id?}. I also see your URLs look like this: https://localhost/app/terminal1 Have you stepped into your tenant ID strategy to make sure the route parsing mechanism works right? That is, app isn't being picked up as the terminal value? Route parsing/handling can be tricky.
Possibly bad settings. The tenant ID strategy only successfully identifies a tenant if there are options that specify that the specific terminal value exists. I don't see where any of those options are configured, which means in this repo there are no tenants defined. Your strategy won't identify anything without that.
If it was me, I'd probably start with a breakpoint in that tenant ID strategy and see what's getting resolved and what's not. It seems somewhat complex from an outside perspective and that's where I'd begin. If that is working, then I'd probably also look at cleaning up the registrations so the ID strategy isn't registered twice. Finally, I get the impression that this isn't all the code in the app; I'd probably look at making a super minimal reproduction that's about the size you actually have posted here. I'd then focus on making that minimal repro work; then once it works, I'd figure out what the difference is between the repro and my larger app.
Unfortunately, that's about all I can offer you. As I mentioned, with the current environment and my current workload, I won't be able to actually sit down and reproduce the whole thing with your code. I know the integration works because I have production apps using it; and there are a lot of tests (unit and integration) to validate it works; so the part that isn't working is likely in your code somewhere... and those are the places I'd start.
So after having identified the tenant identification as the problem, it seems like RouteValues isn't resolved with the HttpContext until later in the request chain. Thus, no tenant gets resolved. It seems to me like a bug in .NET Core. The problem got bypassed by using the request path instead:
public class TerminalResolverStrategy : ITenantIdentificationStrategy
{
private readonly TerminalAppSettings settings;
private readonly IHttpContextAccessor httpContextAccessor;
public TerminalResolverStrategy(
IOptions<TerminalAppSettings> options,
IHttpContextAccessor httpContextAccessor
)
{
this.httpContextAccessor = httpContextAccessor;
settings = options.Value;
}
public bool TryIdentifyTenant(out object terminal)
{
var httpCtx = httpContextAccessor.HttpContext;
terminal = null;
try
{
if (httpCtx != null)
{
string thisPath = httpCtx.Request.Path.Value;
var allTerminals = settings.Terminals.GetEnumerator();
while (allTerminals.MoveNext())
{
if (thisPath.Contains(allTerminals.Current.Key)) {
terminal = allTerminals.Current.Key;
return true;
}
}
}
}
catch (Exception) { }
return false;
}
}

How to test an NServiceBus handler that uses Schedule

I'm playing with a simple handler that implements IWantToRunWhenBusStartsAndStops and in the start, it schedules a task like so:
public void Start()
{
_schedule.Every(TimeSpan.FromSeconds(5), Moo);
}
_schedule is injected via the constructor. The test I'm trying write is to make sure the task is scheduled when the handler starts. But I can't find a way to mock Schedule as it doesn't have a no-arg constructor and it doesn't implement an interface. I tried creating an actual instance of it with a mocked IBuilder but can't figure out what expectations to set on the IBuilder. Also, I looked at the source to see how they were testing Schedule but it looks like we're on an earlier version (v5.0.0 via nuget) because we don't have a DefaultScheduler which appears to be what they use in their current tests.
In fact NServiceBus team has already covered the scheduler with unit/acceptance test, i.e. there is no need to check whether the task was actually scheduled when your handler is executed. Instead you would probably want to unit test your handler itself, thus check if call to scheduler.Every() has been made. Here is simple example of how your unit test might look like:
[TestClass]
public class Tests
{
[TestMethod]
public void When_executing_handler_the_task_should_be_scheduled()
{
//arrange
var scheduler = new FakeSheduler();
//act
var handler = new TestHandler(scheduler);
handler.Start();
//assert
Assert.IsTrue(scheduler.WasCalled);
}
}
The handler itself:
class TestHandler: IWantToRunWhenBusStartsAndStops
{
readonly IMyScheduler _scheduler;
public TestHandler(IMyScheduler scheduler)
{
_scheduler = scheduler;
}
public void Start()
{
_scheduler.Every(TimeSpan.FromSeconds(5), () => { });
}
public void Stop() { }
}
Finally, you have to abstract from direct usage of NServiceBus scheduler in order to make it testable, here is the idea:
interface IMyScheduler
{
void Every(TimeSpan interval, Action action);
}
//your real implementation
class MySheduler: IMyScheduler
{
readonly Schedule _schedule;
public MySheduler(Schedule schedule)
{
_schedule = schedule;
}
public void Every(TimeSpan interval, Action action)
{
_schedule.Every(TimeSpan.FromSeconds(5), () => { });
}
}
//fake for the testing
class FakeSheduler: IMyScheduler
{
public bool WasCalled { get; set; }
public void Every(TimeSpan interval, Action action)
{
WasCalled = true;
}
}

how to pass context arguments to advice in spring aop

I am learning spring aop now,and I have no idea to pass context arguments to the advice.
Note I mean the context arguments,not the normal arguments.
It is simple to pass the normal arguments,for example:
a join point:
public void read(String something){
}
#Aspect
public class SessionAspect {
#Pointcut("execution(* *.*(String)) &&args(something)")
public void sess() {
}
#Before("sess()")
public void checkSessionExist(String something) {
//Here
}
}
Then the something argument will be passed to the the advice checkSessionExist.
But how about I want to get the context arguments like HttpSession or something else?
a join point:
public void listUser(){
dao.list(User.class,.....);
}
#Aspect
public class SessionAspect {
#Pointcut("execution(* *.*(String))")
public void sess() {
}
#Before("sess()")
public void checkSessionExist(String something) {
//Here
}
}
In this example,the listUser join point is only allowed for logined user.
So I want to check if there is a identify in the current HttpSession,so I need to get an instance of HttpSession at the advice checkSessionExist.
But how to get it?
The simplest way is to add the HttpSession argumets to all the joit points like this:
public void listUser(HttpSession session){
dao.list(User.class,.....);
}
However this have gone against the AOP it self. In my opinion,the join point even does not need to know the exist of the Aspect,isn't it?
How to fix it ?
Instead of passing HttpSession via #Pointcuts, you could fetch HttpSession reference in the #Aspect itself
RequestContextHolder.currentRequestAttributes()
.getAttribute("user", RequestAttributes.SCOPE_SESSION)
#Aspect
public class SessionAspect {
// fetch the current HttpSession attributes and use as required
private ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
#Pointcut("execution(* *.*(String))")
public void sess() {
}
#Before("sess()")
public void checkSessionExist(String something) {
//Here
}
}