Get HttpHeaders from HttpRequestException? - asp.net-web-api

I have a Web API, When the incoming request is not valid then the API sends back a HttpStatusCode.BadRequest and API would also add a CorrelationId into Response's HttpHeader. Something like below
public class ValidateRequestAttribute : ActionFilterAttribute
{
public ValidateRequestAttribute()
{
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ModelState.IsValid == false)
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.HttpContext.Response.Headers.Add("x-correlationid", "someid");
context.Result = new ContentResult()
{
Content = "bad request."
};
}
}
}
On client side im using HttpClient to access the API. I am not sure how client would retrieve HttpStatusCode and HttpHeader here. Here is my client code
public bool Process(url)
{
bool result = false;
try
{
Task.Run(async () => await _httpClient.GetStringAsync(url).ConfigureAwait(false)).Result;
}
catch (Exception ex)
{
if(ex is AggregateException)
{
var aggregateException = ex as AggregateException;
foreach(var innerException in aggregateException.InnerExceptions)
{
if (innerException is HttpRequestException)
{
var httpRequestException = innerException as HttpRequestException;
// how do i get StatusCode and HttpHeader values here??
}
}
}
}
return result;
}
I have already gone through SO post here and MSDN article here and also Stephen Cleary's article here
Even though its recommended to make async all the way down, I this case Client and API are both disconnected from each other and client is synchronous. Note that Client's Process method is synchronous method.

Like this:
public bool Process(string url)
{
var result = _httpClient.GetAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
if (result.StatusCode == HttpStatusCode.BadRequest)
{
IEnumerable<string> values;
if (result.Headers.TryGetValues("x-correlationid", out values))
{
// Should print out "someid"
Console.WriteLine(values.First());
}
}
return result.IsSuccessStatusCode;
}
Also note that doing .GetAwaiter().GetResult(); vs .Result; is recommended since it makes the code easier to work with because it does not throw an AggregateException.
If you want to read the response content as a string just do:
var content = result.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();
If you want to make your code async though you should use the async/await keyword and skip the .GetAwaiter().GetResult();.

Related

Xamarin Form - Can't get return value data from Service class method into MainPage method for filtering

I'm new to Xamarin, I have an app in Xamarin-Form that it's fetching data from web api and getting user input from Entry control.
The web api service class is working fine and reaches the deserialization in the getCourses method as seen below in Code Snippet 1.
The Entry control as well is working fine until it retrieves the user input on the MainPage class, OnOkGetCourseButton method as seen below Code Snippet 2.
What I want to achieve is, inside MainPage.xaml.cs, I create a method that takes the user input data and check agaisnt the deseriaized json data (the Id specially),
if it finds the Id in deserialized List of data, then it can send the found data to another ViewPage and display them.
if It cannot find the data, it shows a dialog box.
So far, I tried to call Task<ObservableCollection> getCourses() method from the MainPage class, inside CheckCourseComplete as seen below but it giving me no value/nothing, some kind of null value.
I don't want to filter the user input against web api json response inside getCourses(),
I want to do that in a separate method to follow S-OLID (Single Responsibility Principle).
If it's not possible in a separate method, then I just need to get it worked.
Please what is the best way to achieve it?
Code Snippet 1
public class CourseService : ICourseService
{
string Base_Url = "https://www.test.com/api/TheCourse";
public async Task<ObservableCollection<Course>> getCourses()
{
try
{
string url = Base_Url;
HttpClient client = new HttpClient();
HttpResponseMessage responseMessage = await client.GetAsync(url);
if (responseMessage.StatusCode == System.Net.HttpStatusCode.OK)
{
var result = await responseMessage.Content.ReadAsStringAsync();
var deserializedClass = JsonConvert.DeserializeObject<ObservableCollection<Course>>(result);
// I don't want to do that here, as it will violate SRP (SOLID)
return deserializedClass;
}
return null;
}
catch (Exception)
{
throw;
}
}
}
Code Snippet 2
namespace CourseMobile
{
public partial class MainPage : ContentPage
{
private string _getEntryText;
private readonly CourseViewModel orderViewModel;
public Course FetchCourse { get; set; }
public MainPage()
{
InitializeComponent();
CheckCourseComplete();
BindingContext = new CourseViewModel();
}
public string GetEntryText
{
get => _getEntryText;
set => _getEntryText = value;
}
public async void OnOkGetCourseButton(object sender, EventArgs e)
{
var inputtedCourseNumber = this.GetEntryText;
if(inputtedCourseNumber == string.Empty)
{
await DisplayAlert("", "Please enter your Course number", "OK 3");
}
else
{
CheckCourseComplete();
this.GetEntryText = inputtedCourseNumber;
await DisplayAlert("New Text", inputtedCourseNumber, "OK 2");
}
}
void Entry_TextChanged(object sender, TextChangedEventArgs e)
{
var newText = e.NewTextValue;
this.GetEntryText = newText;
}
public async void CheckCourseComplete()
{
CourseService myCourse = new CourseService();
await myCourse.getCourses(); // It doesn't return the json data (web api data)
// I need to check user input + (web api data) here
}
}
}
getCourses is async, so you need to use await when calling it
public async void CheckCourseComplete()
{
CourseService myCourse = new CourseService();
var data = await myCourse.getCourses();
// now filter data
}

Enabling OPTIONS method in CORS during REST request from AJAX on WCF Service

I have scratched my head for 7 hours trying to figure this out. I have searched all over the web but no luck. I have an Angular App that is making requests to a WCF command-line hosted service application. I managed to get by CORS by using these two classes:
public class CustomHeaderMessageInspector : IDispatchMessageInspector
{
Dictionary<string, string> requiredHeaders;
public CustomHeaderMessageInspector(Dictionary<string, string> headers)
{
requiredHeaders = headers ?? new Dictionary<string, string>();
}
public object AfterReceiveRequest(ref Message request,
System.ServiceModel.IClientChannel channel,
System.ServiceModel.InstanceContext instanceContext)
{
return null;
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
var httpHeader = reply.Properties["httpResponse"] as HttpResponseMessageProperty;
foreach (var item in requiredHeaders)
{
httpHeader.Headers.Add(item.Key, item.Value);
}
}
}
And:
public class EnableCorsBehavior : BehaviorExtensionElement, IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{ }
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{ }
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
var requiredHeaders = new Dictionary<string, string>();
requiredHeaders.Add("Access-Control-Allow-Origin", "*");
requiredHeaders.Add("Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS");
requiredHeaders.Add("Access-Control-Allow-Headers", "X-Requested-With,Content-Type");
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new CustomHeaderMessageInspector(requiredHeaders));
}
public void Validate(ServiceEndpoint endpoint) { }
public override Type BehaviorType
{
get { return typeof(EnableCorsBehavior); }
}
protected override object CreateBehavior()
{
return new EnableCorsBehavior();
}
}
Adding this custom extension to the app.config file solved my CORS problem. My current problem is whenever I make a POST request, I get the error:
Request Method:OPTIONS
Status Code:405 Method Not Allowed
I am quite new to C# and I can't seem to find where to place the code that will allow me to get past this. I have an idea that it should be placed somewhere in the BeforeSendReply() method. Please help me! I will really really appreciate it!
Regards!
I figured out the solution to this and i hope this helps everyone who comes across this same issue. In the CustomHeaderMessageInspector class that I posted in the question, I edited the following code in the AfterReceiveRequest method as follows:
// return null;
var httpRequest = (HttpRequestMessageProperty)request
.Properties[HttpRequestMessageProperty.Name];
return new
{
origin = httpRequest.Headers["Origin"],
handlePreflight = httpRequest.Method.Equals("OPTIONS",
StringComparison.InvariantCultureIgnoreCase)
};
What I hoped that code did is monitor any request with the OPTIONS method and "tag" it with a preflight state. Then I modified the code in the BeforeSendReply to look as follows:
var state = (dynamic)correlationState;
if (state.handlePreflight)
{
reply = Message.CreateMessage(MessageVersion.None, "PreflightReturn");
var httpResponse = new HttpResponseMessageProperty();
reply.Properties.Add(HttpResponseMessageProperty.Name, httpResponse);
httpResponse.SuppressEntityBody = true;
httpResponse.StatusCode = HttpStatusCode.OK;
}
var httpHeader = reply.Properties["httpResponse"] as HttpResponseMessageProperty;
foreach (var item in requiredHeaders)
{
httpHeader.Headers.Add(item.Key, item.Value);
}
What that does (i hope) is get any request tagged with OPTIONS and handle it by returning a 200 status code. This got it finally working and I hope it helps someone!
In addition to realnsleo answer:
I had problems to use (dynamic)correlationState because my project has to be
in Framework 3.5
I tried to simplify some lines, too:
private class CORSHeaderInjectingMessageInspector : IDispatchMessageInspector
{
private static IDictionary<string, string> _headersToInject = new Dictionary<string, string>
{
{ "Access-Control-Allow-Origin", "*" },
{ "Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS" },
{ "Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Origin,Accept" },
{ "Access-Control-Request-Headers", "POST" }
};
public object AfterReceiveRequest( ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
var httpRequest = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
return httpRequest.Method.Equals("OPTIONS", StringComparison.InvariantCulture);
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
if ((bool) correlationState)
{
var httpResponse = (HttpResponseMessageProperty)reply.Properties[HttpResponseMessageProperty.Name];
httpResponse.SuppressEntityBody = true;
httpResponse.StatusCode = HttpStatusCode.OK;
}
var httpHeader = reply.Properties["httpResponse"] as HttpResponseMessageProperty;
foreach (var item in _headersToInject)
{
httpHeader.Headers.Add(item.Key, item.Value);
}
}

Implement HTTP Cache (ETag) in ASP.NET Core Web API

I am working on ASP.NET Core (ASP.NET 5) Web API application and have to implement HTTP Caching with the help of Entity Tags. Earlier I used CacheCow for the same but it seems it does not support ASP.NET Core as of now. I also didn't find any other relevant library or framework support details for the same.
I can write custom code for the same but before that I want to see if anything is already available. Kindly share if something is already available and what is the better way to implement that.
After a while trying to make it work with middleware I figured out that MVC action filters are actually better suited for this functionality.
public class ETagFilter : Attribute, IActionFilter
{
private readonly int[] _statusCodes;
public ETagFilter(params int[] statusCodes)
{
_statusCodes = statusCodes;
if (statusCodes.Length == 0) _statusCodes = new[] { 200 };
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.Request.Method == "GET")
{
if (_statusCodes.Contains(context.HttpContext.Response.StatusCode))
{
//I just serialize the result to JSON, could do something less costly
var content = JsonConvert.SerializeObject(context.Result);
var etag = ETagGenerator.GetETag(context.HttpContext.Request.Path.ToString(), Encoding.UTF8.GetBytes(content));
if (context.HttpContext.Request.Headers.Keys.Contains("If-None-Match") && context.HttpContext.Request.Headers["If-None-Match"].ToString() == etag)
{
context.Result = new StatusCodeResult(304);
}
context.HttpContext.Response.Headers.Add("ETag", new[] { etag });
}
}
}
}
// Helper class that generates the etag from a key (route) and content (response)
public static class ETagGenerator
{
public static string GetETag(string key, byte[] contentBytes)
{
var keyBytes = Encoding.UTF8.GetBytes(key);
var combinedBytes = Combine(keyBytes, contentBytes);
return GenerateETag(combinedBytes);
}
private static string GenerateETag(byte[] data)
{
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(data);
string hex = BitConverter.ToString(hash);
return hex.Replace("-", "");
}
}
private static byte[] Combine(byte[] a, byte[] b)
{
byte[] c = new byte[a.Length + b.Length];
Buffer.BlockCopy(a, 0, c, 0, a.Length);
Buffer.BlockCopy(b, 0, c, a.Length, b.Length);
return c;
}
}
And then use it on the actions or controllers you want as an attribute:
[HttpGet("data")]
[ETagFilter(200)]
public async Task<IActionResult> GetDataFromApi()
{
}
The important distinction between Middleware and Filters is that your middleware can run before and after MVC middlware and can only work with HttpContext. Also once MVC starts sending the response back to the client it's too late to make any changes to it.
Filters on the other hand are a part of MVC middleware. They have access to the MVC context, with which in this case it's simpler to implement this functionality. More on Filters and their pipeline in MVC.
Building on Eric's answer, I would use an interface that could be implemented on an entity to support entity tagging. In the filter you would only add the ETag if the action is returning a entity with this interface.
This allows you to be more selective about what entities get tagged and allows you have each entity control how its tag is generated. This would be much more efficient than serializing everything and creating a hash. It also eliminates the need to check the status code. It could be safely and easily added as a global filter since you are "opting-in" to the functionality by implementing the interface on your model class.
public interface IGenerateETag
{
string GenerateETag();
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class ETagFilterAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
if (request.Method == "GET" &&
context.Result is ObjectResult obj &&
obj.Value is IGenerateETag entity)
{
string etag = entity.GenerateETag();
// Value should be in quotes according to the spec
if (!etag.EndsWith("\""))
etag = "\"" + etag +"\"";
string ifNoneMatch = request.Headers["If-None-Match"];
if (ifNoneMatch == etag)
{
context.Result = new StatusCodeResult(304);
}
context.HttpContext.Response.Headers.Add("ETag", etag);
}
}
}
I am using a middleware that works fine for me.
It adds HttpCache headers to responses (Cache-Control, Expires, ETag, Last-Modified), and implements cache expiration & validation models.
You can find it on nuget.org as a package called Marvin.Cache.Headers.
You could find more information from its Github home page:
https://github.com/KevinDockx/HttpCacheHeaders
Here's a more extensive version for MVC Views (tested with asp.net core 1.1):
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Net.Http.Headers;
namespace WebApplication9.Middleware
{
// This code is mostly here to generate the ETag from the response body and set 304 as required,
// but it also adds the default maxage (for client) and s-maxage (for a caching proxy like Varnish) to the cache-control in the response
//
// note that controller actions can override this middleware behaviour as needed with [ResponseCache] attribute
//
// (There is actually a Microsoft Middleware for response caching - called "ResponseCachingMiddleware",
// but it looks like you still have to generate the ETag yourself, which makes the MS Middleware kinda pointless in its current 1.1.0 form)
//
public class ResponseCacheMiddleware
{
private readonly RequestDelegate _next;
// todo load these from appsettings
const bool ResponseCachingEnabled = true;
const int ActionMaxAgeDefault = 600; // client cache time
const int ActionSharedMaxAgeDefault = 259200; // caching proxy cache time
const string ErrorPath = "/Home/Error";
public ResponseCacheMiddleware(RequestDelegate next)
{
_next = next;
}
// THIS MUST BE FAST - CALLED ON EVERY REQUEST
public async Task Invoke(HttpContext context)
{
var req = context.Request;
var resp = context.Response;
var is304 = false;
string eTag = null;
if (IsErrorPath(req))
{
await _next.Invoke(context);
return;
}
resp.OnStarting(state =>
{
// add headers *before* the response has started
AddStandardHeaders(((HttpContext)state).Response);
return Task.CompletedTask;
}, context);
// ignore non-gets/200s (maybe allow head method?)
if (!ResponseCachingEnabled || req.Method != HttpMethods.Get || resp.StatusCode != StatusCodes.Status200OK)
{
await _next.Invoke(context);
return;
}
resp.OnStarting(state => {
// add headers *before* the response has started
var ctx = (HttpContext)state;
AddCacheControlAndETagHeaders(ctx, eTag, is304); // intentional modified closure - values set later on
return Task.CompletedTask;
}, context);
using (var buffer = new MemoryStream())
{
// populate a stream with the current response data
var stream = resp.Body;
// setup response.body to point at our buffer
resp.Body = buffer;
try
{
// call controller/middleware actions etc. to populate the response body
await _next.Invoke(context);
}
catch
{
// controller/ or other middleware threw an exception, copy back and rethrow
buffer.CopyTo(stream);
resp.Body = stream; // looks weird, but required to keep the stream writable in edge cases like exceptions in other middleware
throw;
}
using (var bufferReader = new StreamReader(buffer))
{
// reset the buffer and read the entire body to generate the eTag
buffer.Seek(0, SeekOrigin.Begin);
var body = bufferReader.ReadToEnd();
eTag = GenerateETag(req, body);
if (req.Headers[HeaderNames.IfNoneMatch] == eTag)
{
is304 = true; // we don't set the headers here, so set flag
}
else if ( // we're not the only code in the stack that can set a status code, so check if we should output anything
resp.StatusCode != StatusCodes.Status204NoContent &&
resp.StatusCode != StatusCodes.Status205ResetContent &&
resp.StatusCode != StatusCodes.Status304NotModified)
{
// reset buffer and copy back to response body
buffer.Seek(0, SeekOrigin.Begin);
buffer.CopyTo(stream);
resp.Body = stream; // looks weird, but required to keep the stream writable in edge cases like exceptions in other middleware
}
}
}
}
private static void AddStandardHeaders(HttpResponse resp)
{
resp.Headers.Add("X-App", "MyAppName");
resp.Headers.Add("X-MachineName", Environment.MachineName);
}
private static string GenerateETag(HttpRequest req, string body)
{
// TODO: consider supporting VaryBy header in key? (not required atm in this app)
var combinedKey = req.GetDisplayUrl() + body;
var combinedBytes = Encoding.UTF8.GetBytes(combinedKey);
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(combinedBytes);
var hex = BitConverter.ToString(hash);
return hex.Replace("-", "");
}
}
private static void AddCacheControlAndETagHeaders(HttpContext ctx, string eTag, bool is304)
{
var req = ctx.Request;
var resp = ctx.Response;
// use defaults for 404s etc.
if (IsErrorPath(req))
{
return;
}
if (is304)
{
// this will blank response body as well as setting the status header
resp.StatusCode = StatusCodes.Status304NotModified;
}
// check cache-control not already set - so that controller actions can override caching
// behaviour with [ResponseCache] attribute
// (also see StaticFileOptions)
var cc = resp.GetTypedHeaders().CacheControl ?? new CacheControlHeaderValue();
if (cc.NoCache || cc.NoStore)
return;
// sidenote - https://tools.ietf.org/html/rfc7232#section-4.1
// the server generating a 304 response MUST generate any of the following header
// fields that WOULD have been sent in a 200(OK) response to the same
// request: Cache-Control, Content-Location, Date, ETag, Expires, and Vary.
// so we must set cache-control headers for 200s OR 304s
cc.MaxAge = cc.MaxAge ?? TimeSpan.FromSeconds(ActionMaxAgeDefault); // for client
cc.SharedMaxAge = cc.SharedMaxAge ?? TimeSpan.FromSeconds(ActionSharedMaxAgeDefault); // for caching proxy e.g. varnish/nginx
resp.GetTypedHeaders().CacheControl = cc; // assign back to pick up changes
resp.Headers.Add(HeaderNames.ETag, eTag);
}
private static bool IsErrorPath(HttpRequest request)
{
return request.Path.StartsWithSegments(ErrorPath);
}
}
}
As an addendum to Erik Božič's answer I found that the HttpContext object was not reporting back the StatusCode correctly when inheriting from ActionFilterAttribute, and applied controller-wide. HttpContext.Response.StatusCode was always 200, indicating it was probably not set by this point in the pipeline. I was instead able to grab the StatusCode from ActionExecutedContext context.Result.StatusCode.
I found an alternative solution which is "closer" to the web api controller method - so you can decide per method which ETag to set...
See my response here: How to use ETag in Web API using action filter along with HttpResponseMessage
We can write simple extension method on ControllerBase class
using Microsoft.AspNetCore.Mvc;
namespace WebApiUtils.Caching
{
public static class ExtensionMethods
{
public static IActionResult OkOr304<T>(
this ControllerBase controller,
T resultObject,
Func<T, string> etagBuilder
)
{
var etag = etagBuilder(resultObject);
if (
// Add additional headers if needed
controller.Request.Headers.Keys.Contains("If-None-Match")
&& controller.Request.Headers["If-None-Match"].ToString() == etag
)
{
return controller.StatusCode(304);
}
controller.Response.Headers.Add("ETag", new[] { etag });
return controller.Ok(resultObject);
}
public static IActionResult OkOr304<T>(this ControllerBase controller, T resultObject)
{
return controller.OkOr304(
resultObject,
x =>
{
// Implement default ETag strategy
return "";
}
);
}
}
}
Then we can use it inside controller with
return this.OkOr304(resultObject, etagBuilder);
or
return this.OkOr304(resultObject);
this works very well if result objects have some version indicator e.g.
return this.OkOr304(resultObject, x => x.VersionNumber.ToString());

Xamarin http webservice issue

I m trying to use http request webservice issue is that when we post wrong username and password the login service generate exception and it can't return any value in async calls.
A code snippet would help assist with the problem ...
However using a try catch should help you catch your exception and prevent application from crashing and handling the exceptions accordingly.
As seen in my sample code below I cater for the incorrect details entered / connectivity problems. I peform the http async request then parse the xml to my model handling the exceptions accordingly
var response = await WebRequestHelper.MakeAsyncRequest(url, content);
if (response.IsSuccessStatusCode == true)
{
Debug.WriteLine("Login Successfull" + "result.IsSuccessStatusCode" + response.IsSuccessStatusCode);
var result = response.Content.ReadAsStringAsync().Result;
result = result.Replace("<xml>", "<LoginResult>").Replace("</xml>", "</LoginResult>");
loginResult = XMLHelper.FromXml<LoginResult>(result);
if (loginResult != null)
{
login.Type = ResultType.OK;
login.Result = loginResult;
}
else
{
login.Type = ResultType.WrongDetails;
}
}
else
{
Debug.WriteLine("Login Failed" + "result.IsSuccessStatusCode" + response.IsSuccessStatusCode);
login.Type = ResultType.WrongDetails;
}
}
catch (Exception ex)
{
login.Type = ResultType.ConnectivityProblem;
}
Web Request
public static async Task<HttpResponseMessage> MakeAsyncRequest(string url, Dictionary<string, string> content)
{
var httpClient = new HttpClient();
httpClient.Timeout = new TimeSpan(0, 5, 0);
httpClient.BaseAddress = new Uri(url);
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type: application/x-www-form-urlencoded", "application/json");
if (content == null)
{
content = new Dictionary<string, string>();
}
var encodedContent = new FormUrlEncodedContent(content);
var result = await httpClient.PostAsync(httpClient.BaseAddress, encodedContent);
return result;
I would recommend wrapping the response in a generic ServiceResponse where you can store the exceptions. await methods can be included in try/catch blocks so the standard process can be followed.
E.G.
public async Task<ServiceResponse<T>> PostAsync<T>(String address, object dto){
var content = Serializer.SerializeObject (dto);
var response = await client.PostAsync (
address,
new StringContent (content));
if (response.IsSuccessStatusCode) {
try {
var responseString = await response.Content.ReadAsStringAsync ();
return new ServiceResponse<T> (Serializer.DeserializeObject<T> (responseString),
response.StatusCode);
} catch (Exception ex) {
return new ServiceResponse<T> (response.StatusCode, ex);
}
} else {
return new ServiceResponse<T> (response.StatusCode);
}
}
With the ServiceResponse defined as :
public class ServiceResponse<T>
{
public HttpStatusCode StatusCode { get; set;}
public T Value { get; set;}
public String Content { get; set;}
public Exception Error {get;set;}
public ServiceResponse(T value, HttpStatusCode httpStatusCode){
this.Value = value;
this.StatusCode = httpStatusCode;
}
public ServiceResponse(HttpStatusCode httpStatusCode, Exception error = null){
this.StatusCode = httpStatusCode;
this.Error = error;
}
}
This will give you a clean way of managing all your HTTP responses and any errors that may occur.

Why WebAPI does not use my JSONP formatter to deserialize model?

I'm very very confuse about Microsoft brand-new framework, ASP.NET MVC WebAPI. I try to create complete solution for cross-site API with JSONP data.
First, I modify their default WebApiConfig to the following code.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}", new {id = RouteParameter.Optional});
// Custom customization
config.Formatters.Clear();
config.Formatters.Add(new JsonpFormatter());
}
}
I use jQuery to create a request to this API website.
// jQuery will create HTTP GET the following URL
// http://localhost:3557/api/FlightAvailability/SearchFlight?callback=jQuery18206342989655677229_1353568617029&origin=JFK&destination=SLC&isOneWayFlight=false&departFlightDate=Wed%2C+28+Nov+2012+17%3A00%3A00+GMT&returnFlightDate=Wed%2C+05+Dec+2012+17%3A00%3A00+GMT&numberOfGuests=1&numberOfChildren=1&numberOfInfants=1&preferredCurrency=USD&query=%7B+Origin%3A+'JFK'+%7D&flightDate=Wed%2C+28+Nov+2012+17%3A00%3A00+GMT&_=1353568618465
$.ajax
({
url: 'http://localhost:3557/api/FlightAvailability/SearchFlight',
dataType: 'jsonp',
data: $.postify(model),
success: processResponse
});
I create action to handle above request. Everything is correct. I can call to this action but WebAPI doesn't use my JSONP formatter to deserialize my query object.
However, I try to directly call ContentNegotiator to get which formatter that handle my request. It's quite surprise that negotiatorResult is my JSONP formatter.
[HttpGet]
public List<FlightInfo> SearchFlight(FlightAvailabilityQuery query)
{
var negotiator = Configuration.Services.GetContentNegotiator();
var negotiatorResult = negotiator.Negotiate(typeof (FlightAvailabilityQuery), Request, Configuration.Formatters);
var flight = new FlightsAvailability();
var result = flight.GetAvailability(WebApiAuthentication.UserInfo.SessionService, query);
return result;
}
Why WebAPI does not use my JSONP formatter to deserialize query FlightAvailabilityQuery object?
PS. I try to break all possible line in JSONP formatter but Visual Studio doesn't hit any break point by it directly go to action method without call at my only one formatter. However, when I directly call ContentNegotiator, it hit at my break point correctly.
Update #1 - Add JSONP formatter source code
public class JsonpFormatter : JsonMediaTypeFormatter
{
private readonly JsonSerializerSettings _serializerSettings;
private string _jsonpCallbackFunction;
public JsonpFormatter()
{
JsonpParameterName = "callback";
_serializerSettings = new JsonSerializerSettings();
_serializerSettings.TypeNameHandling = TypeNameHandling.Objects;
_serializerSettings.Converters.Add(new IsoDateTimeConverter());
MediaTypeMappings.Add(new ExtendedQueryStringMapping(JsonpParameterName, "application/json"));
}
public string JsonpParameterName { get; set; }
public override bool CanReadType(Type type)
{
return true;
}
public override bool CanWriteType(Type type)
{
return true;
}
public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
{
var formatter = new JsonpFormatter()
{
_jsonpCallbackFunction = GetJsonCallbackFunction(request)
};
// this doesn't work unfortunately
//formatter.SerializerSettings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;
formatter.SerializerSettings.Converters.Add(new StringEnumConverter());
formatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
formatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
return formatter;
}
public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
{
// Create a serializer
var serializer = JsonSerializer.Create(_serializerSettings);
// Create task reading the content
return Task.Factory.StartNew(() =>
{
using (var streamReader = new StreamReader(stream, Encoding.UTF8))
{
using (var jsonTextReader = new JsonTextReader(streamReader))
{
return serializer.Deserialize(jsonTextReader, type);
}
}
});
}
public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContent content, TransportContext transportContext)
{
if (string.IsNullOrEmpty(_jsonpCallbackFunction))
return base.WriteToStreamAsync(type, value, stream, content, transportContext);
StreamWriter writer = null;
// write the pre-amble
try
{
writer = new StreamWriter(stream);
writer.Write(_jsonpCallbackFunction + "(");
writer.Flush();
}
catch (Exception ex)
{
try
{
if (writer != null)
writer.Dispose();
}
catch { }
var tcs = new TaskCompletionSource<object>();
tcs.SetException(ex);
return tcs.Task;
}
return base.WriteToStreamAsync(type, value, stream, content, transportContext)
.ContinueWith(innerTask =>
{
if (innerTask.Status == TaskStatus.RanToCompletion)
{
writer.Write(")");
writer.Flush();
}
}, TaskContinuationOptions.ExecuteSynchronously)
.ContinueWith(innerTask =>
{
writer.Dispose();
return innerTask;
}, TaskContinuationOptions.ExecuteSynchronously)
.Unwrap();
}
private string GetJsonCallbackFunction(HttpRequestMessage request)
{
if (request.Method != HttpMethod.Get)
return null;
var query = HttpUtility.ParseQueryString(request.RequestUri.Query);
var queryVal = query[this.JsonpParameterName];
if (string.IsNullOrEmpty(queryVal))
return null;
return queryVal;
}
}
Your action does not get hit because it cannot model bind your query parameter. Also JsonP is for HTTP GET only, so your formatter will not be selected for deserialization. How do you expect your FlightAvailabilityQuery being deserialized? I saw a lot of query parameters from your URL, do you want that be turned into FlightAvailabilityQuery?
The easiest way to get that is to use FromUri.
public List<FlightInfo> SearchFlight([FromUri]FlightAvailabilityQuery query)
If for some reason that does not work, you can try to add individual query parameter name on the action, such as origin, isOneWay, destination. etc. Then inside your action construct the FlightAvailabilityQuery object.
Also, if you have a lot of actions that you want to reuse this model binding logic, you can register a custom parameter binding to solve that. Please see this link for how to register a custom parameter binding to solve this.
Hope this helps!

Resources