Photo by Nikhita S
Recently I've been working on adding "Office for the Web" support to our system. This will enable us to edit office documents uploaded to our system directly in the browser. To add support for Office for the web you need to implement a protocol known as WOPI. Sadly I can't provide a complete (code) solution for this since the code I've written is proprietary...but I can still share a bunch of tips and tricks!
There's a few steps that you need to do before Office for the web will work.
- Be a member of the Office 365 - Cloud Storage Partner Program
- Implement the WOPI protocol - a set of REST endpoints that expose information about the documents that you want to view or edit in Office for the web.
- Add support for WOPI Discovery
- Create a HTML page that will embedd the Office for the web iframe.
My tips will focus on step 2 - The REST endpoints (using ASP.NET Core).
Use multiple endpoints
The WOPI protocol uses the same URL path for multiple different operations. They provide a header,
X-WOPI-Override, that you can inspect and then decide which code to run. In our old solution we only had one endpoint that mapped to this path and the code is quite messy because of that (a lot of switching/if/else).
IMO, a better solution is to inspect the header and then map it to a corresponding action method instead to achieve a better separation of concerns. In ASP.NET Core this is quite easy to achieve using IActionConstraint. You can check out my previous blog post for a complete example.
PascalCase your JSON responses
I spent a few hours wondering why my WOPI integration didn't work, I had green integration tests and everything. Turns out I returned camelCased JSON. WOPI requires PascalCased JSON. Big facepalm when I finally figured that one out.
Write Integration tests
Whenever I build an API I always create integration tests for my endpoints. I use them mainly for veryfing that my API behaves as expected (returning correct status codes, response body and so on). It's also really easy to write integration tests in ASP.NET core so there's really no reason for not doing it :).
Let's take the GetLock WOPI operation as an example. The specification says that your API should return the following status codes:
- 200 OK – Success; an X-WOPI-Lock response header containing the value of the current lock on the file must always be included when using this response code
- 401 Unauthorized – Invalid access token
- 404 Not Found – Resource not found/user unauthorized
- 409 Conflict – Lock mismatch/locked by another interface; an X-WOPI-Lock response header containing the value of the current lock on the file must always be included when using this response code
- 500 Internal Server Error – Server error
- 501 Not Implemented – Operation not supported
I then proceed to create a test for all of the different cases (the only thing I mock/fake in my integration tests are external dependencies like databases) to ensure that my API follows the specification.
Get the test application up and running ASAP
WOPI provides something they call Interactive Validation.
This is an automated tool that will send requests towards your application and validate your implementation. It's AWESOME...
...but beware of (lack of) SessionContext
The validation application does not support SessionContext.
This also wasted a few hours for me. Luckily for me I could work around this with a hack, I basically look for the SessionContext header, if it's not present I will construct it manually using the currently authenticated user data.
I use session context for storing information about the document currently being viewed/edited. The key I use for this looks like this UserId-FileId.
One thing the documentation fails to mention is that the session context value returned from WOPI is base64 encoded.
I wrote a custom AuthenticationHandler that reads the access token from the header/querystring and then creates an identity based on the information in the access token. The access token is simply a key that I use to look up additional information regarding the user that I've stored in redis.
We have our documents in Azure blob storage. To access the documents we have a microservice that's responsible for reading/writing data from/to Azure.
When doing the integration I focused on getting the wopi endpoints working before doing the integration towards our microservice. What I did was that I created a interface in my WOPI code called IFileService and then I created an in memory implementation of this interface. The in memory implementation contained a few different basic office files. I then used this implementation when validating my WOPI endpoints. When I was sure that my endpoints worked as they should, I simply switched the implementation from the InMemoryFileService to using the real one.
Return empty header
When reading the documentation you will see something like this:
...the X-WOPI-Lock response header should be set to the empty string or omitted completely.
I chose to omit it completley but then the validation application failed a couple of tests. So I then chose to return an empty header instead and updated my integration tests. All green, nice. But when I ran the validation application it still failed the tests with the same error, weird? Turns out that when you're running your application behind IIS in a Windows App Service, the empty header will simply be removed.
Luckily the GitHub issue contains a workaround, simply add a space to your empty header and you're good to go.
The reason for using a WINDOWS App Service you might ask...?
This was the first time that I used remote debugging. It's a really powerful feature, especially when combining it with the validation application. If I had some failing tests, I could just hook up my debugger and then run the tests again. It was really valuable to be able to inspect the WOPI requests to get a better understanding of the protocol.