How to Properly Inject SignalR IHubContext to Quartz.net Job? - signalr

I have a ASP.NET MVC Core project with a SignalR hub called MyHub and Quartz.net job called MyJob which could resolve all registered services but IHubContext. However, the IHubContext is successfully resolved inside all controllers.
Here is the error I get inside my JobFactory:
System.InvalidOperationException: 'Unable to resolve service for type
'Microsoft.AspNetCore.SignalR.IHubContext`1[MyProject.Hubs.MyHub]' while
attempting to activate 'MyProject.Jobs.MyJob'.'
Would someone let me know how to resolve the IHubContext inside the Quartz.net MyJob please?
Here is my JobFactory, MyHub, and MyJob:
public class JobFactory : IJobFactory
{
protected readonly IServiceProvider ServiceProvider;
public JobFactory(IServiceProvider serviceProvider)
=> ServiceProvider = serviceProvider;
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
=> ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob;
public void ReturnJob(IJob job)
=> (job as IDisposable)?.Dispose();
}
public class MyHub : Hub
{
private static readonly List<string> ConnectionIds = new();
public static bool Connected => ConnectionIds.Count > 0;
public override Task OnConnectedAsync()
{
ConnectionIds.Add(Context.ConnectionId);
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
ConnectionIds.Remove(Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
}
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
private readonly IHubContext<MyHub> _context;
public MyJob(ILogger<MyJob> logger, IHubContext<MyHub> context)
{
_logger = logger;
_context = context;
}
public Task Execute(IJobExecutionContext context)
{
return Task.CompletedTask;
}
}

Related

How to get an email provider into a logger using DI in ASP.NET Core?

Sorry this is a bit new to me so I don't quite 'get it'.
I already have a logging provider
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(loggingBuilder =>
{
var loggingSection = Configuration.GetSection("Logging");
loggingBuilder.AddFile(loggingSection);
loggingBuilder.AddConsole();
loggingBuilder.AddDebug();
I am using the package NReco.Logging.File to define AddFile etc.
I want to make it so that exceptions are emailed to me too. So I followed https://learn.microsoft.com/en-us/dotnet/core/extensions/custom-logging-provider to create a custom logger.
public sealed class EmailLoggerConfiguration
{
public int EventId { get; set; }
public string EmailToSendTo { get; set; }
public IEmailSender EmailSender { get; set; }
}
internal class EmailLoggingProvider : ILoggerProvider
{
private readonly IDisposable? _onChangeToken;
private EmailLoggerConfiguration _currentConfig;
private readonly ConcurrentDictionary<string, EmailLogger> _loggers =
new(StringComparer.OrdinalIgnoreCase);
private readonly IEmailSender emailSender;
public EmailLoggingProvider(
IOptionsMonitor<EmailLoggerConfiguration> config)
{
_currentConfig = config.CurrentValue;
_onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig);
}
public ILogger CreateLogger(string categoryName) =>
_loggers.GetOrAdd(categoryName, name => new EmailLogger(name, GetCurrentConfig ));
private EmailLoggerConfiguration GetCurrentConfig() => _currentConfig;
public void Dispose()
{
_loggers.Clear();
_onChangeToken?.Dispose();
}
}
internal class EmailLogger : ILogger
{
private readonly string categoryName;
private Func<EmailLoggerConfiguration> getCurrentConfig;
IEmailSender emailSender;
public EmailLogger(string categoryName, Func<EmailLoggerConfiguration> getCurrentConfig)
{
this.getCurrentConfig = getCurrentConfig;
this.categoryName = categoryName;
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!;
public bool IsEnabled(LogLevel logLevel) => !String.IsNullOrEmpty(getCurrentConfig().EmailToSendTo);
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
var emailTo = getCurrentConfig().EmailToSendTo;
//var emailServer = getCurrentConfig().EmailSender;
if (!String.IsNullOrEmpty(emailTo) && exception != null)
{
emailSender.SendEmailAsync(emailTo, "Admin exception", exception.ToString());
}
}
}
public static class EmailLoggingExtensions
{
public static ILoggingBuilder AddEmailLogger(
this ILoggingBuilder builder)
{
builder.AddConfiguration();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, EmailLoggingProvider>());
LoggerProviderOptions.RegisterProviderOptions<EmailLoggerConfiguration, EmailLoggingProvider>(builder.Services);
return builder;
}
public static ILoggingBuilder AddEmailLogger(
this ILoggingBuilder builder,
Action<EmailLoggerConfiguration> configure)
{
builder.AddEmailLogger();
builder.Services.Configure(configure);
return builder;
}
}
You can see that EmailLogger.Log requires emailSender which should be an IEmailSender but I cannot figure out how to get it there using DI.
I realise that you can chain dependencies in DI but ???? I don't see how in this context.
I tried this
loggingBuilder.AddEmailLogger(c =>
{
c.EmailToSendTo = Configuration["Logging:Email:EmailToSendTo"];
c.EmailSender = new AuthMessageSender(????, Configuration);
});
but that didn't help and wouldn't even be right anyway.
In fact, by default, EmailSender is the implementation method of IEmailSender, which is used to call the SendEmailAsync() method. You don't need to go and set c.EmailSender = xxx.
You can consider the following dependency injection approach:
public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string message);
}
public class EmailSender : IEmailSender
{
//...
private readonly ILogger<EmailSender> logger;
public EmailSender(ILogger<EmailSender> logger) {
//...
this.logger = logger;
}
public Task SendEmailAsync(string email, string subject, string message) {
//...
}
}
At this point, IEmailSender will exist as a custom interface instead of inheriting from Microsoft.AspNetCore.Identity.UI.Services.
And you need to register it as a service:
services.AddTransient<IEmailSender, EmailSender>();
Helpful links:
Add ILogger to send email service
Should I use IEmailSender?
Using IEmailSender from Configure() in my Startup.cs file
Hope this will help you better understand IEmailSender and dependency injection.

How to inject my AddDbContext<ContainerContext> into my DAL project (Core 3.1)

I'm using MVC 5, Core 3.1
I have 'AddDbContext' added to my service in Startup.cs.
I then have a Class library core 3.1 project which is my ADO Dal layer.
This is added as a service as well in The ConfigureServices of Startup.cs.
I want to inject the Connection String into the DAL application.
I have:
public partial class ContainerContext : DbContext
{
public ContainerContext()
{
}
public ContainerContext(DbContextOptions<ContainerContext> options) : base(options)
{
}
}
In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
var connection = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<ContainerContext>(options => options.UseSqlServer(connection));
services.AddDAL();
}
In the Dal project:
public static class ServiceCollectionExtensions
{
// Add parameters if required, e.g. for configuration
public static IServiceCollection AddDAL(this IServiceCollection services)
{
// Register all services as required
services.AddScoped<ILeaseBll, LeaseDal>();
return services;
}
}
The Dal class.
public class LeaseDal : ILeaseBll
{
private string conString;
public LeaseDal(???????)
{
// Some validation for the Context maybe (isNull etc?) throw new ArgumentNullException("conString");
//this.connectionString = conString;
}
How would / should it be done?
Thanks
There is a philosophy change with Dot-Net-Core and Dot-Net-Framework....
public class LeaseDal : ILeaseBll
{
private string conString;
This is not best practice in dot-net-CORE.
You do NOT inject your "connection string" in your concrete DataAccessLayer object.
You inject the db-context.
(and the db-context already has been wired to the Ioc...with its correct connection string)
Something like this:
public interface IDepartmentQueryDomainData()
{
Task<int> GetCountAsync(CancellationToken token);
}
..
public class DepartmentQueryEntityFrameworkDomainDataLayer : IDepartmentQueryDomainData
{
public const string ErrorMessageILoggerFactoryIsNull = "ILoggerFactory is null";
public const string ErrorMessageMyCoolDbContextIsNull =
"MyCoolDbContext is null";
private readonly ILogger<DepartmentQueryEntityFrameworkDomainDataLayer> logger;
private readonly MyCoolDbContext entityDbContext;
public DepartmentQueryEntityFrameworkDomainDataLayer(
ILoggerFactory loggerFactory,
MyCoolDbContext context
{
if (null == loggerFactory)
{
throw new ArgumentNullException(ErrorMessageILoggerFactoryIsNull, (Exception)null);
}
this.logger = loggerFactory.CreateLogger<DepartmentQueryEntityFrameworkDomainDataLayer>();
this.entityDbContext = context ?? throw new ArgumentNullException(
ErrorMessageMyCoolDbContextIsNull,
(Exception)null);
}
public async Task<int> GetCountAsync(CancellationToken token)
{
int returnValue = await this.entityDbContext.Departments.AsNoTracking().CountAsync(token);
this.logger.Log(
new LogEntry(
LoggingEventTypeEnum.Trace,
string.Format(
LogMessages.Count,
returnValue)));
return returnValue;
}
}
You can also "see" this here:
https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/
public class MyController
{
private readonly ApplicationDbContext _context;
public MyController(ApplicationDbContext context)
{
_context = context;
}
}
I would never inject the dbContext into a "controller"...(I agree with you that the Dal should be a separate layer)...
but besides that "miscue" on the microsoft example, you do see that you inject the dbContext.
Also see:
https://hovermind.com/aspnet-core/using-dbcontext-with-dependency-injection.html

Writing tests with xunit and dependency inject in .net core 5

I have a simple controller with one post method. The constructor has some dependencies which are injected from startup.cs. Now i want to write some tests but getting error as System.InvalidOperationException : Can't load type Registration-Tests.Startup in 'Registration-Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', not sure what is causing this. Here is my implementation.
public class MyControllerBase
{
#region Properties
protected readonly IBusiness _buss;
protected readonly IQueueManager _queman;
protected readonly ILogger _logger;
#endregion
#region Constructor
public MyControllerBase(IBusiness buss, IQueueManager queman, ILogger logger)
{
this._buss = buss;
this._queman = queman;
this._logger = logger;
}
This is my Controller.
[ApiController]
[Route("[controller]")]
public class RegistrationController : MyControllerBase
{
#region Properties
protected readonly IRegistrationBusiness _regbuss;
#endregion
#region Constructor
public RegistrationController(IRegistrationBusiness regbuss, IBusiness buss, IQueueManager queman, ILogger<RegistrationController> logger)
: base(buss, queman, logger)
{
_regbuss = regbuss;
}
[HttpPost]
[Route("/api/register")]
public async Task<RegistrationResponse> RegisterUserV2(RegistrationModel data)
{
// do the working and send response
}
#endregion
}
These are the classes in my xunit project
public class ContainerFixture
{
public ContainerFixture()
{
var serviceCollection = new ServiceCollection();
serviceCollection
.AddHttpContextAccessor()
.AddHttpClient();
ServiceProvider = serviceCollection.BuildServiceProvider();
}
public ServiceProvider ServiceProvider { get; private set; }
}
/
public class RegistrationControllerUnitTests : IClassFixture<ContainerFixture>
{
RegistrationController registrationController;
private ServiceProvider _serviceProvider;
public RegistrationControllerUnitTests(ContainerFixture fixture)
{
System.Diagnostics.Debugger.Launch();
_serviceProvider = fixture.ServiceProvider;
var regBuss = _serviceProvider.GetService<IRegistrationBusiness>();
var buss = _serviceProvider.GetService<IBusiness>();
var queman = _serviceProvider.GetService<IQueueManager>();
var logger = _serviceProvider.GetService<ILogger<RegistrationController>>();
registrationController = new RegistrationController(regBuss, buss, queman, logger);
}
[Fact]
public async Task RegisterUserV2()
{
//Arrange
var farModel = new RegistrationModel
// Act
var tResponse = await registrationController.RegisterUserV2(farModel) as RegistrationResponse;
Assert.True(tResponse.Result);
}
}

How to do UnitOfWork + Repository patterns with entity framework core and unit testing

I'm running into an issue while unit testing where if I run multiple tests at once, the DbContext will lose track of records I've added during unit tests and I think this may have to do with how services are registered in my ServiceCollection.
I have the following setup:
IUnitOfWork:
public interface IUnitOfWork : IDisposable
{
IUserRepository Users { get; }
int Complete();
}
UnitOfWork
public class UnitOfWork : IUnitOfWork
{
private readonly MyDbContext _context;
public IUserRepository Users { get; }
public UnitOfWork(MyDbContext context,
IUserRepository userRepository)
{
_context = context;
Users = userRepository;
}
public void Dispose() => _context.Dispose();
public int Complete() => _context.SaveChanges();
}
UserRepository
public class UserRepository : Repository<User>, IUserRepository
{
public UserRepository(MyDbContext context) : base(context) { }
public MyDbContext MyDbContext => Context as MyDbContext;
public Task<User?> GetUserDetailsAsync(int userID)
{
var user = MyDbContext.Users.Where(user => user.Id == userID)
.Include(user => user.Emails)
.Include(user => user.PhoneNumbers).FirstOrDefault();
return Task.FromResult(user);
}
}
Here is my base test:
public abstract class BaseTest : IDisposable
{
protected ServiceProvider ServiceProvider { get; }
private MyDbContext MyDbContext { get; }
protected IUnitOfWork UnitOfWork { get; }
public BaseTest()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<IUserService, UserService>()
.AddScoped<IUnitOfWork, UnitOfWork>()
.AddScoped(typeof(IRepository<>), typeof(Repository<>))
.AddScoped<IOrganizationRepository, OrganizationRepository>()
.AddScoped<IExercisePostRepository, ExercisePostRepository>()
.AddScoped<IUserRepository, UserRepository>()
.AddTransient<IRestClient, RestClient>()
.AddAutoMapper(typeof(Startup).Assembly)
.AddDbContext<MyDbContext>(options =>
options.UseInMemoryDatabase("Core")
.EnableSensitiveDataLogging());
ServiceProvider = serviceCollection.BuildServiceProvider();
SpotcheckrCoreContext = ServiceProvider.GetRequiredService<MyDbContext>();
MyDbContext.Database.EnsureCreated();
UnitOfWork = ServiceProvider.GetRequiredService<IUnitOfWork>();
}
public void Dispose()
{
MyDbContext.Database.EnsureDeleted();
UnitOfWork.Dispose();
}
}
Sample test:
public class UserServiceTests : BaseTest
{
private readonly IUnitOfWork UnitOfWork;
private readonly IUserService Service;
public UserServiceTests()
{
UnitOfWork = ServiceProvider.GetRequiredService<IUnitOfWork>();
Service = ServiceProvider.GetRequiredService<IUserService>();
}
[Fact]
public async void GetUserAsync_WithValidUser_ReturnsUser()
{
var user = new User
{
FirstName = "John",
LastName = "Doe"
};
UnitOfWork.Users.Add(user);
UnitOfWork.Complete();
var result = await Service.GetUserAsync(user.Id);
Assert.Equal(user.Id, result.Id);
}
}
If I run this test by itself, then it will correctly pass and I can see the user in the repository. However if I run it with other tests and debug, then that user is lost once I inspect UnitOfWork.Users in the repository but I do see it in the UnitOfWork.Users in the test.
What is the correct approach here?
Edit 1:
Tried some other changes but no luck yet. Adjusted UnitOfWork to take in the interfaces of each repository and registering them in BaseTest as scoped services. Also tried marking BaseTest as implementing IDisposable and then executing:
public void Dispose()
{
MyDbContext.Database.EnsureDeleted();
UnitOfWork.Dispose();
}
In the service layer I'll see the Users just fine but as soon as I step into the repository layer I'll lose the Users :/ I have a suspicion it is related to dependency injection AddScoped vs AddTransient and how all of that works with running multiple unit tests.
Edit 2:
Tried some more things...Used IClassFixture<BaseTest> on each test class and then ensured that each test class implemented IDisposable and in there I would ensure the Context database was deleted; also ensured in the test class constructor that it was created. With this I ended up with the following error:
The instance of entity type cannot be tracked because another instance with the same key value for {'Id'} is already being tracked
And so I added .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) but the problem still persisted.
This is very annoying to setup.
This is what has resolved it for me for now.
Summary: Created a new ServiceFixture. This ServiceFixture is applied to a BaseTest class as IClassFixture<ServiceFixture>. The ServiceFixture is responsible for initializing the service collection and allowing for it to be reused across different test classes. The purpose of the BaseTest is to allow for disposal of the database and other clean up that is necessary after each test. The Dispose method of this class will detach entity state and also delete the database.
ServiceFixture.cs
public class ServiceFixture
{
public ServiceProvider ServiceProvider { get; }
public ServiceFixture()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<IUserService, UserService>()
.AddScoped<ICertificationService, CertificationService>()
.AddScoped<IOrganizationService, OrganizationService>()
.AddScoped<ICertificateService, CertificateService>()
.AddScoped<IUnitOfWork, UnitOfWork>()
.AddScoped<IUserRepository, UserRepository>()
.AddScoped<IExercisePostRepository, ExercisePostRepository>()
.AddScoped<IEmailRepository, EmailRepository>()
.AddScoped<IPhoneNumberRepository, PhoneNumberRepository>()
.AddScoped<ICertificationRepository, CertificationRepository>()
.AddScoped<ICertificateRepository, CertificateRepository>()
.AddScoped<IOrganizationRepository, OrganizationRepository>()
.AddTransient<IRestClient, RestClient>()
.AddSingleton<NASMCertificationValidator>()
.AddAutoMapper(typeof(Startup).Assembly)
.AddDbContext<SpotcheckrCoreContext>(options =>
options.UseInMemoryDatabase("Spotcheckr-Core")
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
.EnableSensitiveDataLogging());
ServiceProvider = serviceCollection.BuildServiceProvider();
}
}
BaseTest.cs
public abstract class BaseTest : IClassFixture<ServiceFixture>, IDisposable
{
protected readonly ServiceProvider ServiceProvider;
protected readonly IUnitOfWork UnitOfWork;
private readonly SpotcheckrCoreContext Context;
public BaseTest(ServiceFixture serviceFixture)
{
ServiceProvider = serviceFixture.ServiceProvider;
Context = serviceFixture.ServiceProvider.GetRequiredService<SpotcheckrCoreContext>();
UnitOfWork = serviceFixture.ServiceProvider.GetRequiredService<IUnitOfWork>();
Context.Database.EnsureCreated();
}
public void Dispose()
{
Context.ChangeTracker.Entries().ToList().ForEach(entry => entry.State = EntityState.Detached);
Context.Database.EnsureDeleted();
}
}
UserServiceTests.cs
public class UserServiceTests : BaseTest
{
private readonly IUserService Service;
public UserServiceTests(ServiceFixture serviceFixture) : base(serviceFixture)
{
Service = serviceFixture.ServiceProvider.GetRequiredService<IUserService>();
}
[Fact]
public async void GetUserAsync_WithInvalidUser_ThrowsException()
{
Assert.ThrowsAsync<InvalidOperationException>(() => Service.GetUserAsync(-1));
}
[Fact]
public void CreateUser_UserTypeAthlete_CreatesAthleteUser()
{
var result = Service.CreateUser(Models.UserType.Athlete);
Assert.IsType<Athlete>(result);
}
[Fact]
public void CreateUser_UserTypePersonalTrainer_CreatesPersonalTrainerUser()
{
var result = Service.CreateUser(Models.UserType.PersonalTrainer);
Assert.IsType<PersonalTrainer>(result);
}
}

Will the dbcontext dispose and close if using adddbcontext this way?

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TestContext>(options => options.UseSqlServer("dbconnection"));
}
TestController.cs
private readonly TestContext _db;
public TestController(TestContext db)
{
_db = db;
}
public void Get()
{
DALUser dal = new DALUser(_db);
var list = dal.Get();
}
DALUser.cs
private readonly TestContext _db;
public DALUser(TestContext db)
{
_db = db;
}
public IQueryable<User> Get()
{
_db.Users.AsQueryable();
}
Will the dbcontext dispose and close after executing Get() function?
Default lifetime for DatabaseContext is scoped . So in your web application , DatabaseContext disposed after your http request finished ( TestController-Get ) .
public static IServiceCollection AddDbContext<TContext>(
[NotNull] this IServiceCollection serviceCollection,
[CanBeNull] Action<DbContextOptionsBuilder> optionsAction = null,
ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
where TContext : DbContext

Resources