I need to store Action<T>
in a ConcurrentDictionary
and I am wrangling my head around the question:
What identifies an action as unique and how to store it in the dictionary so the dictionary ends up without duplicates?
In my scenario uniquness means if two instances of a class add the action to the dictionary they are unique.
A static method can be added only once.
Thoughts I had to identify the uniqueness (aka answer for "what have you tried so far?")
Thought 1:
My first approach has been to store the action directly but the compiler told me it isn't allowed due to the mismatch between generics Action<T>
and the definition of the dictionary ConcurrentDictionary<Action<ISomething>, string>
.
So I tried to flip key and value but what to take as key unique key then?
Thought 2
Using action.GetHashCode()
may result in conflicts.
Thought 3
If I go with action.Method.DeclaringType
plus action.Method.Name
both would have the same key.
If I go with action.Target.GetType().FullName
+ action.Method.Name
it won't work because the action can be static and action.Taget will be null.
Provide some code:
Please feel free to copy paste this executable sample into a .NET6 ConsoleApplication template Program.cs
file within Visual Studio 2022.
See the method Container.Add
to find my problem.
using System.Collections.Concurrent;
using System.Diagnostics.Metrics;
namespace UniquenessOfActionsInCSharp
{
public interface IContainer
{
void Add<T>(Action<T> action) where T : ISomething;
void Remove<T>(Action<T> action) where T : ISomething;
void Share<T>(T something) where T : ISomething;
}
/// <summary>
/// Given is a container class holding a dictionary of actions.
/// </summary>
public class Container : IContainer
{
//protected readonly ConcurrentDictionary<Action<ISomething>, string> InternalDict = new();
protected readonly ConcurrentDictionary<Type, ConcurrentDictionary<string, Action<ISomething>>> InternalDict = new();
protected readonly ConcurrentQueue<ISomething> InternalQueue = new ConcurrentQueue<ISomething>();
// returns the amount of added elements
public int Count<T>() => InternalDict.TryGetValue(typeof(T), out var innerDict) ? innerDict.Count : 0;
// adds an element if it is not already added
// and yes you need to leave the signature as it is
public void Add<T>(Action<T> action) where T : ISomething
{
// check uniqueness of an action and only add to the InternalDict if it is not already added
// TODO: the question is how to implement this method
//InternalSet.Add((x) => action((T)x));
}
public void Remove<T>(Action<T> action) where T : ISomething {}
public void Share<T>(T something) where T : ISomething
{
// add something to a queue
// start BackgroundJob for invoking actions added to the given type
}
// iterates over all added elements
protected void BackgroundJob()
{
while (InternalQueue.TryDequeue(out ISomething something))
{
if (InternalDict.TryGetValue(something.GetType(), out var innerDict))
{
foreach (var kvp in innerDict)
{
kvp.Value(something);
}
}
}
}
}
// there are multiple implementations of ISomething
public interface ISomething
{
string Foo { get; set; }
}
// but for the sake of simplicity I just added SomethingA
public class SomethingA : ISomething
{
public string Foo { get; set; } = "Bar";
// some other properties (different to each implementation)
}
public class SomethingB : ISomething
{
public string Foo { get; set; } = "Foo";
}
// some class providing the actions that should be added to the dictionary
public class Registrant
{
public static int StaticCount { get; private set; }
public int CountA { get; private set; }
public int CountB { get; private set; }
public static void TheStaticAction(SomethingA a) { StaticCount++; }
public void TheActionA(SomethingA a) { CountA++; }
public void TheActionB(SomethingB b) { CountB++; }
}
// an executable code sample for those who mutters if it isn't there
public class Program
{
// the use case
static void Main(string[] args)
{
// create the setup
Container container = new Container();
Registrant instance1 = new Registrant();
Registrant instance2 = new Registrant();
Registrant instance3 = new Registrant();
// do the add calls and check state
// add 1: valid
container.Add<SomethingA>(instance1.TheActionA);
Console.WriteLine($"valid: {container.Count<SomethingA>() == 1} > instance1.TheActionA<SomethingA>(...) added");
// add 2: invalid (the action is already registered)
container.Add<SomethingA>(instance1.TheActionA);
Console.WriteLine($"valid: {container.Count<SomethingA>() == 1} > instance1.TheActionA<SomethingA>(...) skipped");
// add 3: valid (same method of a class but different instance of the class)
container.Add<SomethingA>(instance2.TheActionA);
Console.WriteLine($"valid: {container.Count<SomethingA>() == 2} > instance1.TheActionA<SomethingA>(...) added");
// add 4: invalid (the action is already registered)
container.Add<SomethingA>(instance2.TheActionA);
Console.WriteLine($"valid: {container.Count<SomethingA>() == 2} > instance1.TheActionA<SomethingA>(...) skipped");
// add 5: valid
container.Add<SomethingA>(Registrant.TheStaticAction);
Console.WriteLine($"valid: {container.Count<SomethingA>() == 3} > Registrant.TheStaticAction<SomethingA>(...) added");
// add 6: invalid (static methods can't be added twice)
container.Add<SomethingA>(Registrant.TheStaticAction);
Console.WriteLine($"valid: {container.Count<SomethingA>() == 3} > Registrant.TheStaticAction<SomethingA>(...) skipped");
// add 7: valid (same method of a class but different instance of the class)
container.Add<SomethingB>(instance3.TheActionB);
Console.WriteLine($"valid: {container.Count<SomethingB>() == 1} > instance1.TheAction<SomethingB>(...) added");
// add 8: invalid (the action is already registered)
container.Add<SomethingB>(instance3.TheActionB);
Console.WriteLine($"valid: {container.Count<SomethingB>() == 1} > instance1.TheAction<SomethingB>(...) skipped");
// invoking
container.Share(new SomethingB());
container.Share(new SomethingA());
Thread.Sleep(5000);
// and cross checking (all actions called only once though tried to add them twice)
Console.WriteLine($"valid: {instance1.CountA == 1 && instance1.CountB == 0} > instance1.CountA == {instance1.CountA} && instance1.CountB == {instance1.CountB}");
Console.WriteLine($"valid: {instance2.CountA == 1 && instance2.CountB == 0} > instance2.CountA == {instance2.CountA} && instance2.CountB == {instance2.CountB}");
Console.WriteLine($"valid: {Registrant.StaticCount == 1} > Registrant.StaticCount == {Registrant.StaticCount}");
Console.WriteLine($"valid: {instance3.CountA == 0 && instance3.CountB == 1} > instance3.CountA = {instance3.CountA} && instance3.CountB == {instance3.CountB}");
}
}
}
If the console output writes "valid: true >" in each line my question is answered.
The hashset approach
I can add with
InternalSet.Add((x) => action((T)x));
but loosing all chance for checking uniqueness. So I decided for a CONCURRENT dictionary where I need some key.
Bounty:
I don't care which collection is used.
I don't care how concurrency is handled as long it is handled.
It is not allowed to change the interface removing the generic.
by the way I already have an working solution in my code using a dictionary but I am asking to may find better solutions because I am not satisfied with my current code.