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.

1 comment:

  1. Oh my God!

    I can't believe no one has commented on this article!

    I have searched for days for a solution to my problem of an API Web 2 action accepting a RESTful POST containing the data in the body (as opposed to a form post), with a Content-Type other than the defaults.

    Everywhere I looked, everybody else was either accepting simple JSON data, and wanting to know how to RETURN data using a MediaTypeFomatter, or they had control over the input, and just needed to prepend a "=" to the incoming data, but nobody, and I mean NOBODY, seemed to need to know how to ACCEPT a non-configurable, non-JSON/XML data post.

    I was pulling my hair out asking isn't ANYBODY writing APIs where they're required to accept data from a non-configurable source, such as a third-party customer, client or partner?

    In my case, I'm re-purposing an existing Web API app to accept a restful post from a vendor that is non-configurable...Unless of course, we pay an exorbitant fee for them to make a 30-second change to their post.

    I had almost given up hope, and was preparing to either kludge a MVC controller onto the app, or write a separate MVC app to re-route the vendor's post.

    But now I don't have to do either. :)

    Thanks for this great post!

    Tony

    ReplyDelete