I have strange behaviour of Web API, .Net 4.5.2. If optional string parameter is null, ModelState has no error. If it is not null and not empty, no errors again. But if it is just an empty string I have model state error.
Why do I get it and how to disable it?
Assuming app served on localhost:82 I have those results:
Url: http://localhost:82/
Response: "null"
Url: http://localhost:82/?q=1
Response: "1"
Url: http://localhost:82/?q=
Response: {
"Message": "The request is invalid.",
"ModelState": {
"q.String": [
"A value is required but was not present in the request."
]
}
}
Test controller and config is below. This is reduced to bare minimum default "Asp.net web application" with "WebApi" in VS2013.
namespace Web.Api.Test.Controllers
{
using System.Web.Http;
[Route]
public class HomeController : ApiController
{
[Route]
[HttpGet]
public IHttpActionResult Search(string q = default(string))
{
return this.ModelState.IsValid
? this.Ok(q ?? "null")
: (IHttpActionResult)this.BadRequest(this.ModelState);
}
}
}
Startup.cs is:
using Microsoft.Owin;
using WebApplication1;
[assembly: OwinStartup(typeof(Startup))]
namespace WebApplication1
{
using System.Web.Http;
using Newtonsoft.Json;
using Owin;
public class Startup
{
public void Configuration(IAppBuilder app)
{
GlobalConfiguration.Configure(config =>
{
config.MapHttpAttributeRoutes();
config.Formatters.JsonFormatter.SerializerSettings.Formatting = Formatting.Indented;
config.Formatters.Remove(config.Formatters.XmlFormatter);
});
}
}
}
PS: This question has a workaround, but it does not answer the main question: why does this situation happen and what reasons are behind this design decision.
I have had the same issue, came up with the following eventually:
public class SimpleTypeParameterBindingFactory
{
private readonly TypeConverterModelBinder converterModelBinder = new TypeConverterModelBinder();
private readonly IEnumerable<ValueProviderFactory> factories;
public SimpleTypeParameterBindingFactory(HttpConfiguration configuration)
{
factories = configuration.Services.GetValueProviderFactories();
}
public HttpParameterBinding BindOrNull(HttpParameterDescriptor descriptor)
{
return IsSimpleType(descriptor.ParameterType)
? new ModelBinderParameterBinding(descriptor, converterModelBinder, factories)
: null;
}
private static bool IsSimpleType(Type type)
{
return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof (string));
}
}
public class Startup
{
public void Configure(IAppBuilder appBuilder)
{
var configuration = new HttpConfiguration();
configuration.ParameterBindingRules.Insert(0, new SimpleTypeParameterBindingFactory(configuration).BindOrNull);
configuration.EnsureInitialized();
}
}
The problem is rooted in some magic code in ModelValidationNode, which creates model errors for null models even if corresponding parameter has default value. The code above just replaces CompositeModelBinder (which calls ModelValidationNode) with TypeConverterModelBinder for simple type parameters.
Why do I get it and how to disable it?
Don't know why you get it. This maybe how you disable it, but after reading I don't think you want to really as there are simpler solutions, e.g:
Use of a model class solves this in a cleaner way.
public class SearchModel
{
public string Q { get; set; }
}
public IHttpActionResult Search([FromUri] SearchModel model)
{
return ModelState.IsValid
? Ok(model.Q ?? "null")
: (IHttpActionResult) BadRequest(ModelState);
}
That's why:
This is a MVC feature which binds empty strings to nulls.
We have found the same behavior in our application, and deep dive debugging with source code
git clone https://github.com/ASP-NET-MVC/aspnetwebstack
makes sense to search in the right direction. Here the method which set whitespace strings to null, and here the error is added to model state:
if (parentNode == null && ModelMetadata.Model == null)
{
string trueModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, ModelMetadata.GetDisplayName());
modelState.AddModelError(trueModelStateKey, SRResources.Validation_ValueNotFound);
return;
}
IMHO it is a bug. But who cares. We used this workaround
Have you tried [DisplayFormat(ConvertEmptyStringToNull = false)]?
We found another solution
public class EmptyStringToNullModelBinder : Attribute, IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.Model = string.IsNullOrWhiteSpace(valueResult?.RawValue?.ToString()) ? null : valueResult.RawValue;
return true;
}
}
and for your case it would be like this:
[Route]
[HttpGet]
public IHttpActionResult Search([FromUri(BinderType = typeof(EmptyStringToNullModelBinder))]string q = null)
{
return this.ModelState.IsValid
? this.Ok(q ?? "null")
: (IHttpActionResult)this.BadRequest(this.ModelState);
}
code below is adapted version of this answer
public class WebApiDefaultValueBinder<T> : IModelBinder
{
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(T))
{
return false;
}
var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return false;
}
var rawValue = val.RawValue as string;
// Not supplied : /test/5
if (rawValue == null)
{
bindingContext.Model = default(T);
return true;
}
// Provided but with no value : /test/5?something=
if (rawValue == string.Empty)
{
bindingContext.Model = default(T);
return true;
}
// Provided with a value : /test/5?something=1
try
{
bindingContext.Model = (T)Convert.ChangeType(val.RawValue, typeof(T));
return true;
}
catch
{
//
}
bindingContext.ModelState.AddModelError(bindingContext.ModelName, $"Cannot convert value to {typeof(T).Name}");
return false;
}
}
Related
In the old .Net Framework MVC implementations, I was creating routes by myself so that I could also influence urls generation. Part of the code:
public class RouteBase : Route
{
public RouteBase(string url, IRouteHandler routeHandler) : base(url, routeHandler) { }
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
if (Url.Contains("{segment}") && !values.ContainsKey("segment"))
values["segment"] = requestContext.HttpContext.Items["segmentValue"];
return base.GetVirtualPath(requestContext, values);
}
}
Thanks to GetVirtualPath, I was able to detect a particular segment in the route template and inject a proper value in the route values dictionary so that the client app did not have to specify it when calling for instance Url.RouteUrl(routeName).
In asp.net core 6, I'm now using attributes based routing and I don't know how to hook into this so that I can inject some value into the route values dictionary when I generate urls. If I have a route template like so:
[Route("{segment}/test", Name = "name"]
When I call this, I want an injection mechanism from somewhere else in the code so that the known segment value is injected into the route values used to build the url:
var url = Url.RouteUrl("name"); // Not passing new { segment = value } as second param
For information, I simply use this in Startup:
app.MapControllers();
You can create and register a custom UrlHelper. It will give you the ability to manipulate the behavior as per your use case:
public class CustomUrlHelper : UrlHelper
{
public CustomUrlHelper(ActionContext actionContext)
: base(actionContext) { }
public override string? RouteUrl(UrlRouteContext routeContext)
{
// if(routeContext.RouteName == "name" && routeContext.Values....)
// routeContext.Values = ....
return base.RouteUrl(routeContext);
}
}
public class CustomUrlHelperFactory : IUrlHelperFactory
{
public IUrlHelper GetUrlHelper(ActionContext context)
{
return new CustomUrlHelper(context);
}
}
and in your Program.cs:
builder.Services.AddSingleton<IUrlHelperFactory, CustomUrlHelperFactory>();
Then by calling the Url.RouteUrl("name"), your CustomUrlHelper will be called.
Amir's answer put me on track to find a solution (bounty award for him). Creating a custom UrlHelper was the way to go, but not with a UrlHelper derived class. For enpoint routing, the framework is using the sealed EndpointRoutingUrlHelper class. So I just needed to derive from UrlHelperBase, paste the code from EndpointRoutingUrlHelper and add my customizations. I was lucky that there were no internal pieces of code in there...
Here is the solution. Note that:
the term "segment" mentioned in the original question is replaced by what I actually have in my code i.e. "lang".
HttpContext.Items["lang"] is set by a middleware.
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc;
// The custom UrlHelper is registered with serviceCollection.AddSingleton<IUrlHelperFactory, LanguageAwareUrlHelperFactory>();
public class LanguageAwareUrlHelperFactory : IUrlHelperFactory
{
private readonly LinkGenerator _linkGenerator;
public LanguageAwareUrlHelperFactory(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public IUrlHelper GetUrlHelper(ActionContext context)
{
return new LanguageAwareUrlHelper(context, _linkGenerator);
}
}
// Source code is taken from https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Routing/EndpointRoutingUrlHelper.cs
// and modified to inject the desired route value
public class LanguageAwareUrlHelper : UrlHelperBase
{
private readonly LinkGenerator _linkGenerator;
public LanguageAwareUrlHelper(ActionContext actionContext, LinkGenerator linkGenerator) : base(actionContext)
{
if (linkGenerator == null)
throw new ArgumentNullException(nameof(linkGenerator));
_linkGenerator = linkGenerator;
}
public override string? Action(UrlActionContext urlActionContext)
{
if (urlActionContext == null)
throw new ArgumentNullException(nameof(urlActionContext));
var values = GetValuesDictionary(urlActionContext.Values);
if (urlActionContext.Action == null)
{
if (!values.ContainsKey("action") && AmbientValues.TryGetValue("action", out var action))
values["action"] = action;
}
else
values["action"] = urlActionContext.Action;
if (urlActionContext.Controller == null)
{
if (!values.ContainsKey("controller") && AmbientValues.TryGetValue("controller", out var controller))
values["controller"] = controller;
}
else
values["controller"] = urlActionContext.Controller;
if (!values.ContainsKey("lang") && ActionContext.HttpContext.Items.ContainsKey("lang"))
values["lang"] = ActionContext.HttpContext.Items["lang"];
var path = _linkGenerator.GetPathByRouteValues(
ActionContext.HttpContext,
routeName: null,
values,
fragment: urlActionContext.Fragment == null ? FragmentString.Empty : new FragmentString("#" + urlActionContext.Fragment));
return GenerateUrl(urlActionContext.Protocol, urlActionContext.Host, path);
}
public override string? RouteUrl(UrlRouteContext routeContext)
{
if (routeContext == null)
throw new ArgumentNullException(nameof(routeContext));
var langRouteValues = GetValuesDictionary(routeContext.Values);
if (!langRouteValues.ContainsKey("lang") && ActionContext.HttpContext.Items.ContainsKey("lang"))
langRouteValues.Add("lang", ActionContext.HttpContext.Items["lang"]);
var path = _linkGenerator.GetPathByRouteValues(
ActionContext.HttpContext,
routeContext.RouteName,
langRouteValues,
fragment: routeContext.Fragment == null ? FragmentString.Empty : new FragmentString("#" + routeContext.Fragment));
return GenerateUrl(routeContext.Protocol, routeContext.Host, path);
}
}
In Asp.Net Core, I use the below two methods and it is able to successfully generate the URL.
[Route("{segment}/test", Name = "name"]
var url1 = Url.RouteUrl("name", new { segment = "aa" });
var url2 = Url.Action("Action", "Controller", new { segment = "aa" });
I've build an Asp.Net Core Controller and I would like to pass Data throw the Url to my Backend.
Throw my URI I would like to paste: filter:"[[{"field":"firstName","operator":"eq","value":"Jan"}]]
So my URI looks like: https://localhost:5001/Patient?filter=%5B%5B%7B%22field%22%3A%22firstName%22,%22operator%22%3A%22eq%22,%22value%22%3A%22Jan%22%7D%5D%5D
and my Controller:
[HttpGet]
public ActionResult<bool> Get(
[FromQuery] List<List<FilterObject>> filter = null)
{
return true;
}
and my FilterObject looks like:
public class FilterObject
{
public string Field { get; set; }
public string Value { get; set; }
public FilterOperator Operator { get; set; } = FilterOperator.Eq;
}
The Problem now is that my Data from the URL is not deserialized in my filter Parameter.
Do anyone have an Idea?
Thans for helping.
Best Regards
Throw my URI I would like to paste: filter:"[[{"field":"firstName","operator":"eq","value":"Jan"}]]
You can achieve the requirement by implementing a custom model binder, the following code snippet is for your reference.
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
// ...
// implement it based on your actual requirement
// code logic here
// ...
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
var model = JsonSerializer.Deserialize<List<List<FilterObject>>>(bindingContext.ValueProvider.GetValue("filter").FirstOrDefault(), options);
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Controller action
[HttpGet]
public ActionResult<bool> Get([FromQuery][ModelBinder(BinderType = typeof(CustomModelBinder))]List<List<FilterObject>> filter = null)
{
Test Result
Everytime my controller receives a date as dd/MM/yyyy it decode as MM/dd/yyyy. Is it possible to tell the controller how to decode the parameter of the url?
My method in controller:
[HttpGet]
public JsonResult<IList<Callers>> GetListOfCallers(DateTime startDate, DateTime endDate)
{
// myCode....
}
My javascript:
var $startDate = $('#startDate').val();
var $endDate = $('#endDate').val();
$.get(rootUrl + "api/report/GetListOfCallers?startDate=" + $startDate + "&endDate=" + $endDate, function (data) {
// myCode....
});
I know I can receive the date in controller as string and then parse it, or change it in my javascript to ISO8601 before putting in the url, but I want to know if I can tell my controller how to decode the parameter received.
EDIT: I was using MVC controller and this was not a problem, it started decoding incorrectly after I changed to ApiController, so the code was working and I hope to keep as it is.
I manage to solve my problem using Model binding as suggested by #Jakotheshadows and #Amy.
I used the code from this answer about ModelBinders in Web Api with a few tweaks from this answer (it's in portuguese, but the code is clear).
So my code right now:
using System;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
namespace Site.Services
{
public class DateTimeModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
ValidateBindingContext(bindingContext);
if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName) ||
!CanBindType(bindingContext.ModelType))
{
return false;
}
var modelName = bindingContext.ModelName;
var attemptedValue = bindingContext.ValueProvider
.GetValue(modelName).AttemptedValue;
try
{
bindingContext.Model = DateTime.Parse(attemptedValue);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
return true;
}
private static void ValidateBindingContext(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
if (bindingContext.ModelMetadata == null)
{
throw new ArgumentException("ModelMetadata cannot be null", "bindingContext");
}
}
public static bool CanBindType(Type modelType)
{
return modelType == typeof(DateTime) || modelType == typeof(DateTime?);
}
}
}
I used try and DateTime.Parse as suggested in the second link, because the first always throwed an exception even with try and catch.
The ModelBinderProvider I used as he suggested:
using System;
using System.Web.Http;
using System.Web.Http.ModelBinding;
namespace Site.Services
{
public class DateTimeModelBinderProvider : ModelBinderProvider
{
readonly DateTimeModelBinder binder = new DateTimeModelBinder();
public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
{
if (DateTimeModelBinder.CanBindType(modelType))
{
return binder;
}
return null;
}
}
}
And I configure as suggested here (also an answer for the first link), but in my WebApiConfig.cs (didn't work in Global.asax), like this:
using Site.Services;
using System;
using System.Web.Http;
namespace Site
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.BindParameter(typeof(DateTime), new DateTimeModelBinder());
config.BindParameter(typeof(DateTime?), new DateTimeModelBinder());
//Rest of my code
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}
I think the globalization of the Web.config, the uiCulture and culture must be set to the culture you want and enableClientBasedCulture be set as true as suggest here, but I'm not sure because I didn't want to change the code to test it.
I am interested in building a startup routine for my API to check that certain configuration values in the web.config are present. If the routine does not contain values I would like to redirect to a route, log the missing configuration item and display a custom application offline page.
Any assistance in pointing me in the right direction would be appreciated.
Guard Class
public static class Guard
{
public static bool ConfigurationValueExists(string key, [CallerMemberName] string caller = null)
{
if (!string.IsNullOrEmpty(Configuration.GetAppConfig(key, string.Empty))) return true;
ApiLogger.Log($"The configuration value {key} is not present and needs to be defined. Calling method is {caller}.");
return false;
}
}
Configuration Class
public static class Configuration
{
public static T GetAppConfig<T>(string key, T defaultVal = default(T))
{
if (null == ConfigurationManager.AppSettings[key])
{
return defaultVal;
}
return string.IsNullOrEmpty(key)
? defaultVal
: Generic.Turn(ConfigurationManager.AppSettings[key], defaultVal);
}
public static bool ConfigurationsAreInPlace()
{
return AssertMainApplicationConfiguration();
}
private static bool AssertMainApplicationConfiguration()
{
return Guard.ConfigurationValueExists("MyKey1");
}
}
I would like to be able to call ConfigurationsAreInPlace on the startup routine and redirect to my custom offline page.
I decided to create an Index Controller and use the Route Attribute of root to override what happens on the page. I then do the check if configurations are in place and issue a new response as needed.
Code if interested:
public class IndexController : ApiController
{
[AllowAnonymous]
[Route]
public HttpResponseMessage GetIndex()
{
string startUrl = "/help/";
if (!Helpers.Configuration.ConfigurationsAreInPlace())
{
startUrl += "offline";
}
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Moved);
string fullyQualifiedUrl = Request.RequestUri.GetLeftPart(UriPartial.Authority);
response.Headers.Location = new Uri(fullyQualifiedUrl + startUrl);
return response;
}
}
I have a problem with the FluentSecurity when the ActionNameSelectorAttribute is used on controller's action.
public static void Configure()
{
var applicationConfiguration = DependencyResolver.Current.GetService<IApplicationConfiguration>();
var superUserGroupName = applicationConfiguration.GetSuperUserGroupName();
var userGroupName = applicationConfiguration.GetUserGroupName();
var securityConfiguration = SecurityConfigurator.Configure(configuration =>
{
configuration.GetAuthenticationStatusFrom(() => HttpContext.Current.User.Identity.IsAuthenticated);
configuration.GetRolesFrom(System.Web.Security.Roles.GetRolesForUser);
configuration.ForAllControllers().DenyAnonymousAccess().CachePerHttpRequest();
configuration.ForAllControllers().RequireAnyRole(superUserGroupName).CachePerHttpRequest();
configuration.For<Elmah.Mvc.ElmahController>().RequireAnyRole(userGroupName).CachePerHttpRequest();
configuration.ApplyProfile<ProjectSecurityProfile>();
configuration.ApplyProfile<ProjectsSecurityProfile>();
configuration.ApplyProfile<RewecoSecurityProfile>();
configuration.DefaultPolicyViolationHandlerIs(() => new HttpUnauthorizedPolicyViolationHandler());
});
securityConfiguration.AssertAllActionsAreConfigured();
}
When I run the application under the configuration above with the AssertAllActionsAreConfigured everything seems to be correct, no exceptions. But as soon as I call the action methods in the ActualHoursAssignmentController where the HttpParamAction is used , which is the class which inherits from ActionNameSelectorAttribute I get the exception.
Security has not been configured for controller PDATA.Web.Controllers.ActualHoursAssignmentController, action ActionChoiceByNameAttributeValue Area: (not set) Controller: ActualHoursAssignment Action: ActionChoiceByNameAttributeValue
public class HttpParamActionAttribute : ActionNameSelectorAttribute
{
public static string ActionChoiceByNameAttributeValue
{
get { return "ActionChoiceByNameAttributeValue"; }
}
public override bool IsValidName([NotNull] ControllerContext controllerContext,
[NotNull] string actionName, [NotNull] MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (actionName == null)
{
throw new ArgumentNullException("actionName");
}
if (methodInfo == null)
{
throw new ArgumentNullException("methodInfo");
}
if (String.IsNullOrWhiteSpace(actionName))
{
throw new ArgumentException("actionName");
}
if (String.IsNullOrWhiteSpace(methodInfo.Name))
{
throw new ArgumentException("methodInfo.Name");
}
if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
return true;
if (!actionName.Equals(ActionChoiceByNameAttributeValue, StringComparison.InvariantCultureIgnoreCase))
return false;
var request = controllerContext.RequestContext.HttpContext.Request;
return request[methodInfo.Name] != null;
}
}
Usage of HttpParamAction attribute in ActualHoursAssignmentController
public class ActualHoursAssignmentController : PdataBaseController
{
[HttpParamAction]
[HttpPost]
public ActionResult UpdateAssignment(ActualHoursAssignmentViewModel vm)
{
}
[HttpParamAction]
[HttpPost]
public ActionResult DeleteAssignment(ActualHoursAssignmentViewModel vm)
{
}
}
UPDATE:
Because I didn't find the solution I temporary eliminate of usage HttpParamActionAttribute. Instead of that I'm using this solution to call multiple buttons in the one Form, but the question persists, maybe it is a bug.
It looks like there is an issue in older versions of FluentSecurity with supporting Controller inheritance, see:
https://github.com/kristofferahl/FluentSecurity/wiki/Securing-controllers#securing-controllers-based-on-inheritance