33

I'm trying to expose a read-only dictionary that holds objects with a read-only interface. Internally, the dictionary is write-able, and so are the objects within (see below example code). My problem is that IReadOnlyDictionary doesn't support covariant conversions because of the reason outlined in the question here. This means I can't just expose my internal dictionary as a read only one.

So my question is, is there an efficient way to convert my internal dictionary to an IReadOnlyDictionary, or some other way to handle this? The options I can think of are:

  1. Hold two internal dictionaries and keep them in sync.
  2. Create a new dictionary when the property is accessed and cast all the objects within.
  3. Cast the IReadOnly's back to NotReadOnly when using it internally.

1 seems like a pain, 2 seems highly inefficient. 3 sounds like the most promising at the moment, but is still ugly. Do I have any other options?

public class ExposesReadOnly
{
    private Dictionary<int, NotReadOnly> InternalDict { get; set; }
    public IReadOnlyDictionary<int, IReadOnly> PublicList
    {
        get
        {
            // This doesn't work...
            return this.InternalDict;
        }
    }

    // This class can be modified internally, but I don't want
    // to expose this functionality.
    private class NotReadOnly : IReadOnly
    {
        public string Name { get; set; }
    }
}

public interface IReadOnly
{
    string Name { get; }
}
Community
  • 1
  • 1
Ocelot20
  • 10,510
  • 11
  • 55
  • 96

5 Answers5

25

You could write your own read-only wrapper for the dictionary, e.g.:

public class ReadOnlyDictionaryWrapper<TKey, TValue, TReadOnlyValue> : IReadOnlyDictionary<TKey, TReadOnlyValue> where TValue : TReadOnlyValue
{
    private IDictionary<TKey, TValue> _dictionary;

    public ReadOnlyDictionaryWrapper(IDictionary<TKey, TValue> dictionary)
    {
        if (dictionary == null) throw new ArgumentNullException("dictionary");
        _dictionary = dictionary;
    }
    public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); }

    public IEnumerable<TKey> Keys { get { return _dictionary.Keys; } }

    public bool TryGetValue(TKey key, out TReadOnlyValue value)
    {
        TValue v;
        var result = _dictionary.TryGetValue(key, out v);
        value = v;
        return result;
    }

    public IEnumerable<TReadOnlyValue> Values { get { return _dictionary.Values.Cast<TReadOnlyValue>(); } }

    public TReadOnlyValue this[TKey key] { get { return _dictionary[key]; } }

    public int Count { get { return _dictionary.Count; } }

    public IEnumerator<KeyValuePair<TKey, TReadOnlyValue>> GetEnumerator()
    {
        return _dictionary
                    .Select(x => new KeyValuePair<TKey, TReadOnlyValue>(x.Key, x.Value))
                    .GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
Joe
  • 122,218
  • 32
  • 205
  • 338
  • +1 I did something like this when I needed a dictionary that had both mutable and immutable views. – Binary Worrier Nov 28 '12 at 10:45
  • this is more like a readonly view of a dictionary. The dictionary itself can still change. – vidstige Jan 09 '13 at 15:36
  • 1
    @vidstige, "The dictionary itself can still change" - of course, which is why it's private. – Joe Jan 11 '13 at 05:40
  • @Joe i don't think you understand. Even if it's private I can still keep a reference to the dictionary after its passed to the constructor. And thus change it later. So more of a readonly view of a writable dictionary, but can still be helpful. – vidstige Jan 11 '13 at 06:43
  • 3
    @vidstige that can easily be solved by creating new dictionary from the passed in dictionary in the constructor. – danze Apr 15 '13 at 18:31
  • 2
    +1 because your class doesn't read the entire source dictionary when it's created. That means it'll be fast even for huge dictionaries. – Daniel Wolf Aug 07 '15 at 09:40
  • 1
    @vidstige All read-only wrappers allow you to modify the source, including the ones included in .Net, because it's impossible for a class to prevent modifications to a randomly given object. The only workaround is to make a defensive copy, but that defeats the purpose of having a wrapper instead of just copying the data in the first place. The important thing is that this wrapper doesn't allow the consumer to modify the source. If the class that made the wrapper doesn't touch the source, nothing can. – relatively_random Jan 31 '22 at 08:31
  • @danze That would defeat the purpose of having a wrapper in the first place. If you're copying data anyway, why not just return the copy directly? – relatively_random Jan 31 '22 at 08:36
2

I would suggest that you might want to define your own covariant interfaces, and include covariant access methods as well as a method which will create a read-only wrapper object which implements either IDictionary or IReadonlyDictionary with the desired types. Simply ignore IEnumerable<KeyValuePair<TKey,TValue>> within your interface.

Depending upon what you're doing, it may be helpful to define an IFetchByKey<out TValue> which is inherited by IFetchByKey<in TKey, out TValue>, with the former accepting queries for any type of object (given an object instance, a collection of Cat should be able to say whether it contains that instance, even if it's a type Dog or ToyotaPrius; the collection won't contain any instances of the latter types, and should be able to say so).

supercat
  • 77,689
  • 9
  • 166
  • 211
0

Maybe this solutions works for you:

public class ExposesReadOnly
{
    private IDictionary<int, IReadOnly> InternalDict { get; set; }
    public IReadOnlyDictionary<int, IReadOnly> PublicList
    {
        get
        {
            IReadOnlyDictionary<int, IReadOnly> dictionary = new ReadOnlyDictionary<int, IReadOnly>(InternalDict);

            return dictionary;
        }
    }

    private class NotReadOnly : IReadOnly
    {
        public string Name { get; set; }
    }

    public void AddSomeValue()
    {
        InternalDict = new Dictionary<int, NotReadOnly>();
        InternalDict.Add(1, new NotReadOnly() { Name = "SomeValue" });
    }
}

public interface IReadOnly
{
    string Name { get; }
}

class Program
{
    static void Main(string[] args)
    {
        ExposesReadOnly exposesReadOnly = new ExposesReadOnly();
        exposesReadOnly.AddSomeValue();

        Console.WriteLine(exposesReadOnly.PublicList[1].Name);
        Console.ReadLine();

        exposesReadOnly.PublicList[1].Name = "This is not possible!";
    }
}

Hope this helps!

Greets

Thomas Mondel
  • 1,944
  • 3
  • 20
  • 25
  • That works for adding values, but I also access them internally and make updates to them. That is where I stood with option #3 that I listed, since I'd have to cast back to NotReadOnly when I retrieved them internally. – Ocelot20 Nov 27 '12 at 22:45
  • With the solution i offered you can fully access, update, delete, add, etc. any values in the dictionary from inside the class and only expose it as a readonly dictionary outwards. Please give a concrete example where this code might not be sufficient so i can understand your issue more clearly. – Thomas Mondel Nov 27 '12 at 23:16
  • `InternalDict[1].Name = "SomeNewValue"` won't work without a cast. – Ocelot20 Nov 28 '12 at 00:34
0

Depending on your use case, you might be able to get away with exposing a Func<int,IReadOnly>.

public class ExposesReadOnly
{
    private Dictionary<int, NotReadOnly> InternalDict { get; set; }
    public Func<int,IReadOnly> PublicDictionaryAccess
    {
        get
        {
            return (x)=>this.InternalDict[x];
        }
    }

    // This class can be modified internally, but I don't want
    // to expose this functionality.
    private class NotReadOnly : IReadOnly
    {
        public string Name { get; set; }
    }
}

public interface IReadOnly
{
    string Name { get; }
}
Colin
  • 588
  • 6
  • 9
-1

Another approach for a specific lack of covariance:

A work around for a specific type of useful covariance on idictionary

public static class DictionaryExtensions
    {
        public static IReadOnlyDictionary<TKey, IEnumerable<TValue>> ToReadOnlyDictionary<TKey, TValue>(
            this IDictionary<TKey, List<TValue>> toWrap)
        {
            var intermediate = toWrap.ToDictionary(a => a.Key, a =>a.Value!=null? a.Value.ToArray().AsEnumerable():null);
            var wrapper = new ReadOnlyDictionary<TKey, IEnumerable<TValue>>(intermediate);
            return wrapper;
        }   
    }
Maslow
  • 18,464
  • 20
  • 106
  • 193