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()
{
}
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)?
I have a service and ILogger that I need to use in my TypeConverter but the constructor with the parameters isn't being used or fired and my logger and service remain null when converting the type.
public class ModelServiceVersionConverter : TypeConverter
{
private static readonly Type StringType = typeof(string);
private static readonly Type VersionType = typeof(ModelServiceVersion);
private readonly ModelServiceVersions modelServiceVersions;
private readonly ILogger<ModelServiceVersionConverter> logger;
public ModelServiceVersionConverter()
{
}
public ModelServiceVersionConverter(ModelServiceVersions modelServiceVersions, ILogger<ModelServiceVersionConverter> logger)
{
this.modelServiceVersions = modelServiceVersions;
this.logger = logger;
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
sourceType == StringType ||
sourceType == VersionType ||
base.CanConvertFrom(context, sourceType);
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is null)
{
return this.modelServiceVersions.ServiceVersions.Last();
}
var modelServiceVersion = this.modelServiceVersions.ServiceVersions.FirstOrDefault(x => x == value.ToString());
if (modelServiceVersion is null)
{
var errorMessage = $"Version {value} unexpected. No implementation of {nameof(IModelService)} for this version.";
this.logger.LogError(errorMessage);
throw new ArgumentException(errorMessage);
}
return modelServiceVersion;
}
}
The ModelServiceVersion class is really simple and has the TypeConverter attribute on it.
[TypeConverter(typeof(ModelServiceVersionConverter))]
public class ModelServiceVersion : ValueObject, IComparer<ModelServiceVersion>
{
private readonly string version;
public static ModelServiceVersion New(string version)
{
return new ModelServiceVersion(version);
}
private ModelServiceVersion(string version) =>
this.version = version;
public static implicit operator string(ModelServiceVersion modelServiceVersion) => modelServiceVersion.version;
public static implicit operator ModelServiceVersion(StringValues stringValues) => New(stringValues[0]);
protected override IEnumerable<object> GetEqualityComponents()
{
yield return this.version;
}
public int Compare(ModelServiceVersion x, ModelServiceVersion y)
{
// TODO these rules can and will likely change
var versionNumberX = FirstOrDefaultDigit(x?.version);
var versionNumberY = FirstOrDefaultDigit(x?.version);
if (y is null || versionNumberY is null)
{
return 1;
}
if (x is null || versionNumberX is null)
{
return -1;
}
if (versionNumberX == versionNumberY)
{
return 0;
}
return versionNumberX > versionNumberY ? 1 : -1;
static int? FirstOrDefaultDigit(string versionString) =>
versionString?.ToCharArray().FirstOrDefault(IsDigit);
}
}
The services are all registered with Microsoft.Extensions.DependencyInjection.IServiceCollection.
I have tried just registering as scoped but it's not firing the type converter constructor to inject the dependencies in.
services.AddScoped<ModelServiceVersionConverter>();
When being used in the action on the controller
public async Task<IActionResult> GetPrediction([FromRoute] int decisionModelId, [FromQuery] ModelServiceVersion version, [FromBody] GetPredictionModelRequest.Request request)
{
var result = await this.predictionModelQueries.GetPrediction(new GetPredictionModelRequest(decisionModelId, request));
return this.Ok(result);
}
Any ideas where I'm going wrong?
Any help greatly appreciated.
Let's assume you have created a custom Attribute and applied it to a method or a class. Will constructor DI work in this case?
No. This is not supported unless you use specific asp.net-core attributes like ServiceFilterAttribute.
So you need to redesign your classes or have static access to DI container IServiceProvider.
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 _);
}
}
I'm trying to grab the "status" and "all" key, value from the requested URL, and can't figure out how to build my class object.
The JSON API specification I'm referring to can be found here:
http://jsonapi.org/recommendations/#filtering
// requested url
/api/endpoint?filter[status]=all
// my attempt at model binding
public class FilterParams
{
public Dictionary<string, string> Filter { get; set; }
}
[HttpGet]
public string Get([FromUri] FilterParams filter)
{
// never gets populated...
var filterStatus = filter.Filter["status"];
}
If you're building json:api apps on .Net Core, I strongly recommend checking out this library: https://github.com/json-api-dotnet/JsonApiDotNetCore
It handles all of the heavy lifting for you and for this specific example, (you need to get the filter value) the solution looks like:
public FooController : JsonApiController<Foo> {
private readonly IQueryAccessor _queryAccessor;
public FooController(IQueryAccessor queryAccessor, /* ... */)
: base(/* ... */) {
_queryAccessor = queryAccessor;
}
[HttpGet]
public override async Task<IActionResult> GetAsync() {
var status = _queryAccessor.GetRequired<string>("status");
// ...
}
}
You could use IModelBinder for that:
Define a model binder:
public class FilterParamsModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(FilterParams)) return false;
Dictionary<string, string> result = new Dictionary<string, string>();
var parameters = actionContext.Request.RequestUri.Query.Substring(1);
if(parameters.Length == 0) return false;
var regex = new Regex(#"filter\[(?<key>[\w]+)\]=(?<value>[\w^,]+)");
parameters
.Split('&')
.ToList()
.ForEach(_ =>
{
var groups = regex.Match(_).Groups;
if(groups.Count == 0)
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Cannot convert value.");
result.Add(groups["key"].Value, groups["value"].Value);
});
bindingContext.Model = new FilterParams { Filter = result};
return bindingContext.ModelState.IsValid;
}
}
Use it:
[HttpGet]
public string Get([ModelBinderAttribute(typeof(FilterParamsModelBinder))] FilterParams filter)
{
//your code
}
If you could define a route like "/api/endpoint?filter=status,all" instead, than you could use a TypeConverter for that:
Define a converter:
public class FilterConverter : TypeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (!(value is string)) return base.ConvertFrom(context, culture, value);
var keyValue = ((string)value).Split(',');
return new FilterParams
{
Filter = new Dictionary<string, string> { [keyValue[0]] = keyValue[1] }
};
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
}
Use it:
[TypeConverter(typeof(FilterConverter))]
public class FilterParams
{
public Dictionary<string, string> Filter { get; set; }
}
[HttpGet]
public string Get(FilterParams filter)
{
var filterStatus = filter.Filter["status"];
}
Is it possible to implicitly convert route argument by controller method from it's string representation into instance of an object with the default Binder?
Let's say I have class BusinessObjectId that contains two properties and can be converted from/to string
public class BusinessObjectId
{
private static readonly IDictionary<bool, char> IdTypeMap = new Dictionary<bool, char> { [false] = 'c', [true] = 'd' };
private static readonly Regex StrIdPattern = new Regex("^(?<type>[cd]{1})(?<number>\\d+)$", RegexOptions.Compiled);
public long Id { get; set; }
public bool IsDraft { get; set; }
public BusinessObjectId() { }
public BusinessObjectId(long id, bool isDraft)
{
Id = id;
IsDraft = isDraft;
}
public BusinessObjectId(string strId)
{
if (string.IsNullOrEmpty(strId)) return;
var match = StrIdPattern.Match(strId);
if (!match.Success) throw new ArgumentException("Argument is not in correct format", nameof(strId));
Id = long.Parse(match.Groups["number"].Value);
IsDraft = match.Groups["type"].Value == "d";
}
public override string ToString()
{
return $"{IdTypeMap[IsDraft]}{Id}";
}
public static implicit operator string(BusinessObjectId busId)
{
return busId.ToString();
}
public static implicit operator BusinessObjectId(string strBussId)
{
return new BusinessObjectId(strBussId);
}
}
These actionlinks are translated into nice urls:
#Html.ActionLink("xxx", "Sample1", "HomeController", new { oSampleId = new BusinessObjectId(123, false) } ... url:"/sample1/c123"
#Html.ActionLink("xxx", "Sample1", "HomeController", new { oSampleId = new BusinessObjectId(123, true) } ... url:"/sample1/d123"
Then I'd like to use parameters in controller methods like this:
public class HomeController1 : Controller
{
[Route("sample1/{oSampleId:regex(^[cd]{1}\\d+$)}")]
public ActionResult Sample1(BusinessObjectId oSampleId)
{
// oSampleId is null
throw new NotImplementedException();
}
[Route("sample2/{sSampleId:regex(^[cd]{1}\\d+$)}")]
public ActionResult Sample2(string sSampleId)
{
BusinessObjectId oSampleId = sSampleId;
// oSampleId is initialized well by implicit conversion
throw new NotImplementedException();
}
}
Method Sample1 doesn't recognize incoming argument and the instance oSampleId is null. In method Sample2 implicit conversion from string represetation works well, but I'd prefer not to call it manually.
Well I've found the answer ... You should write custom TypeConverter that can convert BusinessObjectId class from string and Decorate it with attribute
[TypeConverter(typeof(BusinessObjectIdConverter))]
public class BusinessObjectId
{ ... }
public class BusinessObjectIdConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return new BusinessObjectId((string)value);
}
}
From now on you can use BusinessObjectId as parameter in controller methods and it will be initialized like a charm :-)
public class HomeController1 : Controller
{
[Route("sample1/{oSampleId:regex(^[cd]{1}\\d+$)}")]
public ActionResult Sample1(BusinessObjectId oSampleId)
{
// TODO: oSampleId is successfully parsed from it's string representations
}
}