In the previous post I looked at how to add a very simple WebApi 2.2 REST service and demonstrated GET method using VS2013 and in particular VB.net (as the years go by VB.net examples are becoming rarer; it seems everyone is moving to C# and whilst I can program in C# I prefer VB.net).
In this post I wanted to know how to enable POST requests from a simple ASP.net webform but it appeared that whilst it was easy to consume simple datatypes with WebAPI 2.2 POST, it appears as though you can’t map POST form variables to simple parameters of a Web API method. After some digging I found one way of doing it.. by encoding/decoding the POST data on the fly within the HttpPost method in the Web API method. Whilst this is of course workable it looked to me that there must be a better solution at which point I came across Rick Strahl’s post “Passing multiple simple POST Values to ASP.NET Web API” which uses the clever method of extending WebApi by creating custom Parameter Binding. This uses a simple class and a Global.asax hookup.
After reading through all of the comments I also noted that RoyiNamir had made some improvements to Rick Strahl’s code so I’ve converted his resultant C# class to VB.net and will explain how to add that to the WebAPI/Webforms project:
I’m going to add this to the basic “Cars” API that we already created in Part 1, so please refer to that first.
1. Add a New Class for the Custom Parameter Binding
- I’ve put this in /App_Code/CC/Net/WebApi/SimplePostModelBinding.vb but feel free to put it elsewhere.
This is the code for the SimplePostModelBinding.vb
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 | ' From: https://github.com/RoyiNamir/SimplePostVariableParameterBindingExtended ' And : http://weblog.west-wind.com/posts/2012/Sep/11/Passing-multiple-simple-POST-Values-to-ASPNET-Web-API ' Add this to Global.asax GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, SimplePostVariableParameterBinding.HookupParameterBinding) Imports System.Collections.Generic Imports System.Collections.Specialized Imports System.Globalization Imports System.Linq Imports System.Net.Http Imports System.Threading Imports System.Threading.Tasks Imports System.Web.Helpers Imports System.Web.Http.Controllers Imports System.Web.Http.Metadata Imports System.Web Imports Newtonsoft.Json Namespace CC.Net.WebApi Public Class SimplePostVariableParameterBinding Inherits HttpParameterBinding Public Const MultipleBodyParameters As String = "MultipleBodyParameters" Public Const MultipleBodyParametersRaw As String = "MultipleBodyParametersRaw" Public Const AllowJsonContentType As Boolean = True Public Sub New(descriptor As HttpParameterDescriptor) MyBase.New(descriptor) End Sub ''' ''' Check for simple binding parameters in POST data. Bind POST ''' data as well as query string data ''' ''' ''' ''' ''' Public Overrides Function ExecuteBindingAsync(metadataProvider As ModelMetadataProvider, actionContext As HttpActionContext, cancellationToken As CancellationToken) As Task Dim stringValue As String = Nothing Dim col As NameValueCollection = TryReadBody(actionContext.Request) If col IsNot Nothing Then stringValue = col(Descriptor.ParameterName) End If ' try reading query string if we have no POST/PUT match If stringValue Is Nothing Then Dim query = actionContext.Request.GetQueryNameValuePairs() If query IsNot Nothing Then Dim matches = query.Where(Function(kv) kv.Key.ToLower() = Descriptor.ParameterName.ToLower()) Dim keyValuePairs = If(TryCast(matches, IList(Of KeyValuePair(Of String, String))), matches.ToList()) If keyValuePairs.Any() Then stringValue = keyValuePairs.First().Value End If End If End If Dim value As Object = StringToType(stringValue) ' Set the binding result here SetValue(actionContext, value) ' now, we can return a completed task with no result Dim tcs As New TaskCompletionSource(Of AsyncVoid)() tcs.SetResult(Nothing) Return tcs.Task End Function ''' ''' Method that implements parameter binding hookup to the global configuration object's ''' ParameterBindingRules collection delegate. ''' ''' This routine filters based on POST/PUT method status and simple parameter ''' types. ''' ''' ''' GlobalConfiguration.Configuration. ''' .ParameterBindingRules ''' .Insert(0,SimplePostVariableParameterBinding.HookupParameterBinding); ''' ''' ''' Public Shared Function HookupParameterBinding(descriptor As HttpParameterDescriptor) As HttpParameterBinding Dim supportedMethods = descriptor.ActionDescriptor.SupportedHttpMethods ' Only apply this binder on POST and PUT operations If supportedMethods.Contains(HttpMethod.Post) OrElse supportedMethods.Contains(HttpMethod.Put) Then Dim supportedTypes = New Type() {GetType(String), GetType(Integer), GetType(System.Nullable(Of Integer)), GetType(Decimal), GetType(System.Nullable(Of Decimal)), GetType(Double), _ GetType(System.Nullable(Of Double)), GetType(Long), GetType(System.Nullable(Of Long)), GetType(Boolean), GetType(System.Nullable(Of Boolean)), GetType(DateTime), _ GetType(System.Nullable(Of DateTime)), GetType(Byte())} If supportedTypes.Count(Function(typ) typ = descriptor.ParameterType) > 0 Then Return New SimplePostVariableParameterBinding(descriptor) End If End If Return Nothing End Function Private Function StringToType(stringValue As String) As Object Dim value As Object = Nothing If stringValue Is Nothing Then value = Nothing ElseIf Descriptor.ParameterType = GetType(String) Then value = stringValue ElseIf Descriptor.ParameterType = GetType(Integer) Then value = Integer.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Integer)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of Integer)), Integer.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(Long) Then value = Long.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Long)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of Long)), Long.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(Decimal) Then value = Decimal.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Decimal)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of Decimal)), Decimal.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(Double) Then value = Double.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Double)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of Double)), Double.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(DateTime) Then value = DateTime.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of DateTime)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of DateTime)), DateTime.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(Boolean) Then value = False If stringValue = "true" OrElse stringValue = "on" OrElse stringValue = "1" Then value = True End If ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Boolean)) Then value = False If String.IsNullOrWhiteSpace(stringValue) Then value = DirectCast(Nothing, System.Nullable(Of Boolean)) ElseIf stringValue = "true" OrElse stringValue = "on" OrElse stringValue = "1" Then value = True End If Else value = stringValue End If Return value End Function ''' ''' Read and cache the request body ''' ''' ''' Private Function TryReadBody(request As HttpRequestMessage) As NameValueCollection Dim result As Object ' try to read out of cache first If Not request.Properties.TryGetValue(MultipleBodyParameters, result) Then Dim contentType = request.Content.Headers.ContentType If contentType IsNot Nothing Then Select Case contentType.MediaType Case "application/json" If True Then If AllowJsonContentType Then result = request.Content.ReadAsStringAsync().Result request.Properties.Add(MultipleBodyParametersRaw, result) Dim values = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(result.ToString()) result = values.Aggregate(New NameValueCollection(), Function(seed, current) seed.Add(current.Key, current.Value) Return seed End Function) request.Properties.Add(MultipleBodyParameters, result) End If End If Exit Select Case "application/x-www-form-urlencoded" result = request.Content.ReadAsFormDataAsync().Result request.Properties.Add(MultipleBodyParameters, result) Exit Select End Select End If End If Return TryCast(result, NameValueCollection) End Function Private Structure AsyncVoid End Structure End Class End Namespace |
' From: https://github.com/RoyiNamir/SimplePostVariableParameterBindingExtended ' And : http://weblog.west-wind.com/posts/2012/Sep/11/Passing-multiple-simple-POST-Values-to-ASPNET-Web-API ' Add this to Global.asax GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, SimplePostVariableParameterBinding.HookupParameterBinding) Imports System.Collections.Generic Imports System.Collections.Specialized Imports System.Globalization Imports System.Linq Imports System.Net.Http Imports System.Threading Imports System.Threading.Tasks Imports System.Web.Helpers Imports System.Web.Http.Controllers Imports System.Web.Http.Metadata Imports System.Web Imports Newtonsoft.Json Namespace CC.Net.WebApi Public Class SimplePostVariableParameterBinding Inherits HttpParameterBinding Public Const MultipleBodyParameters As String = "MultipleBodyParameters" Public Const MultipleBodyParametersRaw As String = "MultipleBodyParametersRaw" Public Const AllowJsonContentType As Boolean = True Public Sub New(descriptor As HttpParameterDescriptor) MyBase.New(descriptor) End Sub ''' ''' Check for simple binding parameters in POST data. Bind POST ''' data as well as query string data ''' ''' ''' ''' ''' Public Overrides Function ExecuteBindingAsync(metadataProvider As ModelMetadataProvider, actionContext As HttpActionContext, cancellationToken As CancellationToken) As Task Dim stringValue As String = Nothing Dim col As NameValueCollection = TryReadBody(actionContext.Request) If col IsNot Nothing Then stringValue = col(Descriptor.ParameterName) End If ' try reading query string if we have no POST/PUT match If stringValue Is Nothing Then Dim query = actionContext.Request.GetQueryNameValuePairs() If query IsNot Nothing Then Dim matches = query.Where(Function(kv) kv.Key.ToLower() = Descriptor.ParameterName.ToLower()) Dim keyValuePairs = If(TryCast(matches, IList(Of KeyValuePair(Of String, String))), matches.ToList()) If keyValuePairs.Any() Then stringValue = keyValuePairs.First().Value End If End If End If Dim value As Object = StringToType(stringValue) ' Set the binding result here SetValue(actionContext, value) ' now, we can return a completed task with no result Dim tcs As New TaskCompletionSource(Of AsyncVoid)() tcs.SetResult(Nothing) Return tcs.Task End Function ''' ''' Method that implements parameter binding hookup to the global configuration object's ''' ParameterBindingRules collection delegate. ''' ''' This routine filters based on POST/PUT method status and simple parameter ''' types. ''' ''' ''' GlobalConfiguration.Configuration. ''' .ParameterBindingRules ''' .Insert(0,SimplePostVariableParameterBinding.HookupParameterBinding); ''' ''' ''' Public Shared Function HookupParameterBinding(descriptor As HttpParameterDescriptor) As HttpParameterBinding Dim supportedMethods = descriptor.ActionDescriptor.SupportedHttpMethods ' Only apply this binder on POST and PUT operations If supportedMethods.Contains(HttpMethod.Post) OrElse supportedMethods.Contains(HttpMethod.Put) Then Dim supportedTypes = New Type() {GetType(String), GetType(Integer), GetType(System.Nullable(Of Integer)), GetType(Decimal), GetType(System.Nullable(Of Decimal)), GetType(Double), _ GetType(System.Nullable(Of Double)), GetType(Long), GetType(System.Nullable(Of Long)), GetType(Boolean), GetType(System.Nullable(Of Boolean)), GetType(DateTime), _ GetType(System.Nullable(Of DateTime)), GetType(Byte())} If supportedTypes.Count(Function(typ) typ = descriptor.ParameterType) > 0 Then Return New SimplePostVariableParameterBinding(descriptor) End If End If Return Nothing End Function Private Function StringToType(stringValue As String) As Object Dim value As Object = Nothing If stringValue Is Nothing Then value = Nothing ElseIf Descriptor.ParameterType = GetType(String) Then value = stringValue ElseIf Descriptor.ParameterType = GetType(Integer) Then value = Integer.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Integer)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of Integer)), Integer.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(Long) Then value = Long.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Long)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of Long)), Long.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(Decimal) Then value = Decimal.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Decimal)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of Decimal)), Decimal.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(Double) Then value = Double.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Double)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of Double)), Double.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(DateTime) Then value = DateTime.Parse(stringValue, CultureInfo.CurrentCulture) ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of DateTime)) Then value = If(String.IsNullOrWhiteSpace(stringValue), DirectCast(Nothing, System.Nullable(Of DateTime)), DateTime.Parse(stringValue, CultureInfo.CurrentCulture)) ElseIf Descriptor.ParameterType = GetType(Boolean) Then value = False If stringValue = "true" OrElse stringValue = "on" OrElse stringValue = "1" Then value = True End If ElseIf Descriptor.ParameterType = GetType(System.Nullable(Of Boolean)) Then value = False If String.IsNullOrWhiteSpace(stringValue) Then value = DirectCast(Nothing, System.Nullable(Of Boolean)) ElseIf stringValue = "true" OrElse stringValue = "on" OrElse stringValue = "1" Then value = True End If Else value = stringValue End If Return value End Function ''' ''' Read and cache the request body ''' ''' ''' Private Function TryReadBody(request As HttpRequestMessage) As NameValueCollection Dim result As Object ' try to read out of cache first If Not request.Properties.TryGetValue(MultipleBodyParameters, result) Then Dim contentType = request.Content.Headers.ContentType If contentType IsNot Nothing Then Select Case contentType.MediaType Case "application/json" If True Then If AllowJsonContentType Then result = request.Content.ReadAsStringAsync().Result request.Properties.Add(MultipleBodyParametersRaw, result) Dim values = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(result.ToString()) result = values.Aggregate(New NameValueCollection(), Function(seed, current) seed.Add(current.Key, current.Value) Return seed End Function) request.Properties.Add(MultipleBodyParameters, result) End If End If Exit Select Case "application/x-www-form-urlencoded" result = request.Content.ReadAsFormDataAsync().Result request.Properties.Add(MultipleBodyParameters, result) Exit Select End Select End If End If Return TryCast(result, NameValueCollection) End Function Private Structure AsyncVoid End Structure End Class End Namespace
Important Step: Make sure this class is set to auto compile….
Right Click is and Choose Properties then Set Build Action to Compile:
2. Hook the Parameter Binding Rules on App_Start
- Add a line to /Global.asax
This is the code for the root Global.asax
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Imports System.Web.Http Imports CC.Net Public Class Global_asax Inherits HttpApplication Sub Application_Start(sender As Object, e As EventArgs) ' Fires when the application is started ' It is important this goes before the Routing line: GlobalConfiguration.Configure(AddressOf WebApiConfig.Register) GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, AddressOf CC.Net.WebApi.SimplePostVariableParameterBinding.HookupParameterBinding) GlobalConfiguration.Configure(AddressOf WebApiConfig.Register) End Sub End Class |
Imports System.Web.Http Imports CC.Net Public Class Global_asax Inherits HttpApplication Sub Application_Start(sender As Object, e As EventArgs) ' Fires when the application is started ' It is important this goes before the Routing line: GlobalConfiguration.Configure(AddressOf WebApiConfig.Register) GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, AddressOf CC.Net.WebApi.SimplePostVariableParameterBinding.HookupParameterBinding) GlobalConfiguration.Configure(AddressOf WebApiConfig.Register) End Sub End Class
We’ve added “GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, AddressOf CC.Net.WebApi.SimplePostVariableParameterBinding.HookupParameterBinding)”
3. Add a simple Html Form
- Add a simple Html form /test/default.htm
This is the code for the root default.htm
1 | API test |
API test
Cars
4. Add the POST method into the CarsController
- Add the following to /Controllers/CarsController.vb
This is the code for the root CarsController.vb
1 2 3 4 | _ Public Function PostMultipleSimpleValues(make As String, model As String) As String Return String.Format("Make: {0}, Model: {1}", make, model) End Function |
_ Public Function PostMultipleSimpleValues(make As String, model As String) As String Return String.Format("Make: {0}, Model: {1}", make, model) End Function
The full /Controllers/CarsController.vb Class should now look 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 | Imports System.Net Imports System.Web.Http Imports System.Net.Http Public Class CarsController Inherits ApiController Private cars As Car() = New Car() _ {New Car() With { _ .Id = 1, _ .Make = "Aston Martin", _ .Model = "V8 Vantage" _ }, New Car() With { _ .Id = 2, _ .Make = "BMW", _ .Model = "Z8" _ }, New Car() With { _ .Id = 3, _ .Make = "Ferrari", _ .Model = "California" _ }, New Car() With { _ .Id = 4, _ .Make = "Pagani", _ .Model = "Zonda" _ }} Public Function GetCars() As IEnumerable(Of Car) Return cars End Function '' POST: api/Test 'Public Sub PostValue(<FromBody()> ByVal value As String) 'End Sub ' _ 'Public Function PostMultipleSimpleValues(name As String, value As Integer, entered As DateTime, Optional action As String = Nothing) As String ' Return String.Format("Name: {0}, Value: {1}, Date: {2}, Action: {3}", name, value, entered, action) 'End Function _ Public Function PostMultipleSimpleValues(make As String, model As String) As String Return String.Format("Make: {0}, Model: {1}", make, model) End Function End Class |
Imports System.Net Imports System.Web.Http Imports System.Net.Http Public Class CarsController Inherits ApiController Private cars As Car() = New Car() _ {New Car() With { _ .Id = 1, _ .Make = "Aston Martin", _ .Model = "V8 Vantage" _ }, New Car() With { _ .Id = 2, _ .Make = "BMW", _ .Model = "Z8" _ }, New Car() With { _ .Id = 3, _ .Make = "Ferrari", _ .Model = "California" _ }, New Car() With { _ .Id = 4, _ .Make = "Pagani", _ .Model = "Zonda" _ }} Public Function GetCars() As IEnumerable(Of Car) Return cars End Function '' POST: api/Test 'Public Sub PostValue(<FromBody()> ByVal value As String) 'End Sub ' _ 'Public Function PostMultipleSimpleValues(name As String, value As Integer, entered As DateTime, Optional action As String = Nothing) As String ' Return String.Format("Name: {0}, Value: {1}, Date: {2}, Action: {3}", name, value, entered, action) 'End Function _ Public Function PostMultipleSimpleValues(make As String, model As String) As String Return String.Format("Make: {0}, Model: {1}", make, model) End Function End Class
5. Testing
- Run the application and navigate to /test/
Enter some values into the webform and submit it:
You should see some output like this:
6. Results
So what we see happening here is form values posted to WebAPI and automatically being mapped to Parameters of Post method within the API. Clever.