Using Forms Authentication with ASP.net Web Api

I found myself recently needing to implement a very simple web api private web service into one of my sites to sync some data between servers. I’ve implemented a full blown API key authentication before with web api in past projects. It really isn’t that hard, but why add all of that extra code when my site already fully implements role based forms authentication? It is so easy to just use the authorize attribute with forms authentication.

        // GET api/sync
        [Authorize (Roles = "ApiUser")]
        public IEnumerable<int> Get(int? days)
        {
             //do some work return some data
        }

The problem isn’t really the code above, that will work fine and happily return an “unauthorized” message when you call it. In order to use the HttpClient properly you first need to login posting to the form that contains your login. This could vary depending on how your form is setup, but this works with the default asp.net template.

        private HttpClient CreateAuthenticatedSession()
        {
            var Client = new HttpClient {BaseAddress = new Uri("https://www.yourdomain.com")};
            var Result = Client.PostAsync("/Account/Logon/",
                             new FormUrlEncodedContent(
                                 new Dictionary<string, string>{ {"UserName", "Your Username"}, 
                                 {
                                     "Password","Your Password"
                                 }})).Result;
            Result.EnsureSuccessStatusCode();
            return Client;
        }

The code above once called will hopefully login to your site through forms authentication and return an HttpClient holding the correct cookie that will be passed with subsequent requests. You can keep talking to your Forms Authenticated web service as long as you hold onto the HttpClient returned after you authenticated. Below is a simple code block that calls CreateAuthenticatedSession() and calls the web api service.

        public void GenerateByDay(int days, string path)
        {
            var HttpClientSession = CreateAuthenticatedSession();
            HttpClientSession.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var Result = HttpClientSession.GetAsync("/api/sync/?days=" + days.ToString(CultureInfo.InvariantCulture)).Result;
            Result.EnsureSuccessStatusCode();
            var ResponseBody = Result.Content.ReadAsStringAsync().Result;
            //deserialize result and continue on 
        }
Tagged: , , ,

Fixing Multiple actions were found that match the request Asp.net Web Api Error

WebApiTest Project Here

I’m currently developing a new project utilizing asp.net Web Api 4.5. I hit a frustrating issue today with regards to routing actions. The error is “Multiple actions were found that match the request” when I added a new method to my code bypassing the built in GET,POST,PUT convention that web API supports out of the box.

The default scaffold controller web api gives you has the following pattern out of the box.

    public class TestController : ApiController
    {
        // GET api/test
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
 
        // GET api/test/5
        public string Get(int id)
        {
            return "value";
        }
 
        // POST api/test
        public void Post([FromBody]string value)
        {
        }
 
        // PUT api/test/5
        public void Put(int id, [FromBody]string value)
        {
        }
 
        // DELETE api/test/5
        public void Delete(int id)
        {
        }
    }

Problems start to appear when you want to go beyond this convention such as another “GET” method that goes by a different name.

    public class TestController : ApiController
    {
        // GET api/test
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
 
        // GET api/test/GetAllWithFilter
        [HttpGet]
        public IEnumerable<string> GetAllWithFilter()
        {
            return new string[] { "value1"};
        }
 
        // GET api/test/5
        public string Get(int id)
        {
            return "value";
        }
 
        // POST api/test
        public void Post([FromBody]string value)
        {
        }
 
        // PUT api/test/5
        public void Put(int id, [FromBody]string value)
        {
        }
 
        // DELETE api/test/5
        public void Delete(int id)
        {
        }
    }

This shouldn’t be that hard to fix on the surface but the built in route that enables this convention actually makes this much more difficult that it should be. I searched around for a while to figure out how to solve this then finally ran across a great Stackoverflow post here that nails it. Sadly this post isn’t even marked as the answer, it should be.

In short replace your built in route in WebApiConfig with this (updated for latest release). Be sure to modify your api path to suit your application.

            config.Routes.MapHttpRoute("DefaultApiWithId", "api/v1/{controller}/{id}", new { id = RouteParameter.Optional }, new { id = @"\d+" });
            config.Routes.MapHttpRoute("DefaultApiWithAction", "api/v1/{controller}/{action}");
            config.Routes.MapHttpRoute("DefaultApiGet", "api/v1/{controller}", new { action = "Get" }, new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) });
            config.Routes.MapHttpRoute("DefaultApiPost", "api/v1/{controller}", new { action = "Post" }, new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) });
            config.Routes.MapHttpRoute("DefaultApiPut", "api/v1/{controller}", new { action = "Put" }, new { httpMethod = new HttpMethodConstraint(HttpMethod.Put) });
            config.Routes.MapHttpRoute("DefaultApiDelete", "api/v1/{controller}", new { action = "Delete" }, new { httpMethod = new HttpMethodConstraint(HttpMethod.Delete) });

This honors the GET,POST,PUT,DELETE convention while allowing you to create differently named actions in the same controller!

Update – Sample Project

As requested here is a sample project that I have verified works.

WebApiTest Project Here

Tagged: , , , ,

Web Api Generic MediaTypeFormatter for File Upload

I’m currently working on a personal project which uses Asp.net WebApi and .net 4.5. I have found several nice examples utilizing the Multipartformdatastreamprovider. I discovered quickly though that using this in a controller was going to multiply boilerplate code. It would be much more efficient to use a custom MediaTypeFormatter to handle the form data and pass me back the information I need in a memorystream which can be directly saved.

Jflood.net Original Code

Jflood.net provides a nice starting point to what I needed to do. However, I had a few more requirements of mine which included a JSON payload in the datafield. I also extended his ImageMedia class into a generic FileUpload class and exposed a few simple methods.

1
2
3
4
5
6
7
8
 public HttpResponseMessage Post(FileUpload<font> upload)
        {
            var FilePath = "Path";
            upload.Save(FilePath); //save the buffer
            upload.Value.Insert(); //save font object to DB
 
            return Request.CreateResponse(HttpStatusCode.OK, upload.Value);
        }

This is the file upload class. Like Jflood’s it includes a payload for the file to be stored and written in memory. I have added a simple save method which performs a few checks and saves to disk. I also have some project specific code that checks if the T type has a property named filename, if so it passes the name to it. Since my Value field is of Type T it is automatically deserialized by Json.net.

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
    public class FileUpload<T>
    {
        private readonly string _RawValue;
 
        public T Value { get; set; }
        public string FileName { get; set; }
        public string MediaType { get; set; }
        public byte[] Buffer { get; set; }
 
        public FileUpload(byte[] buffer, string mediaType, string fileName, string value)
        {
            Buffer = buffer;
            MediaType = mediaType;
            FileName = fileName.Replace("\"","");
            _RawValue = value;
 
            Value = JsonConvert.DeserializeObject<T>(_RawValue);
        }
 
        public void Save(string path)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            var NewPath = Path.Combine(path, FileName);
            if (File.Exists(NewPath))
            {
                File.Delete(NewPath);
            }
 
            File.WriteAllBytes(NewPath, Buffer);
 
            var Property = Value.GetType().GetProperty("FileName");
            Property.SetValue(Value,FileName, null);
        }
    }

This is essentially the same thing as jflood is doing, however, I have added a section to parse out my “data” field that contains json.

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
    public class FileMediaFormatter<T> : MediaTypeFormatter
    {
 
        public FileMediaFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
        }
 
        public override bool CanReadType(Type type)
        {
            return type == typeof(FileUpload<T>);
        }
 
        public override bool CanWriteType(Type type)
        {
            return false;
        }
 
        public async override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
 
            if (!content.IsMimeMultipartContent())
            {
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
            }
 
            var Parts = await content.ReadAsMultipartAsync();
            var FileContent = Parts.Contents.First(x =>
                SupportedMediaTypes.Contains(x.Headers.ContentType));
 
            var DataString = "";
            foreach (var Part in Parts.Contents.Where(x => x.Headers.ContentDisposition.DispositionType == "form-data" 
                                                        && x.Headers.ContentDisposition.Name == "\"data\""))
            {
                var Data = await Part.ReadAsStringAsync();
                DataString = Data;
            }
 
            string FileName = FileContent.Headers.ContentDisposition.FileName;
            string MediaType = FileContent.Headers.ContentType.MediaType;
 
            using (var Imgstream = await FileContent.ReadAsStreamAsync())
            {
                byte[] Imagebuffer = ReadFully(Imgstream);
                return new FileUpload<T>(Imagebuffer, MediaType,FileName ,DataString);
            }
        }
 
        private byte[] ReadFully(Stream input)
        {
            var Buffer = new byte[16 * 1024];
            using (var Ms = new MemoryStream())
            {
                int Read;
                while ((Read = input.Read(Buffer, 0, Buffer.Length)) > 0)
                {
                    Ms.Write(Buffer, 0, Read);
                }
                return Ms.ToArray();
            }
        }
 
 
    }

Finally in your application_start you must include this line below.

1
GlobalConfiguration.Configuration.Formatters.Add(new FileMediaFormatter<font>());

Now it is really simple to accept uploads from other controllers. Be sure to tweak the MIME type for your own needs. Right now this only accepts application/octet-stream but it can easily accept other formats by adding other MIME types.

Update – How to add model validation support.

Quick update for the fileupload class. I’ve seen a few posts on stackoverflow about how to not only deserialize your object using a method such as mine, but also maintain the data annotation validation rules. That turns out to be pretty easy to do. Basically combine reflection and TryValidateProperty() and you can validate properties on demand. In my example it shows how you can get the validation messages. It simply puts them into an array. Here is sample below.

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
public class FileUpload<T>
    {
        private readonly string _RawValue;
 
        public T Value { get; set; }
        public string FileName { get; set; }
        public string MediaType { get; set; }
        public byte[] Buffer { get; set; }
 
        public List<ValidationResult> ValidationResults = new List<ValidationResult>(); 
 
        public FileUpload(byte[] buffer, string mediaType, string fileName, string value)
        {
            Buffer = buffer;
            MediaType = mediaType;
            FileName = fileName.Replace("\"","");
            _RawValue = value;
 
            Value = JsonConvert.DeserializeObject<T>(_RawValue);
 
            foreach (PropertyInfo Property in Value.GetType().GetProperties())
            {
                var Results = new List<ValidationResult>();
                Validator.TryValidateProperty(Property.GetValue(Value),
                                              new ValidationContext(Value) {MemberName = Property.Name}, Results);
                ValidationResults.AddRange(Results);
            }
        }
 
        public void Save(string path, int userId)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            var SafeFileName = Md5Hash.GetSaltedFileName(userId,FileName);
            var NewPath = Path.Combine(path, SafeFileName);
            if (File.Exists(NewPath))
            {
                File.Delete(NewPath);
            }
 
            File.WriteAllBytes(NewPath, Buffer);
 
            var Property = Value.GetType().GetProperty("FileName");
            Property.SetValue(Value, SafeFileName, null);
        }
    }
Tagged: ,