Question

I have need to add to an ICollection<string> property of a class of which I have an IEnumerable of. Here is a complete program which illustrates the problem:

using System;
using System.Collections.Generic;
using System.Linq;

namespace CollectionAddingTest
{
    public class OppDocumentServiceResult
    {
        public OppDocumentServiceResult()
        {
            this.Reasons = new List<string>();
        }

        public Document Document { get; set; }

        public bool CanBeCompleted
        {
            get
            {
                return !Reasons.Any();
            }
        }

        public ICollection<string> Reasons { get; private set; }
    }

    public class Document
    {
        public virtual string Name { get; set; }
    }

    public class Program
    {
        private static void Main(string[] args)
        {
            var docnames = new List<string>(new[] {"test", "test2"});

            var oppDocResult = docnames
                .Select(docName
                        => new OppDocumentServiceResult
                               {
                                   Document = new Document { Name = docName }
                               });

            foreach (var result in oppDocResult)
            {
                result.Document.Name = "works?";
                result.Reasons.Add("does not stick");
                result.Reasons.Add("still does not stick");
            }

            foreach (var result in oppDocResult)
            {
                // doesn't write "works?"
                Console.WriteLine(result.Document.Name);

                foreach (var reason in result.Reasons)
                {
                    // doesn't even get here
                    Console.WriteLine("\t{0}", reason);
                }
            }
        }
    }
}

I would expect that each OppDocumentServiceResult would have its referenced Document.Name property set to works? and each OppDocumentServiceResult should have two reasons added to it. However, neither is happening.

What is special about the Reasons property that I cannot add things to it?

Was it helpful?

Solution

Fixed like this, converting to List instead of keeping the IEnumerable:

var oppDocResult = docnames
        .Where(docName => !String.IsNullOrEmpty(docName))
        .Select(docName
            => new OppDocumentServiceResult
            {
                Document = docName
            }).ToList();

I can only guess (this is a shot in the dark really!) that the reason behind this is that in an IEnumerable the elements are like "proxies" of the real elements? basically the Enumerable defined by the Linq query is like a "promise" to get all the data, so everytime you iterate you get back the original items? That does not explain why a normal property still sticks...

So, the fix is there but the explanation I am afraid is not... not from me at least :(

OTHER TIPS

The problem is that oppDocResult is the result of a LINQ query, using deferred execution.

In other words, every time you iterate over it, the query executes and new OppDocumentServiceResult objects are created. If you put diagnostics in the OppDocumentServiceResult constructor, you'll see that.

So the OppDocumentServiceResult objects you're iterating at the end are different ones to the ones you've added the reasons to.

Now if you add a ToList() call, then that materializes the query into a "plain" collection (a List<OppDocumentServiceResult>). Each time you iterate over that list, it will return references to the same objects - so if you add reasons the first time you iterate over them, then you print out the reasons when you iterate over them again, you'll get the results you're looking for.

See this blog post (among many search results for "LINQ deferred execution") for more details.

The issue is your initial Select you are instantiating new OppDocumentServiceResult objects. Add a ToList and you should be good to go:

var oppDocResult = docnames
    .Select(docName
            => new OppDocumentServiceResult
                   {
                       Document = new Document { Name = docName }
                   }).ToList();

As Servy pointed out I should have added a bit more detail to my answer, but thankfully the comment he left on Tallmaris' answer takes care of that. In his answer Jon Skeet further expands on the reason, but what it boils down to "is that oppDocResult is the result of a LINQ query, using deferred execution."

ForEach() is defined against only List<T> you will not be able to use it to for an ICollection<T>.

You have to options:

((List<string>) Reasons).ForEach(...)

Or

Reasons.ToList().ForEach(...)

Yet, my preferred approach

I would define this extension which can help automating this for you without wasting resources:

public static class ICollectionExtensions
{
    public static void ForEach(this ICollection<T> collection, Action<T> action)
    {
        var list = collection as List<T>;
        if(list==null)
            collection.ToList().ForEach(action);
        else
            list.ForEach(action);
    }
}

Now I can use ForEach() against ICollection<T>.

Just change your code inside your class

public List<string> Reasons { get; private set; }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top