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>(instance2.TheActionB);
Console.WriteLine($"valid: {container.Count<SomethingB>() == 1} > instance1.TheAction<SomethingB>(...) skipped");
// invoking
container.Share(new SomethingB());
container.Share(new SomethingA());
// 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 == 1 && instance1.CountB == 0");
Console.WriteLine($"valid: {instance2.CountA == 1 && instance2.CountB == 0} > instance2.CountA == 1 && instance2.CountB == 0");
Console.WriteLine($"valid: {Registrant.StaticCount == 1} > Registrant.StaticCount == 1");
Console.WriteLine($"valid: {instance3.CountA == 0 && instance3.CountB == 1} > instance3.CountA == 0 && instance3.CountB == 1");
}
}
}
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.
2
Answers
The
Action<T> delegate
already implements anEquals()
method which fits your requirement:This means that in your example, the action
instance1.TheAction
is not the same as theinstance2.TheAction
action, because they reference different targets (instance1
andinstance2
reference different instances). And these actions are not equal to theRegistrant.TheStaticAction
action, because they don’t reference the same method (TheAction
vs.TheStaticAction
) as well as not reference the same target (instance1
vs.null
).This allows you to simply use a
ISet<T>
, which doesn’t allow adding a second instance which is equal with an element already in the set. If you change the type of theInternalDict
field to aISet
you will get your desired requirements. See the following code:This will generate the following output:
As you see, all your asserts are
true
. You also see by the additional debug lines that the two example actions you have are not equal in any way (be it==
,Equals()
orReferenceEquals()
).Using the correct working generic type/arguments however, which I have removed here, is a different problem…
It looks to me that you cannot store a
Action<ISomething>
, because you don’t know what exact type theAction
will take as a parameter. You can only upcast it to a more derivedISomething
, not downcast it to an interface.So instead we can just declare it as
Delegate
(which has equality functions built in), and we can cast it toT
because we know we will get the right one from the dictionary usingtypeof(T)
.In order to call this, we could use reflection. But a much better option is to instead store with each delegate, a lambda that knows how to cast an
ISomething
toT
. So each one gets stored in the inner dictionary as a pair ofDelegate, Action<ISomething>
, and the lambda is created simply asobj => action((T)obj)
.