Summary
We need a thread management API for the Microsoft Office addins so that code can safely move from background work to the main thread of the Office application before calling application, Office, or NetOffice COM APIs.
This feature should provide an explicit and reviewable boundary:
await mainThread.DispatchToMainThreadAsync((Application app, Presentation presentation) =>
{
// PowerPoint / NetOffice / COM calls are allowed here.
});
Background
The immediate need comes from the embedded CefSharp Chromium browser. JavaScript messages can arrive on a CefSharp/CEF background thread, while PowerPoint automation must be performed on the application's main thread. Today, this requirement is easy to miss because the code that receives a browser message and the code that touches PowerPoint can live in the same async flow.
Problem
Microsoft Office COM automation is thread-affine. In practice, calls into an Office object model must be made from the correct Office main thread. CefSharp does not guarantee that incoming browser messages are raised on that thread.
This creates a risky flow:
- A WebView is created for the currently active Office context.
- The context may be a PowerPoint presentation, Excel workbook, Word document, Outlook item, or another host-specific object.
- JavaScript sends a message to the host.
- The host receives the message on a CefSharp/CEF thread.
- The handler performs async work, for example a network call.
- While the handler is waiting, the user switches to a different document, workbook, presentation, inspector, or active window.
- The handler resumes and reads global active state such as
ActivePresentation, ActiveWorkbook, ActiveDocument, or ActiveInspector.
- The command runs against the wrong Office context, or against invalid Office state.
Why This Needs an API
Without a dedicated API, each message handler has to remember several rules:
- Do not call Office, application-specific, or NetOffice COM APIs from a CefSharp callback thread.
- Do not rely on global active Office state after async work has happened.
- Do not assume
await returns to the Office main thread.
- Do not keep using an Office context after its browser, document, workbook, presentation, item, or host window has been closed.
- Do not mix network waits and COM calls inside one long "main thread" block.
Those rules are reasonable, but they are too easy to apply inconsistently across handlers. A thread management API gives the team one safe pattern instead of many local interpretations.
It also makes code review easier. Reviewers should be able to scan a handler and see:
- background work happens outside the Office-thread callback;
- Office COM work happens only inside the callback;
- the callback receives the Office application and the captured Office context;
- the handler does not rediscover its target from global active state.
Proposed Mental Model
Treat each browser-backed Office workflow as a small session with access to the Office thread and the Office context that created it.
CefSharp message thread
-> parse browser command
-> perform background or network work
-> enter Office context session
-> switch to Office main thread
-> validate session is still alive
-> provide Office Application and captured context
-> execute Office / NetOffice / COM work
This is similar to an actor model, but scoped to Office automation. The session owns the right execution context and the right Office object context. Callers do not need to know how dispatching is implemented.
Example Code and Usage
var browser = new Browser();
var presentationContext = new OfficeThreadContext(Application.ActivePresentation);
browser.JavascriptMessageReceived += (_, e) =>
{
_ = HandleMessageAsync(e);
};
private async Task HandleMessageAsync(JavascriptMessageReceivedEventArgs e)
{
var command = e.ConvertMessageToT<BrowserCommand>();
var result = await LoadDataForCommandAsync(command);
await presentationContext.InvokeAsync((PowerPoint.Application app, Presentation presentation) =>
{
// Only here do we touch PowerPoint / NetOffice / COM objects.
// This presentation is the one that owns the browser session,
// not whatever Application.ActivePresentation is right now.
var appName = app.Name;
var presentationName = presentation.Name;
ApplyResultToPresentation(presentation, result);
});
}
The important part is that presentationContext is created when the WebView is attached to a presentation. At that time, Application.ActivePresentation may be used to capture the intended presentation. After that point, browser messages should use the captured presentation context, not active global state.
Context Ownership
The API should make the owner of a browser-backed workflow explicit.
For example, a browser session should not ask the Office application, "What is active right now?" when it receives a message. It should ask its own session, "What Office context did I belong to when I was created?"
That distinction matters because Office users often work with multiple open objects:
- multiple PowerPoint presentations;
- multiple Excel workbooks or windows;
- multiple Word documents;
- multiple Outlook inspectors, explorers, mail items, meetings, or accounts.
Async work makes this more visible. A handler may start while one Office object is active and resume after the user has moved somewhere else. The session must keep the original context stable across that time gap.
Compiler and Analyzer Expectations
C# and .NET do not provide Swift-style @MainActor compiler enforcement for this scenario. A custom awaiter or dispatcher can move execution to the right thread, but it does not make the compiler understand that Office COM APIs are main-thread-only.
Because of that, the API should be designed so that misuse is visible:
- COM access should be concentrated inside
InvokeAsync callbacks.
- Methods that require the Office thread should call a runtime assertion such as
ThrowIfNotOnOfficeThread().
- We should consider adding Roslyn analyzer rules later to flag direct Office or NetOffice access outside approved boundaries.
Compiler analysis can help us catch patterns, but the first layer of safety should be a clear API that guides normal development.
Non-Goals
This feature should not:
- replace all async/await usage in Office integrations;
- introduce a distributed actor framework;
- make CefSharp callbacks run on the Office thread;
- hold the Office thread while waiting for network calls;
- hide context lifecycle problems by falling back to active Office state;
- force every Office host to use the same context type;
- make every Office operation globally serialized unless that is required by the Office thread model.
Discovery Questions
- What is the reliable source of the Office main-thread dispatcher in each host?
- Is the dispatcher per Office application process, per add-in instance, or per hosted UI surface?
- When exactly should an Office context session be created and disposed?
- How do we detect that the captured Office context has been closed or detached?
- Should stale messages be ignored, canceled, or reported back to JavaScript as a known error?
- Which Office or NetOffice objects are safe to store as session identity, and which should be reacquired on the Office thread?
- Do we need separate sessions per browser, per document/workbook/presentation/item, or per browser-context attachment?
- Should the API expose only synchronous callbacks, or also support async callbacks with strict guidance?
Success Criteria
The feature is successful when:
- browser message handlers have one standard way to enter Office-thread work;
- handlers no longer read global active Office state after async background work to find their target;
- Office and NetOffice COM calls are easy to identify during review;
- stale Office context or browser messages are handled deliberately;
- each Office host can define its own context type without changing the dispatching model;
- new handlers can follow the safe pattern without needing deep COM threading knowledge.
Summary
We need a thread management API for the Microsoft Office addins so that code can safely move from background work to the main thread of the Office application before calling application, Office, or NetOffice COM APIs.
This feature should provide an explicit and reviewable boundary:
Background
The immediate need comes from the embedded CefSharp Chromium browser. JavaScript messages can arrive on a CefSharp/CEF background thread, while PowerPoint automation must be performed on the application's main thread. Today, this requirement is easy to miss because the code that receives a browser message and the code that touches PowerPoint can live in the same async flow.
Problem
Microsoft Office COM automation is thread-affine. In practice, calls into an Office object model must be made from the correct Office main thread. CefSharp does not guarantee that incoming browser messages are raised on that thread.
This creates a risky flow:
ActivePresentation,ActiveWorkbook,ActiveDocument, orActiveInspector.Why This Needs an API
Without a dedicated API, each message handler has to remember several rules:
awaitreturns to the Office main thread.Those rules are reasonable, but they are too easy to apply inconsistently across handlers. A thread management API gives the team one safe pattern instead of many local interpretations.
It also makes code review easier. Reviewers should be able to scan a handler and see:
Proposed Mental Model
Treat each browser-backed Office workflow as a small session with access to the Office thread and the Office context that created it.
This is similar to an actor model, but scoped to Office automation. The session owns the right execution context and the right Office object context. Callers do not need to know how dispatching is implemented.
Example Code and Usage
The important part is that
presentationContextis created when the WebView is attached to a presentation. At that time,Application.ActivePresentationmay be used to capture the intended presentation. After that point, browser messages should use the captured presentation context, not active global state.Context Ownership
The API should make the owner of a browser-backed workflow explicit.
For example, a browser session should not ask the Office application, "What is active right now?" when it receives a message. It should ask its own session, "What Office context did I belong to when I was created?"
That distinction matters because Office users often work with multiple open objects:
Async work makes this more visible. A handler may start while one Office object is active and resume after the user has moved somewhere else. The session must keep the original context stable across that time gap.
Compiler and Analyzer Expectations
C# and .NET do not provide Swift-style
@MainActorcompiler enforcement for this scenario. A custom awaiter or dispatcher can move execution to the right thread, but it does not make the compiler understand that Office COM APIs are main-thread-only.Because of that, the API should be designed so that misuse is visible:
InvokeAsynccallbacks.ThrowIfNotOnOfficeThread().Compiler analysis can help us catch patterns, but the first layer of safety should be a clear API that guides normal development.
Non-Goals
This feature should not:
Discovery Questions
Success Criteria
The feature is successful when: