Goal:
Fundamentally I am trying to add a background job, that has dependencies injected, to a console application.
Problem:
Although the jobs are queued, they are never executed.
Program.cs
var appSettings = ConfigHelper.GetConfig();
Console.WriteLine("Initialising Hangfire Server...");
GlobalConfiguration.Configuration.UseSqlServerStorage(appSettings.ConnectionString);
using (var server = new BackgroundJobServer())
{
Console.WriteLine("Hangfire Server started.");
var t = serviceProvider.GetService<ITestService>();
t.Test();
Console.ReadKey();
}
ServiceProviderFactory.cs
public static void Setup()
{
IServiceCollection services = new ServiceCollection();
...
services.AddDbContext<Db>(x => x.UseSqlServer(appSettings.ConnectionString));
services.AddTransient<IInsertLogJob, InsertLogJob>();
services.AddTransient<ITestService, TestService>();
_serviceProvider = services.BuildServiceProvider();
}
TestService.cs
public interface ITestService
{
void Test();
}
public class TestService : ITestService
{
private readonly ILogger<TestService> _logger;
public TestService(ILogger<TestService> logger)
{
_logger = logger;
}
public void Test()
{
logger.LogInformation("Test");
_logger.LogError("Error");
}
}
Logger.cs
public class Logger : ILogger
{
...
Log log = new Log()
{
Message = message,
EventId = eventId.Id,
ObjectId = eventId.Name,
LogLevel = logLevel.ToString(),
CreatedTime = DateTime.Now
};
BackgroundJob.Enqueue<IInsertLogJob>(j => j.Insert(log));
}
InsertLogJob.cs
public interface IInsertLogJob
{
void Insert(Log log);
}
public class InsertLogJob : IInsertLogJob
{
private Db _dataContext;
public InsertLogJob(Db context)
{
_dataContext = context;
}
public void Insert(Log log)//<-- this never happens
{
_dataContext.Logs.Add(log);
_dataContext.SaveChanges();
}
}
DB Record
So all the code up to the point where the data has to be inserted into the database runs, the Hangfire job gets inserted as per the picture above, but the code is never executed.
Related
Currently I'm able to handle IServiceCollection to inject mocks for particular services in the following manner.
public class TestClass
{
private IMediator _mediatr;
private void SetupProvider(IUnitOfWork unitOfWork, ILogger logger)
{
configuration = new ConfigurationBuilder().Build();
_services = new ServiceCollection();
_services.AddSingleton(configuration);
_services.AddScoped(x => unitOfWork);
_services.AddSingleton(logger);
_services.AddMediatR(Assembly.Load("Application"));
_services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggerBehaviour<,>));
_mediator = _services.BuildServiceProvider().GetService<IMediator>();
}
[Fact]
public async void UnitTest_Success()
{
var unitOfWork = new Mock<IUnitOfWork>();
var logger = new Mock<ILogger>();
SetupProvider(unitOfWork.Object, logger.Object);
var fixture = new Fixture();
var command = fixture.Create<MediatorCommand>();
unitOfWork.Setup(x => x.Repository.FindAll(It.IsAny<IList<long>>(), It.IsAny<bool?>()))
.ReturnsAsync(new List<Domain.Model>());
var response = await _mediatr.Send(command);
using (new AssertionScope())
{
response.Should().NotBeNull();
response.IsSuccess.Should().BeTrue();
}
}
}
For the following subject under test
public class MediatorCommand : IRequest<CommandResponse>
{
public string Name { get; set ;}
public string Address { get; set; }
}
public class MediatorCommandHandler : IRequestHandler<MediatorCommand, CommandResponse>
{
private readonly ILogger _logger;
private readonly IUnitOfWork _unitOfWork;
public MediatorCommandHandler(IUnitOfWork unitOfWork, ILogger logger)
{
_logger = logger;
_unitOfWork = unitOfWork;
}
public async Task<CommandResponse> Handle(MediatorCommand command, CancellationToken cancellationToken)
{
var result = new CommandResponse { IsSuccess = false };
try
{
var entity = GetEntityFromCommand(command);
await _unitOfWork.Save(entity);
result.IsSuccess = true;
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
}
return result;
}
}
This test runs fine and the unitOfWork and logger mocks are used in the command handlers.
I'm try to move this so that the IServiceCollection construction happens per class instead of each test using the following:
public class SetupFixture : IDisposable
{
public IServiceCollection _services;
public IMediator Mediator { get; private set; }
public Mock<IUnitOfWork> UnitOfWork { get; private set; }
public SetupFixtureBase()
{
UnitOfWork = new Mock<IUnitOfWork>();
configuration = new ConfigurationBuilder().Build();
_services = new ServiceCollection();
_services.AddSingleton(configuration);
_services.AddScoped(x => UnitOfWork);
_services.AddSingleton(new Mock<ILogger>().Object);
_services.AddMediatR(Assembly.Load("Application"));
_services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggerBehaviour<,>));
Mediator = _services.BuildServiceProvider().GetService<IMediator>();
}
public void Dispose()
{
Mediator = null;
_services.Clear();
_services = null;
}
}
public class TestClass : IClassFixture<SetupFixture>
{
protected readonly SetupFixture _setupFixture;
public UnitTestBase(SetupFixture setupFixture)
{
_setupFixture = setupFixture;
}
[Fact]
public async void UnitTest_Success()
{
var fixture = new Fixture();
var command = fixture.Create<MediatorCommand>();
_setupFixture.UnitOfWork.Setup(x => x.Repository.FindAll(It.IsAny<IList<long>>(), It.IsAny<bool?>()))
.ReturnsAsync(new List<Domain.Model>());
var response = await _mediatr.Send(command);
using (new AssertionScope())
{
response.Should().NotBeNull();
response.IsSuccess.Should().BeTrue();
}
}
}
Unfortunately with this method my mocks do not get injected on the command handler. Is there a way to get this to work?
Thank you,
I found the issue and it is not related to moving to IClassFixuture<>. The issue was that I was initializing Mediator on a base class an then adding the mock UnitOfWork on a derived class.
This cause the Mediator initialization to fail because one of the beheviours expected the UnitOfWork which at the time was not yet on the container.
Moving the initialization of Mediator after all the services have been added helped me resolve the issue and now all works as expected.
If you try the same thing, please make sure to include all the services in the container before initializing any objects that require those dependencies.
Thank you all those who had input.
After converted all my tables to start using Guid type in identity columns, I failed to seed data, so I simplified a lot the code to localize the error, and ended with a seeding class as follows:
public class SeedTest
{
private readonly MyDbContext _context;
public SeedTest(MyDbContext context)
{
_context = context;
}
public async Task SeedTest()
{
Values value1 = new Values
{
Id = Guid.Parse("29c48913-1b5c-47b8-g144-08d6d2273deb"),
ValueName = "value 1",
Created = DateTime.Now
};
_context.Values.Add(value1);
await _context.SaveChangesAsync();
}
public SeedTest()
{
}
}
This class is called from another one:
public interface IDatabaseInitializer
{
Task SeedAsync();
}
public class DatabaseInitializer : IDatabaseInitializer
{
public async Task SeedAsync()
{
SeedTest _seedTest = new SeedTest();
await _seedTest.SeedTest();
}
}
which is called from startup.cs
public class Startup
{
public IConfiguration Configuration { get; }
private readonly IHostingEnvironment _hostingEnvironment;
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
Configuration = configuration;
_hostingEnvironment = env;
}
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMyDbContext<MyDbContext>(options =>
options.UseSqlServer("ConnectionStrings:MyCn"));
...
// DB Seeding
services.AddTransient<IDatabaseInitializer, DatabaseInitializer>();
...
...
}
And here is how it is triggered from program.cs
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var databaseInitializer = services.GetRequiredService<IDatabaseInitializer>();
databaseInitializer.SeedAsync().Wait();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogCritical(LoggingEvents.INIT_DATABASE, ex, LoggingEvents.INIT_DATABASE.Name);
}
}
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
Unfortunately this implementation didn't seed any data in the database, the unique error I could find is in the logs files, and it says:
System.NullReferenceException: Object reference not set to an instance
of an object. and it points to the last line of SeedTest class.
So what am I doing wrong ?
new SeedTest() does not initialize its _context field. You could use DI on your DatabaseInitializer to instantiate a SeedTest with a MyDbContext.
public class DatabaseInitializer : IDatabaseInitializer
{
private readonly MyDbContext _context;
public DatabaseInitializer(MyDbContext context)
{
_context = context;
}
public async Task SeedAsync()
{
SeedTest _seedTest = new SeedTest(_context);
await _seedTest.SeedTest();
}
}
You are explicitly newing an instance of SeedTest in DatabaseInitialize, while the instance of DatabaseInitialize is being created by the dependency injection service. Register the SeedTest class in the services with the correct scope and let the dependency injection do its thing.
In ConfigureServices add something like
services.AddTransient<SeedTest>();
Modify DatabaseInitializer
public class DatabaseInitializer : IDatabaseInitializer{
private readonly SeedTest _seedTest;
public DatabaseInitializer(SeedTest seedTest)
{
_seedTest = seedTest;
}
public async Task SeedAsync()
{
await _seedTest.SeedTest();
}
}
Remove the parameterless SeedTest constructor and make sure the MyDbContext type registered is what is passed in the other constructor as you have both MyDbContext and DbContext.
You can try this, i have used .net core 2.2 for this sample -
MyDbContext.cs
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
{
Database.EnsureCreated();
}
public DbSet<Values> Values { get; set; }
}
SeedTest.cs
public class SeedTest
{
private readonly MyDbContext _context;
public SeedTest(MyDbContext context)
{
_context = context;
}
public async Task SeedTest1()
{
Values value1 = new Values
{
Id = Guid.Parse("AFE1052A-A694-48AF-AA77-56D2D945DE31"),
ValueName = "value 1",
Created = DateTime.Now
};
_context.Values.Add(value1);
var value = await _context.SaveChangesAsync();
}
public SeedTest()
{
}
}
Service
public interface IDatabaseInitializer
{
Task SeedAsync();
}
public class DatabaseInitializer : IDatabaseInitializer
{
private readonly MyDbContext _cotext;
// Inject DbContext
public DatabaseInitializer(MyDbContext dbContext)
{
_cotext = dbContext;
}
public async Task SeedAsync()
{
// Object with contructor which having DbContext parameter
SeedTest _seedTest = new SeedTest(_cotext);
await _seedTest.SeedTest1();
}
}
startup.cs
services.AddTransient<IDatabaseInitializer, DatabaseInitializer>();
services.AddDbContext<MyDbContext>(option=> option.UseSqlServer("Data Source=localhost;Initial Catalog=StackOverFlow1;Integrated Security=True"));
program.cs
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var databaseInitializer = services.GetRequiredService<IDatabaseInitializer>();
databaseInitializer.SeedAsync().Wait();
}
catch (Exception ex)
{
}
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
You can read more about seed data.
I'm trying make a telegram bot with reminder. I'm using Telegram.Bot 14.10.0, Quartz 3.0.7, .net core 2.0. The first version should : get message "reminder" from telegram, create job (using Quartz) and send meaasage back in 5 seconds.
My console app with DI looks like:
Program.cs
static IBot _botClient;
public static void Main(string[] args)
{
// it doesn't matter
var servicesProvider = BuildDi(connecionString, section);
_botClient = servicesProvider.GetRequiredService<IBot>();
_botClient.Start(appModel.BotConfiguration.BotToken, httpProxy);
var reminderJob = servicesProvider.GetRequiredService<IReminderJob>();
reminderJob.Bot = _botClient;
Console.ReadLine();
_botClient.Stop();
// it doesn't matter
}
private static ServiceProvider BuildDi(string connectionString, IConfigurationSection section)
{
var rJob = new ReminderJob();
var sCollection = new ServiceCollection()
.AddSingleton<IBot, Bot>()
.AddSingleton<ReminderJob>(rJob)
.AddSingleton<ISchedulerBot>(s =>
{
var schedBor = new SchedulerBot();
schedBor.StartScheduler();
return schedBor;
});
return sCollection.BuildServiceProvider();
}
Bot.cs
public class Bot : IBot
{
static TelegramBotClient _botClient;
public void Start(string botToken, WebProxy httpProxy)
{
_botClient = new TelegramBotClient(botToken, httpProxy);
_botClient.OnReceiveError += BotOnReceiveError;
_botClient.OnMessage += Bot_OnMessage;
_botClient.StartReceiving();
}
private static async void Bot_OnMessage(object sender, MessageEventArgs e)
{
var me = wait _botClient.GetMeAsync();
if (e.Message.Text == "reminder")
{
var map= new Dictionary<string, object> { { ReminderJobConst.ChatId, e.Message.Chat.Id.ToString() }, { ReminderJobConst.HomeWordId, 1} };
var job = JobBuilder.Create<ReminderJob>().WithIdentity($"{prefix}{rnd.Next()}").UsingJobData(new JobDataMap(map)).Build();
var trigger = TriggerBuilder.Create().WithIdentity($"{prefix}{rnd.Next()}").StartAt(DateTime.Now.AddSeconds(5).ToUniversalTime())
.Build();
await bot.Scheduler.ScheduleJob(job, trigger);
}
}
}
Quartz.net not allow use constructor with DI. That's why I'm trying to create property with DI.
ReminderJob.cs
public class ReminderJob : IJob
{
static IBot _bot;
public IBot Bot { get; set; }
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await System.Console.Out.WriteLineAsync("HelloJob is executing.");
}
}
How can I pass _botClient to reminderJob in Program.cs?
If somebody looks for answer, I have one:
Program.cs (in Main)
var schedBor = servicesProvider.GetRequiredService<ISchedulerBot>();
var logger = servicesProvider.GetRequiredService<ILogger<DIJobFactory>>();
schedBor.StartScheduler();
schedBor.Scheduler.JobFactory = new DIJobFactory(logger, servicesProvider);
DIJobFactory.cs
public class DIJobFactory : IJobFactory
{
static ILogger<DIJobFactory> _logger;
static IServiceProvider _serviceProvider;
public DIJobFactory(ILogger<DIJobFactory> logger, IServiceProvider sp)
{
_logger = logger;
_serviceProvider = sp;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
IJobDetail jobDetail = bundle.JobDetail;
Type jobType = jobDetail.JobType;
try
{
_logger.LogDebug($"Producing instance of Job '{jobDetail.Key}', class={jobType.FullName}");
if (jobType == null)
{
throw new ArgumentNullException(nameof(jobType), "Cannot instantiate null");
}
return (IJob)_serviceProvider.GetRequiredService(jobType);
}
catch (Exception e)
{
SchedulerException se = new SchedulerException($"Problem instantiating class '{jobDetail.JobType.FullName}'", e);
throw se;
}
}
// get from https://github.com/quartznet/quartznet/blob/139aafa23728892b0a5ebf845ce28c3bfdb0bfe8/src/Quartz/Simpl/SimpleJobFactory.cs
public void ReturnJob(IJob job)
{
var disposable = job as IDisposable;
disposable?.Dispose();
}
}
ReminderJob.cs
public interface IReminderJob : IJob
{
}
public class ReminderJob : IReminderJob
{
ILogger<ReminderJob> _logger;
IBot _bot;
public ReminderJob(ILogger<ReminderJob> logger, IBot bot)
{
_logger = logger;
_bot = bot;
}
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await _bot.Send(userId.ToString(), "test");
}
}
I have written a Windows Service which logs its operations using Serilog
The application also uses Autofac for dependency Injection support and Seq for structured logging.
using Autofac;
public class ContainerInitiator
{
public static IContainer BuildContainer()
{
var _builder = new ContainerBuilder();
var logger = LoggerUtility.CreateLogger();
_builder.RegisterInstance(logger).As<ILogger>().SingleInstance();
var container = _builder.Build();
ContainerFactory.SetContainer(container);
return container;
}
}
Here is Log utility class
public class LoggerUtility
{
private static string connectionString = ConfigurationManager.AppSettings["applicationName"];
public static ILogger CreateLogger()
{
return GetLoggerConfiguration().CreateLogger();
}
private static LoggerConfiguration GetLoggerConfiguration()
{
var config = new LoggerConfiguration()
.ReadFrom.AppSettings();
config = config.
Enrich.WithProperty("ApplicationName", connectionString).
Enrich.WithExceptionDetails().
Enrich.WithMachineName().
Enrich.WithProcessId().
Enrich.WithThreadId().
ReadFrom.AppSettings();
return config;
}
}
Here is my service class, which is logging the activities
public class Engine : IEngine
{
private readonly ILogger _logger;
public RedistributeEngine(IContainerFactory containerFactory)
{
_logger = containerFactory.GetInstance<ILogger>();
}
public void Start()
{
_logger.Information("Engine started!");
}
}
As you see below Seq logs, it has two entries on the same time!
You're reading the configuration from App.Settings twice:
private static LoggerConfiguration GetLoggerConfiguration()
{
var config = new LoggerConfiguration()
.ReadFrom.AppSettings(); // <<<<<<<<<<<< *#*#*#*#*#*#*#*#
config = config.
Enrich.WithProperty("ApplicationName", connectionString).
Enrich.WithExceptionDetails().
Enrich.WithMachineName().
Enrich.WithProcessId().
Enrich.WithThreadId().
ReadFrom.AppSettings(); // <<<<<<<<<<<< *#*#*#*#*#*#*#*#
return config;
}
According to documents when I configure DbContext like below DI register it in scope (per http request)
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<DBData>(options => {
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);
}
);
The problem appears when I am trying to access it in another thread.
public class HomeController : Controller
{
private readonly DBData _context;
public HomeController(DBData context)
{
_context = context;
}
public IActionResult StartInBackground()
{
Task.Run(() =>
{
Thread.Sleep(3000);
//System.ObjectDisposedException here
var res = _context.Users.FirstOrDefault(x => x.Id == 1);
});
return View();
}
}
I want to configure DbContext creation per each call (AddTransition). It would give me possibility to write next code
public void ConfigureServices(IServiceCollection services)
{
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<DBData>(options => {
//somehow configure it to use AddTransient
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);
}
);
services.AddTransient<IUnitOfWorkFactoryPerCall, UnitOfWorkFactory>();
services.AddScoped<IUnitOfWorkFactoryPerRequest, UnitOfWorkFactory>();
services.AddMvc();
}
public interface IUnitOfWorkFactoryPerCall : IUnitOfWorkFactory { }
public interface IUnitOfWorkFactoryPerRequest : IUnitOfWorkFactory { }
public interface IUnitOfWorkFactory : IDisposable
{
DBData Context { get; }
}
public class UnitOfWorkFactory : IUnitOfWorkFactoryPerCall, IUnitOfWorkFactoryPerRequest
{
public UnitOfWorkFactory(DBData context)
{
Context = context;
}
public DBData Context
{
get; private set;
}
public void Dispose()
{
Context.Dispose();
}
}
So now if I want to create DBContext per request I will use IUnitOfWorkFactoryPerRequest, and when I want to use DBContext in some background thread I can use IUnitOfWorkFactoryPerCall.
My temporary solution.
I created singleton which can create Context "in transient way"
public class AppDependencyResolver
{
private static AppDependencyResolver _resolver;
public static AppDependencyResolver Current
{
get
{
if (_resolver == null)
throw new Exception("AppDependencyResolver not initialized. You should initialize it in Startup class");
return _resolver;
}
}
public static void Init(IServiceProvider services)
{
_resolver = new AppDependencyResolver(services);
}
private readonly IServiceProvider _serviceProvider;
public AppDependencyResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IUnitOfWorkFactory CreateUoWinCurrentThread()
{
var scopeResolver = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
return new UnitOfWorkFactory(scopeResolver.ServiceProvider.GetRequiredService<DBData>(), scopeResolver);
}
}
Then I call init method in Startup Configure method
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
AppDependencyResolver.Init(app.ApplicationServices);
//other configure code
}
And after all I can call AppDependencyResolver.Current.CreateUoWinCurrentThread() in some background thread.
If someone can provide more elegant solution I will be appreciated.
Within your controller, why are you trying to inject into private readonly DBData _context;? If you've registered your IUnitOfWorkFactoryPerCall via DI, you should be injecting that into your controller I believe? You then access your context via the interface.
To expand, this is what I am suggesting you do:
public class HomeController : Controller
{
private readonly IUnitOfWorkFactoryPerCall _contextFactory;
public HomeController(IUnitOfWorkFactoryPerCall contextFactory)
{
_contextFactory = contextFactory;
}
public IActionResult StartInBackground()
{
Task.Run(() =>
{
Thread.Sleep(3000);
//System.ObjectDisposedException here
var res = _contextFactory.Context.Users.FirstOrDefault(x => x.Id == 1);
});
return View();
}
}