See the question and my original answer on StackOverflow

Here is a .NET Core version that uses CsWin32 nuget for interop code generation. At Shell level it uses IShellItem and IParentAndItem interfaces that are much easier to work with than IShellFolder.

Since it's using IShellItem and not necessarily physical files, this code


will select c: and d: in "This PC" shell namespace.

C# .csproj file:

<Project Sdk="Microsoft.NET.Sdk">
    <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">

NativeMethods.txt file that define what Win32 interfaces and functions we want to import


Sample program.cs file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Windows.Win32;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.Shell.Common;

[assembly: SupportedOSPlatform("windows6.0.6000")]

namespace ConsoleApp;

internal class Program
    static void Main()
          @"z:\Music\Thursday Blues\01. I wish it was friday.mp3",
          @"z:\Music\Counting Sheep\01. Sheep #1.mp3",
          @"z:\Music\Counting Sheep\02. Sheep #2.mp3"

        // select c: and d: in "This PC"

public static class ShellUtilities
    public static unsafe void OpenFoldersAndSelectFiles(IEnumerable<string> files)

        // get all items & group by folder (path)
        var items = Item.FromPaths(files);

        foreach (var group in items.GroupBy(g => g.ToString()))
            var childPidls = group.Select(g => (nint)g.ChildPidl).ToArray();
            var pidls = Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(childPidls));
            PInvoke.SHOpenFolderAndSelectItems(group.First().ParentPidl, (uint)group.Count(), (ITEMIDLIST**)pidls, 0);
        items.ForEach(i => i.Dispose());

    private unsafe sealed class Item : IDisposable
        public char* ParentPath;
        public ITEMIDLIST* ParentPidl;
        public ITEMIDLIST* ChildPidl;
        public override string ToString() => new(ParentPath);

        public static List<Item> FromPaths(IEnumerable<string> files) // return list cause unsafe code cannot appear in iterators
            var list = new List<Item>();
            foreach (var file in files)
                // build IShellItem
                if (PInvoke.SHCreateItemFromParsingName(file, null, typeof(IShellItem).GUID, out var obj).Failed)

                var item = (IShellItem)obj;
                item.GetParent(out var parent);
                parent.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING, out var path);

                // get parent & item relative pidls
                ITEMIDLIST* childPidl; ITEMIDLIST* parentPidl;
                ((IParentAndItem)item).GetParentAndItem(&parentPidl, null, &childPidl);
                list.Add(new Item { ParentPath = path.Value, ParentPidl = parentPidl, ChildPidl = childPidl });
            return list;

        public void Dispose()
            if (ParentPath != null) { Marshal.FreeCoTaskMem((nint)ParentPath); ParentPath = null; }
            if (ParentPidl != null) Marshal.FreeCoTaskMem((nint)ParentPidl); ParentPidl = null;
            if (ChildPidl != null) Marshal.FreeCoTaskMem((nint)ChildPidl); ChildPidl = null;