Web API Parameter Binding - asp.net-web-api

Request Payload does not get coverted to Custom Request Object.
payload
appl5=MC~IC&i~PhoneToPhone~inet_ptt_cb_phn~1=440&inet_ptt_cb_phn~3=7406&i~PhoneToPhone~inet_ptt_cb_delay=0&BeenHere=TRUE
It has ~ in keyvalue pair (both in key and value).
I have a Request Model that convert the input params to avalid Object.
Note: I cannot have ~ in my C# property. ( Can i ? )
My Post method has the following code
public HttpResponseMessage Post(ClientRequest request)
{
HttpResponseMessage response;
try
{
ProcessRequest target = new ProcessRequest(myRepository, myService);
response = target.Process(request);
}
catch (Exception exception)
{
response = Request.CreateErrorResponse(HttpStatusCode.NotFound, exception.Message);
//TODO : Log Exception.
}
return response;
}
Model
public class ClientRequest
{
public string Appl5 { get; set; }
public string I_PhoneToPhone_inet_ptt_cb_phn_1 { get; set; }
public string I_PhoneToPhone_inet_ptt_cb_delay { get; set; }
public string Inet_ptt_cb_phn_3 { get; set; }
public string BeenHere { get; set; }
}
My request object does not have the values for i~PhoneToPhone~inet_ptt_cb_phn~1, its null.
My understanding was the model binding is not happening because the payload key does not match
with my model (ClientRequest) which does not have ~ for i~PhoneToPhone~inet_ptt_cb_phn~1
in stead i have i_PhoneToPhone_inet_ptt_cb_phn_1
Should i use Custom Binding ?

At last, Added Custom Model binder
public class PostParameterModelBinder : IModelBinder
{
bool IModelBinder.BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
bool success = false;
if (bindingContext.ModelType == typeof(ClientRequest))
{
NameValueCollection postData = null;
postData = actionContext.Request.Content.ReadAsFormDataAsync().Result;
ClientRequest clientrequest = MapPostDataToRequest(postData);
bindingContext.Model = clientrequest;
success = true;
}
return success;
}
}
}

Related

Asp.NET core 3 custom model attribute error handling

I need to catch the exception form my custom model attribute on validating it.
Here is my HttpStatusCodeExceptionMiddleware:
public class HttpStatusCodeExceptionMiddleware
{
private readonly RequestDelegate _next;
public HttpStatusCodeExceptionMiddleware(RequestDelegate next)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (HttpStatusCodeException ex)
{
if (context.Response.HasStarted)
{
throw;
}
context.Response.Clear(); //<-possible Angular CORS error
context.Response.StatusCode = ex.StatusCode;
context.Response.ContentType = ex.ContentType;
ProblemDetails responseBody = new ProblemDetails(ex.Message, ex.StatusCode, "Request error", ex.Key);
await context.Response.WriteAsync(JsonConvert.SerializeObject(responseBody));
return;
}
}
public class HttpStatusCodeException : Exception
{
public int StatusCode { get; set; }
public string ContentType { get; set; } = #"text/plain";
//key for translation
public string Key { get; set; }
public HttpStatusCodeException(HttpResponseType statusCode)
{
this.StatusCode = (int)statusCode;
}
public HttpStatusCodeException(HttpResponseType statusCode, string message, string key) : base(message)
{
StatusCode = (int)statusCode;
Key = key;
}
public HttpStatusCodeException(HttpResponseType statusCode, Exception inner, string key) : base(inner.ToString())
{
Key = key;
}
public HttpStatusCodeException(HttpResponseType statusCode, JObject errorObject, string key) : this(statusCode, errorObject.ToString(), key)
{
this.ContentType = #"application/json";
}
}
public static class HttpStatusCodeExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseHttpStatusCodeExceptionMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<HttpStatusCodeExceptionMiddleware>();
}
}
And I am using it in the Startup.cs Configure method like this:
app.UseHttpStatusCodeExceptionMiddleware();
But in this scenario I need to catch the model attribute validation exception, but my solution only catches the controller exceptions.
Is there a way to do it?
thnx
You need to throw a HttpStatusCodeException then you could hit the catch (HttpStatusCodeException ex):
1.Custom validation attribute:
public class TestNameAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
if (value.ToString().StartsWith("a"))
{
throw new HttpStatusCodeException(HttpStatusCode.BadRequest, "Name could not start with a", value.ToString());
//return new ValidationResult("Name could not start with a");
}
return ValidationResult.Success;
}
}
2.Model:
public class YourModel
{
public long Id { get; set; }
[TestName]
public string Name { get; set; }
public string DisplayName { get; set; }
}
3.Test action:
[HttpPost]
public async Task<ActionResult<Mood>> Post(YourModel model)
{
_context.YourModel.Add(model);
await _context.SaveChangesAsync();
return CreatedAtAction("Get", new { id = model.Id }, model);
}

Issue is why my Web API not returning JSON

see my web api controller actions. i am return a response class from my action which has wrapped customer data ,status and message etc. but when i am invoking my web action from browser then action returning this symbol {} only which is very odd. see my web api code
my code as follows
[RoutePrefix("api/customer")]
public class CustomerController : ApiController
{
static readonly ICustomerRepository repository = new CustomerRepository();
[HttpGet, Route("GetAll")]
public HttpResponseMessage GetAllCustomers()
{
var Response=new Response(true, "SUCCESS", repository.GetAll());
//return Response;
//return Request.CreateResponse(HttpStatusCode.OK, Response);
HttpResponseMessage response = Request.CreateResponse<Response>(HttpStatusCode.OK, Response);
return response;
}
[HttpGet, Route("GetByID/{customerID}")]
public Response GetCustomer(string customerID)
{
Customer customer = repository.Get(customerID);
if (customer == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return new Response(true, "SUCCESS", customer);
//return Request.CreateResponse(HttpStatusCode.OK, response);
}
[HttpGet, Route("GetByCountryName/{country}")]
public IEnumerable<Customer> GetCustomersByCountry(string country)
{
return repository.GetAll().Where(
c => string.Equals(c.Country, country, StringComparison.OrdinalIgnoreCase));
}
public HttpResponseMessage PostCustomer(Customer customer)
{
customer = repository.Add(customer);
var response = Request.CreateResponse<Customer>(HttpStatusCode.Created, customer);
string uri = Url.Link("DefaultApi", new { customerID = customer.CustomerID });
response.Headers.Location = new Uri(uri);
return response;
}
public void PutProduct(string customerID, Customer customer)
{
customer.CustomerID = customerID;
if (!repository.Update(customer))
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
public void DeleteProduct(string customerID)
{
Customer customer = repository.Get(customerID);
if (customer == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
repository.Remove(customerID);
}
}
public class Response
{
bool IsSuccess = false;
string Message;
object ResponseData;
public Response(bool status, string message, object data)
{
IsSuccess = status;
Message = message;
ResponseData = data;
}
}
public class Customer
{
public string CustomerID { get; set; }
public string CompanyName { get; set; }
public string ContactName { get; set; }
public string ContactTitle { get; set; }
public string Address { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Fax { get; set; }
}
this way i am calling from winform using httpclient
var baseAddress = "http://localhost:38762/api/customer/GetAll";
using (var client = new HttpClient())
{
using (var response = client.GetAsync(baseAddress).Result)
{
if (response.IsSuccessStatusCode)
{
var customerJsonString = await response.Content.ReadAsStringAsync();
var cust = JsonConvert.DeserializeObject<Response>(customerJsonString);
}
else
{
Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}
}
}
tell me what is wrong in my code for GetAll actions which is not returning json rather return {}
i have to return my response class instead of IEnumerable<Customer> so show me the path what to change in code.
if my method looks like
[HttpGet, Route("GetAll")]
public Response GetAllCustomers()
{
var Response = new Response(true, "SUCCESS", repository.GetAll());
//return Response;
//return Request.CreateResponse(HttpStatusCode.OK, Response);
//HttpResponseMessage response = Request.CreateResponse<Response>(HttpStatusCode.OK, Response);
return Response;
}
OR
[HttpGet, Route("GetAll")]
public HttpResponseMessage GetAllCustomers()
{
var Response=new Response(true, "SUCCESS", repository.GetAll());
//return Response;
//return Request.CreateResponse(HttpStatusCode.OK, Response);
HttpResponseMessage response = Request.CreateResponse<Response>(HttpStatusCode.OK, Response);
return response;
}
but not returning any data or json. just return {} means null.
this way i give instruction to my web api as a result it should return json.
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
}
I think the issue could be that you are already sending SUCCESS in your Response then creating another response using the Request.CreateResponse(..) method.
You can try to modify your method as below
public Response GetAllCustomers()
{
var data = repository.GetAll();
if (data !=null)
return Request.CreateResponse(HttpStatusCode.OK, data);
else
return Request.CreateErrorResponse(HttpStatusCode.NotFound,"No records found");
}
You can return these
HttpStatusCodes

How to model bind a class that implements an interface?

The model binding worked fine until i implemented interfaces on top of the following classes:
public class QuestionAnswer : IQuestionAnswer
{
public Int32 Row_ID { get; set; }
public Int32 Column_ID { get; set; }
public String Value { get; set; }
}
public class HiddenAnswer : IHiddenAnswer
{
public Int32 Hidden_Field_ID { get; set; }
public String Hidden_Field_Value { get; set; }
}
public class SurveyAnswer : ISurveyAnswer
{
public string SessionID { get; set; }
public List<IQuestionAnswer> QuestionAnswerList { get; set; }
public List<IHiddenAnswer> HiddenAnswerList { get; set; }
public SurveyAnswer()
{
QuestionAnswerList = new List<IQuestionAnswer>();
HiddenAnswerList = new List<IHiddenAnswer>();
}
}
Now that the interfaces are there, i get a 500 (Internal Server Error)
The javascript that i use to model bind is the following:
$('#submitbutton').click(function () {
var answers = new Array();
var hiddenfields = new Array();
var formname = "#" + $("#formname").val();
$(':input', formname).each(function () {
if ($(this).is(":text") || $(this).is(":radio") || $(this).is(":checkbox"))
{
var answerObject = {
Column_ID: $(this).attr('data-column_id'),
Row_ID: $(this).attr('data-row_id'),
Value: $(this).attr('data-theValue')
};
answers.push(answerObject);
}
else if($(this).is(":hidden")) {
var hiddenObject =
{
Hidden_Field_ID: $(this).attr('data-hidden_field_id'),
Hidden_Field_Value: $(this).attr('data-hidden_field_value')
}
hiddenfields.push(hiddenObject);
}
});
$('textarea', formname).each(function () {
var answerObject = {
Column_ID: $(this).attr('data-column_id'),
Row_ID: $(this).attr('data-row_id'),
Value: $(this).val(),
};
answers.push(answerObject);
});
var allAnswers = {
SessionID: 0,
QuestionAnswerList: answers,
HiddenAnswerList: hiddenfields
}
postForm(allAnswers);
});
The Controller Action looks like this:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SubmitSurvey(SurveyAnswer answers)
{
// Dette tillader CORS
Response.AppendHeader("Access-Control-Allow-Origin", "*");
bc.SaveSurvey(answers);
return null;
}
what am i doing wrong?
what am i doing wrong?
You cannot expect the model binder to know that when it encounters the IQuestionAnswer interface on your SurveyAnswer view model it should use the QuestionAnswer type. It's nice that you have declared this implementation of the interface but the model binder has no clue about it.
So you will have to write a custom model binder for the IQuestionAnswer interface (same for the IHiddenAnswer interface) and indicate which implementation do you want to be used:
public class QuestionAnswerModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var type = typeof(QuestionAnswer);
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
which will be registered in your Application_Start:
ModelBinders.Binders.Add(typeof(IQuestionAnswer), new QuestionAnswerModelBinder());

Custom model binder for inner model

I got a model like this:
public class MainModel
{
public string Id {get;set;}
public string Title {get;set;}
public TimePicker TimePickerField {get;set;}
}
TimePicker is an inner model which looks like this:
public class TimePicker
{
public TimeSpan {get;set;}
public AmPmEnum AmPm {get;set;}
}
I'm trying to create a custom model binding for inner model: TimePicker
The question is: How do I get values in custom model binder which was submitted in form into TimePicker model fields?
If I try to get it like this:
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
I just get null in value.
I'm not sure how to implement the model binder correctly.
public class TimePickerModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var result = new TimePicker();
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value != null)
{
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
try
{
//result = Duration.Parse(value.AttemptedValue);
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
}
}
return result;
}
}
The following works for me.
Model:
public enum AmPmEnum
{
Am,
Pm
}
public class TimePicker
{
public TimeSpan Time { get; set; }
public AmPmEnum AmPm { get; set; }
}
public class MainModel
{
public TimePicker TimePickerField { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MainModel
{
TimePickerField = new TimePicker
{
Time = TimeSpan.FromHours(1),
AmPm = AmPmEnum.Pm
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(MainModel model)
{
return View(model);
}
}
View (~/Views/Home/Index.cshtml):
#model MainModel
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.TimePickerField)
<button type="submit">OK</button>
}
Custom editor template (~/Views/Shared/EditorTemplates/TimePicker.cshtml) which merges the Time and AmPm properties into a single input field and which will require a custom model binder later in order to split them when the form is submitted:
#model TimePicker
#Html.TextBox("_picker_", string.Format("{0} {1}", Model.Time, Model.AmPm))
and the model binder:
public class TimePickerModelBinder : IModelBinder
{
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext
)
{
var key = bindingContext.ModelName + "._picker_";
var value = bindingContext.ValueProvider.GetValue(key);
if (value == null)
{
return null;
}
var result = new TimePicker();
try
{
// TODO: instead of hardcoding do your parsing
// from value.AttemptedValue which will contain the string
// that was entered by the user
return new TimePicker
{
Time = TimeSpan.FromHours(2),
AmPm = AmPmEnum.Pm
};
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName,
ex.Message
);
// This is important in order to preserve the original user
// input in case of error when redisplaying the view
bindingContext.ModelState.SetModelValue(key, value);
}
return result;
}
}
and finally register your model binder in Application_Start:
ModelBinders.Binders.Add(typeof(TimePicker), new TimePickerModelBinder());

Action name containing a question mark?

Using asp.net mvc 3.0 what would i have to do to provide the following route
public class ProductController : Controller
{
// ************************
// URL : Product/Create
// ************************
public ActionResult Create()
{
return View();
}
// ************************
// URL : Product/Create?Page=Details
// ************************
[ActionName("Create?Page=Details")]
public ActionResult CreateDetails()
{
return View();
}
}
Thanks
Rohan
public class QueryStringConstraint : IRouteConstraint
{
public QueryStringConstraint(string value, bool ignoreCase = true)
{
Value = value;
IgnoreCase = ignoreCase;
}
public string Value { get; private set; }
public bool IgnoreCase { get; private set; }
public virtual bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var currentValue = httpContext.Request.QueryString[parameterName];
return IgnoreCase ? currentValue.ToLowerInvariant() == Value.ToLowerInvariant() : currentValue == Value;
}
}
routes.MapRoute("Create page details", "Product/Create",
new { controller = "Product", action = "CreateDetails" },
new { page = new QueryStringConstraint("details") });
Alternatively if you have different models for those actions, you could do something like this (with standard "{controller}/{action}/{optional id}" route):
public class RequireRequestValueAttribute : ActionMethodSelectorAttribute
{
public RequireRequestValueAttribute(string name, string value = null, bool ignoreCase = true)
{
Name = name;
Value = value;
IgnoreCase = ignoreCase;
}
public string Name { get; private set; }
public string Value { get; private set; }
public bool IgnoreCase { get; private set; }
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
var value = controllerContext.HttpContext.Request[Name];
return value != null && (Value == null || (IgnoreCase ? Value.ToLowerInvariant() == value.ToLowerInvariant() : Value == value));
}
}
[RequireRequestValue("Page", "Detail")]
public ActionResult Create(ProductDetailModel model)
{
return View(model);
}
[RequireRequestValue("Page", "Overview")]
public ActionResult Create(ProductOverviewModel model)
{
return View(model);
}
An action name cannot contain a question mark. The question mark is a reserved character in a URL indicating the beginning of the query string.
What if you not create another action? Just call your "Create" action with the query string.
http://localhost/Home/Create?Page=Details
public ActionResult Create()
{
var page = Request.QueryString["Page"];
// do your stuff, or redirect here if you like
// return RedirectToAction("Create" + page);
return View();
}

Resources