Click here to Skip to main content
15,881,455 members
Articles / Programming Languages / C# 4.0
Tip/Trick

Use of IReadOnlyDictionary and IReadOnlyList Properties in .NET Objects

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
28 May 2016CPOL3 min read 35.3K   2   2
Control the changes to structured data exposes by your .NET objects

Introduction

The principles of object oriented software development identify the encapsulation of computing logic that is unique to a problem space and is the sole authority of that computation. The objective is the normalization of computing logic into discrete computing components that can be orchestrated into complex systems by using easier to manage and understand units. The interface to any object consists of the exposed properties and methods that allow other objects to interact with it. Unfortunately, many times, standard structure properties such as dictionaries and lists, are exposed creating a temptation to manipulate the data in these properties, which create a major violation of control. In fact, you will recognize “IDictionary” was written with the exposed read only collections “Keys” and “Values” instead of an actual list. One could only imagine the complications if the IDictionary exposes the real list in “Keys” and then the caller started adding to the list. Although one can argue that the list of keys does not really exist in the implementation of IDictionary, it is easy to see where this can be an issue.

Approach

Read only properties of .NET objects are a fundamental principle of any quality software development, which allows an object to maintain control of the data it is tasked to manage. Unfortunately, it is not uncommon to have a complex property that is a dictionary or list, which exposes the data of the object to changes outside the control of the object. The .NET Framework 4.5 finally provided us “IReadOnlyDictionary<TKey, TValue>” and “IReadOnlyList<TKey, TValue>” to rectify this. Now all objects that expose these structures should use them instead as follows:

C#
public class ExampleClass
{
    public class ExampleData
    {
        public int ID { get; private set; }
        public string Value { get; private set;  }
        public ExampleData(int id, string value)
        {
            ID = id;
            Value = value;
        }
    }

    private Dictionary<string, ExampleData> _keyData;
    public IReadOnlyDictionary<string, ExampleData> KeyData { get { return _keyData; } }

    private List<ExampleData> _listData;
    public IReadOnlyList<ExampleData> ListData { get { return _listData; } }

    public ExampleClass()
    {
        _keyData = new Dictionary<string, ExampleData>();
        _listData = new List<ExampleData>();
    }

   // Implementation...
}
VB.NET
Public Class ExampleClass
    Public Class ExampleData
        Public ReadOnly Property ID As Integer
        Public ReadOnly Property Value As String

        Public Sub New(ByVal id As Integer, ByVal value As String)
            Me.ID = id
            Me.Value = value
        End Sub
    End Class

    Private _keyData As Dictionary(Of String, ExampleData)
    Public ReadOnly Property KeyData As IReadOnlyDictionary(Of String, ExampleData)
        Get
            Return _keyData
        End Get
    End Property

    Private _listData As List(Of ExampleData)
    Public ReadOnly Property ListData As IReadOnlyList(Of ExampleData)
        Get
            Return _listData
        End Get
    End Property

    Public Sub New()
        _keyData = New Dictionary(Of String, ExampleData)()
        _listData = New List(Of ExampleData)
    End Sub

    ' Implementation...
End Class

Using this coding pattern prevents manipulation of these data structures outside the control of the class, while providing direct access to the data within these structures.

Unfortunately, not all objects that implement the “IDictionary<TKey, TValue>” or “IList<TKey>” interface also implements the corresponding “IReadOnlyDictionary<TKey, TValue>” or “IReadOnlyList<TKey>” interfaces. An example of this was the original implementation of “ConcurrentDictionary<TKey, TValue>” and properties of various collections such as “IDictionary<TKey, TValue>.Values”. To achieve this, you need a wrapper object that limits the interface of these structures to the read-only interfaces such as the following:

C#
/// <summary>
/// Wrapper class for a IDictionary object to implement the IReadOnlyDictionary
/// interface for the dictionary.
/// </summary>
/// <typeparam name="TKey">Data type for the dictionary key.</typeparam>
/// <typeparam name="TValue">Data type for the dictionary value.</typeparam>
public class ReadOnlyDictionaryWrapper<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
    private IDictionary<TKey, TValue> _dictionary;

    public ReadOnlyDictionaryWrapper(IDictionary<TKey, TValue> dictionary)
    {
        _dictionary = dictionary;
    }
    public TValue this[TKey key] { get { return _dictionary[key]; } }
    public int Count { get { return _dictionary.Count; } }
    public IEnumerable<TKey> Keys { get { return _dictionary.Keys; } }
    public IEnumerable<TValue> Values { get { return _dictionary.Values; } }
    public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); }
    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() { return _dictionary.GetEnumerator(); }
    public bool TryGetValue(TKey key, out TValue value) { return _dictionary.TryGetValue(key, out value); }
    IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_dictionary).GetEnumerator(); }
}
/// <summary>
/// Wrapper class for IList object to implement the IReadOnlyList interface for the list.
/// </summary>
/// <typeparam name="TValue">Data type of the list value.</typeparam>
public class ReadOnlyListWrapper<TValue> : IReadOnlyList<TValue>
{
    private IList<TValue> _list;

    public ReadOnlyListWrapper(IList<TValue> list)
    {
        _list = list;
    }
    public TValue this[int index] { get { return _list[index]; } }
    public int Count { get { return _list.Count; } }
    public IEnumerator<TValue> GetEnumerator() { return _list.GetEnumerator(); }
    IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_list).GetEnumerator(); }
}
VB.NET
''' <summary>
''' Wrapper class for a IDictionary object to implement the IReadOnlyDictionary
''' interface for the dictionary.
''' </summary>
''' <typeparam name="TKey">Data type for the dictionary key.</typeparam>
''' <typeparam name="TValue">Data type for the dictionary value.</typeparam>
Public Class ReadOnlyDictionaryWrapper(Of TKey, TValue)
    Implements IReadOnlyDictionary(Of TKey, TValue)

    Private _dictionary As IDictionary(Of TKey, TValue)

    Public Sub New(dictionary As IDictionary(Of TKey, TValue))
        Me._dictionary = dictionary
    End Sub

    Public ReadOnly Property Count As Integer _
        Implements IReadOnlyCollection(Of KeyValuePair(Of TKey, TValue)).Count
        Get
            Return Me._dictionary.Count
        End Get
    End Property

    Default Public ReadOnly Property Item(key As TKey) As TValue _
        Implements IReadOnlyDictionary(Of TKey, TValue).Item
        Get
            Return Me._dictionary(key)
        End Get
    End Property

    Public ReadOnly Property Keys As IEnumerable(Of TKey) _
        Implements IReadOnlyDictionary(Of TKey, TValue).Keys
        Get
            Return Me._dictionary.Keys
        End Get
    End Property

    Public ReadOnly Property Values As IEnumerable(Of TValue) _
        Implements IReadOnlyDictionary(Of TKey, TValue).Values
        Get
            Return Me._dictionary.Values
        End Get
    End Property

    Public Function ContainsKey(key As TKey) As Boolean _
        Implements IReadOnlyDictionary(Of TKey, TValue).ContainsKey
        Return Me._dictionary.ContainsKey(key)
    End Function

    Public Function GetEnumerator() As IEnumerator(Of KeyValuePair(Of TKey, TValue)) _
        Implements IEnumerable(Of KeyValuePair(Of TKey, TValue)).GetEnumerator
        Return Me._dictionary.GetEnumerator()
    End Function

    Public Function TryGetValue(key As TKey, ByRef value As TValue) As Boolean _
        Implements IReadOnlyDictionary(Of TKey, TValue).TryGetValue
        Return Me._dictionary.TryGetValue(key, value)
    End Function

    Private Function IEnumerable_GetEnumerator() As IEnumerator _
        Implements IEnumerable.GetEnumerator
        Return CType(Me._dictionary, IEnumerable).GetEnumerator()
    End Function
End Class
''' <summary>
''' Wrapper class for IList object to implement the IReadOnlyList interface for the list.
''' </summary>
''' <typeparam name="TValue">Data type of the list value.</typeparam>
Public Class ReadOnlyListWrapper(Of TValue)
    Implements IReadOnlyList(Of TValue)

    Private _list As IList(Of TValue)

    Public Sub New(list As IList(Of TValue))
        Me._list = list
    End Sub

    Public ReadOnly Property Count As Integer _
        Implements IReadOnlyCollection(Of TValue).Count
        Get
            Return Me._list.Count
        End Get
    End Property

    Default Public ReadOnly Property Item(index As Integer) As TValue _
        Implements IReadOnlyList(Of TValue).Item
        Get
            Return Me._list(index)
        End Get
    End Property

    Public Function GetEnumerator() As IEnumerator(Of TValue) _
        Implements IEnumerable(Of TValue).GetEnumerator
        Return Me._list.GetEnumerator()
    End Function

    Private Function IEnumerable_GetEnumerator() As IEnumerator _
        Implements IEnumerable.GetEnumerator
        Return CType(Me._list, IEnumerable).GetEnumerator()
    End Function
End Class

This allows any “IDictionary<TKey, TValue>” or “IList<TKey>” object to be published as a read only object as in the following example:

C#
public class ExampleClass
{
    public class ExampleData
    {
        public int ID { get; private set; }
        public string Value { get; private set; }
        public ExampleData(int id, string value)
        {
            ID = id;
            Value = value;
        }
    }

    private Dictionary<string, ExampleData> _keyData;
    public IReadOnlyDictionary<string, ExampleData> KeyData { get; private set; }

    private List<ExampleData> _listData;
    public IReadOnlyList<ExampleData> ListData { get; private set; }

    public ExampleClass()
    {
        _keyData = new Dictionary<string, ExampleData>();
        KeyData = new ReadOnlyDictionaryWrapper<string, ExampleData>(_keyData);
        _listData = new List<ExampleData>();
        ListData = new ReadOnlyListWrapper<ExampleData>(_listData);
    }

    // Implementation...
}
VB.NET
Public Class ExampleClass
    Public Class ExampleData
        Public ReadOnly Property ID As Integer
        Public ReadOnly Property Value As String

        Public Sub New(ByVal id As Integer, ByVal value As String)
            Me.ID = id
            Me.Value = value
        End Sub
    End Class

    Private _keyData As Dictionary(Of String, ExampleData)
    Private _keyReadOnlyData As ReadOnlyDictionaryWrapper(Of String, ExampleData)
    Public ReadOnly Property KeyData As IReadOnlyDictionary(Of String, ExampleData)
        Get
            Return _keyReadOnlyData
        End Get
    End Property

    Private _listData As List(Of ExampleData)
    Private _listReadOnlyData As ReadOnlyListWrapper(Of ExampleData)
    Public ReadOnly Property ListData As IReadOnlyList(Of ExampleData)
        Get
            Return _listData
        End Get
    End Property

    Public Sub New()
        _keyData = New Dictionary(Of String, ExampleData)()
        _keyReadOnlyData = New ReadOnlyDictionaryWrapper(Of String, ExampleData)(_keyData)
        _listData = New List(Of ExampleData)
        _listReadOnlyData = New ReadOnlyListWrapper(Of ExampleData)(_listData)
    End Sub

    ' Implementation...
End Class

The .NET Framework does implement wrapper classes in the namespace “System.Collections.ObjectModel” called “ReadOnlyDictionary” and “ReadOnlyCollection”, but unfortunately these objects are designed to make a copy of the data structure properties, such as “Keys” and “Values”, when created. The advantage of the wrapper classes “ReadOnlyDictionaryWrapper” and “ReadOnlyListWrapper” above is the implementation is very lightweight, continues to track changes in the underlying structure and the users of the object can maintain references to the object.

The disadvantage of these objects is the same as the original object, the underlying dictionary or list structure may change while iterating through the structure. This can be easily resolved by creating copy objects that derived from these wrapper objects as such:

C#
/// <summary>
/// Read only copy of an IDictionary object.
/// </summary>
/// <typeparam name="TKey">Data type for the dictionary key.</typeparam>
/// <typeparam name="TValue">Data type for the dictionary value.</typeparam>
public class ReadOnlyDictionaryCopy<TKey, TValue> : ReadOnlyDictionaryWrapper<TKey, TValue>
{
    public ReadOnlyDictionaryCopy(IEnumerable<KeyValuePair<TKey, TValue>> dictionaryList)
        : base(_copy(dictionaryList))
    { }

    private static IDictionary<TKey, TValue> _copy(IEnumerable<KeyValuePair<TKey, TValue>> dictionaryList)
    {
        var result = new Dictionary<TKey, TValue>();

        foreach (KeyValuePair<TKey, TValue> kv in dictionaryList)
            result.Add(kv.Key, kv.Value);

        return result;
    }
}
/// <summary>
/// Read only copy of an IList object.
/// </summary>
/// <typeparam name="TValue">Data type of the list value.</typeparam>
public class ReadOnlyListCopy<TValue> : ReadOnlyListWrapper<TValue>
{
    public ReadOnlyListCopy(IEnumerable<TValue> list)
        : base(new List<TValue>(list))
    { }
}
VB.NET
''' <summary>
''' Read only copy of an IDictionary object.
''' </summary>
''' <typeparam name="TKey">Data type for the dictionary key.</typeparam>
''' <typeparam name="TValue">Data type for the dictionary value.</typeparam>
Public Class ReadOnlyDictionaryCopy(Of TKey, TValue)
    Inherits ReadOnlyDictionaryWrapper(Of TKey, TValue)

    Public Sub New(ByVal dictionaryList As IEnumerable(Of KeyValuePair(Of TKey, TValue)))
        MyBase.New(_copy(dictionaryList))
    End Sub

    Private Shared Function _copy(ByVal dictionaryList As IEnumerable(Of KeyValuePair(Of TKey, TValue))) _
        As IDictionary(Of TKey, TValue)
        Dim result As New Dictionary(Of TKey, TValue)()

        For Each kv As KeyValuePair(Of TKey, TValue) In dictionaryList
            result.Add(kv.Key, kv.Value)
        Next

        Return result
    End Function
End Class
''' <summary>
''' Read only copy of an IList object.
''' </summary>
''' <typeparam name="TValue">Data type of the list value.</typeparam>
Public Class ReadOnlyListCopy(Of TValue)
    Inherits ReadOnlyListWrapper(Of TValue)

    Public Sub New(ByVal list As IEnumberable(Of TValue))
        MyBase.New(New List(Of TValue)(list))
    End Sub
End Class

A caller can then easily make a read-only copy of any dictionary or list by using the example code below:

C#
var myKeyData = new ReadOnlyDictionaryCopy<string, ExampleData>(myInstance.KeyData);
var myListData = new ReadOnlyListCopy<ExampleData>(myInstance.ListData);
VB.NET
Dim myKeyData As New ReadOnlyDictionaryCopy(Of String, ExampleData)(myInstance.KeyData)
Dim myListData As New ReadOnlyListCopy(Of ExampleData)(myInstance.ListData)

Conclusion

Maintaining control of the data within an object should be paramount in any well-structured object oriented, since objects are intended to provide services and control logic.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Erickson and Associates
United States United States
Principle Software Engineer for the consulting firm of Erickson and Associates in Seattle WA. BA in Architecture from Virginia Tech and BS in Computer Science, University of WA.

Comments and Discussions

 
QuestionQuestion about usage Pin
zarkok11-Jan-18 2:21
zarkok11-Jan-18 2:21 
What prevents a someone from casting IReadOnlyDictionary (KeyData) to plain Dictionary and manipulate data in the first example?
AnswerRe: Question about usage Pin
Michael B. Erickson6-Nov-18 9:19
professionalMichael B. Erickson6-Nov-18 9:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.