Asp.net Core Quartz job calling controller method, but context - asp.net-core

I have an aspnetcore server (serving a Blazor wasm app), which has a Quartz scheduled job running. When the job triggers, it is calling a method on one of my server's controllers.
If I call this method on my controller normally (e.g. via a web API call), it works fine.
When I call the method from the Quartz IJob, the DbContext used in the controller seems to be disposed.
I've tried injecting the controller into the job in the normal way, and also via IServiceProvider, and both have the same result.
Controller:
public class NotificationController : ControllerBase
{
private readonly ApplicationDbContext context;
public NotificationService(ApplicationDbContext context)
{
this.context = context;
}
public async Task MyMethod()
{
await context.SaveChangesAsync(); //This is where it fails when Quartz calls it, seems context is not populated
}
}
My job (IServiceProvider attempt):
public class ReminderJob : IJob
{
private readonly IServiceProvider serviceProvider;
public ReminderJob(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public async Task Execute(IJobExecutionContext jobcontext)
{
using (var scope = serviceProvider.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<NotificationController>().MyMethod();
}
}
}
My job (DI attempt):
public class ReminderJob : IJob
{
private readonly NotificationController notificationController;
public ReminderJob(NotificationController notificationController)
{
this.notificationController = notificationController;
}
public async Task Execute(IJobExecutionContext jobcontext)
{
await notificationController.MyMethod();
}
}
My Startup.cs (relevant lines in ConfigureServices):
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
services.AddControllersWithViews();
services.AddMvcCore().AddControllersAsServices();
services.AddRazorPages();
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionScopedJobFactory(); //I also tried passing options.CreateScope to this method, but no difference
q.AddJobAndTrigger<ReminderJob>(configuration);
});
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
}
No exception is thrown in VS when it attempts to do context.SaveChangesAsync(), however, a breakpoint directly after it is not hit, however when I check the details of context while debugging, it doesn't seem to be populated correctly.
How do I use the Controller from within the IJob, and ensure the Controller's dependencies are not disposed of?

How do I use the Controller from within the IJob
Do not use the controller in the Job.
Move/Extract/Refactor the desired functionality into a service abstraction
//Service abstraction
public interface INotificationService {
Task MyMethod();
}
public class NotificationService : INotificationService {
private readonly ApplicationDbContext context;
public NotificationService(ApplicationDbContext context) {
this.context = context;
}
public async Task MyMethod() {
await context.SaveChangesAsync();
}
}
and have the job and controller depend on the service to invoke the desired functionality.
public class NotificationController : ControllerBase {
private readonly INotificationService service;
public NotificationController (INotificationService service ) {
this.service = service ;
}
public async Task<IActionResult> MyMethod() {
await service.MyMethod();
return Ok();
}
}
public class ReminderJob : IJob {
private readonly INotificationService service;
public ReminderJob(INotificationService service) {
this.service = service;
}
public Task Execute(IJobExecutionContext jobcontext) {
return service.MyMethod();
}
}
And of course register all the necessary services with the DI container.
//...
services.AddScoped<INotificationService, NotificationService>();
//...

Related

Unable to cast object of type 'MyAPI.Api.Middleware.MyFilter' to type 'Microsoft.AspNetCore.Mvc.Filters.IFilterMetadata'

My requirement is to modify the response from the controller before sending it back to the client.
I am trying to implement IActionResult interface.
public class MyFilter: IActionResult
{
ApiResponse _response = new ApiResponse();
public async Task ExecuteResultAsync(ActionContext context)
{
var httpResponse = context.HttpContext.Response;
if (httpResponse != null)
{
if (httpResponse.StatusCode == 200)
{
if (context.Result.GetType().Equals(typeof(Microsoft.AspNetCore.Mvc.ObjectResult)))
{
_response.Response = ((Microsoft.AspNetCore.Mvc.ObjectResult)context.Result).Value;
_response.Errors = null;
_response.IsSuccess = true;
}
}
}
return ;
}
}
my program.cs has
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSingleton<MyFilter>();
builder.Services.AddAppServices();
builder.Services.AddControllers();
builder.Services.AddDataContext(builder.Configuration);
builder.Services.AddRepositories();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseRouting();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
my controller has the attribute
[Route("api/[controller]")]
[ApiController]
[ServiceFilter(typeof(MyFilter))]
public class MyController : ControllerBase
{
}
But when I invoke any of the controller action methods it doesn't reach and gives me an exception as response.
is there something I am missing completely?
You should implement IActionFilter or IAsynActionFilter not IActionResult!
To create an Acton filter, we need to create a class that inherits either from the IActionFilter interface or IAsyncActionFilter interface or from the ActionFilterAttribute class which is the implementation of the IActionFilter, IAsyncActionFilter, and a few different interfaces as well:
public abstract class ActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata,
IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter
for example:
namespace ActionFilters.Filters
{
public class ActionFilterExample : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// our code before action executes
}
public void OnActionExecuted(ActionExecutedContext context)
{
// our code after action executes
}
}
}
then on your startup.cs (or program.cs in .net 6 and above):
builder.Services.AddScoped<ActionFilterExample>();
Finally, to use a filter registered on the Action or Controller level, we need to place it on top of the Controller or Action as a ServiceType:
namespace AspNetCore.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
[ServiceFilter(typeof(ActionFilterExample))]
public IEnumerable<string> Get()
{
return new string[] { "example", "data" };
}
}
}
read more: codeMaze microsoft docs

My ApplicationDbContext is null for some reason

I get the error System.NullReferenceException. Object reference not set to an instance of an object. At Club.BLL.MaintenanceBLL.Maintenance.DoMaintenance()
But the context code is the same as on scaffolded pages, so not sure what I need to do. Any help is appreciated.
The code is:
namespace Club.BLL.MaintenanceBLL
{
public class Maintenance
{
private readonly Club.Data.ApplicationDbContext Context;
public Maintenance(ApplicationDbContext context)
IN DEBUG MODE context IS INDICATED
AS OBJECT REFERENCE NOT SET TO AN INSTANCE OF AN OBJECT.
HOW DO I FIX THIS.
{
Context = context;
}
public Maintenance()
{
}
public void DoMaintenance()
{
//Parse Maintenance table and action those items
//where Active=True and ActionDate has passed
//==================================
//Retrieve list of rows in Maintenance table
var maintenances = Context.Maintenance; PROGRAM FAILS ON THIS LINE.
I imagine I am missing something fundamental as I am 'learning by doing'. The lines that call up the DoMaintenance routine are located in root/Pages/Index, which is a scaffolded page. The DoMaintenance routine is called from the following lines in the root/Pages/index.cshtml page:
public void OnGet()
{
Maintenance maintenance = new Maintenance();//Create new instance
maintenance.DoMaintenance();
}
AND startup.cs includes the lines
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddRoles()
.AddEntityFrameworkStores();
ANY HELP WOULD BE APPRECIATED.
Thanks and cheers....Alan
According to your description and codes, I found you have register the dbcontext as a service in your application that means if you want to use it, you should inject the dbcontext into your Maintenance class.
Normally, we will create a service as BLL service in asp.net core.
Then we could register the service in the startup.cs and inject it in the razor page.
I suggest you could try to modify the Maintenance class as below codes shows:
You could create a interface called IMaintenance:
public interface IMaintenance
{
public void DoMaintenance();
}
Then you could let Maintenance inherit IMaintenance as below:
public class Maintenance : IMaintenance
{
private readonly TestDbcontext Context;
public Maintenance(TestDbcontext testDbcontext ) {
Context = testDbcontext;
}
public void DoMaintenance() {
var maintenances = Context.Employees.ToList() ;
}
}
At last, you could register the Maintenance as service in startup.cs ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<TestDbcontext>(ServiceLifetime.Transient);
services.AddTransient<IMaintenance, Maintenance>();
}
You could directly use the Maintenance in razor pages like below, the asp.net core dependency injection will inject the dbcontext automatically into Maintenance.
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private readonly IStringLocalizer<IndexModel> _localizer;
private readonly IMaintenance _maintance;
public CustomerModel Customer { get; set; }
public IndexModel(ILogger<IndexModel> logger, IStringLocalizer<IndexModel> localizer, IMaintenance maintance)
{
_logger = logger;
_localizer = localizer;
_maintance = maintance;
}
public void OnGet()
{
_maintance.DoMaintenance()
}
}
namespace Club.Pages
{
[AllowAnonymous]
public class IndexModel : PageModel
{
private readonly IMaintenance _maintenance;
public IndexModel (IMaintenance<IndexModel> maintenance)
{
_maintenance = maintenance;
}
public void OnGet()
{
_maintenance.DoMaintenance();
}
}
}
You might have missed to inject your context and since there's default ctor, everything works fine without setting Context property.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<Context>(() => ...));
}

EF Core how to get a DBContext in a class instance?

The ASP NET Core project uses the Entity Framework Core.
From the method of the Home controller I call Task.Run for the method of the instance of the class MyClass, where I need to access the data from the database.
I tried to pass _dbContext to the constructor of the MyClass class, but I get the exception
Cannot access a disposed object.
HomeController
[Route("api/[controller]")]
[EnableCors("AllowAllOrigin")]
[ApiController]
public class HomeController : ControllerBase
{
private readonly DataBaseContext _dbContext;
public HomeController (DataBaseContext dbContext)
{
_dbContext = dbContext;
}
[EnableCors("AllowAllOrigin")]
[HttpPost("[action]")]
public string UpdateBotSettings()
{
MyClass myClass = new MyClass(_dbContext);
Task task = Task.Run(() => myClass.AnyMethod());
}
}
MyClass
public class MyClass
{
private readonly DataBaseContext _dbContext;
public MyClass(DataBaseContext dbContext)
{
_dbContext = dbContext;
}
public async void AnyMethodAsync()
{
MyData myData = _dbContext.AnyData.Where(d => d.id == 1).FirstOrDefault(); // <- exception
}
}
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
string connection = Configuration.GetConnectionString("ConnectionDB");
services.AddDbContext<DataBaseContext>(options => options.UseSqlServer(connection));
// another code
}
Can you please tell me how to access dbContext inside an instance of MyClass?
Most important mistake in your code is that you are using async void in your AnyMethodAsync() method of the Myclass. Don't use async void in your code, instead use async Task. Here is the details: C# – beware of async void in your code
So write your AnyMethodAsync() as follows:
public async Task AnyMethodAsync()
{
MyData myData = await _dbContext.AnyData.Where(d => d.id == 1).FirstOrDefault();
}
Now you can register your MyClass to the ASP.NET Core DI container as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<MyClass>(); // <-- here it is
string connection = Configuration.GetConnectionString("ConnectionDB");
services.AddDbContext<DataBaseContext>(options => options.UseSqlServer(connection));
// another code
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Now use it as follows:
[Route("api/[controller]")]
[EnableCors("AllowAllOrigin")]
[ApiController]
public class HomeController : ControllerBase
{
private readonly DataBaseContext _dbContext;
private readonly MyClass _myClass
public HomeController (DataBaseContext dbContext, MyClass myClass)
{
_dbContext = dbContext;
_myClass = myClass;
}
[EnableCors("AllowAllOrigin")]
[HttpPost("[action]")]
public async Task<string> UpdateBotSettings()
{
await _myClass.AnyMethod();
// rest of codes
}
}

Convert HttpContext.User before API Method

I currently have a .Net Core API application with a bunch of API get methods. Currently in every single method I am needing to write this line:
[ProducesResponseType(200, Type = typeof(MetadataAttributeModel))]
[ProducesResponseType(400, Type = typeof(ValidationResultModel))]
[ProducesResponseType(500, Type = typeof(ErrorResultModel))]
public ActionResult<MetadataAttributeModel> GetAsync(string name)
{
List<Entities.DocumentAttributeView> attributes = documentAttributeViewRepo.GetByAttributeName(name);
SiteUser currentUser = new SiteUser(db, User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress").Value);
return Unauthorized();
}
Is there a way I can convert the HttpContext.User object to our own SiteUser object before I get to the method? I don't want to have to write this line in ALL of the API methods:
SiteUser currentUser = new SiteUser(db, HttpContext.User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress").Value);
TIA,
Alex
The AspNet Mvc mechanism for "Do something for every Action" is Filters.
Filters can run before the method is called, and they can, for instance, set the Http.Context.User.
A filter can be applied to an action, a controller, or (by writing code in Startup) globally.
[SwapUserToAuthorizedDatabaseUser]
public class MyController
{
public IActionResult About() => Ok(User);
}
Which will invoke this filter for every Action on the Controller :
public class SwapUserToAuthorizedDatabaseUserAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
SiteUser currentUser = new SiteUser(db, User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress").Value);
if (currentUser == null)
{
context.Result= new RedirectToRouteResult("/Identity/Logout");
}
else
{
var claimsIdentity =
new ClaimsIdentity(
new Claim[]
{
new Claim("Id", currentUser.Id),
new Claim("UserName", currentUser.UserName),
new Claim("WhateverElseYourSiteUserHas", currentUser.Something.ToString()),
}
);
context.HttpContext.User = new ClaimsPrincipal(new[]{claimsIdentity});
}
}
public void OnActionExecuted(ActionExecutedContext context){}
}
If overwriting the HttpContext.User isn't what you need, then it's much less code to use HttpContext.Items :
public class SwapUserToAuthorizedDatabaseUserAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Items["SiteUser"]= new SiteUser(db, User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress").Value);
}
public void OnActionExecuted(ActionExecutedContext context){}
}
Instead of an IActionFilter to run on every Action, you can use an IAuthorizationFilter which has a public void OnAuthorization(AuthorizationFilterContext context) method. This would save repeatedly calling the database, but does mean you must cache your currentUser somewhere, presumably in Session.
The problem is, how do you get access to your database? If you go the route of adding a Global filter by adding it in Startup:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc(o=>o.Filters.Add(new SwapUserToAuthorizedDatabaseUserAttribute(provide a db instance here)));
}
Then you can give your Filter a constructor and pass in a database. There's also an overload for using the DependencyInjection system.
If you don't use the startup method, you have to do some DIY injection, for instance by having a static method to return a DbContext.
You can move this logic to a service:
public class UserService : IUserService
{
private readonly HttpContext context;
private readonly Db db;
public UserService(IHttpContextAccessor context, Db db)
{
this.context = context.HttpContext;
this.db = db;
}
public SiteUser GetUser()
{
return new SiteUser(db, context.User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress").Value);
}
}
Inject it to controllers where it's required:
public MyController(IUserService userService) { ... }
Register it as a Scoped service in ConfigureServices in Startup.cs along with IHttpContextAccessor (should be singleton):
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<UserService>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

How can I inject multiple repositories in a NServicebus message handler?

I use the following:
public interface IRepository<T>
{
void Add(T entity);
}
public class Repository<T>
{
private readonly ISession session;
public Repository(ISession session)
{
this.session = session;
}
public void Add(T entity)
{
session.Save(entity);
}
}
public class SomeHandler : IHandleMessages<SomeMessage>
{
private readonly IRepository<EntityA> aRepository;
private readonly IRepository<EntityB> bRepository;
public SomeHandler(IRepository<EntityA> aRepository, IRepository<EntityB> bRepository)
{
this.aRepository = aRepository;
this.bRepository = bRepository;
}
public void Handle(SomeMessage message)
{
aRepository.Add(new A(message.Property);
bRepository.Add(new B(message.Property);
}
}
public class MessageEndPoint : IConfigureThisEndpoint, AsA_Server, IWantCustomInitialization
{
public void Init()
{
ObjectFactory.Configure(config =>
{
config.For<ISession>()
.CacheBy(InstanceScope.ThreadLocal)
.TheDefault.Is.ConstructedBy(ctx => ctx.GetInstance<ISessionFactory>().OpenSession());
config.ForRequestedType(typeof(IRepository<>))
.TheDefaultIsConcreteType(typeof(Repository<>));
}
}
My problem with the threadlocal storage is, is that the same session is used during the whole application thread. I discovered this when I saw the first level cache wasn't cleared. What I want is using a new session instance, before each call to IHandleMessages<>.Handle.
How can I do this with structuremap? Do I have to create a message module?
You're right in that the same session is used for all requests to the same thread. This is because NSB doesn't create new threads for each request. The workaround is to add a custom cache mode and have it cleared when message handling is complete.
1.Extend the thread storage lifecycle and hook it up a a message module
public class NServiceBusThreadLocalStorageLifestyle : ThreadLocalStorageLifecycle, IMessageModule
{
public void HandleBeginMessage(){}
public void HandleEndMessage()
{
EjectAll();
}
public void HandleError(){}
}
2.Configure your structuremap as follows:
For<<ISession>>
.LifecycleIs(new NServiceBusThreadLocalStorageLifestyle())
...
Hope this helps!