This issue was raise twice on SO, I have tried those solutions, but it is not working for me.
I am trying to create CustomRouteConstraint for a ComplexObject which is a parameter in ActionMethod, where HttpPost is used.
I am following this url to implement Custom Routing.
https://learn.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
1. Creating CustomRouteConstraint.
public class CustomRouteConstraint : IHttpRouteConstraint
{
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
object value;
if (values.TryGetValue(parameterName, out value) && value != null)
{
return true;
}
return false;
}
}
2. MapAttribute in WebAPIConfig.cs file
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("CustomObjRouteConstraint", typeof(CustomRouteConstraint));
config.MapHttpAttributeRoutes(constraintResolver);
}
}
3. Using Constraint in ActionMethod.
[Route("Address/{param:CustomObjRouteConstraint}")]
[HttpPost]
public AddressUpdateResult UpdateCustomerAddresses(UpdateCustomerAddressCommand param)
{
Handle(param);
return xyz;
}
At run-time I get error :
"DefaultInlineResolver was unable to resolve CustomRouteConstraint "
Related
MSDN recommends setting a time-out value in all regular expression pattern-matching operations.
How we can set match timeout in route attribute of asp.net core projects
[Route("[controller]/[action]/{test:regex(^(\\w+$)}")]
public string Get(string test)
{
//...
}
You could refer to the following sample to create a custom route constraint, then, set the Timeout.
Public class MyCustomConstraint : IRouteConstraint
{
private Regex _regex;
public MyCustomConstraint()
{
_regex = new Regex(#"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
}
public bool Match(HttpContext httpContext, IRouter route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out object value))
{
var parameterValueString = Convert.ToString(value,
CultureInfo.InvariantCulture);
if (parameterValueString == null)
{
return false;
}
return _regex.IsMatch(parameterValueString);
}
return false;
}
}
Then, register the above constraint in the Startup.ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddRouting(options =>
{
options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
});
}
Then, apply the constrain in the action method, like this:
// GET /api/test/my/3
[HttpGet("my/{id:customName}")]
public IActionResult Get(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
The debug screenshot like this:
Besides, you can also set the timeout value for all Regex matching operations in an application domain by calling the AppDomain.SetData method, code in the Program.cs file:
public static void Main(string[] args)
{
AppDomain domain = AppDomain.CurrentDomain;
// Set a timeout interval of 200 milliseconds.
domain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromMilliseconds(200));
CreateHostBuilder(args).Build().Run();
}
Then, there is no need to set the timeout in the custom route constraint, check this screenshot:
I am migrating controllers from .NET Framework to .NET Core and I want to be compatibility with API calls from previous version. I have problem with handling multiple routes from Query Params.
My example controller:
[Route("/api/[controller]")]
[Route("/api/[controller]/[action]")]
public class StaticFileController : ControllerBase
{
[HttpGet("{name}")]
public HttpResponseMessage GetByName(string name)
{
}
[HttpGet]
public IActionResult Get()
{
}
}
Calling api/StaticFile?name=someFunnyName will lead me to Get() action instead of expected GetByName(string name).
What I want to achieve:
Calling GET api/StaticFile -> goes to Get() action
Calling GET
api/StaticFile?name=someFunnyName -> goes to GetByName() action
My app.UseEndpoints() from Startup.cs have only these lines:
endpoints.MapControllers();
endpoints.MapDefaultControllerRoute();
If I use [HttpGet] everywhere and add ([FromQuery] string name) it gets me AmbiguousMatchException: The request matched multiple endpoints
Thank you for your time to helping me (and maybe others)
What I want to achieve:
Calling GET api/StaticFile -> goes to Get() action
Calling GET api/StaticFile?name=someFunnyName -> goes to GetByName() action
To achieve above requirement of matching request(s) to expected action(s) based on the query string, you can try to implement a custom ActionMethodSelectorAttribute and apply it to your actions, like below.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class QueryStringConstraintAttribute : ActionMethodSelectorAttribute
{
public string QueryStingName { get; set; }
public bool CanPass { get; set; }
public QueryStringConstraintAttribute(string qname, bool canpass)
{
QueryStingName = qname;
CanPass = canpass;
}
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
StringValues value;
routeContext.HttpContext.Request.Query.TryGetValue(QueryStingName, out value);
if (QueryStingName == "" && CanPass)
{
return true;
}
else
{
if (CanPass)
{
return !StringValues.IsNullOrEmpty(value);
}
return StringValues.IsNullOrEmpty(value);
}
}
}
Apply to Actions
[Route("api/[controller]")]
[ApiController]
public class StaticFileController : ControllerBase
{
[HttpGet]
[QueryStringConstraint("name", true)]
[QueryStringConstraint("", false)]
public IActionResult GetByName(string name)
{
return Ok("From `GetByName` Action");
}
[HttpGet]
[QueryStringConstraint("name", false)]
[QueryStringConstraint("", true)]
public IActionResult Get()
{
return Ok("From `Get` Action");
}
}
Test Result
The parameter for HttpGet sets the route, not query string parameter name.
You should add FromQuery attribute for action parameter and use HttpGet without "{name}":
[HttpGet]
public HttpResponseMessage GetByName([FromQuery] string name)
{
// ...
}
You can also set different name for query parameter:
[HttpGet]
public HttpResponseMessage GetByName([FromQuery(Name = "your_query_parameter_name")] string name)
{
// ...
}
But now you have two actions matching same route so you will get exception. The only way to execute different logic based on query string part only (the route is the same) is to check query string inside action:
[HttpGet]
public IActionResult Get([FromQuery] string name)
{
if (name == null)
{
// execute code when there is not name in query string
}
else
{
// execute code when name is in query string
}
}
So you have only one action which handles both cases using same route.
I got my solution from https://www.strathweb.com/2016/09/required-query-string-parameters-in-asp-net-core-mvc/
public class RequiredFromQueryAttribute : FromQueryAttribute, IParameterModelConvention
{
public void Apply(ParameterModel parameter)
{
if (parameter.Action.Selectors != null && parameter.Action.Selectors.Any())
{
parameter.Action.Selectors.Last().ActionConstraints.Add(new RequiredFromQueryActionConstraint(parameter.BindingInfo?.BinderModelName ?? parameter.ParameterName));
}
}
}
public class RequiredFromQueryActionConstraint : IActionConstraint
{
private readonly string _parameter;
public RequiredFromQueryActionConstraint(string parameter)
{
_parameter = parameter;
}
public int Order => 999;
public bool Accept(ActionConstraintContext context)
{
if (!context.RouteContext.HttpContext.Request.Query.ContainsKey(_parameter))
{
return false;
}
return true;
}
}
For example, if using [RequiredFromQuery] in StaticFileController we are able to call /api/StaticFile?name=withoutAction and /api/StaticFile/GetByName?name=wAction but not /api/StaticFile/someFunnyName (?name= and /)
Workaround solution for that is to create separate controller action to handle such requests
I have a class derived from webapi controller.In that controller i have a method
[HttpGet]
[Route("MessageCount")]
public string GetMessageCountCount()
{
OrganisationMembership organisationMembership = organisationService.Find(organisationId);
SessionService.Get.SetUserConversationMessages(organisationMembership);
return SessionService.Get.UnreadMessagesCount.ToString();
}
Here is SessionService
public class SessionService : ISessionService
{
private static SessionService _sessionService;
public static SessionService Get
{
get
{
if (_sessionService == null)
{
_sessionService = new SessionService(new RakletDb());
}
return _sessionService;
}
}
public int UnreadMessagesCount
{
get { return GetSessionInt("UnreadMessagesCount"); }
private set { SetSessionValue("UnreadMessagesCount", value); }
}
private int GetSessionInt(string key)
{
var s = HttpContext.Current.Session[key];
if (s != null)
{
return Int32.Parse(s.ToString());
}
return 0;
}
private void SetSessionValue(string key, object o)
{
HttpContext.Current.Session[key] = o;
}
public void SetUserConversationMessages(OrganisationMembership organisationMembership)
{
this.UnreadMessagesCount = somelinq query;
}
}
My project succesfully compiled. But when i call GetMessageCount method from browser it gives me an error like this:
error
{"Message":"An error has occurred.","ExceptionMessage":"Object reference not set to an instance of an object.","ExceptionType":"System.NullReferenceException","StackTrace":" at Services.SessionService.SetSessionValue(String key, Object o)...
I use something like this in a controller :SessionService.UnreadMessageCount
But in webapi I can only use SessionService.Get.UnreadMessageCount
so what problem can be?
The problem is on HttpContext.Current.Session[key] = o;,Because REST web api by design is stateless. So Default you can't use Session.
I would not suggest you use session on web api.
If you want to use Session,you need to make a HttpHandler which's implement IHttpHandler and let it implement System.Web.SessionState.IRequiresSessionState.
You might be like
public class SessionHander :
System.Web.Http.WebHost.HttpControllerHandler,
System.Web.SessionState.IRequiresSessionState
{
public SessionHander(RouteData routeData) : base(routeData)
{ }
}
public class SeesionRouterHanlder :
System.Web.Http.WebHost.HttpControllerRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new SessionHander(requestContext.RouteData);
}
}
And this code on Global.asax
var route = RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
route.RouteHandler = new SeesionRouterHanlder();
public class ContactsController : ApiController
{
static readonly IContactsRepository repository = new ContactsRepository();
//
// GET: /Contacts/
public IEnumerable<Contact> GetAllContacts()
{
return repository.GetAll();
}
}
The above code works fine with the API Call /api/contacts/GetAllContacts and returns a list of contacts from my database. I also want to add an method which returns a specific contact using something like /api/contacts/getcontacts? However, once I add in the following code:
public Contacts GetContact(int id)
{
Contacts item = repository.Get(id);
if (item == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return item;
}
My original call (/api/contacts/GetAllContact) will not work and displays the following error:
"Message": "The request is invalid.",
"MessageDetail": "The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'ReservationsAPI.Models.Contacts GetContact(Int32)' in 'ReservationsAPI.Controllers.ContactsController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter."
EDIT:
route config
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}
Remove ContactS class that you manually created and try this;
public class ContactController : ApiController
{
static readonly IContactsRepository repository = new ContactsRepository();
// GET api/Contact
public IEnumerable<Contact> GetContact()
{
return repository.GetAll();
}
// GET api/Contact/5
public IHttpActionResult GetContact(int id)
{
var contact = repository.Get(id);
if (contact == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return contact;
}
Then try to call these urls;
/api/Contact
/api/Contact/1
With this setup, you don't need to define action in your routing.
I'm using Wcf data service(V3). From IOS App they will send Signature through URL. Problem is sometimes user enters long signature in that situation it is giving an error like "Url is too long". how can i fix this issue on wcf data services.
Advance Thanks.
If the message client want to give to service is large, it is recommended to use POST.
You can find the guide for Actions in WCF Data Service V3 here:
http://blogs.msdn.com/b/odatateam/archive/2011/10/17/actions-in-wcf-data-services.aspx
And here is quick demo for setting up a WCF DS service with Action support:
public class Service : DataService<Context>, IServiceProvider
{
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceActionAccessRule("*", ServiceActionRights.Invoke);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
}
public object GetService(Type serviceType)
{
return typeof(IDataServiceActionProvider) == serviceType ? new ActionProvider() : null;
}
}
public class ActionProvider : IDataServiceActionProvider, IDataServiceActionResolver
{
private static List<ServiceAction> actions;
static ActionProvider()
{
ServiceAction movieRateAction = new ServiceAction(
"Action1", // name of the action
ResourceType.GetPrimitiveResourceType(typeof(string)), // no return type i.e. void
null, // no return type means we don’t need to know the ResourceSet so use null.
OperationParameterBindingKind.Never,
new ServiceActionParameter[] {
new ServiceActionParameter("val", ResourceType.GetPrimitiveResourceType(typeof(string)))
}
);
movieRateAction.SetReadOnly();
actions = new List<ServiceAction>() { movieRateAction };
}
public IEnumerable<ServiceAction> GetServiceActions(DataServiceOperationContext operationContext)
{
return actions;
}
public bool TryResolveServiceAction(DataServiceOperationContext operationContext, string serviceActionName,
out ServiceAction serviceAction)
{
serviceAction = null;
return false;
}
public IEnumerable<ServiceAction> GetServiceActionsByBindingParameterType(DataServiceOperationContext operationContext,
ResourceType bindingParameterType)
{
return Enumerable.Empty<ServiceAction>();
}
public IDataServiceInvokable CreateInvokable(DataServiceOperationContext operationContext, ServiceAction serviceAction,
object[] parameterTokens)
{
return new DataServiceInvokable(parameterTokens);
}
public bool AdvertiseServiceAction(DataServiceOperationContext operationContext, ServiceAction serviceAction, object resourceInstance, bool resourceInstanceInFeed, ref ODataAction actionToSerialize)
{
actionToSerialize = null;
return false;
}
public bool TryResolveServiceAction(DataServiceOperationContext operationContext, ServiceActionResolverArgs resolverArgs, out ServiceAction serviceAction)
{
serviceAction = actions[0];
return true;
}
}
public class DataServiceInvokable : IDataServiceInvokable
{
private readonly object[] parameters;
private string result;
public DataServiceInvokable(object[] parameters)
{
this.parameters = parameters;
}
public object GetResult()
{
return result;
}
public void Invoke()
{
result = parameters[0] as string;
}
}
Then you could send a POST request to http://example.org/service.svc/Action1
Header:
Content-Type: Application/json
Request Body:
{"val":"MessageToPostHere..."}
If you are using .Net 4.0 or above, you could experiment with your web.config settings file, with this:
<system.web>
...
<httpRuntime maxUrlLength="500" />
....
</system.web>