Avoid Exposing Collections Directly as Properties

Date Published: 22 February 2011

Avoid Exposing Collections Directly as Properties

Introduction

Sometimes your domain objects have one-to-many or many-to-many relationships with other objects. For instance, your Customers might have collections of Orders associated with them. The simplest way to model this is to expose a List<Order> field or property off of your Customer class. Unfortunately, this has a number of negative consequences because of how it breaks encapsulation. Fortunately, there are some effective ways to shield yourself from these issues while still exposing the collection's data to your class's clients.

Doing It Wrong

Doing it the simplest way that could possibly work would mean simply exposing the internal state of the Customer class by creating a public field of List<Order>.

At first glance, this appears to solve the requirements of the system. But, this approach has its weaknesses.

Guarding Against Destructive Actions

The current implementation would allow a user of Customer to set its OrderHistory field to null. This is a problem.

The simplest fix to this is to change our OrderHistory to use a property, and to make it readonly by giving it a private setter. With this change in place, we should be safe from this problem.

However, users of the system can still perform destructive actions on the OrderHistory by using the List<Order> type's methods. For instance, they can Clear() the collection.

In order to remove access to the List<T> class's methods, we can hide the implementation details of our internal representation behind a read-only interface, like IEnumerable. However, changing to IEnumerable also means we can no longer add directly to OrderHistory - this is also a good thing, since that was exposing too much internal state. Thus we add a method to Customer specifically for adding orders to the order history.

Overriding Intended Behavior

Even if you only expose your type as an IEnumerable, sneaky clients of your class can still get to its underlying methods if they guess correctly. If a consumer of your class is able to guess (or determine through reflection or a decompiler) the actual underlying type of your interface, then all bets are off and they can destroy your data at will.

We can probably safely say that any developer who so blatantly overcomes your attempts at encapsulating data deserves the bugs they create by doing so. However, there is one more alternative approach.

Wrapping Collections

The built-in .NET class, ReadOnlyCollection<T> is the standard way to wrap a collection and make available only a read-only version of the collection's contents. The collection lives in the System.Collections.ObjectModel namespace. Once we update our Order class to expose this collection as the type of its OrderHistory property, the above problem should be fixed.

Summary

Encapsulation of object state is fundamental to proper object oriented programming and design. C# makes it very easy to create properties and to work with strongly typed collections of objects, and often this results in object models that expose too much functionality when it comes to collections. By using interfaces or wrapper classes like the ReadOnlyCollection<T>, we can ensure our classes' internal state remains safe from calling code that might inadvertently introduce bugs by changing it inappropriately.

Originally published on ASPAlliance.com

Steve Smith

About Ardalis

Software Architect

Steve is an experienced software architect and trainer, focusing on code quality and Domain-Driven Design with .NET.


Ardalis

Copyright © 2021