Showing posts with label WebAPI. Show all posts
Showing posts with label WebAPI. Show all posts

Wednesday, 20 May 2015

(Trying to) use ADAL from Mono

For a recent project I needed to consume a Web API secured using Azure Active Directory from a Linux file server. I had an existing Windows program that did the same thing so I decided to try running it under Mono, the Linux port of .NET.


I hadn't used Mono before and was pleasantly surprised to discover that most things just worked (except for a few quirks with HttpWebRequest which I will describe in another post).  The only problem came when I tried to call Web API methods secured with Azure Active Directory. In the Windows program I was using the excellent Active Directory Authentication Library for .NET (ADAL). This wasn't working in the Mono version and the "Break on Exception" functionality in MonoDevelop isn't great so I created a simple test app for 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
using System;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Net;

namespace BearerAuthTest
{
 class MainClass
 {
  public const string DownloadTarget = "https://yourapp.com/";
  public const string Authority = "https://login.windows.net/common";
  public const string ClientId = "e74f20b9-d3b3-4b68-8a54-088ea85b55a8";
  public const string ServerApplicationIdentifier = "https://your-azure-ad.com/your-server-app";
  public const string ClientRedirectUri = "https://your-azure-ad.com/your-client-app";

  public static void Main (string[] args)
  {
   AuthenticationContext context = new AuthenticationContext (Authority);
   AuthenticationResult result = context.AcquireToken(
    ServerApplicationIdentifier,
    ClientId,
    new Uri(ClientRedirectUri)
   );
   WebClient webClient = new WebClient ();
   string header = result.CreateAuthorizationHeader ();
   Console.WriteLine ("Header={0}", header);
   webClient.Headers.Add(HttpRequestHeader.Authorization, header);
   string downloaded = webClient.DownloadString(DownloadTarget);
   Console.WriteLine ("Downloaded:\n\n {0}", downloaded);
   Console.ReadLine ();
  }
 }
}

I've edited the constants to remove the references to my own application and Azure AD directory, but I'll just explain what they are:

  1. DownloadTarget - this is the Url in our Web API that we are attempting to do a GET from.
  2. Authority - Url of the Azure AD multi tenant app login screen.
  3. ClientId - Guid of the native application; we must have set this up in Azure AD and granted it access to our Web API.
  4. ServerApplicationIdentifier - Uri identifier of the the Azure AD application that the Web API uses for authentication,
  5. ClientRedirectUri - Redirect Uri of the native application.
Just to make that clear, there are two Azure AD applications involved here; the Web Application that hosts the Web API, whose configuration will look like this:


And the native application that is attempting to connect to the Web API, the configuration of which looks like this:


When I run this in MonoDevelop on my Ubuntu dev machine I get this exception when I hit line 18:


An exception occurs when attempting to create a WindowsFormsWebAuthenticationDialog object. If I had to guess I would say this object is probably trying to raise a dialogue box to attempt an authentication operation with a web endpoint (see, if you make the type name long enough you don't need documentation). Getting out ILSpy I can see that the Type Initialiser of the base class of WindowsFormsWebAuthenticationDialog is calling a native method in IEFrame.dll - obviously that's not going to be there on a Linux machine, hence the exception.

I didn't give up hope yet though, because I'd read in a post on on Vittorio Bertocci's blog that it is possible to request an Azure AD authentication token without raising the authentication dialogue, passing the username and password in the request. So I replaced lines 18-22 of my code above with this:


AuthenticationResult result = context.AcquireToken (ServerApplicationIdentifier, ClientId,
 new UserCredential ("user1@your-azure-ad.com", "YourPassword"));

And this worked! I thought I'd cracked it, so I updated my code and deployed it only to find that as soon as I tried to use it I got an "Operation is not Supported" error. After a bit of head scratching I worked out that the only significant difference was that I was trying to log in with a user from a different Azure AD directory, my live directory rather than my test one. And this turns out to be the significant difference because:
  1. The native "Your Client App" app and the test user are both from the "your-azure-ad.com" directory, which means that the users from this domain do not need to grant consent to be able to use the app.
  2. The native app and the live user are from different directories, so when a token is requested using the AcquireToken method the user does need to give their consent.
  3. Re-reading Vittorio's post on the AcquireToken overload that allows you to pass a username and password in the request he says (in the Constraints and Limitations section):
"Users do not have any opportunity of providing consent if username & password are passed directly."
So I'm officially stuffed. Multi-tenant apps will always need to establish consent and that can only be done by showing the UI, which depends on native Windows components.

I'm not without hope that this might work in the future; the pre-release of ADAL 3 includes support for raising the authentication dialogue on Android and iOS platforms, so maybe this will work on other Mono platforms eventually.

In the meantime for my app I have had to pursue other options for authenticating from Mono. I am currently using Client Certificates which seems to be working OK, although as there doesn't seem to be anyone who supplies these commercially I'm generating them myself.

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.