I have the following class which I have simplified for this example
public class SearchService : ISearchService
{
private static Dictionary<long, object> _resultCache = new Dictionary<long, object>();
public SearchService()
{
_resultCache.Add(new Random().Next(),new Cat());
}
}
This service is registered in a .net core service as
service.AddScoped<ISearchService,SearchService>();
Meaning a new service will be registered for every request
Would the static variable in the instance class cause the class not to be disposed of?
and therefore cause a memory leak
If you actual intent here is to associate your Cat
object with every SearchService
object currently allocated without preventing the garbage collection of instances of either, you can use ConditionalWeakTable<TKey,TValue>
for this:
The
ConditionalWeakTable<TKey,TValue>
class differs from other collection objects in its management of the object lifetime of keys stored in the collection. Ordinarily, when an object is stored in a collection, its lifetime lasts until it is removed (and there are no additional references to the object) or until the collection object itself is destroyed. However, in theConditionalWeakTable<TKey,TValue>
class, adding a key/value pair to the table does not ensure that the key will persist, even if it can be reached directly from a value stored in the table (for example, if the table contains one key, A, with a value V1, and a second key, B, with a value P2 that contains a reference to A). Instead,ConditionalWeakTable<TKey,TValue>
automatically removes the key/value entry as soon as no other references to a key exist outside the table. [1]Instances of the
ConditionalWeakTable<TKey,TValue>
class are thread safe. They do not require callers to do any additional locking.
Thus if you modify your SearchService
as follows:
public class SearchService : ISearchService
{
static ConditionalWeakTable<SearchService, object> _resultCache = new();
public SearchService()
{
_resultCache.Add(this, new Cat());
}
public static bool TryGetCat(ISearchService iService, out Cat cat)
{
cat = null;
return iService is SearchService service && _resultCache.TryGetValue(service, out var obj) && ((cat = obj as Cat) != null);
}
public static IEnumerable<Cat> AllCats => _resultCache.Select(p => p.Value).OfType<Cat>();
public static IEnumerable<ISearchService> AllServces => _resultCache.Select(p => p.Key);
}
The following test will now execute successfully and demonstrate that no references to SearchService
or Cat
were retained after they were no longer used elsewhere:
static void TestSearchService()
{
static void Setup()
{
ISearchService service = new SearchService();
Assert.That(SearchService.TryGetCat(service, out var cat));
Assert.That(SearchService.AllServces.Count() > 0);
Assert.That(SearchService.AllCats.Count() > 0);
GC.KeepAlive(service); // Make sure service is not collected before the above asserts complete.
}
Setup();
GC.Collect();
Assert.That(SearchService.AllServces.Count() == 0);
Assert.That(SearchService.AllCats.Count() == 0);
}
Demo fiddle here.
[1] Some testing shows the documentation statement to be imprecise. Instead it appears from the unit tests for ConditionalWeakTable
that it automatically removes the key/value entry as soon as no other references to the key exist outside the table other than weak references and the key gets garbage collected. (The tests trigger this by calling GC.Collect()
directly.)
See also the source comments:
** Lifetimes of keys and values: ** ** Inserting a key and value into the dictonary will not ** prevent the key from dying, even if the key is strongly reachable ** from the value. ** ** Prior to ConditionalWeakTable, the CLR did not expose ** the functionality needed to implement this guarantee. ** ** Once the key dies, the dictionary automatically removes ** the key/value entry.
Since your requirement here is to prevent memory leaks and allow garbage collection of SearchService
and Cat
, this is sufficient.