I tried this,

public class Solution {
    public string FrequencySort(string s) {
        var dict = new Dictionary<char, int>();
 
        foreach (char c in s) {
            if (dict.ContainsKey(c)) dict[c]++;
            else dict[c] = 1;
        }
 
        var pq = new PriorityQueue<char, (int f, int o)>(Comparer<(int, int)>.Create((a, b) => b.CompareTo(a)));
 
        foreach (char c in s) {
            pq.Enqueue(c, (dict[c], c - 'a'));
        }
 
 
        var sb = new StringBuilder();
        while (pq.Count > 0) {
            sb.Append(pq.Dequeue());
        }
 
        return sb.ToString();
    }
}

We get frequencies in dict. We make max heap with tuple priority (freq and ‘order alphabetical’ kind of).
Then, we enqueue each char in s in the heap. Value is char itself, priority is tuple of freq and character order alphabetically (so keep same characters together).
Then we dequeue it all.


Something better,

public class Solution {
    public string FrequencySort(string s) {
        var freq = new Dictionary<char, int>();
        foreach (char c in s)
            freq[c] = freq.GetValueOrDefault(c) + 1;
 
        var pq = new PriorityQueue<char, int>(
            freq.Select(kv => (kv.Key, kv.Value)),
            Comparer<int>.Create((a, b) => b.CompareTo(a)));   // max-heap by frequency
 
        var sb = new StringBuilder();
        while (pq.TryDequeue(out var c, out var count))
            sb.Append(c, count);                               // append c, count times
 
        return sb.ToString();
    }
}

First, we only keep distinct chars in heap. And in dequeue, we append as many times as count.