Add Custom Header to Xamarin WebViewClient - xamarin

Though there were already some threads on issues like this a few years ago (e.g. Add Authorization header to all requests in Xamarin Forms Android WebView), I am wondering now if there were a solid solution meanwhile to add custom headers to all reqests (GET, PUT, POST...) while using WebFormClient with Xamarin.
ShouldInterceptRequest, ShouldOverrideUrlLoading methods overridden to add custom headers but the headers were not added:
public override WebResourceResponse ShouldInterceptRequest(WebView view, IWebResourceRequest urlResource)
{
if (urlResource.RequestHeaders != null && !urlResource.RequestHeaders.ContainsKey("X-LPPTAgent"))
{
urlResource.RequestHeaders.Add("key", "value");
}
return base.ShouldInterceptRequest(view, urlResource);
}
public override bool ShouldOverrideUrlLoading(WebView view, IWebResourceRequest request)
{
WebView.SetWebContentsDebuggingEnabled(true);
if (request.RequestHeaders != null && !request.RequestHeaders.ContainsKey("X-LPPTAgent"))
{
request.RequestHeaders.Add("key", "value");
}
return base.ShouldOverrideUrlLoading(view, request);
}
Another version of ShouldInterceptRequest - this works, but only for GET requests:
public override WebResourceResponse ShouldInterceptRequest(WebView view, IWebResourceRequest urlResource)
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("key", "value");
var requestMessage = GetRequestMessage(urlResource.Url.ToString(), urlResource.Method);
var result = client.SendAsync(requestMessage).Result;
var stream = result.Content.ReadAsStreamAsync().Result;
var contentType = result.Content.Headers.ContentType;
if (contentType != null)
{
return new WebResourceResponse(contentType.MediaType, contentType.CharSet, (int)result.StatusCode, result.ReasonPhrase, urlResource.RequestHeaders, stream);
}
return new WebResourceResponse(string.Empty, string.Empty, (int)result.StatusCode, result.ReasonPhrase, urlResource.RequestHeaders, stream);
}
since body cannot be sent with as it is not available in WebResourcerequest.

Related

I Applied Gzip compression but stream is not ending while responding back to client (Web API, .NetFrameWork)

Apologies for my English :) Following is my Web API .NET Framework 4.8 Compression Handler which i copied from an old blog to compress the response manually for Android Client
public class CompressionHandler : HttpContent
{
private HttpContent originalContent;
private string encodingType;
public CompressionHandler(HttpContent content, string encodingType)
{
if (content == null)
{
throw new ArgumentNullException("content");
}
if (encodingType == null)
{
throw new ArgumentNullException("encodingType");
}
originalContent = content;
this.encodingType = encodingType.ToLowerInvariant();
if (this.encodingType != "gzip" && this.encodingType != "deflate")
{
throw new InvalidOperationException(string.Format("Encoding '{0}' is not supported. Only supports gzip or deflate encoding.", this.encodingType));
}
// copy the headers from the original content
foreach (KeyValuePair<string, IEnumerable<string>> header in originalContent.Headers)
{
this.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
this.Headers.ContentEncoding.Add(encodingType);
}
protected override bool TryComputeLength(out long length)
{
length = originalContent.Headers.ContentLength.GetValueOrDefault();
return false;
}
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
Stream compressedStream = null;
if (encodingType == "gzip")
{
compressedStream = new GZipStream(stream, CompressionMode.Compress, leaveOpen: false);
}
else if (encodingType == "deflate")
{
compressedStream = new DeflateStream(stream, CompressionMode.Compress, leaveOpen: true);
}
return originalContent.CopyToAsync(compressedStream).ContinueWith(tsk =>
{
if (compressedStream != null)
{
compressedStream.Dispose();
}
});
}
}
Following is the code in delegate handler to compress the response contents.
return await base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>((responseToCompleteTask) =>
{
HttpResponseMessage response = responseToCompleteTask.Result;
if (response.RequestMessage?.Headers?.AcceptEncoding != null)
{
string encodingType = response.RequestMessage.Headers.AcceptEncoding.First().Value;
IEnumerable<string> headerValues2 = null;
request.Headers.TryGetValues("pHubDeCompression", out headerValues2);
var id = headerValues2?.FirstOrDefault();
if (id == "1")
response.Content = new CompressionHandler(response.Content, encodingType);
}
return response;
},
TaskContinuationOptions.OnlyOnRanToCompletion);
Problem is when I hit the api from post man It keep loading the response which ends in connection reset but I get HTTP 200 already. Please see the following image (Same Request Images at different time).

Receive data and file in method POST

I have a WebService that is working and receiving files using the POST method, but in which I also need to receive data, simultaneously.
ASP.NET WebApi code:
public Task<HttpResponseMessage> Post()
{
HttpRequestMessage request = this.Request;
if (!request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
string root = System.Web.HttpContext.Current.Server.MapPath("~/App_Data/uploads");
var provider = new MultipartFormDataStreamProvider(root);
var task = request.Content.ReadAsMultipartAsync(provider).
ContinueWith<HttpResponseMessage>(o =>
{
string file1 = provider.FileData.First().LocalFileName;
return new HttpResponseMessage()
{
Content = new StringContent("File uploaded.")
};
}
);
return task;
}
And the client, developed for Android, is sending the file and the data like this (the send of the file is tested and working, the sending of the data is still not tested, as I need it to be working in the server side):
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = new MultipartBuilder()
.type(MultipartBuilder.FORM)
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"title\""),
RequestBody.create(null, "Sample Text Content"))
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"file\"; filename=\"" + fileName + ".png\""),
RequestBody.create(MEDIA_TYPE_PNG, bitmapdata))
.addFormDataPart("fullpath", "test")
.build();
final com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder()
.url(url)
.post(requestBody)
.build();
How can I change the server to read not only the file but also the data?
Can any one help?
Thanks in advance.
The client in this case android is sending additional values in the body like media_type_png. I had to do something similar however the client was angular and not a mobile app, after some searching back then I found code from the following stackoverflow. Which resulted in the code below.
First receive the incoming message and check that you can process it i.e. [IsMimeMultipartContent][1]()
[HttpPost]
public async Task<HttpResponseMessage> Upload()
{
// Here we just check if we can support this
if (!Request.Content.IsMimeMultipartContent())
{
this.Request.CreateResponse(HttpStatusCode.UnsupportedMediaType);
}
// This is where we unpack the values
var provider = new MultipartFormDataMemoryStreamProvider();
var result = await Request.Content.ReadAsMultipartAsync(provider);
// From the form data we can extract any additional information Here the DTO is any object you want to define
AttachmentInformationDto attachmentInformation = (AttachmentInformationDto)GetFormData(result);
// For each file uploaded
foreach (KeyValuePair<string, Stream> file in provider.FileStreams)
{
string fileName = file.Key;
// Read the data from the file
byte[] data = ReadFully(file.Value);
// Save the file or do something with it
}
}
I used this to unpack the data:
// Extracts Request FormatData as a strongly typed model
private object GetFormData(MultipartFormDataMemoryStreamProvider result)
{
if (result.FormData.HasKeys())
{
// Here you can read the keys sent in ie
result.FormData["your key"]
AttachmentInformationDto data = AttachmentInformationDto();
data.ContentType = Uri.UnescapeDataString(result.FormData["ContentType"]); // Additional Keys
data.Description = Uri.UnescapeDataString(result.FormData["Description"]); // Another example
data.Name = Uri.UnescapeDataString(result.FormData["Name"]); // Another example
if (result.FormData["attType"] != null)
{
data.AttachmentType = Uri.UnescapeDataString(result.FormData["attType"]);
}
return data;
}
return null;
}
The MultipartFormDataMemoryStreamProvider is defined as follows:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
namespace YOURNAMESPACE
{
public class MultipartFormDataMemoryStreamProvider : MultipartMemoryStreamProvider
{
private readonly Collection<bool> _isFormData = new Collection<bool>();
private readonly NameValueCollection _formData = new NameValueCollection(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Stream> _fileStreams = new Dictionary<string, Stream>();
public NameValueCollection FormData
{
get { return _formData; }
}
public Dictionary<string, Stream> FileStreams
{
get { return _fileStreams; }
}
public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
{
if (parent == null)
{
throw new ArgumentNullException("parent");
}
if (headers == null)
{
throw new ArgumentNullException("headers");
}
var contentDisposition = headers.ContentDisposition;
if (contentDisposition == null)
{
throw new InvalidOperationException("Did not find required 'Content-Disposition' header field in MIME multipart body part.");
}
_isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName));
return base.GetStream(parent, headers);
}
public override async Task ExecutePostProcessingAsync()
{
for (var index = 0; index < Contents.Count; index++)
{
HttpContent formContent = Contents[index];
if (_isFormData[index])
{
// Field
string formFieldName = UnquoteToken(formContent.Headers.ContentDisposition.Name) ?? string.Empty;
string formFieldValue = await formContent.ReadAsStringAsync();
FormData.Add(formFieldName, formFieldValue);
}
else
{
// File
string fileName = UnquoteToken(formContent.Headers.ContentDisposition.FileName);
Stream stream = await formContent.ReadAsStreamAsync();
FileStreams.Add(fileName, stream);
}
}
}
private static string UnquoteToken(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
return token;
}
if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1)
{
return token.Substring(1, token.Length - 2);
}
return token;
}
}
}

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());

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!

MVC 3 GET Webservice and Response

I'm attempting to build a GET webservice that would from website 1 initiate a GET request...sending that request to website 2 and website two would respond by sending a list of objects. I using Json.net to serialize and deserialize the List of objects.
I've put together a POST webservice with the assistance of this question.. WebService ASP.NET MVC 3 Send and Receive
But I've been unsuccessful so far at adapting that example for my new requirement.
Here is what I have so far from website 1..
public static List<ScientificFocusArea> ScientificFocusAreas()
{
string apiURL = "http://localhost:50328/Api/GetAPI";
//Make the post
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
//var bytes = Encoding.Default.GetBytes(body);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(apiURL);
Stream stream = null;
try
{
request.KeepAlive = false;
request.ContentType = "application/x-www-form-urlencoded";
request.Timeout = -1;
request.Method = "GET";
}
finally
{
if (stream != null)
{
stream.Flush();
stream.Close();
}
}
List<ScientificFocusArea> listSFA = WebService.GetResponse_ScientificFocusArea(request);
return listSFA;
}
public static List<ScientificFocusArea> GetResponse_ScientificFocusArea(HttpWebRequest request)
{
List<ScientificFocusArea> listSFA = new List<ScientificFocusArea>();
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
using (Stream responseStream = response.GetResponseStream())
{
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created)
{
throw new HttpException((int)response.StatusCode, response.StatusDescription);
}
var end = string.Empty;
using (StreamReader reader = new StreamReader(responseStream))
{
end = reader.ReadToEnd();
reader.Close();
listSFA = JsonConvert.DeserializeObject<List<ScientificFocusArea>>(end);
}
response.Close();
}
}
return listSFA;
}
Then on the website 2...
public class GetAPIController : Controller
{
//
// GET: /Api/GetAPI/
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult GetScientificFocusAreas()
{
//Get list of SFAs
List<ScientificFocusArea> ListSFA = CreateList.ScientificFocusArea();
string json = JsonConvert.SerializeObject(ListSFA, Formatting.Indented);
//Send the the seralized object.
return Json(json);
}
}
Also, on website 2, I've registered this route for the incoming request...
context.MapRoute(
"GetScientificFocusAreas",
"Api/GetAPI/",
new
{
controller = "GetAPI",
action = "GetScientificFocusAreas",
id = UrlParameter.Optional
}
);
I'm currently getting the error.. he remote server returned an error: (404) Not Found.
Any help would me greatly appreciated.
The problem seems like a routing issue. I would start with the RouteDebugger which can be found here. This tool gives insight into which routes your URL is hitting.
The code I use for a HTTP GET is a bit different that what you have above. It's included below.
public T Get<T>(string url)
{
try
{
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
using (Stream responseStream = response.GetResponseStream())
{
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created)
{
throw new HttpException((int)response.StatusCode, response.StatusDescription);
}
var end = string.Empty;
using (StreamReader reader = new StreamReader(responseStream))
{
end = reader.ReadToEnd();
reader.Close();
}
responseStream.Close();
response.Close();
JsonSerializer serializer = new JsonSerializer();
serializer.Binder = new DefaultSerializationBinder();
JsonReader jsonReader = new JsonTextReader(new StringReader(end));
T deserialize = serializer.Deserialize<T>(jsonReader);
return deserialize;
}
}
catch (Exception ex)
{
throw new ApiException(string.Format("An error occured while trying to contact the API. URL: {0}", url), ex);
}
}
The other issue I see is in the GetScientificFocusAreas() method. On the second line of the code the objects are converted to JSON. Which is fine, but the last line of code the json is passed into the Json() method. Which converts the string into Json yet again. When using the JSON.Net library use the Content() method in the return instead of Json() and set the content type to application/json
The reasoning for using an external Json converter rather than the internal converter is simply the internal json converter has a few known issues. JSON.Net has been around for years and is solid.

Resources