I have created a simple windows services to consume messages from Azure service bus queue. I used the Topshelf to create windows service. Code snipped below following example from here: https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues
var hf = HostFactory.New(x =>
{
x.Service<ServiceBusHelper>(s =>
{
s.ConstructUsing(serviceProvider.GetService<ServiceBusHelper>);
s.WhenStarted(async service => await service.ReceiveMessagesAsync());
s.WhenStopped(async service => await service.Stop());
});
x.RunAsNetworkService()
.StartAutomatically()
.EnableServiceRecovery(rc => rc.RestartService(1));
x.SetServiceName("MyWindowsService");
x.SetDisplayName("MyWindowsService");
x.SetDescription("MyWindowsService");
});
hf.Run();
ServiceBusHelper class:
public async Task ReceiveMessagesAsync()
{
var connectionString = _configuration.GetValue<string>("ServiceBusConnectionString");
var queueName = _configuration.GetValue<string>("ServiceBusQueueName");
await using (ServiceBusClient client = new ServiceBusClient(connectionString))
{
ServiceBusProcessor processor = client.CreateProcessor(queueName, new ServiceBusProcessorOptions());
processor.ProcessMessageAsync += MessageHandler;
processor.ProcessErrorAsync += ErrorHandler;
await processor.StartProcessingAsync();
System.Threading.Thread.Sleep(1000);//Wait for a minute before stop processing
await processor.StopProcessingAsync();
}
}
public async Task MessageHandler(ProcessMessageEventArgs args)
{
string body = args.Message.Body.ToString();
var messageBytes = Encoding.ASCII.GetBytes(body);
ProcessMessage(messageBytes);
await args.CompleteMessageAsync(args.Message);
}
public Task ErrorHandler(ProcessErrorEventArgs args)
{
return Task.CompletedTask;
}
public Task Stop()
{
return Task.CompletedTask;
}
Window service gets installed successfully and the status show running. However, it would not automatically consume the message from the service bus. If I manually stop and start the service it will pick up the message from the queue. Not sure what am I missing with this implementation. Any suggestions appreciated.
.NetCore 3.1 introduced a new extension to work along side Microsoft.AspNetCore.Hosting
Adding NuGet package Microsoft.Extensions.Hosting.WindowsServices
you can add
.UseWindowsService(). this will allow you run this as a windows service or Console app.
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureAppConfiguration((context, config) =>
{
// configure the app here.
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<QueueWorker>();
}).UseSerilog();
}
you can then create a background worker to start and stop processing the servicebus queue. Here is my implementaion:
public class QueueWorker : BackgroundService, IDisposable
{
protected ILogger<QueueWorker> _logger;
protected IQueueMessageReceiver _queueProcessor;
public QueueWorker()
{
}
public QueueWorker(ILogger<QueueWorker> logger, IQueueMessageReceiver queueMessageReceiver)
{
_logger = logger;
_queueProcessor = queueMessageReceiver;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.CompletedTask.ConfigureAwait(false);
}
public override Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service Starting");
var task = _queueProcessor.StartProcessor(cancellationToken);
task.Wait();
if (task.IsFaulted)
{
throw new Exception("Unable to start Processor");
}
return base.StartAsync(cancellationToken);
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping Service");
await _queueProcessor.StopProcessor().ConfigureAwait(false);
await base.StopAsync(cancellationToken).ConfigureAwait(false);
}
public override void Dispose()
{
_logger.LogInformation("Disposing Service");
var loopCount = 0;
while (_queueProcessor != null && !_queueProcessor.IsClosedOrClosing() && loopCount < 5)
{
var task = Task.Delay(600);
task.Wait();
loopCount++;
}
base.Dispose();
GC.SuppressFinalize(this);
}
And The actual processor:
public class QueueMessageReceiver : IQueueMessageReceiver
{
private readonly ServiceBusClient _queueClient;
private ServiceBusProcessor _processor;
private readonly ReceiverConfiguration _configuration;
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
private Dictionary<string, string> _executionMatrix;
private readonly IServiceProvider _provider;
private CancellationToken _cancellationToken;
public QueueMessageReceiver(ReceiverConfiguration configuration, ILogger<QueueMessageReceiver> logger, IExecutionMatrix executionMatrix, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
if (configuration == null) throw new ArgumentException($"Configuration is missing from the expected ");
_configuration = configuration;
_logger = logger;
_loggerFactory = loggerFactory;
_executionMatrix = executionMatrix.GetExecutionMatrix();
_provider = serviceProvider;
_queueClient = new ServiceBusClient(_configuration.ConnectionString);
if (string.IsNullOrWhiteSpace(configuration.ConnectionString)) throw new ArgumentException($"ServiceBusConnectionString Object missing from the expected configuration under ConnectionStrings ");
if (configuration.QueueName == null) throw new ArgumentException($"Queue Name value missing from the expected configuration");
}
public async Task StartProcessor(CancellationToken cancellationToken)
{
if (!IsClosedOrClosing())
{
throw new FatalSystemException("ServiceBusProcessor is already running. ");
}
_cancellationToken = cancellationToken;
var options = new ServiceBusProcessorOptions
{
AutoCompleteMessages = _configuration.AutoComplete,
MaxConcurrentCalls = _configuration.MaxConcurrentCalls,
MaxAutoLockRenewalDuration = _configuration.MaxAutoRenewDuration
};
_processor = _queueClient.CreateProcessor(_configuration.QueueName, options);
_processor.ProcessMessageAsync += ProcessMessagesAsync;
_processor.ProcessErrorAsync += ProcessErrorAsync;
await _processor.StartProcessingAsync().ConfigureAwait(false);
}
public async Task StopProcessor()
{
await _processor.StopProcessingAsync();
await _processor.CloseAsync();
}
private Task ProcessErrorAsync(ProcessErrorEventArgs args)
{
_logger.LogError(args.Exception, "Uncaught handled exception", args.ErrorSource, args.FullyQualifiedNamespace, args.EntityPath);
return Task.CompletedTask;
}
private async Task ProcessMessagesAsync(ProcessMessageEventArgs args)
{
var message = args.Message;
// Process the message.
var sbMessage = $"Received message: SequenceNumber:{message.SequenceNumber} Body:{Encoding.UTF8.GetString(message.Body)}";
_logger.LogInformation(sbMessage);
//Handle your message
}
public bool IsClosedOrClosing()
{
return ((_processor == null) || _processor.IsClosed || !_processor.IsProcessing);
}
}
Related
I need to create software that can read events from Azure Event Hub in an infinite way and save them on the db following some logic. The first question I wanted to ask is if such thing is feasible using:
EventProcessorClient (or rather EventProcessor<TPartition> as I would always like to use the SqlServer for checkpoints instead of an Azure Blob Storage) in a BackgroundService as Windows Service and then run it in infinitely and every so many events to checkpoint?
I tried this code reading from a single partition but the application crashes after an exception and the loop ends without continuing to process the events.
public class LogConsumerBackgroundService : BackgroundService
{
private const string EventHubConnectionString =
"ConnString";
private const string ConsumerGroup = "ConsGroup";
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};
private readonly ILogger<LogConsumerBackgroundService> _logger;
private readonly IServiceProvider _serviceProvider;
private int _count;
public LogConsumerBackgroundService(ILogger<LogConsumerBackgroundService> logger, IServiceProvider serviceProvider)
{
Guard.Against.Null(logger);
Guard.Against.Null(serviceProvider);
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
var eventHubConsumerClientOptions = new EventHubConsumerClientOptions
{
ConnectionOptions =
{
TransportType = EventHubsTransportType.AmqpWebSockets
}
};
await using var consumer = new EventHubConsumerClient(ConsumerGroup, EventHubConnectionString, eventHubConsumerClientOptions);
var startingPosition = EventPosition.Earliest;
var partitionIds = await consumer.GetPartitionIdsAsync(CancellationToken.None);
var partitionId = partitionIds.First();
var scope = _serviceProvider.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<IRepository>();
while (!stoppingToken.IsCancellationRequested)
{
await foreach (var receivedEvent in consumer.ReadEventsFromPartitionAsync(partitionId, startingPosition,
CancellationToken.None))
{
try
{
// Some logic to save data on bd
_logger.LogInformation("Processed row: {Count}", countInfo += _count);
await repository.SaveChangesAsync();
}
catch (Exception e)
{
_logger.LogError(e.Demystify(), "{EventBody}", receivedEvent.Data?.EventBody);
}
}
}
}
catch (TaskCanceledException e)
{
_logger.LogError(e.Demystify(), "Task was canceled");
}
catch (Exception e)
{
_logger.LogError(e.Demystify(), "An unhandled exception has occurred");
}
}
}
I'm fairly new to bot dev but know a thing or too about C# however I've been looking for quite some time and still cannot seem to find a solution to my problem. I'm using Discord.NET and my bot was working great until I introduced Discord.Addons.Interactive addon and I don't know how to fix it.
Whenever I run my program I get the error: System.InvalidOperationException: 'No type reader found for type CommandContext, one must be specified'.
Here is my Program.cs:
class Program
{
public static void Main(string[] args)
{
new Program().MainAsync().GetAwaiter().GetResult();
}
private DiscordSocketClient _client;
private CommandService _commands;
private IServiceProvider _services;
public Program()
{
}
public async Task MainAsync()
{
_client = new DiscordSocketClient();
_commands = new CommandService();
_services = new ServiceCollection()
.AddSingleton(_client)
.AddSingleton(_commands)
.AddSingleton<CommandHandler>()
.AddSingleton<InteractiveService>()
.BuildServiceProvider();
_client.Log += LogAsync;
_client.Ready += ReadyAsync;
var tocken = "my_tocken";
await _client.LoginAsync(TokenType.Bot, tocken);
await _client.StartAsync();
await _services.GetRequiredService<CommandHandler>().InstallCommandAsync();
await Task.Delay(-1);
}
private Task LogAsync(LogMessage msg)
{
Console.WriteLine(msg.ToString());
return Task.CompletedTask;
}
private Task ReadyAsync()
{
Console.WriteLine($"Connected as -> [{_client.CurrentUser}]");
return Task.CompletedTask;
}
}
Here is my CommandHandler.cs:
public class CommandHandler
{
private readonly DiscordSocketClient _client;
private readonly CommandService _commands;
private readonly IServiceProvider _services;
public CommandHandler(DiscordSocketClient client, CommandService commands, IServiceProvider services, InteractiveService interactions)
{
_commands = commands;
_client = client;
_services = services;
_commands.CommandExecuted += CommandExecutedAsync;
_client.MessageReceived += HandleCommandAsync;
}
public async Task InstallCommandAsync()
{
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
}
private async Task HandleCommandAsync(SocketMessage messageParam)
{
var message = messageParam as SocketUserMessage;
var context = new SocketCommandContext(_client, message);
if (message.Author.IsBot) return;
int argPos = 0;
if (message.HasCharPrefix('-', ref argPos))
{
await _commands.ExecuteAsync(context, argPos, _services);
}
}
public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
{
if (!command.IsSpecified)
{
Console.WriteLine($"Command failed to execute for [{context.User.Username}] <-> [{result.ErrorReason}]!");
return;
}
if (result.IsSuccess)
{
Console.WriteLine($"Command [{command.Value.Name}] execute for [{context.User.Username}]!");
return;
}
await context.Channel.SendMessageAsync($"{context.User.Username} Something went wrong -> [{result}]!");
}
}
If you need anymore information then just let me know.
EDIT:
I found the issue, I had put a CommandContext parameter in one of my commands.
Hope this helps anyone in the future :)
I am trying to implement a background service, QueuedBackground service with Mediator.
So far I am able to implement the queues but I am unable to execute Mediator.
Interface
public interface IBackgroundTaskQueueService
{
void QueueBackgroundWorkItem(object workItem, CancellationToken token);
Task<object> DequeueAsync(
CancellationToken cancellationToken);
}
Implementation
public class BackgroundTaskQueueService : IBackgroundTaskQueueService
{
private readonly ConcurrentQueue<(object,CancellationToken)> _workItems =
new ConcurrentQueue<(object,CancellationToken)>();
private SemaphoreSlim _signal = new SemaphoreSlim(0);
public void QueueBackgroundWorkItem(object workItem, CancellationToken token)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_workItems.Enqueue((workItem,token));
_signal.Release();
}
public async Task<object> DequeueAsync( CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
return workItem.Item1;
}
}
Background Service
public class QueuedHostedService : BackgroundService
{
private readonly ILogger _logger;
private readonly IMediator _mediator;
public QueuedHostedService(IBackgroundTaskQueueService taskQueue, ILoggerFactory loggerFactory, IMediator mediator)
{
TaskQueue = taskQueue;
_mediator = mediator;
_logger = loggerFactory.CreateLogger<QueuedHostedService>();
}
public IBackgroundTaskQueueService TaskQueue { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (false == stoppingToken.IsCancellationRequested)
{
try
{
var workItem = await TaskQueue.DequeueAsync(stoppingToken);
await _mediator.Send(workItem, stoppingToken);
// await _mediator.Send(new UpdateProductCostByMaterialRequestModel()
// {
// Id = 1
// }, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error occurred executing Work item.");
}
}
}
}
Usage
_queueService.QueueBackgroundWorkItem(new UpdateProductCostByMaterialRequestModel()
{
Id = request.ProductId
}, CancellationToken.None);
Now with the above code I am able to receive the class object but when I pass it in the Mediator I get InvalidOperation Handler not registered.
I am confused.
Okay I found the issue
Instead of passing it from the Constructor I had to use the ServiceFactory Interface
My Solution
BackgroundService is a Singleton. You cannot inject a Scoped into a Singleton.
public class QueuedHostedService : BackgroundService
{
private readonly ILogger _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
public QueuedHostedService(IBackgroundTaskQueueService taskQueue, ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory)
{
TaskQueue = taskQueue;
_serviceScopeFactory = serviceScopeFactory;
_logger = loggerFactory.CreateLogger<QueuedHostedService>();
}
public IBackgroundTaskQueueService TaskQueue { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _serviceScopeFactory.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
while (false == stoppingToken.IsCancellationRequested)
{
try
{
var workItem = await TaskQueue.DequeueAsync(stoppingToken);
if (workItem is IRequest<object> item)
{
await mediator.Send(workItem, stoppingToken);
}
// await _mediator.Send(new UpdateProductCostByMaterialRequestModel()
// {
// Id = 1
// }, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error occurred executing Work item.");
}
}
}
}
I am running a timed background task to send out emails, and in the email I want to include a generated link.
When I send out other emails via user interactions in the controller, I'm using this little method to generate the link:
public string BuildUrl(string controller, string action, int id)
{
Uri domain = new Uri(Request.GetDisplayUrl());
return domain.Host + (domain.IsDefaultPort ? "" : ":" + domain.Port) +
$#"/{controller}/{action}/{id}";
}
Of course, a background task does not know anything about the Http context, so I would need to replace the domain-part of the link, like this:
public string BuildUrl(string controller, string action, int id)
{
return aStringPassedInFromSomewhere + $#"/{controller}/{action}/{id}";
}
I'm starting the background task in startup.cs ConfigureServices like this:
services.AddHostedService<ProjectTaskNotifications>();
I was thinking to maybe get the domainname from a resource file, but then I might as well just hard code it into the task method.
Is there some way to pass this information dynamically to the background task?
MORE INFO
Here is the entire background task:
internal class ProjectTaskNotifications : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;
private readonly IServiceScopeFactory scopeFactory;
private readonly IMapper auto;
public ProjectTaskNotifications(
ILogger<ProjectTaskNotifications> logger,
IServiceScopeFactory scopeFactory,
IMapper mapper)
{
_logger = logger;
this.scopeFactory = scopeFactory;
auto = mapper;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(30));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation("Timed Background Service is working.");
// Connect to the database and cycle through all unsent
// notifications, checking if some of them are due to be sent:
using (var scope = scopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
List<ProjectTaskNotification> notifications = db.ProjectTaskNotifications
.Include(t => t.Task)
.ThenInclude(o => o.TaskOwner)
.Include(t => t.Task)
.ThenInclude(p => p.Project)
.ThenInclude(o => o.ProjectOwner)
.Where(s => !s.IsSent).ToList();
foreach (var notification in notifications)
{
if (DateTime.UtcNow > notification.Task.DueDate
.AddMinutes(-notification.TimeBefore.TotalMinutes))
{
SendEmail(notification);
notification.Sent = DateTime.UtcNow;
notification.IsSent = true;
}
}
db.UpdateRange(notifications);
db.SaveChanges();
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
public void SendEmail(ProjectTaskNotification notification)
{ // Trimmed down for brevity
// Key parts
string toAddr = notification.Task.TaskOwner.Email1;
BodyBuilder bodyBuilder = new BodyBuilder
{
HtmlBody = TaskInfo(auto.Map<ProjectTaskViewModel>(notification.Task))
};
return;
}
public string TaskInfo(ProjectTaskViewModel task)
{ // Trimmed down for brevity
return $#"<p>{BuildUrl("ProjectTasks", "Edit", task.Id)}</p>";
}
public string BuildUrl(string controller, string action, int id)
{
// This is where I need the domain name sent in from somewhere:
return "domain:port" + $#"/{controller}/{action}/{id}";
}
}
You can pass in any object to the IHostedService provider via the constructor.
public ProjectTaskNotifications(IUrlPrefixProvider provider)
{
_urlPrefixProvider = urlPrefixProvider
}
private string BuildUrl(<Your args>)
{
var prefix = _urlPrefixProvider.GetPrefix(<args>);
....
}
In startup.cs you can have
services.AddSingleton<IUrlPrefixProvider, MyUrlPrefixProvider>()
services.AddHostedService<ProjectTaskNotifications>();
and let dependency injection take care of the rest.
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");
}
}