Parsing NuGet packages.config with Parallel.ForEach Below is a file parser for old style nuget package configuration files. The Parallel.ForEach is probably overkill; in most cases its fast enough without. However, when using the parallel loop, can I avoid the need for the lock / shared list?    

class Program
{
    private static readonly object _lock = new Object();
    private static readonly List&lt;Package&gt; _packages = new List&lt;Package&gt;();

    static void Main(string[] args)
    {
        string[] files = Directory.GetFiles(Directory.GetCurrentDirectory(), "packages.config", SearchOption.AllDirectories);

        Parallel.ForEach(files, GetPackagesForFile);

        IEnumerable&lt;IGrouping&lt;string, Package&gt;&gt; idGroups =
            _packages.GroupBy(p =&gt; p.Id).OrderBy(g =&gt; g.Key);

        foreach (IGrouping&lt;string, Package&gt; idGroup in idGroups)
        {
            IEnumerable&lt;IGrouping&lt;string, Package&gt;&gt; versionGroups = idGroup.GroupBy(p =&gt; p.Version);
            Console.WriteLine($"{idGroup.Key} ({versionGroups.Count()})");

            foreach (IGrouping&lt;string, Package&gt; versionGroup in versionGroups)
                Console.WriteLine($"\t{versionGroup.Key}");
        }
    }

    private static void GetPackagesForFile(string filepath)
    {
        var filePackages = XDocument.Load(filepath).Root.Elements("package").Select(GetPackageFromElement);
        lock(_lock) _packages.AddRange(filePackages);
    }

    private static Package GetPackageFromElement(XElement element) =&gt;
        new Package(
            element.Attribute("id").Value,
            element.Attribute("version").Value,
            element.Attribute("targetFramework").Value);
}
If you want to do it this way then you need a lock but since the rest of your code is using Linq I would suggest you convert it over to PLINQ instead of using Parallel.ForEach, 

Also you are iterating over the enumerable versionGroups twice. In your specific case it shouldn't cause an issue but that's not best practices with an IEnumerable.  

First I'm going to change the GetPackagesForFile to return an array of Packages instead of using the static _packages field.

private static Package[] GetPackagesForFile(string filepath)
{
    return XDocument.Load(filepath).Root?.Elements("package").Select(GetPackageFromElement).ToArray() ??
           new Package[0];
}


I also changed it so to not throw a NullException if root not defined.  That's up to you if you want the exception there to tell you something was wrong or not.  So you might want to remove the ? and the ?? part if you want the exception to be thrown. 

Now if you want to use PLINQ we just need to use the AsParallel() extension method. I'm going to use an anonymous class but you could create one if that's what you prefer.  Since I'm using an anonymous class I can't specify the type so going to use the var keyword, which doesn't match your coding style. 

static void Main(string[] args)
{

    string[] files = Directory.GetFiles(Directory.GetCurrentDirectory(), "packages.config",
        SearchOption.AllDirectories);

    var idGroups = files.AsParallel()
        .SelectMany(GetPackagesForFile)
        .GroupBy(p =&gt; p.Id)
        .Select(g =&gt; new
        {
            Id = g.Key,
            Version = new HashSet&lt;string&gt;(g.Select(p =&gt; p.Version))
        }).OrderBy(p =&gt; p.Id);

    foreach (var idGroup in idGroups)
    {
        Console.WriteLine($"{idGroup.Id} ({idGroup.Version.Count})");

        foreach (string version in idGroup.Version)
            Console.WriteLine($"\t{version}");
    }
}


Since in your code all you care about is the distinct list of versions for each ID and the count I put them in a hashset instead of doing the grouping.  I could have used the g.Select(p =&gt; p.Version).Distinct().ToList() but just adding them to a hashset seems simpler code.  

And in closing like you already stating making this parallel might be overkill.  If you want to test it compared to not being paralleled then you just need to remove the AsParallel() and the rest of the code can stay the same.