See the question and my original answer on StackOverflow

In a console context, you must ensure windows messages are processed as clipboard depends on it, so for example, you can use Winforms' DoEvents method (if you don't have a real window and nothing pumps messages):

class Program
{
    static void Main()
    {
        Windows.ApplicationModel.DataTransfer.Clipboard.ContentChanged += (s, e) => Console.WriteLine("ContentChanged");
        Console.WriteLine("Press any key to quit");
        do
        {
            System.Windows.Forms.Application.DoEvents();
            if (Console.KeyAvailable)
                break;

            Thread.Sleep(100); // for example
        }
        while (true);
    }
}

To enable Winforms support in a .NET 5 Console project, here is the simplest way of doing it (you don't even need to add the Windows.SDK nuget package), just modify the .csproj to something like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <DisableWinExeOutputInference>true</DisableWinExeOutputInference>
  </PropertyGroup>

</Project>

If you don't have .NET 5, or don't want to reference Winforms, then you can declare your own message pump using P/Invoke, like this:

class Program
{
    [STAThread]
    static void Main()
    {
        Windows.ApplicationModel.DataTransfer.Clipboard.ContentChanged += (s, e) => Console.WriteLine("ContentChanged");
        Console.WriteLine("Press any key to quit");
        do
        {
            while (GetMessage(out var msg, IntPtr.Zero, 0, 0) != 0)
            {
                TranslateMessage(ref msg);
                DispatchMessage(ref msg);
            }
            if (Console.KeyAvailable)
                break;

            Thread.Sleep(100);
            Console.Write(".");
        }
        while (true);
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct MSG
    {
        public IntPtr hwnd;
        public int message;
        public IntPtr wParam;
        public IntPtr lParam;
        public int time;
        public POINT pt;
        public int lPrivate;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct POINT
    {
        public int x;
        public int y;
    }

    [DllImport("user32")]
    private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, int wMsgFilterMin, int wMsgFilterMax);

    [DllImport("user32")]
    private static extern bool TranslateMessage(ref MSG lpMsg);

    [DllImport("user32")]
    private static extern IntPtr DispatchMessage(ref MSG lpmsg);
}