zondag 25 maart 2012

Readonly collections with Entity Framework

If you want to use readonly collections inside Domain Objects (like I did here) and are using Entity Framework Code First you currently have a problem, because collections of type ReadOnlyCollection are not supported.

Luckily I've found a way to work around this.

Let's start with an object which has a  ReadOnlyCollection :

public class Phase
{
   public int Id { getset; }
   public bool IsClosed { getset; }

   private List<Task> _tasks;
   public  ReadOnlyCollection<Task> ReadOnlyTasks
   {
      get
      {
         return new ReadOnlyCollection<Task>(_tasks);
      }
   }
} 

The problem with the code above is that Entity Framework can't map to private fields and it can't fill a ReadOnlyCollection.
What we can do is create a custom List which behaves like a readonly collection. For starters this means that the Add method is disabled:

public class ReadOnlyList<T> : List<T>
{
   /// <summary>
   ///  Not supported, because it's not allowed to add
   ///  items to a readonly list
   /// </summary>
   public new void Add(T item)
   {
      throw new NotSupportedException();
   }
}

Now we can use this  ReadOnlyList in our Domain Object:

public class Phase
{
   public int Id { getset; }
   public bool IsClosed { getset; }

   public ReadOnlyList<Task> ReadOnlyTasks
   {
      get;
      private set;
   }
}  

One problem solved: Entity Framework is able to fill the ReadOnlyList we created.
But now the Add method isn't supported anymore, how can we fill this list ourself? We have to create another add method, but this time we make it internal:

public class ReadOnlyList<T> : List<T>
{
   /// <summary>
   ///  Not supported, because it's not allowed to add
   ///  items to a readonly list
   /// </summary>
   public new void Add(T item)
   {
      throw new NotSupportedException();
   }

   internal  void AddItem(T item)
   {
      base.Add(item);
   }
}

We can use the AddItem method like this:

public class Phase
{
   public int Id { getset; }
   public bool IsClosed { getset; }

   public ReadOnlyList<Task> ReadOnlyTasks
   {
      get;
      private set;
   }

   public void AddTask(Task task)
   {
      ReadOnlyTasks.AddItem(task);
   }
}  

So how do we use the Phase class after we wrote this code? For example an MVC controller can look like this:

public ActionResult Index() 
{
   var phase = PhaseRepository.GetById(1);
   phase.AddTask(new Task());
} 

What would happen if we call the Add method on the ReadOnlyTasks list, like this:

public ActionResult Index() 
{
   var phase = PhaseRepository.GetById(1);
   phase.ReadOnlyTasks.Add(new Task());
} 

The code above will throw an  NotSupportedException, just as we wrote. So it is indeed a readonly list. But it's not very nice that we can call the Add method, the code compiles without any problem, but at runtime it will fail. It would be much nicer if we can do something to the Add method so we can't call it. Unfortunately we can't make it private or internal, because the public Add method of the List we inherited will become available. But there's another solution: mark the method with the System.Obsolete parameter, like this:

public class ReadOnlyList<T> : List<T>
{
   /// <summary>
   ///  Not supported, because it's not allowed to add
   ///  items to a readonly list
   /// </summary>
   [Obsolete("Not supported, because it is not allowed to add items to a readonly list", true)]
   public new void Add(T item)
   {
      throw new NotSupportedException();
   }
   ...
}

Now, if we try to call the Add method from an MVC Controller, we see this:





This helps us a lot by not making the mistake of calling Add on the readonly list. And if still continue, and type something like this:

phase.Tasks.Add(new Task());

We will get an error at compile time, so our code will never run. We now can only add items to the readonly list by calling the AddTask method on the Phase class, exactly as we wanted.


Btw. this solution is not completely air tight, because we can still cast our Readonly list to a List and the Add method is back available again. But at least we have a pretty clean solution for Readonly lists with Entity Framework.

1 opmerking: