I may know the answer to my posted question: I'm using constructor dependency injection throughout the entire application which is a looped C# console application that does not exit after each request.
I suspect the life time of all of the included objects is essentially infinite due to this. When attempting to adjust the life time while registering, it warns that a transient object cannot be implemented on a singleton object due to dependencies (which inspired looking at memory utilization and this question).
This is my first ground up console application, a bot, that logs into a service provider and waits for messages. I come from .NET Core Web API which again has dependencies all over, but I think the key difference here is below all of my code is the platform itself which handles each request individually then kills the thread that ran.
How close am I? Would I have to separate the bot itself from the base console application listening to the service provider and attempt to replicate the platform that IIS/kestrel/MVC routing provides to separate the individual requests?
Edit: Originally I intended this question as more of a design principal, best practice, or asking for direction direction. Folks requested reproducible code so here we go:
namespace BotLesson
{
internal class Program
{
private static readonly Container Container;
static Program()
{
Container = new Container();
}
private static void Main(string[] args)
{
var config = new Configuration(args);
Container.AddConfiguration(args);
Container.AddLogging(config);
Container.Register<ITelegramBotClient>(() => new TelegramBotClient(config["TelegramToken"])
{
Timeout = TimeSpan.FromSeconds(30)
});
Container.Register<IBot, Bot>();
Container.Register<ISignalHandler, SignalHandler>();
Container.Register<IEventHandler, EventHandler>();
Container.Register<IEvent, MessageEvent>();
Container.Verify();
Container.GetInstance<IBot>().Process();
Container?.Dispose();
}
}
}
Bot.cs
namespace BotLesson
{
internal class Bot : IBot
{
private readonly ITelegramBotClient _client;
private readonly ISignalHandler _signalHandler;
private bool _disposed;
public Bot(ITelegramBotClient client, IEventHandler handler, ISignalHandler signalHandler)
{
_signalHandler = signalHandler;
_client = client;
_client.OnCallbackQuery += handler.OnCallbackQuery;
_client.OnInlineQuery += handler.OnInlineQuery;
_client.OnInlineResultChosen += handler.OnInlineResultChosen;
_client.OnMessage += handler.OnMessage;
_client.OnMessageEdited += handler.OnMessageEdited;
_client.OnReceiveError += (sender, args) => Log.Error(args.ApiRequestException.Message, args.ApiRequestException);
_client.OnReceiveGeneralError += (sender, args) => Log.Error(args.Exception.Message, args.Exception);
_client.OnUpdate += handler.OnUpdate;
}
public void Process()
{
_signalHandler.Set();
_client.StartReceiving();
Log.Information("Application running");
_signalHandler.Wait();
Log.Information("Application shutting down");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) _client.StopReceiving();
_disposed = true;
}
}
}
EventHandler.cs
namespace BotLesson
{
internal class EventHandler : IEventHandler
{
public void OnCallbackQuery(object? sender, CallbackQueryEventArgs e)
{
Log.Debug("CallbackQueryEventArgs: {e}", e);
}
public void OnInlineQuery(object? sender, InlineQueryEventArgs e)
{
Log.Debug("InlineQueryEventArgs: {e}", e);
}
public void OnInlineResultChosen(object? sender, ChosenInlineResultEventArgs e)
{
Log.Debug("ChosenInlineResultEventArgs: {e}", e);
}
public void OnMessage(object? sender, MessageEventArgs e)
{
Log.Debug("MessageEventArgs: {e}", e);
}
public void OnMessageEdited(object? sender, MessageEventArgs e)
{
Log.Debug("MessageEventArgs: {e}", e);
}
public void OnReceiveError(object? sender, ReceiveErrorEventArgs e)
{
Log.Error(e.ApiRequestException, e.ApiRequestException.Message);
}
public void OnReceiveGeneralError(object? sender, ReceiveGeneralErrorEventArgs e)
{
Log.Error(e.Exception, e.Exception.Message);
}
public void OnUpdate(object? sender, UpdateEventArgs e)
{
Log.Debug("UpdateEventArgs: {e}", e);
}
}
}
SignalHandler.cs
This isn't directly related to my problem, but it is holding the application in a waiting pattern while the third party library listens for messages.
namespace BotLesson
{
internal class SignalHandler : ISignalHandler
{
private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false);
private readonly SetConsoleCtrlHandler? _setConsoleCtrlHandler;
public SignalHandler()
{
if (!NativeLibrary.TryLoad("Kernel32", typeof(Library).Assembly, null, out var kernel)) return;
if (NativeLibrary.TryGetExport(kernel, "SetConsoleCtrlHandler", out var intPtr))
_setConsoleCtrlHandler = (SetConsoleCtrlHandler) Marshal.GetDelegateForFunctionPointer(intPtr,
typeof(SetConsoleCtrlHandler));
}
public void Set()
{
if (_setConsoleCtrlHandler == null) Task.Factory.StartNew(UnixSignalHandler);
else _setConsoleCtrlHandler(WindowsSignalHandler, true);
}
public void Wait()
{
_resetEvent.WaitOne();
}
public void Exit()
{
_resetEvent.Set();
}
private void UnixSignalHandler()
{
UnixSignal[] signals =
{
new UnixSignal(Signum.SIGHUP),
new UnixSignal(Signum.SIGINT),
new UnixSignal(Signum.SIGQUIT),
new UnixSignal(Signum.SIGABRT),
new UnixSignal(Signum.SIGTERM)
};
UnixSignal.WaitAny(signals);
Exit();
}
private bool WindowsSignalHandler(WindowsCtrlType signal)
{
switch (signal)
{
case WindowsCtrlType.CtrlCEvent:
case WindowsCtrlType.CtrlBreakEvent:
case WindowsCtrlType.CtrlCloseEvent:
case WindowsCtrlType.CtrlLogoffEvent:
case WindowsCtrlType.CtrlShutdownEvent:
Exit();
break;
default:
throw new ArgumentOutOfRangeException(nameof(signal), signal, null);
}
return true;
}
private delegate bool SetConsoleCtrlHandler(SetConsoleCtrlEventHandler handlerRoutine, bool add);
private delegate bool SetConsoleCtrlEventHandler(WindowsCtrlType sig);
private enum WindowsCtrlType
{
CtrlCEvent = 0,
CtrlBreakEvent = 1,
CtrlCloseEvent = 2,
CtrlLogoffEvent = 5,
CtrlShutdownEvent = 6
}
}
}
My original point is based off of some assumptions I am making on SimpleInject--or more specifically the way I am using SimpleInject.
The application stays running, waiting on SignalHandler._resetEvent. Meanwhile messages come in via any of the handlers on Bot.cs constructor.
So my thought/theory is Main launches Bot.Process which has a direct dependency on ITelegramClient and IEventHandler. In my code there isn't a mechanism to let these resources go and I suspect I was assuming the IoC was going to perform magic and release resources.
However, sending messages to the bot continuously increases the number of objects, according to Visual Studio memory usage. This is reflected in actual process memory as well.
Though, while editing this post for approval, I think I may have ultimately been misinterpreting Visual Studio's diagnostic tools. The application's memory utilization seems to hang out at around 36 MB after 15 minutes of run time. Or it's simply increasing so little at a time that it's difficult to see.
Comparing Memory Usage snapshots I took at 1 minute versus 17 minutes, there appears to have been 1 of each of the objects above created. If I am reading this properly, I imagine that proves the IoC is not creating new objects (or they are being disposed before I have a chance to create a snapshot.
The key to your answer is in the resume of your observation when profiling your application's memory: "there appears to have been 1 of each of the objects above created". Since all those objects live inside an infinite application loop, you don't have to worry about their lifetime.
From the code you've posted, the only expensive objects that are created dynamically but won't accumulate during the lifetime of Bot
are the exception objects (and their associated call stacks), especially when exceptions are caught by a try-catch.
Assuming that the "Simple Injector" library you are using works properly, there is no reason to doubt the lifetime management being correctly implemented like you did. This means it only depends the way your container is configured.
Right now all your instances have a Transient lifetime, which is the default. It's important to notice this, as it appears you are expecting a Singleton lifetime.
Transient means a new instance for every request opposed to Singleton where the same shared instance is returned for each request. To achieve this behavior you must explicitly register the export with a Singleton lifetime defined:
// Container.GetInstance<IBot>() will now always return the same instance
Container.Register<IBot, Bot>(Lifestyle.Singleton);
Never use a Service Locator, especially when using Dependency Injection, just to manage an object's lifetime. As you can see, the IoC conatiner is designed to handle that. It's a key feature that is implemented by every IoC library. Service Locator can be and should be replaced by proper DI e.g., instead of passing around the IoC container you should inject abstract factories as a dependency. A direct dependency on the Service Locator introduces an unwanted tight coupling. It's very difficult to mock a dependency on a Service Locator when writing test cases.
The current implementation of Bot
is also quite dangerous whe thinking about memory leaks, especially in case of the exported TelegramBotClient
instance being Singleton and the EventHandler
having a transient lifetime.
You hook the EventHandler
to the TelegramBotClient
. When the lifetime of Bot
ends, you still have the TelegramBotClient
keeping the EventHandler
alive, which creates a memory leak. Also every new instance of Bot
would attach new event handlers to the TelegramBotClient
, resulting in multiple duplicate handler invocations.
To always be on the safe side you should either unsubscribe from the events immediately when they are handled or when the scopes lifetime ends e.g. in a Closed
event handler or in the Dispose
method. In this case make sure the object is disposed properly by client code. Since you can't always guarantee that a type like Bot
is disposed properly, you should consider to create configured shared instances of the TelegramBotClient
and EventHandler
using an abstract factory. This factory returns a shared TelegramBotClient
where all its events are observed by the shared EventHandler
.
This ensures that events are subscribe to only once.
But the most preferable solution is to use the Weak-Event pattern.
You should notice this as you seem to have some trouble to determine object lifetimes and potential memory leaks.
Using your code it is very easy to create a memory leak accidentally.
If you want to write robust applications, it is essential to know the main pitfalls to create memory leaks: Fighting Common WPF Memory Leaks with dotMemory, 8 Ways You can Cause Memory Leaks in .NET, 5 Techniques to avoid Memory Leaks by Events in C# .NET you should know