I'm currently building a project that deals with Discord users. For those who don't know, each Discord user has a unique Id, stored as ulong.
I am trying to route this Id to my page (currently looks like this)
#page "{UserId:ulong?}"
#model BotFrameworkInterface.Pages.Bot.UserDetailsModel
#{
ViewData["Title"] = "UserDetails";
}
<h1>UserDetails</h1>
#foreach(var u in Model.SelectedUsers)
{
<Partial name="_Summary" model="u"/>
}
but it breaks and gives me the following exception:
InvalidOperationException: The constraint reference 'ulong' could not
be resolved to a type. Register the constraint type with
'Microsoft.AspNetCore.Routing.RouteOptions.ConstraintMap'.
is there any way I can pass in a ulong into my ASP.NET (core) page? (the partial view works fine, I checked it)
ulong data type is not included in the applicable routing constraints , reference: Route constraint reference
If you want to check the id by Route constraint, you could implement your own constraint by implementing IRouteConstraint.
1.UlongRouteConstraint
public class UlongRouteConstraint: IRouteConstraint
{
public static string UlongRouteConstraintName = "UlongConstraint";
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
object dateValue;
if (values.TryGetValue("id", out dateValue))
{
ulong date;
if (UInt64.TryParse(dateValue.ToString(), out date))
{
return true;
}
}
return false;
}
}
2.Register UlongRouteConstraint
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add(UlongRouteConstraint.UlongRouteConstraintName, typeof(UlongRouteConstraint));
});
3.Use Case
#page "{id:UlongConstraint?}"
#model RazorPages2_2Test.Pages.Users.DetailsModel
without "id" param hardcode :)
public class UlongRouteConstraint : IRouteConstraint
{
public static string UlongRouteConstraintName = "ulong";
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values,
RouteDirection routeDirection)
{
if (routeKey == null)
throw new ArgumentNullException(nameof(routeKey));
if (values == null)
throw new ArgumentNullException(nameof(values));
if (!values.TryGetValue(routeKey, out var routeValue) || routeValue == null) return false;
if (routeValue is ulong)
return true;
var valueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
return ulong.TryParse(valueString, out var _);
}
}
Related
Preamble
My problem is already discussed in a similar way in several other posts on stackoverflow. But I just can get it done to put all together. Maybe because it isn't really possible to do so in the way I try to.
Problem
I have a route like https://<server>/api/segment1/segment2/.... Segments in that route can be almost "infinite". I implemented a SegmentTypeConverter which should split the segments into an array of strings:
public class SegmentedPath
{
public string[] Segments { get; set; }
public SegmentedPath()
{
}
public SegmentedPath(string[] segments)
{
Segments = segments;
}
}
public class SegmentedPathConverter : TypeConverter
{
// some special segments are ignored
private readonly string[] SpecialSegments = new[] { "root", "special" };
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string pathValue)
{
pathValue = string.IsNullOrEmpty(pathValue) ? string.Empty : pathValue;
var segments = pathValue
.Split('/')
.Where(s => SpecialSegments.All(special => special.Equals(s, StringComparison.InvariantCultureIgnoreCase)))
.ToArray();
return new SegmentedPath(segments);
}
return base.ConvertFrom(context, culture, value);
}
}
I have added a API Controller with Action and configured a route to it:
public class MyApiController : ApiController
{
[HttpGet]
public HttpResponseMessage Get([FromUri] SegmentedPath path)
{
// ...
}
}
// Route configuration:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
"MyRoute",
"api/{*url}",
new { controller = typeof(MyApiController).ControllerName() });
}
But when I try to call the API the TypeConverter does not get called and so path is null.
Questions
What am I doing wrong here? Is it the routing? Is it wrong how I use the TypeConverter?
Are there other solutions that may be better (I know, this question is opinion based)?
How do I pass an environment variable to an attribute?
example:
[HttpPost(**VARIABLEX**)]
public IActionResult PostCustomer()
{
}
If you want this to be specifically in attribute you can achieve this by a bit hackish custom route constraint:
/// <summary>
/// Matches value from "Test" environment variable
/// </summary>
class EnvRouteConstraint : IRouteConstraint
{
public bool Match(
HttpContext httpContext,
IRouter route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}
var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (routeValueString is null)
{
return false;
}
return Environment.GetEnvironmentVariable("Test")
?.Equals(routeValueString, StringComparison.OrdinalIgnoreCase) ?? false;
}
}
Then in services registration:
builder.Services.AddRouting(options => options.ConstraintMap.Add("fromVarTest", typeof(EnvRouteConstraint)));
And on controller:
[HttpPost("{_:fromVarTest}")]
public IEnumerable<WeatherForecast> SomeMethod()
{
}
I have a custom ModelMetadataProvider, which sets the displayname based on adding spaces after capital letters in the property name:
public class ModelMetadataProviderPlus : ModelMetadataProvider
{
private readonly ModelMetadataProvider _BaseProvider;
public ModelMetadataProviderPlus(ModelMetadataProvider baseProvider)
{
_BaseProvider = baseProvider;
}
private void SetMissingDisplayName(ModelMetadata modelMetaData)
{
if (modelMetaData.DisplayName == null && modelMetaData.PropertyName != null)
{
modelMetaData.DisplayName = modelMetaData.PropertyName.AddSpacesAfterCaps();
}
}
public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
{
var Result = _BaseProvider.GetMetadataForProperties(container, containerType);
return Result.Select(r =>
{
SetMissingDisplayName(r);
return r;
});
}
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
{
var Result = _BaseProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
SetMissingDisplayName(Result);
return Result;
}
public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
{
var Result = _BaseProvider.GetMetadataForType(modelAccessor, modelType);
SetMissingDisplayName(Result);
return Result;
}
}
I configure the MVC framework to use it using
ModelMetadataProviders.Current = new ModelMetadataProviderPlus(ModelMetadataProviders.Current);
This all works fine, however the validation error messages don't seem to use this provider, as the error message for the [Required] attribute says (for example): The DefectCount field is required..
I use nInject, so i tried doing:
kernel.Bind<ModelMetadataProvider>().ToConstant(new ModelMetadataProviderPlus(ModelMetadataProviders.Current));
but although the labels etc. appeared correctly the validation messages still didn't.
I've searched for a while now and although I've found a couple of places where the same issue is noted, I can't find anything with a solution.
I have created strongly typed ID classes in my project as there is currently confusion with interchangeable string ID's which is causing bugs which are easy to miss.
I have changed all the string id parameters in my action methods to the new strongly typed to realise that the MVC model binder can not now bind the strings to the new type (despite implicit string conversion operators existing for this type).
e.g.
public ActionResult Index(JobId jobId)
{
//...
}
I have read around about creating custom model binders, but all the tutorials are about binding a POCO class when we know the names of the query parameters / form values.
I just want to be able to tell the framework 'if parameter is strongly typed id type, instantiate using this constructor', so it will always work no matter what the name of the parameter is.
Can anyone point me in the right direction?
EDIT
This is the base class that the strongly typed ID's inherit from:
public class StronglyTypedId
{
private readonly string _id;
public StronglyTypedId(string id)
{
_id = id;
}
public static bool operator ==(StronglyTypedId a, StronglyTypedId b)
{
if (ReferenceEquals(a, b))
{
return true;
}
if (((object)a != null) && ((object)b == null) || ((object)a == null))
{
return false;
}
return a._id == b._id;
}
public static bool operator !=(StronglyTypedId a, StronglyTypedId b)
{
return !(a == b);
}
public override string ToString()
{
return _id;
}
public override bool Equals(object obj)
{
if (!(obj is StronglyTypedId))
{
return false;
}
return ((StronglyTypedId)obj)._id == _id;
}
public Guid ToGuid()
{
return Guid.Parse(_id);
}
public bool HasValue()
{
return !string.IsNullOrEmpty(_id);
}
}
I figured out a way to do this just now using a custom model binder. This way will work no matter what the name of the parameter that needs to be bound:
public class JobIdModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(JobId))
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
string id = value.AttemptedValue;
return new JobId(id);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
So it's pretty much the same as if you are implementing a custom binder for a specific model type, except this is the bit of magic var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName) which means it will work regardless of naming of parameters.
Then you simply register the binder in your Global.cs like so:
ModelBinders.Binders.Add(typeof(JobId), new JobIdModelBinder());
I have the following problem with the Asp.net Web Api.
I try to use the following object as parameter of my action
[DataContract]
public class MyObject
{
[DataMember]
public object MyIdProperty {get;set;}
}
the property MyIdProperty can contains either a Guid or an Int32
In MVC I did an ModelBinder and it works like a charm so I did one for the WebApi like this
public class HttpObjectIdPropertyModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != ObjectType
|| (bindingContext.ModelName.TrimHasValue()
&& !bindingContext.ModelName.EndsWith("Id", StringComparison.OrdinalIgnoreCase)))
{
return false;
}
ValueProviderResult result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (result == null || result.RawValue == null || result.RawValue.GetType() == ObjectType)
{
bindingContext.Model = null;
return true;
}
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, result);
string stringValue = result.RawValue as string;
if (stringValue == null)
{
string[] stringValues = result.RawValue as string[];
if (stringValues != null && stringValues.Length == 1)
{
stringValue = stringValues[0];
}
if (stringValue == null)
{
return false;
}
}
Guid guid;
int integer;
if (Guid.TryParse(stringValue, out guid))
{
bindingContext.Model = guid;
}
else if (int.TryParse(stringValue, out integer))
{
bindingContext.Model = integer;
}
else
{
return false;
}
return true;
}
private static readonly Type ObjectType = typeof(object);
private static HttpParameterBinding EvaluateRule(HttpObjectIdPropertyModelBinder binder, HttpParameterDescriptor parameter)
{
if (parameter.ParameterType == ObjectType
&& parameter.ParameterName.EndsWith("Id", StringComparison.OrdinalIgnoreCase))
{
return parameter.BindWithModelBinding(binder);
}
return null;
}
public static void Register()
{
var binder = new HttpObjectIdPropertyModelBinder();
GlobalConfiguration.Configuration.Services.Insert(typeof(ModelBinderProvider), 0, new SimpleModelBinderProvider(typeof(object), binder));
GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, param => EvaluateRule(binder, param));
}
}
It's the first time I'm doing a model binder for the WebApi so I'm not even sure if I'm doing it well and if it's a good way to fix this problem.
Anyway with this model binder if I have an action like this
public IEnumerable<MyObject> Get(object id)
{
// code here...
}
The parameter id is deserialized properly using the Json formatter or the Xml formatter and the model binder
But if I use the following action
public void Post(MyObject myObject)
{
// code here...
}
The parameter myObject is deserialized perfectly when I use the Xml formatter but when I use the Json formatter the property MyIdProperty contains a string instead of a Guid or Int32.
And in both case my model binder is NOT used at all. It's like it stop the evaluation of the model at the action paramaters compare to MVC that use the model binders for each property with a complex type.
Note: I would like to not use the true type or use a internal or protected property with the true type because I have this kind of property in a lot of different of object and the code will become really hard to maintain if I have to duplicate them each time