skip to Main Content

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

enter image description here

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


  1. The Action<T> delegate already implements an Equals() method which fits your requirement:

    The methods and targets are compared for equality as follows:

    • If the two methods being compared are both static and are the same method on the same class, the methods are considered equal and the targets are also considered equal.

    • If the two methods being compared are instance methods and are the same method on the same object, the methods are considered equal and the targets are also considered equal.

    • Otherwise, the methods are not considered to be equal and the targets are also not considered to be equal.

    This means that in your example, the action instance1.TheAction is not the same as the instance2.TheAction action, because they reference different targets (instance1 and instance2 reference different instances). And these actions are not equal to the Registrant.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 the InternalDict field to a ISet you will get your desired requirements. See the following code:

    /// <summary>
    /// Given is a container class holding a dictionary of actions.
    /// </summary>
    public class Container
    {
    
        protected readonly ISet<Action<ISomething>> InternalDict = new HashSet<Action<ISomething>>();
    
        // returns the amount of added elements
        public int Count => InternalDict.Count;
    
        // adds an element if it is not already added
        // and yes you need to leave the signature as it is
        public void Add(Action<ISomething> action)
        {
            InternalDict.Add(action);
        }
    
        // iterates over all added elements
        public void Invoke()
        {
            SomethingA something = new SomethingA();
            foreach (var kvp in InternalDict)
            {
                kvp(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)
    }
    
    // some class providing the actions that should be added to the dictionary
    public class Registrant
    {
    
        public static int StaticCount { get; private set; }
    
        public int Count { get; private set; }
    
        public static void TheStaticAction(ISomething a) { StaticCount++; }
    
        public void TheAction(ISomething a) { Count++; }
    }
    
    public class Program
    {       
        public static void Main(string[] args)
        {
            // create the setup
            Container  container = new Container();
            Registrant instance1 = new Registrant();
            Registrant instance2 = new Registrant();
    
            // do the add calls and check state
    
            // add 1: valid
            container.Add(instance1.TheAction);
            Console.WriteLine($"valid: {container.Count == 1} > instance1.TheAction<SomethingA>(...) added");
    
            // add 2: invalid (the action is already registered)
            container.Add(instance1.TheAction);
            Console.WriteLine($"valid: {container.Count == 1} > instance1.TheAction<SomethingA>(...) skipped");
    
            // add 3: valid (same method of a class but different instance of the class)
            container.Add(instance2.TheAction);
            Console.WriteLine($"valid: {container.Count == 2} > instance1.TheAction<SomethingA>(...) added");
    
            // add 4: invalid (the action is already registered)
            container.Add(instance2.TheAction);
            Console.WriteLine($"valid: {container.Count == 2} > instance1.TheAction<SomethingA>(...) skipped");
    
            // add 5: valid
            container.Add(Registrant.TheStaticAction);
            Console.WriteLine($"valid: {container.Count == 3} > Registrant.TheStaticAction<SomethingA>(...) added");
    
            // add 6: invalid (static methods can't be added twice)
            container.Add(Registrant.TheStaticAction);
            Console.WriteLine($"valid: {container.Count == 3} > Registrant.TheStaticAction<SomethingA>(...) skipped");
    
            // invoking
            container.Invoke();
    
            // and cross checking (all actions called only once though tried to add them twice)
            Console.WriteLine($"valid: {instance1.Count == 1} > instance1.Count == 1");
            Console.WriteLine($"valid: {instance2.Count == 1} > instance2.Count == 1");
            Console.WriteLine($"valid: {Registrant.StaticCount == 1} > Registrant.StaticCount == 1");
    
            // for showing the Equals() behavior:
            var foo = instance1.TheAction;
            Console.WriteLine(foo);
            var foo2 = instance2.TheAction;
            Console.WriteLine(foo2);
            Console.WriteLine(foo == foo2);
            Console.WriteLine(foo.Equals(foo2));
            Console.WriteLine(Equals(foo, foo2));
            Console.WriteLine(ReferenceEquals(foo, foo2));
        }
    }  
    

    This will generate the following output:

    valid: True > Registrant.TheStaticAction<SomethingA>(...) added
    valid: True > Registrant.TheStaticAction<SomethingA>(...) skipped
    valid: True > instance1.Count == 1
    valid: True > instance2.Count == 1
    valid: True > Registrant.StaticCount == 1
    System.Action`1[Testing.ISomething]
    System.Action`1[Testing.ISomething]
    False
    False
    False
    False
    

    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() or ReferenceEquals()).

    Using the correct working generic type/arguments however, which I have removed here, is a different problem…

    Login or Signup to reply.
  2. It looks to me that you cannot store a Action<ISomething>, because you don’t know what exact type the Action will take as a parameter. You can only upcast it to a more derived ISomething, 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 to T because we know we will get the right one from the dictionary using typeof(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 to T. So each one gets stored in the inner dictionary as a pair of Delegate, Action<ISomething>, and the lambda is created simply as obj => action((T)obj).

    public class Container : IContainer
    {
        protected readonly ConcurrentDictionary<Type, ConcurrentDictionary<Delegate, 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
        public void Add<T>(Action<T> action) where T : ISomething
        {
            InternalDict.AddOrUpdate(typeof(T),
                (key, arg) => new ConcurrentDictionary<Delegate, Action<ISomething>>(new[] { arg }),
                (key, innerDict, arg) => {
                    innerDict.TryAdd(arg.Key, arg.Value);
                    return innerDict;
                },
                new KeyValuePair<Delegate, Action<ISomething>>(action, obj => action((T)obj))
            );
        }
    
        public void Remove<T>(Action<T> action) where T : ISomething
        {
            if (InternalDict.TryGetValue(typeof(T), out var innerDict))
            {
                innerDict.TryRemove(action, out var actor);
            }
        }
        
        public void Share<T>(T something) where T : ISomething
        {
            InternalQueue.Enqueue(something);
            // start BackgroundJob for invoking actions added to the given type
            // unclear what should go here, maybe a Task.Run??
        }
    
        // 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);
                    }
                }
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search