dotnet/wpf · error · InvalidOperationException

SR.PTProvider_VerifyAccess

Error message

SR.PTProvider_VerifyAccess

What it means

PTProvider is an STA-affine object bound to the thread that created it; VerifyAccess throws InvalidOperationException when a method is called from a different thread. The provider also throws if it has already been disposed. GetPrintCapabilities, MergeAndValidatePrintTicket, ConvertDevModeToPrintTicket and ConvertPrintTicketToDevMode all funnel through this check.

Solutions

  1. Create and use the PTProvider/PrintQueue/PrintTicket on the same thread, or marshal the call back to the creating thread (Dispatcher.Invoke).
  2. Create a new PTProvider instance on the thread where it will be used.
  3. Check/ensure the provider has not been disposed before calling.
  4. Catch InvalidOperationException and re-create the provider for that thread.

Example fix

// before
Task.Run(() => queue.GetPrintCapabilities(ticket)); // wrong thread
// after
Dispatcher.Invoke(() => queue.GetPrintCapabilities(ticket)); // same thread that created the queue
Defensive patterns

Strategy: try-catch

Validate before calling

if (provider == null || provider._providerHandle == null) throw new ObjectDisposedException(nameof(PTProvider));
// capture the creating thread at construction and compare before calling

Type guard

bool CanUseProvider(PTProvider p, Thread creatorThread) => p != null && Thread.CurrentThread == creatorThread;

Try / catch

try
{
    var caps = queue.GetPrintCapabilities(ticket);
}
catch (InvalidOperationException)
{
    // wrong thread or disposed: recreate on current thread
    queue = new PrintQueue(server, queueName);
    caps = queue.GetPrintCapabilities();
}

Prevention

When it happens

Trigger: Calling any PTProvider public method (directly or via PrintQueue/PrintTicket APIs) from a background/worker thread different from the one that created the provider, or calling after Release()/dispose.

Common situations: Printing work moved to a Task.Run/ThreadPool thread while the PrintQueue/PrintTicket was created on the UI thread; async printing continuations on a different thread; reusing a disposed provider instance.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of dotnet/wpf@81131a70a4 (2026-09-14). Data as JSON: /api/errors/644371b322e9c163. Report an issue: GitHub.

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/ReachFramework/PrintConfig/PTProvider.cs:558

                _deviceName = null;
                _schemaVersion = 0;
                _thread = null;
            }

            _disposed = true;
        }

        #endregion Dispose Pattern

        #region Private Methods

        private void VerifyAccess()
        {
            ObjectDisposedException.ThrowIf(_providerHandle is null, typeof(PTProvider));

            if(_thread != Thread.CurrentThread)
            {
                throw new InvalidOperationException(SR.PTProvider_VerifyAccess);
            }
        }
        
        /// <summary>
        /// Copies the managed source stream data to a native buffer and exposes the native buffer as an unmanaged IStream
        /// </summary>
        /// <remarks>
        /// This method reads the stream from its current cursor to the end.
        /// Caller is responsible for freeing the native buffer created (by using Marshal.ReleaseComObject on the IStream).
        /// The input memory stream is expected to have a publicly visible byte buffer
        /// </remarks>
        /// <param name="stream">the source MemoryStream</param>
        /// <returns>IStream copy of the input stream</returns>
        private static IStream IStreamFromMemoryStream(MemoryStream stream)
        {
            if (stream == null)
            {
                return null;

View on GitHub (pinned to 81131a70a4)