Net Core 3.1 OData returns error 500 when using both $filter and $select and output is xml - sql

I'm currently developing an OData API in .Net Core 3.1 which fetches data from SQL server. Using postman, I'm sending GET requests to the API with Accept headers text/xml and application/json.
With this url: <http://localhost:8008/odata/Contact?$filter=No_ eq 'T20-1234567'&$select=No_> and an application/json Accept-Header (or No Accept-Header) the response is
json response
But with Accept-Header application/xml or text/xml:
An unhandled exception was thrown by the application.
System.ArgumentException: Object of type 'System.Linq.EnumerableQuery1[Microsoft.AspNet.OData.Query.Expressions.SelectExpandBinder+SelectSome1[Models.Contact]]' cannot be converted to type 'System.Collections.Generic.IEnumerable`1[Models.Contact]'.
The strange thing is that when the $select part is removed from the url, the request is correctly handled by the application (200).
My controller action:
[HttpGet]
[ODataRoute(nameof(Contact))]
public IQueryable<Contact> GetContact()
{
return _context.Contact;
}
Has anyone seen this type of behaviour?

To answer my own question, here's a possible workaround:
services
.AddMvc(config =>
{
...
config.OutputFormatters.Add(new CustomXmlOutputFormatter());
config.RespectBrowserAcceptHeader = true;
});
public class CustomXmlOutputFormatter : TextOutputFormatter
{
public CustomXmlOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
protected override bool CanWriteType(Type type)
{
return true;
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (selectedEncoding == null) throw new ArgumentNullException(nameof(selectedEncoding));
var httpContext = context.HttpContext;
var json = JsonConvert.SerializeObject(new { item = context.Object });
var xml = JsonConvert.DeserializeXNode(json, "root");
var buffer = new StringBuilder(xml.ToString());
await httpContext.Response.WriteAsync(buffer.ToString());
}
}

Related

Is there any in-built function/method to return, IActionResult/ActionResult instead of HttpResponseMessage in .Net Core 3.1

My Action method is returning HttpResponseMessage but, I want to get rid off Microsoft.AspNetCore.Mvc.WebApiCompatShim NuGet Package (which is basically provided to bridge the gap while porting Asp.Net Web API code into .Net Core) and use IActionResult/ActionResult instead of HttpResponseMessage.
My Action method looks like this:
[HttpGet]
[Route("GetTemplate")]
public async Task<HttpResponseMessage> GetTemplate(string id) {
var userAgent = this.Request.Headers.UserAgent;
bool IsWindows = true;
if(userAgent.ToString().ToLower().Contains("apple")) {
IsWindows = false; //false
}
var template = await _templateService.GetTemplateContent(id);
HttpResponseMessage responseMsg = new HttpResponseMessage();
if(IsWindows) {
responseMsg.Content = new StringContent(JsonConvert.SerializeObject(template));
responseMsg.RequestMessage = Request;
responseMsg.StatusCode = HttpStatusCode.OK;
responseMsg.Content.Headers.ContentType =
new MediaTypeHeaderValue("application/json");
} else {
responseMsg.Content = new ByteArrayContent(template.ContentBytes);
responseMsg.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileNameStar = template.Name };
responseMsg.Content.Headers.Add("x-filename", template.Name);
responseMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
responseMsg.Content.Headers.ContentLength = template.ContentBytes.Length;
responseMsg.RequestMessage = Request;
responseMsg.StatusCode = HttpStatusCode.OK;
}
return (responseMsg);
}
Since you aren’t doing anything fancy there, you can translate your return object directly into corresponding action results here. In your case, you want a JsonResult and a FileResult with a custom response header:
[HttpGet]
[Route("GetTemplate")]
public async Task<HttpResponseMessage> GetTemplate(string id)
{
var userAgent = this.Request.Headers.UserAgent;
bool IsWindows = !userAgent.ToString().ToLower().Contains("apple");
var template = await _templateService.GetTemplateContent(id);
if (IsWindows)
{
return Json(template);
}
else
{
Response.Headers.Add("x-filename", template.Name);
return File(template.ContentBytes, "application/octet-stream", template.Name);
}
}
There are a lot similar utility methods on the Controller and ControllerBase type that help you create a variety of different response messages. For most use cases, there should be a built-in way to produce the response.
1stly change the signature of your action to this:
public async Task<IActionResult> GetTemplate
You will then return your data in the response something like this return Ok(data). You do not have to serialize your data, you can send a POCO class. This would represent .StatusCode = HttpStatusCode.OK
If you want to add extra headers to your response, you will do so using the Response field from ControllerBase. Eg. Response.Headers.Add for adding key value pairs to your Response header.

ASP.NET Core 3.1 - PostAsync/PostAsJsonAsync method in Integration Test always returns Bad Request

This is my register method inside the AuthController.
[HttpPost(ApiRoutes.Auth.Register)]
public async Task<IActionResult> Register(UserRegistrationRequest request)
{
var authResponse = await _authService.RegisterAsync(request.Email, request.Password);
if (!authResponse.Success)
{
return BadRequest(new AuthFailedResponse
{
Errors = authResponse.Errors
});
}
return Ok(new AuthSuccessResponse
{
Token = authResponse.Token,
RefreshToken = authResponse.RefreshToken
});
}
I'm trying to call this method by using TestClient.PostAsync() method, unfortunately it always returns Bad Request. I've already tried calling the TestClient.PostAsJsonAsync(ApiRoutes.Auth.Register, user) method by importing Microsoft.AspNet.WebApi.Client package, the result is the same.
var user = new UserRegistrationRequest
{
Email = "user1#testtest.com",
Password = "P#ssw0rd1!!!!!"
};
var response = await TestClient.PostAsync(
ApiRoutes.Auth.Register,
new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8)
{
Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
});
You are missing the FromBody attribute from you action parameter. When you are sending json data to a controller that will be part of the request body. You can tell to the controller how to bind the incoming data, in your case from the body. So you code should look like:
public async Task<IActionResult> Register([FromBody]UserRegistrationRequest request)
{
…
}
You could read more about bindings in the official documentation.

ServiceStack doesn't populate the response DTO when throwing HttpErrors

ServiceStack doesn't populate the original response in the WebServiceException's responseDTO property.
I'm running the code below which should always return a 404 response code with the ResponseStatus property of the TestResponse populated with "Some bad request" but it also seems like should return the original good response with it's output property populated from the request's input property. However I get null when I look at the WebServiceException responseDTO
public TestResponse Post(Test request)
{
var response = new TestResponse() { Output = request.Input };
throw new HttpError(response, (int)HttpStatusCode.BadRequest, "Some bad request");
}
public TestResponse Get(Test request)
{
try
{
using (var client = new JsonServiceClient("http://localhost:5000"))
{
var response = client.Post(request);
return response;
}
}
catch (WebServiceException ex)
{
throw;
}
}
In general I was expecting that the responseDTO property in the WebServiceException will contain the endpoint's DTO as long as it's passed in when throwing the HttpError but that doesn't seem to be the case. I see only default values and nulls for each property in the responseDTO.
When an Exception is thrown only the ResponseStatus is preserved, you can add any additional info to its Meta dictionary.
Alternatively you can return a failed HTTP Response:
public TestResponse Post(Test request)
{
var response = new TestResponse() { Output = request.Input };
base.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return response;
}

Custom Content-Type validation filter?

I want to implement a custom Content-Type validation filter so that a custom error model on a 415 Unsupported Media Type can be provided.
Something like this:
public class ValidateContentTypeFilterAttribute : ActionFilterAttribute
{
private const string JsonMimeType = "application/json";
public override void OnActionExecuting(ActionExecutingContext context)
{
string requestMethod = context.HttpContext.Request.Method.ToUpper();
if (requestMethod == WebRequestMethods.Http.Post || requestMethod == WebRequestMethods.Http.Put)
{
if (request.ContentType != JsonMimeType)
{
// "Unsupported Media Type" HTTP result.
context.Result = new HttpUnsupportedMediaTypeResult();
return;
}
}
}
}
The problem is that the MVC pipeline seems to be "catching" unsupported or invalid Content-Type values before executing any custom filters. Even the 'application/xml' content type will be refused.
Where would this be configured?
My MVC configuration consists of not much more than this:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include;
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
options.SerializerSettings.Converters.Add(new SquidJsonConverter());
})
.AddMvcOptions(options =>
{
options.Filters.Add(typeof(ValidateAntiForgeryTokenAttribute));
options.Filters.Add(typeof(ValidateContentTypeFilterAttribute));
options.Filters.Add(typeof(ValidateAcceptFilterAttribute));
options.Filters.Add(typeof(ValidateModelFilterAttribute));
});
...
}
Action filters are too late in the processing pipeline for what you are trying to achieve here.
The filter execution order for an "incoming" request is the following:
Authorization filters' OnAuthorization.. method invocation
Resource filters' OnResourceExecuting.. method invocation Model
Model binding happens (this is the place where the content type check is
made)
Action filters' OnActionExecuting.. method invocation
Action execution happens
You could instead create a resource filter. An example:
public class CustomResourceFilter : IResourceFilter
{
private readonly string jsonMediaType = "application/json";
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (context.HttpContext.Request.Method == "PUT" || context.HttpContext.Request.Method == "POST")
{
if (!string.Equals(
MediaTypeHeaderValue.Parse(context.HttpContext.Request.ContentType).MediaType,
jsonMediaType,
StringComparison.OrdinalIgnoreCase))
{
context.Result = new JsonResult(new { Error = "An error message here" }) { StatusCode = 415 };
}
}
}
}
If you would like to modify all types of UnsupportedMediaTypeResult responses, then you could write a Result filter instead.
The filter pipeline for outgoing response is:
Action filters' OnActionExecuted... method invocation
Result filters' OnResultExecuting.. method invocation
Result filters' OnResultExecuted.. method invocation
Resource filters' OnResourceExecuted.. method invocation
An example with a Result filter:
public class CustomResultFilter : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var result = context.Result as UnsupportedMediaTypeResult;
if (result != null)
{
context.Result = new JsonResult(new { Error = "An error message here" }) { StatusCode = 415 };
}
}
}

Problems implementing ValidatingAntiForgeryToken attribute for Web API with MVC 4 RC

I'm making JSON-based AJAX requests and, with MVC controllers have been very grateful to Phil Haack for his Preventing CSRF with AJAX and, Johan Driessen's Updated Anti-XSRF for MVC 4 RC. But, as I transition API-centric controllers to Web API, I'm hitting issues where the functionality between the two approaches is markedly different and I'm unable to transition the CSRF code.
ScottS raised a similar question recently which was answered by Darin Dimitrov. Darin's solution involves implementing an authorization filter which calls AntiForgery.Validate. Unfortunately, this code does not work for me (see next paragraph) and - honestly - is too advanced for me.
As I understand it, Phil's solution overcomes the problem with MVC AntiForgery when making JSON requests in the absence of a form element; the form element is assumed/expected by the AntiForgery.Validate method. I believe that this may be why I'm having problems with Darin's solution too. I receive an HttpAntiForgeryException "The required anti-forgery form field '__RequestVerificationToken' is not present". I am certain that the token is being POSTed (albeit in the header per Phil Haack's solution). Here's a snapshot of the client's call:
$token = $('input[name=""__RequestVerificationToken""]').val();
$.ajax({
url:/api/states",
type: "POST",
dataType: "json",
contentType: "application/json: charset=utf-8",
headers: { __RequestVerificationToken: $token }
}).done(function (json) {
...
});
I tried a hack by mashing together Johan's solution with Darin's and was able to get things working but am introducing HttpContext.Current, unsure whether this is appropriate/secure and why I can't use the provided HttpActionContext.
Here's my inelegant mash-up.. the change is the 2 lines in the try block:
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
try
{
var cookie = HttpContext.Current.Request.Cookies[AntiForgeryConfig.CookieName];
AntiForgery.Validate(cookie != null ? cookie.Value : null, HttpContext.Current.Request.Headers["__RequestVerificationToken"]);
}
catch
{
actionContext.Response = new HttpResponseMessage
{
StatusCode = HttpStatusCode.Forbidden,
RequestMessage = actionContext.ControllerContext.Request
};
return FromResult(actionContext.Response);
}
return continuation();
}
My questions are:
Am I correct in thinking that Darin's solution assumes the existence of a form element?
What's an elegant way to mash-up Darin's Web API filter with Johan's MVC 4 RC code?
Thanks in advance!
You could try reading from the headers:
var headers = actionContext.Request.Headers;
var cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
Note: GetCookies is an extension method that exists in the class HttpRequestHeadersExtensions which is part of System.Net.Http.Formatting.dll. It will most likely exist in C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Net.Http.Formatting.dll
Just wanted to add that this approach worked for me also (.ajax posting JSON to a Web API endpoint), although I simplified it a bit by inheriting from ActionFilterAttribute and overriding the OnActionExecuting method.
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var cookieName = AntiForgeryConfig.CookieName;
var headers = actionContext.Request.Headers;
var cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
}
catch
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
}
}
}
Extension method using Darin's answer, with a check for the presence of the header. The check means that the resulting error message is more indicative of what's wrong ("The required anti-forgery form field "__RequestVerificationToken" is not present.") versus "The given header was not found."
public static bool IsHeaderAntiForgeryTokenValid(this HttpRequestMessage request)
{
try
{
HttpRequestHeaders headers = request.Headers;
CookieState cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = string.Empty;
if (headers.Any(x => x.Key == AntiForgeryConfig.CookieName))
rvt = headers.GetValues(AntiForgeryConfig.CookieName).FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
}
catch (Exception ex)
{
LogHelper.LogError(ex);
return false;
}
return true;
}
ApiController Usage:
public IHttpActionResult Get()
{
if (Request.IsHeaderAntiForgeryTokenValid())
return Ok();
else
return BadRequest();
}
An implementation using AuthorizeAttribute:
using System;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
using System.Web.Http.Controllers;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ApiValidateAntiForgeryToken : AuthorizeAttribute {
public const string HeaderName = "X-RequestVerificationToken";
private static string CookieName => AntiForgeryConfig.CookieName;
public static string GenerateAntiForgeryTokenForHeader(HttpContext httpContext) {
if (httpContext == null) {
throw new ArgumentNullException(nameof(httpContext));
}
// check that if the cookie is set to require ssl then we must be using it
if (AntiForgeryConfig.RequireSsl && !httpContext.Request.IsSecureConnection) {
throw new InvalidOperationException("Cannot generate an Anti Forgery Token for a non secure context");
}
// try to find the old cookie token
string oldCookieToken = null;
try {
var token = httpContext.Request.Cookies[CookieName];
if (!string.IsNullOrEmpty(token?.Value)) {
oldCookieToken = token.Value;
}
}
catch {
// do nothing
}
string cookieToken, formToken;
AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken);
// set the cookie on the response if we got a new one
if (cookieToken != null) {
var cookie = new HttpCookie(CookieName, cookieToken) {
HttpOnly = true,
};
// note: don't set it directly since the default value is automatically populated from the <httpCookies> config element
if (AntiForgeryConfig.RequireSsl) {
cookie.Secure = AntiForgeryConfig.RequireSsl;
}
httpContext.Response.Cookies.Set(cookie);
}
return formToken;
}
protected override bool IsAuthorized(HttpActionContext actionContext) {
if (HttpContext.Current == null) {
// we need a context to be able to use AntiForgery
return false;
}
var headers = actionContext.Request.Headers;
var cookies = headers.GetCookies();
// check that if the cookie is set to require ssl then we must honor it
if (AntiForgeryConfig.RequireSsl && !HttpContext.Current.Request.IsSecureConnection) {
return false;
}
try {
string cookieToken = cookies.Select(c => c[CookieName]).FirstOrDefault()?.Value?.Trim(); // this throws if the cookie does not exist
string formToken = headers.GetValues(HeaderName).FirstOrDefault()?.Trim();
if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken)) {
return false;
}
AntiForgery.Validate(cookieToken, formToken);
return base.IsAuthorized(actionContext);
}
catch {
return false;
}
}
}
Then just decorate your controller or methods with [ApiValidateAntiForgeryToken]
And add to the razor file this to generate your token for javascript:
<script>
var antiForgeryToken = '#ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader(HttpContext.Current)';
// your code here that uses such token, basically setting it as a 'X-RequestVerificationToken' header for any AJAX calls
</script>
If it helps anyone, in .net core, the header's default value is actually just "RequestVerificationToken", without the "__". So if you change the header's key to that instead, it'll work.
You can also override the header name if you like:
services.AddAntiforgery(o => o.HeaderName = "__RequestVerificationToken")