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
- Create and use the PTProvider/PrintQueue/PrintTicket on the same thread, or marshal the call back to the creating thread (Dispatcher.Invoke).
- Create a new PTProvider instance on the thread where it will be used.
- Check/ensure the provider has not been disposed before calling.
- 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
- Create and use PrintQueue/PrintTicket/PTProvider on one thread (typically the UI thread)
- Use Dispatcher.Invoke for UI-thread-bound print objects
- Don't share PTProvider instances across threads; create one per thread
- Release the provider deterministically with using/Dispose and never reuse it after
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
- SR.Format(SR.AxRequiresApartmentThread, clsid.ToString())
- SR.RequiresSTA
- SR.RequiresSTA
- SR.Verify_ApartmentState (template: Verify_ApartmentState…
- ArgumentException.NonNegativeValue (Parameter 'squareScale')
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)