Thursday, 23 April 2015

Packaging Complex Azure Cloud Services Projects

I've been working on an Azure Cloud Services Project for which the packaging process is a little complicated and thought it worth sharing how I've made this work. Although there's only a single Web Role and no Worker Roles, the Web Role:

  • Has two websites,
  • Hosts a Click Once project,
  • Hosts a downloadable zip file containing some PowerShell extensions.
The Click Once projects and the zip files are rebuilt each time the package is built. I'm only going to talk in this post about how the packaging occurs, that is, the process by which I create a cspkg and cscfg file; the deployment can be carried out using your favoured method (Visual Studio, TFS Build, PowerShell script) once you have created the package.

The websites are developed within the single Visual Studio 2013 solution MyApp.Server.sln and the Click Once and command line tools within another solution MyApp.Client.sln. These are located in the same folder in the file system / TFS as shown below:


The MyApp.Common project contains common functionality that is shared by all the client and server projects.

Extending the Packaging Process

All the processes that I know of that deploy Azure Cloud Services packages use MsBuild.exe to build the package itself. It would therefore be best if whatever we do to ensure that all the components of our Web Role end up in the package is something that will be triggered by MsBuild. We could try and do this the hard way by creating MsBuild tasks and customising the build process, but I went for the easy way: setting Pre-Build commands on the Cloud Services project.

Deploying Two Websites to a Single Web Role

After I've created a Web  Role for the first Web Project, MyApp.WebSite1, the ServiceDefinition.csdef file for my Cloud Service looks like this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="MyApp.CloudService" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" schemaVersion="2014-06.2.4">
  <WebRole name="MyApp.WebSite1" vmsize="Small">
    <Sites>
      <Site name="Web">
        <Bindings>
          <Binding name="Main" endpointName="WebSite1" />
        </Bindings>
      </Site>
    </Sites>
    <Endpoints>
      <InputEndpoint name="WebSite1" protocol="http" port="80" />
    </Endpoints>
  </WebRole>
</ServiceDefinition>

To add my second Web Project to this role I need to manually edit this file, adding the following:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="MyApp.CloudService" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" schemaVersion="2014-06.2.4">
  <WebRole name="MyApp.WebSite1" vmsize="Small">
    <Sites>
      <Site name="Web">
        <Bindings>
          <Binding name="Main" endpointName="WebSite1" />
        </Bindings>
      </Site>
      <Site name="Web2" physicalDirectory=".\..\..\..\MyApp.WebSite2">
        <Bindings>
          <Binding name="Main" endpointName="WebSite2" />
        </Bindings>
      </Site>
    </Sites>
    <Endpoints>
      <InputEndpoint name="WebSite1" protocol="http" port="80" />
      <InputEndpoint name="WebSite2" protocol="http" port="82" />
    </Endpoints>
  </WebRole>
</ServiceDefinition>

(The \..\..\..\ in the "physicalDirectory" attribute is needed because the current directory at this point is "MyApp.CloudService\csx\Debug")

If I press F5 my two websites now fire up correctly in the Azure emulator.  But let's build a package to deploy to Azure and see what actually gets included. If I right click on the MyApp.CloudService project and select the "Package" option then the following is built:


Rename the .cspkg to a .zip, look inside it and we find:


Extract the .cssx file, rename it to a .zip, look inside it and we find (if we drill down):


You can see the problem here: although the site has been included in the package, all the source code files and other development artifacts are included instead of just the binaries, config and content. Worse still:

  1. The project is not built when we package our MyApp.CloudService project as it is not a dependency, so the build that we deploy is the previous build; this might be Debug rather than Release, and might have out of date binaries that do not include the latest changes.
  2. The web.config transforms are not applied, so the web.config that we deploy to production contains all our Debug settings.
So how can we fix the problem?  What I need is to deploy a copy of MyApp.WebSite2 that has been built using the correct configuration and then had the development artifacts removed and the web.config transforms applied.  As this is exactly what the "Publish" process does I will use a Pre-Build command on the Cloud Services project to publish MyApp.WebSite2 to a temporary directory and then point the .csdef at that temporary directory instead of the source directory. Before we can do this we need to set up a Publish Profile for MyApp.WebSite2 as shown below:



Note that we choose "Custom" not "Microsoft Azure Websites" because by "publish" here what we really mean is "build using my chosen configuration and apply the web.config transformations of my chosen configuration, copying over only the binaries, content and configuration".

Step 2 of the wizard:


At this step I enter the path to a temporary folder that is a sibling of the MyApp.WebSite2 project folder. The remaining steps of the wizard can be clicked through accepting the default settings. Next I edit the .csdef file to point to this temporary folder instead of the source folder:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="MyApp.CloudService" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" schemaVersion="2014-06.2.4">
  <WebRole name="MyApp.WebSite1" vmsize="Small">
    <Sites>
      <Site name="Web">
        <Bindings>
          <Binding name="Main" endpointName="WebSite1" />
        </Bindings>
      </Site>
      <Site name="Web2" physicalDirectory=".\..\..\..\MyApp.WebSite2.TempPublish">
        <Bindings>
          <Binding name="Main" endpointName="WebSite2" />
        </Bindings>
      </Site>
    </Sites>
    <Endpoints>
      <InputEndpoint name="WebSite1" protocol="http" port="80" />
      <InputEndpoint name="WebSite2" protocol="http" port="82" />
    </Endpoints>
  </WebRole>
</ServiceDefinition>

And add the following to the Pre-Build commands of the MyApp.CloudService project:


"%ProgramFiles(x86)%\MSBuild\12.0\Bin\msbuild.exe" "$(ProjectDir)..\MyApp.WebSite2\MyApp.WebSite2.csproj" /p:DeployOnBuild=true /p:PublishProfile="Pre Azure Publish" /p:VisualStudioVersion=12.0 /p:Configuration=$(ConfigurationName)

Most of the parameters are as you would expect, the only surprise is the "/p:VisualStudioVersion=12.0"; see this blog post on msbuild for the reason why this is needed.

Hit F5 and it starts up in the Azure emulator without any problems. Build a package, drill down into the .cspkg file and this time we find:



All the development artifacts have been removed, and if we look at the contents of the web.config file the transformations have been applied.

Building the Click Once Project

Before thinking about how we can ensure that the Click Once project is correctly deployed to Azure I need to give you some more details of how it is set up. The MyApp.ClickOnce project is configured to Publish to a folder within the MyApp.WebSite1 project using the Publish wizard as shown below:


In the next step we need to enter the Url that users will download the app from on our Cloud Services website:


In the next step I select whether the app can be launched independently of the website:


After the wizard has run and published the application I open the Properties of the MyApp.ClickOnce project and suppress the creation of the "publish.htm" file:


Finally I clear the option to auto-increment the version on each publish operation and ensure that the version number is set to 1.0.0.1:



I click "Publish Now" to carry out another publish operation with the new settings. To ensure that the app is deployed along with MyApp.WebSite1 I need to include it in the project and ensure that all the files are marked as "Content":



I also at this point ensure that these files are not source controlled by undoing the "add" operations in Team Explorer, Pending Changes. Note that Visual Studio is quite happy for files to be part of the project without being in TFS.

With this publishing set up the most recent build of MyApp.ClickOnce will be included in the Cloud Services package when it is built. To ensure that this build is up to date we need... another Pre Build command! I add the following to the Pre Build commands of MyApp.CloudService:


"%ProgramFiles(x86)%\MSBuild\12.0\Bin\msbuild.exe" "$(ProjectDir)..\MyApp.ClickOnce\MyApp.ClickOnce.csproj" /p:DeployOnBuild=true /p:VisualStudioVersion=12.0 /p:Configuration=$(ConfigurationName) /p:Platform=AnyCPU /target:publish /p:InstallUrl=http://www.mydomain.com/downloads/ClientApp/ "/p:PublishDir=$(ProjectDir)..\MyApp.WebSite1\downloads\ClientApp\\"

A few important things to note about this command:

  1. The "InstallUrl" and "PublishDir" properties need to be set via the command line even though we have already specified them when setting up publishing for MyApp.ClickOnce.
  2. The double trailing slash on the PublishDir is needed!
  3. If $(ProjectDir) contains any spaces (which will normally be the case as the default location for projects is "%USERPROFILE%\Documents\Visual Studio 2013\Projects") then this will not work; you will get an error message from msbuild that says

    MSBUILD : error MSB1008: Only one project can be specified.

    The only solution I found to this was to replace "$(ProjectDir).." in the PublishDir with the literal path using short folder names, like this:

"%ProgramFiles(x86)%\MSBuild\12.0\Bin\msbuild.exe" "$(ProjectDir)..\MyApp.ClickOnce\MyApp.ClickOnce.csproj" /p:DeployOnBuild=true /p:VisualStudioVersion=12.0 /p:Configuration=$(ConfigurationName) /p:Platform=AnyCPU /target:publish /p:InstallUrl=http://www.mydomain.com/downloads/ClientApp/ "/p:PublishDir=C:\Users\Alex\Documents\Visual~1\Projects\MyApp\MyApp.WebSite1\downloads\ClientApp\\"

It's a shame I have to do this, but at least it works! Every time I want to increment the version number of the Click Once app I have to ensure that I include the build outputs from the new version in the MyApp.WebSite1 project (and I'll probably want to exclude the outputs from the old version).

Zip File containing PowerShell Extensions

Outputs of my MyApp.PowerShell project are:


I want to end up with these outputs (less the .pdb files) in a zip file in the MyApp.WebSite1 project like this:


What's the best way to do this? You can't throw a brick in Linux without hitting a command line zip tool, but in Windows they're a bit less abundant, so I decided to use PowerShell, and added the following to the MyApp.CloudService Pre Build command line to build and zip the MyApp.PowerShell project:


1
2
"%ProgramFiles(x86)%\MSBuild\12.0\Bin\msbuild.exe" "$(ProjectDir)..\MyApp.PowerShell\MyApp.PowerShell.csproj" /p:VisualStudioVersion=12.0 /p:Configuration=$(ConfigurationName) /target:build
powershell -Command "[void] [System.Reflection.Assembly]::LoadFrom('$(ProjectDir)..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll');$zip = [ICSharpCode.SharpZipLib.Zip.ZipFile]::Create('$(ProjectDir)..\MyApp.WebSite1\downloads\MyApp.PowerShell.zip');$zip.BeginUpdate();ls '$(ProjectDir)..\MyApp.PowerShell\bin\$(ConfigurationName)' -exclude *.pdb,*.xml | Foreach-Object {$zip.Add($_.FullName, $_.Name)};$zip.CommitUpdate();"

The second line isn't very readable in that format so here it is split on to multiple lines:


1
2
3
4
5
[void][System.Reflection.Assembly]::LoadFrom('$(ProjectDir)..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll');
$zip = [ICSharpCode.SharpZipLib.Zip.ZipFile]::Create('$(ProjectDir)..\MyApp.WebSite1\downloads\MyApp.PowerShell.zip');
$zip.BeginUpdate();
ls '$(ProjectDir)..\MyApp.PowerShell\bin\$(ConfigurationName)' -exclude *.pdb,*.xml | Foreach-Object {$zip.Add($_.FullName, $_.Name)};
$zip.CommitUpdate();

For this to work you need to add the SharpZipLib package to one of the projects in the solution. You can use Microsoft's System.IO.Compression.FileSystem.dll assembly but this only allows you to zip whole folders so we would not be able to exclude the .pdb files.

Conclusion

Pre Build events are very useful for extending the Azure packaging process, or for extending any other Visual Studio publishing process.

The only final note to add is that if you wanted your project to start up slightly faster when debugging you could excluded some of the Pre Build commands from a Debug build by prefixing them with:


if /i "$(ConfigurationName)" == "Release" 

Enjoy!

Monday, 20 April 2015

How Backbone.Model.isNew works

I'm using Backbone in one of my current projects for a rich and responsive UI. When creating a new data item client side, I sometimes need to do a server call to get the data for the new model because setting the defaults in the new model requires logic that is only found on the server.  But (importantly) the model is still "new" at this point; it hasn't been persisted to the server.  So two server round trips are required to create the object:

It would be possible to create the model on the first call to the server and then update it on the second call, but that sets up a slightly different workflow in several ways:

  1. If the user wants to abandon the creation in step (3) we need to delete the model, or inform the user that they need to do this.
  2. If operations are auditing this would be audited as a create and an update rather than simply a create.
  3. This wouldn't work at all if some values can only be set at creation time.
So I have gone with the two stage "get defaults, commit" process shown above.

This leads to a problem; when I parse the model returned from the server at step 2 as a Backbone Model I find that Model.isNew() returns false. This means that when I commit the model through a Model.save() an "update" operation occurs instead of a "create"  Why is this?  Let's look at the definition of isNew from backbone.js:


    // A model is new if it has never been saved to the server, and lacks an id.
    isNew: function() {
      return !this.has(this.idAttribute);
    },

My model has the default value for idAttribute, "id". The id of my model on the server is a .NET
Guid, so when the server model is serialised to be returned in step 2 it will be serialised as:


{id: "00000000-0000-0000-0000-000000000000", title: null, anotherAtt: "Clever server logic set this"}

As this model does have a value for "id", Model.has("id") will return true and Model.isNew() will return false.
Help is at hand though: when I define my model I can simply override the isNew() function:


var MyModel = Backbone.Model.extend({
 isNew: function() {
  return !this.has(this.idAttribute) || this.id === '00000000-0000-0000-0000-000000000000';
 }
});

Or alternatively if I'm going to use this behaviour throughout my application I can modify this behaviour on the Model prototype:


    Backbone.Model.prototype.isNew = function () {
        return typeof this.attributes.id !== 'undefined' && this.attributes.id === '00000000-0000-0000-0000-000000000000';
    };

Either way, Backbone will now correctly distinguish between models that have been persisted and those that have not.

Friday, 17 April 2015

Handling PUT Content of any MIME Type using Web API

In a recent post I described how to create and register a Web API Media Type Formatter to handle incoming content that we want to handle as a raw Stream.  The only problem with that solution (as noted at the end of the post) is that our Media Type Formatter will only be invoked when the incoming content has a Content-Type of application/octet-stream.  What if we want to handle content of any type?

In this post I describe not 1 but 5! (5!!) possible ways of solving this problem.  Most of them have problems of some kind, so pick the one that works best for you. Or find another method and tell me about it in the comments!

1. Manually retrieve incoming Stream from the Request property on the Controller.

This will work for any content type because none of the method arguments need to be extracted from the HTTP Request body, so no Media Type Formatter is invoked.


1
2
3
4
5
6
7
        [HttpPut]
        public HttpResponseMessage AddDocument(string id)
        {
            Task<Stream> value = Request.Content.ReadAsStreamAsync();
            // Add document to backing store
            return new HttpResponseMessage(HttpStatusCode.Created);
        }

(As all the methods to read the content are asynchronous you would normally change the method declaration to be:

public async Task<HttpResponseMessage> AddDocument(string id)

so that you can use the await keyword to handle the asynchronous calls).

This works!  BUT there are some...

Problems with this approach:

Your Controller method needs to contain the logic to extract the Stream content from the incoming HTTP Request. The principle of Separation of Concerns says, in summary, that each unit of functionality in your program (whether that's a method or a class) should do just one thing.  Separate tasks should be placed in separate classes/methods. Following Separation of Concerns makes your code more testable and easier to change without breaking everything.  I do not recommend this approach.

2. Create a Media Type Formatter to handle every type of content you can think of.

Using the really big list of content types from this post you can create a Media Type Formatter that will handle any of them:


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;

namespace WebApplication5
{
    public class StreamMediaTypeFormatter : MediaTypeFormatter
    {
        public StreamMediaTypeFormatter()
        {
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/h323"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/3gpp2"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/3gpp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-7z-compressed"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/audible"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/aac"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/vnd.audible.aax"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/ac3"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/msaccess.addin"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/msaccess"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/msaccess.cab"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/msaccess.runtime"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/msaccess.webapplication"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/msaccess.ftemplate"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/internet-property-stream"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-bridge-url"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/vnd.dlna.adts"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/postscript"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-aiff"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/aiff"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.adobe.air-application-installer-package+zip"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-mpeg"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ms-application"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-jg"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-ms-asf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/atom+xml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/basic"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-msvideo"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/olescript"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-bcpio"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/bmp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-caf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-office.calx"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-pki.seccat"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-cdf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-x509-ca-cert"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-java-applet"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-msclip"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-cmx"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/cis-cod"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-ms-contact"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-cpio"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-mscardfile"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/pkix-crl"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-csh"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/css"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-director"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-dv"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-msdownload"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/dlm"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/msword"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-word.document.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-word.template.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.wordprocessingml.template"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-dvi"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("drawing/x-dwf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("message/rfc822"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/etl"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-setext"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/envoy"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.fdf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/fractals"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("x-world/x-vrml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-flv"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/fsharp-script"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/gif"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-ms-group"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-gsm"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-gtar"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-gzip"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-hdf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-hdml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-oleobject"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/winhlp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/mac-binhex40"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/hta"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-component"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/webviewhtml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-icon"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/ief"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-iphone"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-internet-signup"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-itunes-ipa"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-itunes-ipg"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-itunes-ipsw"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-ms-iqy"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-itunes-ite"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-itunes-itlp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-itunes-itms"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-itunes-itpc"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-ivf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/java-archive"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/liquidmotion"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/pjpeg"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-java-jnlp-file"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-javascript"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/jscript"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-latex"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/windows-library+xml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ms-reader"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-la-asf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-msmediaview"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/mpeg"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/vnd.dlna.mpeg-tts"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-mpegurl"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/m4a"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/m4b"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/m4p"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-m4r"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-m4v"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-macpaint"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-troff-man"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ms-manifest"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-msaccess"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-troff-me"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-shockwave-flash"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/mid"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-smaf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-msmoney"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/quicktime"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-sgi-movie"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/mpeg"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/mp4"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-mediapackage"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-project"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-troff-ms"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-miva-compiled"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-mmxp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-netcdf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/oda"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-ms-odc"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.oasis.opendocument.presentation"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/oleobject"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.oasis.opendocument.text"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/onenote"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/opensearchdescription+xml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/pkcs10"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-pkcs12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-pkcs7-certificates"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/pkcs7-mime"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-pkcs7-certreqresp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/pkcs7-signature"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-portable-bitmap"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-podcast"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/pict"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/pdf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-portable-graymap"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-pki.pko"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/scpls"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-perfmon"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-portable-anymap"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-powerpoint"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-powerpoint.template.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.presentationml.template"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-powerpoint.addin.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-portable-pixmap"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-powerpoint.slideshow.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.presentationml.slideshow"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-powerpoint.presentation.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.presentationml.presentation"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/pics-rules"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/powershell"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-mspublisher"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-html-insertion"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-quicktime"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-quicktimeplayer"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-pn-realaudio"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-cmu-raster"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/rat-file"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/vnd.rn-realflash"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-rgb"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.rn-realmedia"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.rn-rn_music_package"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-troff"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-pn-realaudio-plugin"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-ms-rqy"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/rtf"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/richtext"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-safari-safariextz"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-msschedule"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/scriptlet"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-sd2"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/sdp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/windows-search-connector+xml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/set-payment-initiation"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/set-registration-initiation"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-sgimb"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/sgml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-sh"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-shar"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-stuffit"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-powerpoint.slide.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.presentationml.slide"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-excel"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ms-license"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-smd"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/futuresplash"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-wais-source"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/streamingmedia"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-pki.certstore"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-pki.stl"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-sv4cpio"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-sv4crc"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-tar"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-tcl"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-tex"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-texinfo"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-compressed"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-officetheme"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/tiff"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-msterminal"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/tab-separated-values"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/iuls"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ustar"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/vbscript"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/x-vcard"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-visio.viewer"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.visio"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/ms-vsi"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vsix"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ms-vsto"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/wav"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-ms-wax"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/vnd.wap.wbmp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-works"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/vnd.ms-photo"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-safari-webarchive"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/wlmoviemaker"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-wlpg-detect"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-wlpg3-detect"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-ms-wm"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("audio/x-ms-wma"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ms-wmd"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-msmetafile"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/vnd.wap.wml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.wap.wmlc"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/vnd.wap.wmlscript"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.wap.wmlscriptc"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-ms-wmp"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-ms-wmv"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-ms-wmx"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ms-wmz"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-wpl"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-mswrite"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("video/x-ms-wvx"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/directx"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xaml+xml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-silverlight-app"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-ms-xbap"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-xbitmap"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xhtml+xml"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-excel.addin.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-excel.sheet.binary.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-excel.sheet.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-excel.template.macroenabled.12"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.spreadsheetml.template"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-xpixmap"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.ms-xpsdocument"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/x-xwindowdump"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-compress"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/x-zip-compressed"));
        }

        public override bool CanReadType(Type type)
        {
            return typeof(Stream) == type;
        }

        public override bool CanWriteType(Type type)
        {
            return false;
        }

        public override Task<object> ReadFromStreamAsync(Type type, 
            Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
            return Task.FromResult((object)readStream);
        }

        public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, 
            HttpContent content, IFormatterLogger formatterLogger, System.Threading.CancellationToken cancellationToken)
        {
            return ReadFromStreamAsync(type, readStream, content, formatterLogger);
        }
    }
}

This works!  BUT there are some fairly obvious...

Problems with this approach:

My programmer's instincts tell me that hard coding a list in this way is not good. What if my customers start using a new MIME type that isn't on my list?  But the list is fairly comprehensive so maybe this is OK in practice.

3. Use a Model Binder instead of a Media Type Formatter.

In my last post I said that Media Type Formatters were what Web API had instead of MVC's Model Binders.  That's sort of true because Media Type Formatters are what you would normally use to parse incoming POSTed or PUT content, but you can also use a Web API Model Binder (NOTE: different from a MVC Model Binder!). Below is an example of a Model Binder that will do exactly this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;

namespace WebApplication5
{
    public class StreamModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            bindingContext.Model = actionContext.Request.Content.ReadAsStreamAsync().Result;
            return true;
        }
    }
}

You also need to add a ModelBinder attribute to the controller method argument, like this:


1
2
3
4
5
6
7
        [HttpPut]
        public HttpResponseMessage AddDocument(string id, 
            [ModelBinder(typeof(StreamModelBinder))]Stream value)
        {
            // Add document to backing store
            return new HttpResponseMessage(HttpStatusCode.Created);
        }

This works!  BUT there are some...

Problems with this approach:

This is a little bit of an abuse of the ModelBinder system, which is meant to be used to parse complex types from url and querystring portions.  But the nastiest thing about it is that we have to call an asynchronous method, ReadAsStreamAsync, in a synchronous manner.  I don't get a thread deadlock (tested it) but I'm not sure how well this would perform under heavy load as the Task Scheduler will be using background threads to read the incoming stream.

4. Use MVC instead of Web API.

If Web API's strategy of breaking incoming content down by content type is becoming a pain then don't use it; use MVC instead! I can create the following controller:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using System.IO;
using System.Net;
using System.Web;
using System.Web.Mvc;

namespace WebApplication5.Controllers
{
    public class ValuesMvcController : Controller
    {
        [HttpPut]
        public ActionResult AddDocument(string id, [ModelBinder(typeof(StreamMvcModelBinder))] Stream value)
        {
            // Add document to backing store
            return new HttpStatusCodeResult(HttpStatusCode.Created);
        }
    }
}

And define the Model Binder (an MVC Model Binder this time) for the argument like this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
using System.Web;
using System.Web.Mvc;

namespace WebApplication5
{
    public class StreamMvcModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            return controllerContext.HttpContext.Request.InputStream;
        }
    }
}

All that remains now is to sort out the routing to ensure that my new controller handles the requests that the Web API controller was handling. My original Web API controller is the ValuesController that Visual Studio creates in a vanilla ASP.NET Web API project.  The Url for doing a PUT to this was

/api/values/1

If I want to use a different controller to handle the PUT requests but keep the same Url schema then I need to add some constraints to the Web API routes to stop them handling PUT requests:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Routing;

namespace WebApplication5
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional },
                constraints: new {httpMethod = new HttpMethodConstraint(HttpMethod.Get, 
                    HttpMethod.Delete, HttpMethod.Post) }
            );
        }
    }
}

I also need to add a fairly custom route for my new MVC controller to ensure that it handles PUT requests to Urls of the form /api/values/n:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace WebApplication5
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(name: "ValuesRoute", url: "api/values/{id}",
                defaults: new { controller = "ValuesMvc", action = "AddDocument", id = UrlParameter.Optional },
                constraints: new {httpMethod = new HttpMethodConstraint("PUT")}
                );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

It works, and I don't have to call any asynchronous methods in a synchronous fashion in the Model Binder as I had to do with the Web API Model Binder. I'm retaining separation of concerns; I'm slightly bending the purpose of a Model Binder by making it read the whole input stream but I don't think that will cause any problems. BUT it's really ugly having to split the functionality for one area of the system between two controllers like this and could well cause confusion to other developers working on the code.  So I don't really recommend this approach.

5. Create your own interface and implementation to abstract out the task of retrieving the content from the incoming HTTP Request

"All programs in computer science can be solved by another level of indirection", as a great programmer said.
When the Web API provides us with so many extensibility points it may seem greedy to create one more, but I'm not really happy with any of the approaches I've shown so far, so this is the one I've gone with.  I create a new interface, IRequestContextProvider, that needs to be implemented by objects that are responsible for retrieving the incoming HTTP stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
using System.IO;
using System.Threading.Tasks;
using System.Web.Http;

namespace WebApplication5
{
    public interface IRequestContextProvider
    {
        Task<Stream> GetInputStream(ApiController controller);
    }
}

My standard implementation of this is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
using System.IO;
using System.Threading.Tasks;
using System.Web.Http;

namespace WebApplication5
{
    public class WebRequestContextProvider : IRequestContextProvider
    {
        public Task<Stream> GetInputStream(ApiController controller)
        {
            return controller.Request.Content.ReadAsStreamAsync();
        }
    }
}

(For my unit tests I will create a Mock using either a separate implementation or a mocking framework like Moq).
My controller class now looks like this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.ModelBinding;

namespace WebApplication5.Controllers
{
    public class ValuesController : ApiController
    {
        private IRequestContextProvider requestContextProvider;

        public ValuesController(IRequestContextProvider requestContextProvider)
        {
            this.requestContextProvider = requestContextProvider;
        }

        [HttpPut]
        public async Task<HttpResponseMessage> AddDocument(string id)
        {
            Stream value = await requestContextProvider.GetInputStream(this);
            // Add document to backing store
            return new HttpResponseMessage(HttpStatusCode.Created);
        }

        // other methods here
    }
}

And I inject the standard implementation of IRequestContextProvider into my controller using Ninject, like this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using Ninject;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

namespace WebApplication5
{
    public class NinjectDependencyResolver : IDependencyResolver
    {
        private IKernel Kernel;

        public NinjectDependencyResolver()
        {
            Kernel = new StandardKernel();
            Kernel.Bind<IRequestContextProvider>().To<WebRequestContextProvider>().InSingletonScope();
        }

        public IDependencyScope BeginScope()
        {
            return this;
        }

        public object GetService(Type serviceType)
        {
            return Kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            return Kernel.GetAll(serviceType);
        }

        public void Dispose()
        {
        }
    }
}

using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace WebApplication5
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            GlobalConfiguration.Configuration.DependencyResolver = new NinjectDependencyResolver();            
        }
    }
}


The only thing you could have against this approach is that it's the most complicated.  But I like it and I'm using it.