IHostedService for database changes hung forever - c#

I am trying to implement database change notificaitons using SignalR in a web api.
below is the code for NotificationService.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Npgsql;
using webapi.DBCalls;
using webapi.Models;
namespace webapi.Notification
{
public class NotificationService : IHostedService
{
private IHubContext<HubConfig> _hub;
private readonly ModelAppSettings _appSettings;
private DBInterceptor _db_interceptor;
public NotificationService(IHubContext<HubConfig> hub, IOptions<ModelAppSettings> appSettings)
{
_appSettings = appSettings.Value;
_db_interceptor = new DBInterceptor(_appSettings);
_hub = hub;
}
public Task StartAsync(CancellationToken stoppingToken)
{
onDataTableChangeListener();
return Task.CompletedTask;
}
public void onDataTableChangeListener()
{
using (var connection = new NpgsqlConnection(_appSettings.ConnectionString))
{
connection.Open();
connection.Notification += (o, e) => notifyDataChange(e.Payload);
using (var cmd = new NpgsqlCommand("LISTEN datachange", connection))
cmd.ExecuteNonQuery();
while (true)
connection.Wait();
}
}
public void notifyDataChange(string payload)
{
//DO some work here
}
public Task StopAsync(CancellationToken stoppingToken)
{
return Task.CompletedTask;
}
}
}
And I am registering this service in Startup.cs under ConfigurationService AS below
services.AddHostedService<NotificationService>();
When I run the program, it never starts and is hung at below line.
while (true)
connection.Wait();
I know I need to rewrite this method, but not sure how to write it.
Any help would be appreciated.

You are not implementing the IHostedService interface correctly. You are blocking the current thread with connection.Wait(). In StartAsync you should start a new thread or a Task on the ThreadPool that will process the notifications in the background. In StopAsync you would terminate processing and perform cleanup if needed.
This is partially implemented by the BackgroundService class where you have to implement only the ExecuteAsync method. Using BackgroundService class your service should like this:
public class NotificationSerice : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var connection = new NpgsqlConnection("ConnectionString");
await connection.OpenAsync();
connection.Notification += (o, e) => notifyDataChange(e.Payload, stoppingToken);
using (var cmd = new NpgsqlCommand("LISTEN datachange", connection))
await cmd.ExecuteNonQueryAsync(stoppingToken);
while (true)
await connection.WaitAsync(stoppingToken);
}
private void notifyDataChange(string payload, CancellationToken stoppingToken)
{
///
}
}
In the example it is implemented as an asynchronous method. It is also possible to use Task.Run() which runs tasks on the ThreadPool. stoppingToken will be triggered when the application shuts down.

Related

Background Hosted Service executing twice in .net core. Any solution?

I have created one background hosted service in .net core 3.1, and based on CRON expression, it is expected to run at desired time. i.e. 3 AM through Mon-Fri. Everything seems to be working fine, the background service is running at desired time, but when it runs, it always runs twice. The app is hosted on IIS.
Here is the code which I am using:
In Startup File, ConfigureServices Methods:
services.AddHostedService<MyTestBackgroundHostedService>();
Here is my service configured:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using NCrontab;
using Serilog;
namespace MyProject
{
public class MyTestBackgroundService : BackgroundService
{
private readonly CrontabSchedule _schedule;
private readonly string _scheduleDetail;
private DateTime _nextRun;
private ILogger _logger;
public MyTestBackgroundService(ILogger logger)
{
_scheduleDetail = "0 0 3 * * 0";
_schedule = CrontabSchedule.Parse(_scheduleDetail, new CrontabSchedule.ParseOptions { IncludingSeconds = true });
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
do {
var now = DateTime.Now;
if (now > _nextRun) {
_logger.Information($"MyTestBackgroundService job started at {now}", now);
Process();
_logger.Information($"MyTestBackgroundService :MyTestBackgroundService job finished at {now}", now);
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
await Task.Delay(5000, stoppingToken);
}
while (!stoppingToken.IsCancellationRequested);
}
public override Task StartAsync(CancellationToken cancellationToken)
{
return base.StartAsync(cancellationToken);
}
public override Task StopAsync(CancellationToken cancellationToken)
{
return base.StopAsync(cancellationToken);
}
private void Process()
{
try {
// do something
}
catch (Exception ex) {
_logger.Error($"MyTestBackgroundService failed : {ex}");
}
}
}
}
Root cause for above problem was:
Background service is configured in our API solution, and API was running on multiple instances, hence resulting in multiple background services running in parallel, which was error prone in our case.
Solution:
Either limit the running instances, but that could not be practical as it may result in performance issues.
Background service should have been configured in a dedicated project.

How do I get my commands to work in Discord.Net

So I THINK I know what may be the problem but I'm unsure how to go about fixing it. I'm very new to C# coding. I've been coding Discord bots in node for over a year now so switching over is kinda difficult. I'm following the instructions from the Discord.NET documentations and guide. Here is the code in the Program file
using System;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
namespace GalacticBot
{
class MainClass
{
public static void Main(string[] args) => new MainClass().MainAsync().GetAwaiter().GetResult();
private DiscordSocketClient client;
// Calls the class holding information
private Config config = new Config();
public async Task MainAsync()
{
client = new DiscordSocketClient();
// Logs to console
client.Log += Log;
// Uses the token to start the bot
await client.LoginAsync(TokenType.Bot, config.TestToken);
await client.StartAsync();
await Task.Delay(-1);
}
private Task Log(LogMessage msg)
{
Console.WriteLine(msg.ToString());
return Task.CompletedTask;
}
}
}
Here is the code in the CommandHandler file
using System;
using System.Reflection;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
namespace GalacticBot
{
public class CommandHandler
{
private readonly DiscordSocketClient client;
private readonly CommandService commands;
private readonly Config config = new Config();
public CommandHandler(DiscordSocketClient _client, CommandService _commands)
{
client = _client;
commands = _commands;
}
public async Task InstallCommandsAsync()
{
client.MessageReceived += HandleCommandAsync;
await commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(), services: null);
}
private async Task HandleCommandAsync(SocketMessage MessageParam)
{
var message = MessageParam as SocketUserMessage;
if (message == null) return;
int ArgPos = 0;
// If there's no prefix or the message is from a bot then nothing happens
if (!(message.HasCharPrefix('!', ref ArgPos) || message.HasMentionPrefix(client.CurrentUser, ref ArgPos)) || message.Author.IsBot) return;
var context = new SocketCommandContext(client, message);
await commands.ExecuteAsync(
context: context,
argPos: ArgPos,
services: null
);
}
}
}
and here is the code for the commands themselves
using System;
using System.Threading.Tasks;
using Discord.Commands;
public class Hi : ModuleBase<SocketCommandContext>
{
[Command("hey")]
[Summary("Just says hi.")]
public async Task SayAsync()
{
Console.WriteLine("Command used");
await Context.Channel.SendMessageAsync("Just saying hi!");
}
}
The Console.WriteLine in the command is for testing purposes to see if it's even attempting to work. My thought is that I'm not calling and using the CommandHandler class anywhere. I don't know if this is the problem and if it is, I don't know what I need to do.
After a lot more Googling and trying a bunch of different ideas, I finally found a solution. Anu6is was right in that you have to create a new instance and call the install method. This led to returning null when starting the bot. First I made a variable for commands and made it static with the _client variable as well as making a variable for the CommandHandler
private static DiscordSocketClient _client;
private static CommandService _commands;
private CommandHandler commandHandler;
Inside MainAsync I had to create a new instance of the CommandService, a new instance of the command handler passing _client and _commands and then call the InstallCommandsAsync method
_commands = new CommandService();
commandHandler = new CommandHandler(_client, _commands);
await commandHandler.InstallCommandsAsync();
After the changes, build came back successful with no warnings and the test command worked. Hope this helps someone else at some point.

Injecting SignalR into a Core WorkerService

I am trying to inject SignalR into a WorkerService project, and then simulate server-side notifications.
Further down in my Worker.cs file, I'm getting some null references - which tells me I'm not properly initially my Hub object.
I first created a NotificationHub solution with a project of the same name.
Here's Notifications.cs - which works fine.
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NotificationHub.Hubs
{
public interface IHubClient
{
Task MessageReceived(string user, string message);
Task MessageReceived(string message);
}
public class Notifications: Hub<IHubClient>
{
[HubMethodName("SendMessageToClient")]
public async Task SendMessage(string username, string message)
{
await Clients.All.MessageReceived(username, "from Notification Hub:" + message);
}
public override Task OnConnectedAsync()
{
Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
_ = SendMessage("From Server", "Hub reconnected on server.");
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
Console.Write("Hub just disconnected on server.");
return base.OnDisconnectedAsync(exception);
}
}
}
The NotificationHub runs in IIS on my local machine, which let's me call the Notification Hub from my TypeScript client.
i.e. I just added a simple HTML button to trigger a message from the client. My function fires:
this.hubConnection.send('SendMessageToClient', 'client', `Message from client side. ${today}`)
.then(() => console.log('Message sent from client.'));
Core WorkerService: My problem is here
I then added a NotificationWorkService, in which I am trying to inject SignalR. The goal is to eventually have this service running on a Windows server which regularly checks for notifications, and delivers them to the browser.
In my WorkerService project, here's Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace HarmonyNotificationWorkerService
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
}
and here is Worker.cs -
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Client;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NotificationHub.Hubs;
namespace HarmonyNotificationWorkerService
{
public interface IHubClient
{
Task MessageReceived(string user, string message);
Task MessageReceived(string message);
}
public class Worker : BackgroundService
{
private IHubContext<Notifications> _hubContext;
private readonly Hub<IHubClient> _hubNotif;
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using (var hubConnection = new HubConnection("/hub"))
{
IHubProxy hubproxy = hubConnection.CreateHubProxy("https://localhost:44311/hub");
await hubproxy.Invoke("SendMessage", "test");
}
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
private async Task CheckForNotifications()
{
await _hubNotif.Clients.All.MessageReceived("from work server", "Dude, you've got mail!");
}
}
}
PROBLEM:
Problem is that _hubContext and _hubNotif objects are always null, so I can't test either one. I'm def not initially them correctly, but I'm unsure of how to do that. C# is also showing me a warning about this null reference.
System.InvalidOperationException: 'Data cannot be sent because the connection is in the disconnected state. Call start before sending any data.`
UPDATE - INSTALL SIGNALR CLIENT PACKAGE
I added using Microsoft.AspNetCore.SignalR.Client; to Worker.cs, and set a private readonly HubConnection _hubConnection; at the top of the Worker class.
Then I modified the following method to check the conn state, then send message:
private async Task CheckForNotifications()
{
if (_hubConnection.State == HubConnectionState.Disconnected)
{
await _hubConnection.StartAsync();
}
_ = _hubConnection.SendAsync("SendMessageToClient", "from service worker", "You've got mail!");
}
It all seems to be working successfully.
The thread does exited with code 0 at times, but I believe it's normal in this case since I'm running on a Task.Delay(5000);
Do not mix ASP.NET SignalR and ASP.NET Core SignalR. They are incompatible with each other. Your server is using ASP.NET Core SignalR so your worker should create a HubConnection by using the HubConnectionBuilder.
https://learn.microsoft.com/aspnet/core/signalr/dotnet-client?view=aspnetcore-3.1&tabs=visual-studio#connect-to-a-hub.

Running a SQL script (Transact-SQL) with a HostedService [ASP.NET CORE 3.1.0 C#]

I'm trying to run a script that saves my database with a service that I created:
Here is the code of my service:
using Microsoft.Extensions.Logging;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;
using System;
using System.Data.SqlClient;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace TEST_BACKUP_DB
{
internal interface IScopedDBService
{
Task DoWork(CancellationToken stoppingToken);
}
public class ScopedDBService : IScopedDBService
{
private int executionCount = 0;
private readonly ILogger _logger;
public ScopedDBService(ILogger<ScopedDBService> logger)
{
_logger = logger;
}
public async Task DoWork(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
string sqlConnectionString =
#"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=C:\Users\user1\Documents\DB_TEST.mdf;Integrated Security=True;Connect Timeout=30";
string script = File.ReadAllText(#"myscript.sql");
try {
SqlConnection conn = new SqlConnection(sqlConnectionString);
Server server = new Server(new ServerConnection(conn));
server.ConnectionContext.ExecuteNonQuery(script);
executionCount++;
_logger.LogInformation(
"Saving DB count: {Count}", executionCount);
}
catch(Exception e)
{
_logger.LogInformation("Error not saved: {e}",e);
}
await Task.Delay(5000, stoppingToken);
}
}
}
}
To consume that service I'm using the following code:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
using static TEST_BACKUP_DB.ScopedDBService;
namespace TEST_BACKUP_DB
{
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedDBService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await Task.CompletedTask;
}
}
}
I have registred them on Startup.cs Class as following
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedDBService, ScopedDBService>();
}
My Transact-SQL(myscript.sql) file is:
BACKUP DATABASE [DB_TEST]
TO DISK = 'D:\BACKUPS\_1.BAK'
MIRROR TO DISK = 'D:\BACKUPS\_2.BAK'
WITH FORMAT
GO
I have tested the scopped service without this part of code, and it's working fine:
Server server = new Server(new ServerConnection(conn));
server.ConnectionContext.ExecuteNonQuery(script);
When I start my app it gives me this exception:
When I debug I get this on the server attribute:
ActiveDirectory = 'server.ActiveDirectory' threw an exception of type
'Microsoft.SqlServer.Management.Smo.UnsupportedFeatureException'
I have installed the nugetPackage: Microsoft.SqlServer.SqlManagementObject to use the Server Class, is the problem coming from there? Does anyone have any alternatives or solutions?
You don't need SMO to run a TSQL batch, and the BACKUP/RESTORE errors will all be in the SqlException.Errors collection. You are only seeing the last one.
EG
using (SqlConnection conn = new SqlConnection(sqlConnectionString))
{
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = script;
try
{
cmd.ExecuteNonQuery();
}
catch (SqlException ex)
{
foreach (var e in ex.Errors)
{
Console.WriteLine(e.ToString());
}
throw;
}
}
will output the root cause, something like:
Microsoft.Data.SqlClient.SqlError: Database 'xxxxx' does not exist. Make sure that the name is entered correctly.
Microsoft.Data.SqlClient.SqlError: BACKUP DATABASE is terminating abnormally.

Does Visual Studio extension packages support asyncronous operations

I am creating a custom visual studio extension. In its initialization code I use async-await to make async WebRequest to a remote endpoint.
The problem is that the moment I call await statement, the execution of my custom extension initialization halts and never gets back to it. I suspect VS extensions initialization code is somehow special when it comes to async programming. If anyone could point me to an explanation of why this is happening, I'd greatly appreciate it.
There is a new Package called AsyncPackage that allows async loading of your package. It has an AsyncInitialize. Taken from MSDN:
You need to add a reference to Microsoft.VisualStudio.Shell.Interop.14.0.DesignTime for this to work
using System;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using System.Threading;
using System.Runtime.CompilerServices;
using System.IO;
namespace asyncpackagetest
{
[ProvideService((typeof(STextWriterService)), IsAsyncQueryable = true)]
[ProvideAutoLoad(VSConstants.UICONTEXT.NoSolution_string, PackageAutoLoadFlags.BackgroundLoad)]
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[Guid(VSPackage1.PackageGuidString)]
public sealed class VSPackage1 : AsyncPackage
{
public const string PackageGuidString = "3dc186f8-2146-4d89-ac33-e216b3ead526";
public VSPackage1()
{
}
protected override async System.Threading.Tasks.Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
this.AddService(typeof(STextWriterService), CreateService);
await base.InitializeAsync(cancellationToken, progress);
ITextWriterService textService = await this.GetServiceAsync(typeof(STextWriterService)) as ITextWriterService;
await textService.WriteLineAsync(#"c:\windows\temp\async_service.txt", "this is a test");
}
public async System.Threading.Tasks.Task<object> CreateService(IAsyncServiceContainer container, CancellationToken cancellationToken, Type serviceType)
{
STextWriterService service = null;
await System.Threading.Tasks.Task.Run(() => {
service = new TextWriterService(this);
});
return service;
}
public class TextWriterService : STextWriterService, ITextWriterService
{
private Microsoft.VisualStudio.Shell.IAsyncServiceProvider serviceProvider;
public TextWriterService(Microsoft.VisualStudio.Shell.IAsyncServiceProvider provider)
{
serviceProvider = provider;
}
public async System.Threading.Tasks.Task WriteLineAsync(string path, string line)
{
StreamWriter writer = new StreamWriter(path);
await writer.WriteLineAsync(line);
writer.Close();
}
public TaskAwaiter GetAwaiter()
{
return new TaskAwaiter();
}
}
public interface STextWriterService
{
}
public interface ITextWriterService
{
System.Threading.Tasks.Task WriteLineAsync(string path, string line);
TaskAwaiter GetAwaiter();
}
}
}

Categories

Resources