The multithreading model to clean ConcurrentQueue in C#
I think the EventfulConcurrentQueue was not designed for this use case.
From the consumer perspective:

Things that are not in use

IsEmpty
Enqueue
ItemEnqueued
ItemDequeued


Things that are missing

DequeueAtMostNItems
WaitUntilThereAreEnoughItems



After I removed the not used things and added the missing ones the class would look like this:
public sealed class EventfulConcurrentQueue&lt;T&gt;
{
    private readonly ConcurrentQueue&lt;T&gt; _queue = new ConcurrentQueue&lt;T&gt;();

    public List&lt;T&gt; DequeueAtMostNItems(int n)
    {
        List&lt;T&gt; result = new();
        T current;
        do
        {
            if(_queue.TryDequeue(out current))
                result.Add(current);
        } while (current != null &amp;&amp; result.Count &lt;= n);

        return result;
    }

    public async Task WaitUntilThereAreEnoughItems(int numberOfItems)
    {
        while (_queue.Count &lt; numberOfItems)
        {
            await Task.Delay(100);
        }
        return;
    }
}

With these methods you are embracing encapsulation. The operation and data are next to each other. And with this approach you don't have to expose anything internal, so the information hiding aspect is covered as well.
If you stick with this implementation then I would suggest a renaming, because the ConcurrentQueue now becomes an implementation detail.
The usage could be simplified like this:
while (true)
{
    await PacketQueue.WaitUntilThereAreEnoughItems(chunkSize);
                    
    var chunk = PacketQueue.DequeueAtMostNItems(chunkSize);
    DataTable toBeInserted = DataHelper.CreateBulkCopyDataTable();
    chunk.ForEach(row =&gt; toBeInserted.Rows.Add(Guid.NewGuid(), row.AtTime, row.GPSTrackerID, row.PacketBytes));
 
    using var bulkCopy = ...
}