1

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

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.


monty
  • 7,888
  • 16
  • 63
  • 100
  • Actions are reference objects, which is not ideal for a key in a dictionary – Daniel A. White Jul 06 '23 at 21:23
  • @DanielA.White thanks for pointing out. So I try to find a way to uniquely identify an action. – monty Jul 06 '23 at 21:26
  • @Progman Any hint what is unclear on a 6 word question (title)? – monty Jul 06 '23 at 21:32
  • 2
    @monty It's not just the title. It's the content of the whole question. Why do you think Actions are unique? Why do you want the Action itself to be the key? What exactly is going to be looking up an Action in a dictionary, and what will it do with one that it finds? – Logarr Jul 06 '23 at 21:42
  • @Logarr Why do I want the Action itself to be the key? I don't persist on it as state by "I tried to flip key and value" – monty Jul 06 '23 at 22:38
  • @Logarr Why do I think Actions are unique? I don't think this but I hope so. So I try to ask if they are or not and if yes how figure it out. – monty Jul 06 '23 at 22:39
  • @Logarr What exactly is going to be looking up an Action in a dictionary, and what will it do with one that it finds? Ahm, whoot? Does it really matter who looks up in the dictionary? If one finds the Action it will be invoked! – monty Jul 06 '23 at 22:41
  • I am currently rewriting my question with an executable code sample. – monty Jul 06 '23 at 22:42
  • updated question with more formatting, explanation and an executable copy paste code sample (and some maybe helpfull links) – monty Jul 06 '23 at 23:19
  • 2
    `yourAction.Equals(otherAction)` should be enough. So using the default comparer will cover all your cases. See https://learn.microsoft.com/en-us/dotnet/api/system.delegate.equals?view=net-7.0 – Charlieface Jul 07 '23 at 00:56
  • @Charlieface That may would work but I don't get the action into the collection/dictionary so I can't compare them. (You will know what I mean if you try.) – monty Jul 07 '23 at 07:46
  • 3
    What does the key of the `ConcurrentDictionary` represent? It seems like it's not actuall being used anywhere. Because it sounds like you just need a `HashSet>` instead, then you can just do `_setOfActions.Add(action);` and it will sort out uniqueness for you. To make it concurrent just do `lock(_setOfActions)` around each usage of it. – Charlieface Jul 07 '23 at 09:24
  • @Charlieface Added a screenshot to the question. Did I get your approach right? – monty Jul 07 '23 at 10:27
  • You need to make it non-generic `public void Add(Action action) => InternalSet.Add(action);` And you don't need to call `Contains` because `Add` will check and not add it if it's there already. Just do `Add` – Charlieface Jul 07 '23 at 10:30
  • @Charlieface Except for the code-as-image thing, and the fact that there is already a [`ConcurrentBag`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentbag-1?view=net-6.0) class, among other [thread-safe collection classes](https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/), out there... – Heretic Monkey Jul 07 '23 at 15:27
  • @HereticMonkey The code-as-image was because there was a compile error and OP was trying to show it, it also wasn't directly part of the question, more of just a big reply to my comment. There is no thread-safe `HashSet` as yet https://stackoverflow.com/questions/18922985/concurrent-hashsett-in-net-framework – Charlieface Jul 07 '23 at 16:45
  • @Charlieface Pointing to a 10 year old question as timely proof is not exactly encouraging; the link I gave was last updated in January of 2023; I'm well aware of what's available. Depending on the use case, a `ConcurrentBag` can be sufficient, and may be lock-free. – Heretic Monkey Jul 07 '23 at 19:18
  • @HereticMonkey You can take a look at `System.Collections.Concurrent` https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent?view=net-7.0 there s still no concurrent collection that implements set-based logic. `ConcurrentBag` does not do this. `ConcurrentDictionary` can be worked in to make some sort of solution, but like I said it's probably easier to just `lock` on a `HashSet`. – Charlieface Jul 08 '23 at 23:49
  • 1
    I think you need to be clearer about the use of generics here, as it doesn't make sense. Think of it this way: suppose you have two classes `Something1 : ISomething` and `Something2 : ISomething`. And then you call `Add` with a `Action`, bear in mind that this lambda *only* accepts `Something1` or more derived, it does not accept just any `ISomething`. You then create a `new Something1()` and call the lambda, what do you expect to happen? This is called *contravariance* and is only allowed if the `Something2` was itself derived from `Something1`. On the other hand if you have .... – Charlieface Jul 09 '23 at 10:00
  • 1
    .... a `Func` then you can use *covariance* to downcast it to a `Func`, as the contract is still fulfilled that it will return *any* `ISomething`. So if you can be clearer about what exactly you are doing, Action vs Func, and how you would use generics in this case, then we can understand better how to achieve that. Otherwise the answer you have so far should work for you existing use case, just with the addition of `lock`. – Charlieface Jul 09 '23 at 10:02
  • Meant to say before: "You then create a **`new Something2()`** and call the lambda" – Charlieface Jul 09 '23 at 16:30
  • @Progman updated the code sample (though it is not executing anymore) But I fear we are loosing focus. I need a way to determine the uniqueness of an action not a full reimplementation of service. But hopefully it makes things clearer how I use it – monty Jul 10 '23 at 05:28
  • @charlieface Updated the code sample and provided as much information as I could without sharing the full propieatary code. But also refocus on the question how to check the uniquness of an action and not the full service implementation. I am not getting it why that much detailed information is needed for the original question – monty Jul 10 '23 at 05:31
  • 2
    It's now much clearer why you need to have this generic, because you are working on a list of different `ISomething` on another thread, but need to only execute the right set of delegates. – Charlieface Jul 10 '23 at 09:31

2 Answers2

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);
                }
            }
        }
    }
}

Note that something.GetType() may not return a result from your dictionary if there is a multi-level inheritance tree. So you may need to recursively go down the inheritance stack

    protected void BackgroundJob()
    {
        while (InternalQueue.TryDequeue(out ISomething something))
        {
            for (var type = something.GetType(); type != typeof(object); type = type.BaseType)
            {
                if (InternalDict.TryGetValue(something.GetType(), out var innerDict))
                {
                    foreach (var kvp in innerDict)
                    {
                        kvp.Value(something);
                    }
                    break;  // continue the while loop
                }
            }
        }
    }
Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • love it (will test it in my code at least tomorrow) – monty Jul 11 '23 at 04:16
  • could you explain me why it works with delegate as key? – monty Jul 11 '23 at 04:16
  • 1
    Because `Delegate` implements `Equals` and `GetHashCode` which internally check whether the method *and* instance are the same. See https://learn.microsoft.com/en-us/dotnet/api/system.delegate.equals?view=net-7.0 – Charlieface Jul 11 '23 at 04:20
  • thx for the comment actually meant why the gernic conflicts don't occur. But never mind. – monty Jul 12 '23 at 04:51
  • Haven't used the big AddOrUpdate but the rest works like charm according to my tests – monty Jul 12 '23 at 04:54
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<Delegate>, 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:

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 ISet<Delegate> actions = new HashSet<Delegate>();

    protected readonly ConcurrentQueue<ISomething> InternalQueue = new ConcurrentQueue<ISomething>();

    // returns the amount of added elements
    public int Count<T>()
    {
        return actions.OfType<Action<T>>().Count();
    }
    
    // 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
    {
        actions.Add(action);
    }

    public void Remove<T>(Action<T> action) where T : ISomething
    {
        actions.Remove(action);
    }

    
    public void Share<T>(T something) where T : ISomething
    {
        // add something to a queue
        // start BackgroundJob for invoking actions added to the given type
        InternalQueue.Enqueue(something);
    }

    // iterates over all added elements
    public void BackgroundJob()
    {
        while (InternalQueue.TryDequeue(out ISomething something))
        {
            //Console.WriteLine("Got a: "+something);
            foreach (Delegate entry in this.actions)
            {
                ParameterInfo[] parameters = entry.Method.GetParameters();
                ParameterInfo parameter = parameters[0];
                Type parameterType = parameter.ParameterType;
                if (parameterType == something.GetType())
                {
                    //Console.WriteLine("Match, call it");
                    entry.DynamicInvoke(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());

        container.BackgroundJob();

        // 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");

    }

}

(For testing purposes, I have made the BackgroundJob() method public)

The BackgroundJob() method is a little bit more complicated since you have to search for the right Action<T>/Delegate to use. This implementation will look at the argument type of the first parameter of the method and compare it against the type of the next queue item. If it is a match, it will execute the action/delegate.

When you run this code you will get the following output:

valid: True > instance1.TheActionA<SomethingA>(...) skipped
valid: True > instance1.TheActionA<SomethingA>(...) added
valid: True > instance1.TheActionA<SomethingA>(...) skipped
valid: True > Registrant.TheStaticAction<SomethingA>(...) added
valid: True > Registrant.TheStaticAction<SomethingA>(...) skipped
valid: True > instance1.TheAction<SomethingB>(...) added
valid: True > instance1.TheAction<SomethingB>(...) skipped
valid: True > instance1.CountA == 1 && instance1.CountB == 0
valid: True > instance2.CountA == 1 && instance2.CountB == 0
valid: True > Registrant.StaticCount == 1
valid: True > instance3.CountA == 0 && instance3.CountB == 1

As you see, all your asserts are true.

You can add lock blocks to handle concurrencies depending on your specific requirements. A simple lock around each access would work, which will fulfill your requirement:

I don't care how concurrency is handled as long it is handled.

Progman
  • 16,827
  • 6
  • 33
  • 48