2

I have a business entities as below,

class Class1
{
   List<Class2> classes = new List<Class2>();

   public IEnumerable<Class2> Classes { get { return classes.AsEnumrable(); }

   public void AddClass(Class2 cls)
   {
       classes.Add(cls);
   }
}

class Class2
{
    public string Property { get; set; }
}

My business logic requires that once a Class2 instance is added using the AddClass method to the top of the Classes list in Class1, no one should be able to edit the properties of the Class2 instances added previously to the list, only the last item in the list could be edited. How do I do this?

I have tried IReadOnlyList, but it appears that it is concerned with making the list structure itself uneditable without preventing the edit of its items' content.

Sisyphus
  • 900
  • 12
  • 32
  • I think the data structure you need is a stack, which is First In Last Out (FILO). This means that only the last item can be popped off the stack. The other items can not be retrieved until the items above them have been removed. – benjrb Feb 11 '18 at 15:26
  • What's the underlying business issue here? – Stephen Kennedy Feb 11 '18 at 15:31
  • It seems like you need your `Class2` to have read-only properties or some sort of lock \ unlock for editing behaviour. You may also make it virtual and write some sort of proxy class which will prevent editing, but it doesn't sound like a proper architectural solution. This has nothing to do with type of collection since collection just returns references to `Class2` objects. – Yeldar Kurmangaliyev Feb 11 '18 at 15:33

5 Answers5

2

It's not a container's job to dictate the behavior of its items. A container is just that - an object that contains other objects. An IReadOnlyList is a container whose items cannot be modified. But the items within it are jsut Class2 instances - there's nothing that the container can do to prevent them from being edited.

Consider this:

myReadOnlyCollection[0].Property = "blah";
var firstItem = myReadOnlyCollection[0];
firstItem.Property = "blah";

Should this be legal in your scenario? What's the difference between the two? firstItem is simply an instance of Class2 that has no idea it was once inside a read-only collection.

What you need is for Class2 itself to be immutable. That's up to the class, not the container. Read up on immutability, which is an important concept to grasp, and implement Class2 accordingly. If you need a regular, mutable Class2 to change its behavior, perhaps add a ToImmutable() method to it which returns a different item, without a setter.

Avner Shahar-Kashtan
  • 14,492
  • 3
  • 37
  • 63
2

Why are you exposing the IReadOnlyCollection. Once you have exposed the objects, the objects themselves have to be immutable.

Why not just expose the only object that you want to expose?

   private IEnumerable<Class2> Classes { get { return classes; }

   public Class2 Class2Instance { get { return classes.Last(); } }
bashrc
  • 4,725
  • 1
  • 22
  • 49
1

I can only see three options. One is to alter Class2 to make it lockable and then lock it once it's added to your list...

class Class1 {
   List<Class2> classes = new List<Class2>();

   public IEnumerable<Class2> Classes {
      get { return classes.AsEnumrable();
   }

   public void AddClass(Class2 cls) {
      cls.Lock();
      classes.Add(cls);
   }
}

class Class2 {
    private string _property;
    private bool _locked;

    public string Property {
       get { return _property; }
       set {
          if(_locked) throw new AccessViolationException();
          _property = value;
       }
    }

    public void Lock() {
       _locked = true;
    }
}

Another option is to only return the values of the list objects instead of the objects themselves...

class Class1 {
   List<Class2> classes = new List<Class2>();

   public IEnumerable<string> Values {
      get { return classes.Select(cls => cls.Property); }
   }

   public void AddClass(Class2 cls) {
      classes.Add(cls);
   }
}

In this second method, anything other than a single value and you'll need to either return a tuple. Alternately, you could create a specific container for Class2 that exposes the values as read-only...

class Class2ReadOnly {
   private Class2 _master;

   public Class2ReadOnly(Class2 master) {
      _master = master;
   }

   public string Property {
      get { return _master.Property; }
   }
}

class Class1 {
   List<Class2ReadOnly> classes = new List<Class2ReadOnly>();

   public IEnumerable<Class2ReadOnly> Classes {
      get { return classes.AsEnumerable(); }
   }

   public void AddClass(Class2 cls) {
      classes.Add(new Class2ReadOnly(cls));
   }
}
dynamichael
  • 807
  • 9
  • 9
1

I know it is an old problem, however I faced the same issue today.

Background: I want to store data in my application, e.g. users can set their custom objects in the project and the Undo-redo mechanism must be adapted to handle batched data storing.

My approach: I created some interfaces, and I made a wrapper for the collection I don't want the users to modify.

public class Repository : IRepository
{
    // These items still can be changed via Items[0].CustomProperty = "asd"
    public readonly List<CustomItem> Items { get; }

    private readonly List<CustomItem> m_Originaltems;

    //However, when I create RepositoryObject, I create a shadow copy of the Items collection

    public Repository(List<CustomItem> items)
    {
        items.ForEach((item) =>
        {
            // By cloning an item you can make sure that any change to it can be easily discarded
            Items.Add((CustomItem)item.Clone());
        });

        // As a private field we can manage the original collection without taking into account any unintended modification
        m_OriginalItems = items;
    }
    
    // Adding a new item works with the original collection
    public void AddItem(CustomItem item)
    {
        m_OriginalItems.Add(item);
    }

    // Of course you have to implement all the necessary methods you want to use (e.g. Replace, Remove, Insert and so on)
}

Pros: Using this approach you basically just wraps the collection into a custom object. The only thing you expect from the user side is to have ICloneable interface implemented. If you want, you can make your wrapper generic as well, and give a constraint like where T : ICloneable

Cons: If you add new items, you won't know about them by checking the Items property. A workaround can be done by creating a copy of the collection whenever Items.get() is called. It is up to you and your requirements. I meant something like this:

public class Repository : IRepository
{
    public List<CustomItem> Items => m_OriginalItems.Select(item => (CustomItem)item.Clone()).ToList();

    private readonly List<CustomItem> m_Originaltems;

    public Repository(List<CustomItem> items)
    {
        m_OriginalItems = items;
    }
    
    public void AddItem(CustomItem item)
    {
        m_OriginalItems.Add(item);
    }

    // Of course you still have to implement all the necessary methods you want to use (e.g. Replace, Count, and so on)
}
0

As other said, it is not the collections job to dictate if (and how) you access it's elements. I do see ways around this:

Exceptions & references:

Modify Class2 so it can take a reference to Class1. If the reference is set, throw excetpions on all setters. Modify Class1.AddClass to set that property.

A softer version of this would be a "read only" property on Class2, that all other code has to check.

Readonly Properties & Constructors:

Just always give Class2 readonly properties (private set). If you want to define the property values, you have to do that in the constructor (which has proper Arguments). This pattern is used heavily by the Exception classes.

Inheritance Shenanigans:

Make Multiple Class2 versions in an inheritance chain, so that that Class2Writebale can be cast to a Class2ReadOnly.

Accept the wrong Y:

You might have stuck yourself into a XY problem: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem If so go a step back to fix it.

Optimistic Peach
  • 3,862
  • 2
  • 18
  • 29
Christopher
  • 9,634
  • 2
  • 17
  • 31