Is there a way to forbid integers passed in as enums without resorting to a string property type?
I already do the following:
return builder.AddJsonOptions(options =>
{
var namingPolicy = JsonNamingPolicy.CamelCase;
// omitted...
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(namingPolicy, allowIntegerValues: false));
// omitted...
});
And I have even tried making my own JsonConverterFactory and JsonConverter<TEnum> but the deserialization from integer to enum still happens.
I only want users to be able to the call the API with the documented STRING enums, not integers.
If you want to restrict api to only accept String Enum, I think you could try to check action parameters with ActionFilter or middlewares.
Here is a simple demo for checking Enum from Query, you could add more checks like from Request Body.
ValidateActionParametersAttribute
public class ValidateActionParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (descriptor != null)
{
var parameters = descriptor.MethodInfo.GetParameters();
foreach (var parameter in parameters)
{
var argument = context.ActionArguments[parameter.Name];
var argumentType = argument.GetType();
if (argumentType.IsEnum)
{
var value = context.HttpContext.Request.Query[parameter.Name];
if (int.TryParse(value, out int argumentValue))
{
context.Result = new BadRequestObjectResult($"{(parameter.Name)} Shold be String");
}
}
}
}
base.OnActionExecuting(context);
}
}
Useage
[HttpGet(Name = "GetWeatherForecast")]
[ValidateActionParameters]
public IEnumerable<WeatherForecast> Get(Season season)
{
//var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter(allowIntegerValues: false) } };
//var r1 = JsonSerializer.Deserialize<Season>("\"42\"", options); // does not fail
//var r2 = JsonSerializer.Deserialize<Season>("42", options); // fails as expected
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
Test Result
As commented by Jeremy Lakeman, this is a known issue that is planned to be fixed in a future release.
See https://github.com/dotnet/runtime/issues/58247
I was able to work it out this morning with a custom model binder:
internal class StringOnlyEnumTypeModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.UnderlyingOrModelType.IsEnum)
{
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new StringOnlyEnumTypeModelBinder(suppressBindingUndefinedValueToEnumType: true, context.Metadata.UnderlyingOrModelType, loggerFactory);
}
return null;
}
private class StringOnlyEnumTypeModelBinder : EnumTypeModelBinder
{
public StringOnlyEnumTypeModelBinder(bool suppressBindingUndefinedValueToEnumType, Type modelType, ILoggerFactory loggerFactory)
: base(suppressBindingUndefinedValueToEnumType, modelType, loggerFactory)
{
}
protected override void CheckModel(ModelBindingContext bindingContext, ValueProviderResult valueProviderResult, object? model)
{
if (bindingContext is null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var paramName = bindingContext.ModelName;
var paramValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
if (int.TryParse(paramValue, out var _))
{
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, bindingContext.ModelMetadata.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor(paramValue, paramName));
}
else
{
base.CheckModel(bindingContext, valueProviderResult, model);
}
}
}
}
// options are MvcOptions
options.ModelBinderProviders.Remove(options.ModelBinderProviders.Single(x => x.GetType() == typeof(EnumTypeModelBinderProvider)));
options.ModelBinderProviders.Insert(0, new StringEnumTypeModelBinderProvider());
Related
In Asp.NET MVC I was using Filters according below: (It works fine)
public class TracerAttribute : ActionFilterAttribute
{
public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
var path = ConfigurationManager.AppSettings["QueueAddress"];
var queue = new MessageQueue(path);
queue.DefaultPropertiesToSend.Recoverable = true;
var assembly = System.Reflection.Assembly.GetExecutingAssembly();
var body = TraceMessageHelper.BuildBody(actionContext, assembly);
try
{
var label = ConfigurationManager.AppSettings["MessageLabel"] + body.Timestamp.ToString("yyyyMMddHHmmss");
var message = new Message
{
Formatter = new XmlMessageFormatter(new Type[] { typeof(TraceMessage) }),
Label = label,
Body = body
};
queue.Send(message);
}
catch (Exception e)
{
var logger = LogManager.GetLogger("LogInFile");
logger.Warn(e, LogMessageHelper.FormatRequest("TRACE SEND FAILED", actionContext.Request));
if (body != null)
{
var tracerlogger = LogManager.GetLogger("TracerInFile");
tracerlogger.Info(JsonConvert.SerializeObject(body));
}
}
queue.Close();
});
}
}
}
In Asp.NET CORE I'm using Filters according below: (It doesn't Work)
public class TracerAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
{
var path = "FormatName:Direct=TCP:013cdnt2305\\private$\\TracerQueue";
var queue = new MessageQueue(path);
queue.DefaultPropertiesToSend.Recoverable = true;
var assembly = System.Reflection.Assembly.GetExecutingAssembly();
var body = TraceMessageHelper.BuildBody(actionContext, assembly);
try
{
var label = "APR_USER_20191018132324";
var message = new Message
{
Formatter = new XmlMessageFormatter(new Type[] { typeof(TraceMessage) }),
Label = label,
Body = body
};
queue.Send(message);
}
catch (Exception e)
{
HttpRequestMessageFeature hreqmf = new HttpRequestMessageFeature(actionContext.HttpContext);
var logger = LogManager.GetLogger("LogInFile");
logger.Warn(e, LogMessageHelper.FormatRequest("TRACE SEND FAILED", hreqmf.HttpRequestMessage));
if (body != null)
{
var tracerlogger = LogManager.GetLogger("TracerInFile");
tracerlogger.Info(JsonConvert.SerializeObject(body));
}
}
queue.Close();
}
}
}
}
In ASP.net Core I need to do change using only one parameter
In Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(o => o.AddPolicy("MyPolicy", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
services.AddControllersWithViews();
services.AddRazorPages();
services.AddMvc(config =>
{
config.Filters.Add(new TracerAttribute());
});
//use our filter as a service type on the Action or Controller level
//services.AddScoped<TracerAttribute>();
}
In ASP.NET MVC (Works Fine)
In ASP.NET CORE(Doesn't work)
Angular return for both:
Anyone help me how to fix it?
If the angular app post json data(Content-Type: application/json) to .net core server side , you can get the values via [FromBody] :
[TracerAttribute]
[HttpPost]
public IActionResult MyAction([FromBody]Mymodel mymodel)
{
}
When action filter is calling , the body stream already has been read and [FromBody] model has been populated. Then you can get the data via :
public class TracerAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
var modelValues = actionContext.ActionArguments["mymodel"] as Mymodel ;
}
}
For a model with a decimal property, if the value from client contains commas as thousand separator, the model binding will fail.
How can we solve this? Any solution (globally, controller/action local or model/property local) is good.
I have a workaround, which is to have a string property that reads and writes to the decimal one. But I'm looking for a cleaner solution.
If your application needs to support only a specific format (or culture), you could specify it in your Configure method as follows:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var cultureInfo = new CultureInfo("en-US");
cultureInfo.NumberFormat.NumberGroupSeparator = ",";
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
[...]
}
If you want to support several cultures and to automatically select the right one for each request, you can use the localization middleware instead, e.g.:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
[...]
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("es"),
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
// Formatting numbers, dates, etc.
SupportedCultures = supportedCultures,
// Localized UI strings.
SupportedUICultures = supportedCultures
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
More info here: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.2
Edit - Decimal binder
If everything above fails, you could also roll your own model binder, e.g.:
public class CustomBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(decimal))
{
return new DecimalModelBinder();
}
return null;
}
}
public class DecimalModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == null)
{
return Task.CompletedTask;
}
var value = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
// Remove unnecessary commas and spaces
value = value.Replace(",", string.Empty).Trim();
decimal myValue = 0;
if (!decimal.TryParse(value, out myValue))
{
// Error
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
"Could not parse MyValue.");
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(myValue);
return Task.CompletedTask;
}
}
Don't forget to register the custom binder in your ConfigureServices method:
services.AddMvc((options) =>
{
options.ModelBinderProviders.Insert(0, new CustomBinderProvider());
});
Now every time you use a decimal type in any of your models, it will be parsed by your custom binder.
In case if localization and custom model binder didn't work for you, as it didn't for me. You can extend serialization settings with custom JsonConverter which will serialize and deseralize all decimal values.
private class CultureInvariantDecimalConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
//your custom parsing goes here
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(decimal) || objectType == typeof(decimal?));
}
}
And apply it with with this extension method
public static IMvcBuilder AddInvariantDecimalSerializer(this IMvcBuilder builder)
{
return builder.AddJsonOptions(options =>
options.SerializerSettings.Converters.Add(new CultureInvariantDecimalConverter()));
}
At the heart of this problem is the fact that decimal.TryParse() relies entirely on the local computer settings. No matter how many Decimal custom binders you code...
Example from the Custom Decimal Binder execution:
value = "11.2"
?decimal.TryParse(value, out decimalValue)
false
value = "11,2"
?decimal.TryParse(value, out decimalValue)
true
So even when the other commas is removed which seems its done by the native DecimalBinder then it still fails when it parses it to decimal in the tryparse method...
Use the tryparse method in this way:
decimal.TryParse(value,System.Globalization.NumberStyles.Any, CultureInfo.InvariantCulture ,out decimalValue)
you must put CultureInfo.InvariantCulture when you want to convert double:
public class CustomBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(double))
{
return new DoubleModelBinder();
}
return null;
}
}
public class DoubleModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == null)
{
return Task.CompletedTask;
}
var value = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
// Remove unnecessary commas and spaces
value = value.Replace(",", string.Empty).Trim();
double myValue = 0;
try
{
myValue = Convert.ToDouble(value, CultureInfo.InvariantCulture);
bindingContext.Result = ModelBindingResult.Success(myValue);
return Task.CompletedTask;
}
catch (Exception m)
{
return Task.CompletedTask;
}
}
}
and in startup.cs
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new CustomBinderProvider());
});
I want to replace all ۱ chars with 1 in my web api .Net Core requests?
e.g:
Number ۱ should convert to Number 1
In MVC 5 I used HttpModule, in .net core I used Middleware as follows:
namespace VistaBest.Api
{
public class PersianCharsMiddleware
{
private readonly RequestDelegate _next;
public PersianCharsMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext httpContext)
{
var collection = httpContext.Request.Form;
foreach (var controlKey in collection.Keys.Where(controlKey => !controlKey.StartsWith("__")))
{
collection[controlKey] = collection[controlKey].ToString().Replace("۱", "1");
}
return _next(httpContext);
}
}
public static class PersianCharsMiddlewareExtensions
{
public static IApplicationBuilder UsePersianCharsMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<PersianCharsMiddleware>();
}
}
}
But collection[controlKey] is Readonly and I can't assign value to it?
Actually, I have to edit all string fields of form, How should I do it?
Can I do it with custom model binder?
I could write it with custom model binder:
public class PersianCharsModelBinder : IModelBinder
{
private readonly IModelBinder _simpleTypeModelBinder;
public PersianCharsModelBinder(IModelBinder simpleTypeModelBinder)
{
_simpleTypeModelBinder = simpleTypeModelBinder;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None) return _simpleTypeModelBinder.BindModelAsync(bindingContext);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
var valueAsString = valueProviderResult.FirstValue;
if (string.IsNullOrWhiteSpace(valueAsString)) return _simpleTypeModelBinder.BindModelAsync(bindingContext);
var model = valueAsString.ToEnglishNumber().RemoveArabicChars();
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
public class PersianCharsBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsComplexType) return null;
var simpleTypeModelBinder = new SimpleTypeModelBinder(context.Metadata.ModelType);
if (context.Metadata.ModelType == typeof(string)) return new PersianCharsModelBinder(simpleTypeModelBinder);
return simpleTypeModelBinder;
}
}
Startup.cs
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new PersianCharsBinderProvider());
});
I'm using Owin and UseWsFederationAuthentication() in AspNetCore MVC 1.0.0. app. Authentication works perfectly but i cant SignOut the user.
This code
public class AccountController : Controller
{
public async Task<IActionResult> SignOut()
{
await this.HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationType);
return RedirectToAction("Index", "Test");
}
}
Throws:
InvalidOperationException: No authentication handler is configured to handle the scheme: Cookies
The HttpContext.Authentication is set to Microsoft.AspNetCore.Http.Authentication.Internal.DefaultAuthenticationManager
Startup.cs:Configure
app.UseOwinAppBuilder(builder =>
{
var authConfig = new WsFederationAuthenticationSettings
{
MetadataAddress = this.Configuration.GetValue<string>("Eyc.Sdk:Authentication:WsFederation:MetadataAddress"),
Realm = this.Configuration.GetValue<string>("Eyc.Sdk:Authentication:WsFederation:Realm"),
UseCookieSlidingExpiration = this.Configuration.GetValue<bool>("Eyc.Sdk:Authentication:WsFederation:UseCookieSlidingExpiration"),
CookieExpireTimeSpan = this.Configuration.GetValue<string>("Eyc.Sdk:Authentication:WsFederation:CookieExpireTimeSpan")
};
builder.UseEycAuthentication(authConfig, app.ApplicationServices);
});
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder UseOwinAppBuilder(this IApplicationBuilder app, Action<global::Owin.IAppBuilder> configuration)
{
return app.UseOwin(setup => setup(next =>
{
var builder = new AppBuilder();
var lifetime = (IApplicationLifetime)app.ApplicationServices.GetService(typeof(IApplicationLifetime));
var properties = new AppProperties(builder.Properties);
properties.AppName = app.ApplicationServices.GetApplicationUniqueIdentifier();
properties.OnAppDisposing = lifetime.ApplicationStopping;
properties.DefaultApp = next;
configuration(builder);
return builder.Build<Func<IDictionary<string, object>, Task>>();
}));
}
}
public static class AppBuilderExtensions
{
public static IAppBuilder UseEycAuthentication(
this IAppBuilder app,
WsFederationAuthenticationSettings authenticationSettings,
IServiceProvider serviceProvider,
bool authenticateEveryRequest = true)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (authenticationSettings == null)
{
throw new ArgumentNullException(nameof(authenticationSettings));
}
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
return app.ConfigureWsFederationAuthentication(serviceProvider, authenticationSettings, authenticateEveryRequest);
}
private static IAppBuilder ConfigureWsFederationAuthentication(
this IAppBuilder app,
IServiceProvider serviceProvider,
WsFederationAuthenticationSettings authenticationSettings,
bool authenticateEveryRequest = true)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
SlidingExpiration = authenticationSettings.UseCookieSlidingExpiration,
ExpireTimeSpan = authenticationSettings.GetCookieExpireTimeSpan()
});
var wsFederationAuthenticationOptions = GetWsFederationAuthenticationOptions(authenticationSettings);
app.UseWsFederationAuthentication(wsFederationAuthenticationOptions);
var eycAuthenticationManager = (IEycAuthenticationManager)serviceProvider.GetService(typeof(IEycAuthenticationManager));
app.Use<EycAuthenticationOwinMiddleware>(eycAuthenticationManager);
// http://stackoverflow.com/questions/23524318/require-authentication-for-all-requests-to-an-owin-application
if (authenticateEveryRequest)
{
app.Use(async (owinContext, next) =>
{
var user = owinContext.Authentication.User;
if (!(user?.Identity?.IsAuthenticated ?? false))
{
owinContext.Authentication.Challenge();
return;
}
await next();
});
}
return app;
}
private static WsFederationAuthenticationOptions GetWsFederationAuthenticationOptions(WsFederationAuthenticationSettings settings)
{
var wsFederationAuthenticationNotifications = GetWsFederationAuthenticationNotifications(settings);
var wsFederationAuthenticationOptions = new WsFederationAuthenticationOptions
{
Wtrealm = settings.Realm,
MetadataAddress = settings.MetadataAddress,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = settings.Realms
},
Notifications = wsFederationAuthenticationNotifications
};
if (settings.UseCookieSlidingExpiration)
{
// this needs to be false for sliding expiration to work
wsFederationAuthenticationOptions.UseTokenLifetime = false;
}
return wsFederationAuthenticationOptions;
}
private static WsFederationAuthenticationNotifications GetWsFederationAuthenticationNotifications(WsFederationAuthenticationSettings settings)
{
var wsFederationAuthenticationNotifications = new WsFederationAuthenticationNotifications();
wsFederationAuthenticationNotifications.AuthenticationFailed = settings.AuthenticationFailed ?? wsFederationAuthenticationNotifications.AuthenticationFailed;
wsFederationAuthenticationNotifications.MessageReceived = settings.MessageReceived ?? wsFederationAuthenticationNotifications.MessageReceived;
wsFederationAuthenticationNotifications.RedirectToIdentityProvider = settings.RedirectToIdentityProvider ?? wsFederationAuthenticationNotifications.RedirectToIdentityProvider;
wsFederationAuthenticationNotifications.SecurityTokenReceived = settings.SecurityTokenReceived ?? wsFederationAuthenticationNotifications.SecurityTokenReceived;
wsFederationAuthenticationNotifications.SecurityTokenValidated = settings.SecurityTokenValidated ?? wsFederationAuthenticationNotifications.SecurityTokenValidated;
return wsFederationAuthenticationNotifications;
}
}
public class EycAuthenticationOwinMiddleware : OwinMiddleware
{
private readonly IEycAuthenticationManager _eycAuthenticationManager;
#region ctors
public EycAuthenticationOwinMiddleware(OwinMiddleware next, IEycAuthenticationManager eycAuthenticationManager)
: base(next)
{
if (eycAuthenticationManager == null)
{
throw new ArgumentNullException(nameof(eycAuthenticationManager));
}
this._eycAuthenticationManager = eycAuthenticationManager;
}
#endregion
public override Task Invoke(IOwinContext context)
{
if (context.Authentication.User != null)
{
context.Authentication.User =
this._eycAuthenticationManager.Authenticate(
context.Request.Uri.AbsoluteUri,
context.Authentication.User);
}
return this.Next.Invoke(context);
}
}
public class EycAuthenticationManager : ClaimsAuthenticationManager, IEycAuthenticationManager
{
private readonly IClaimsTransformer _claimsTransformer;
#region ctors
public EycAuthenticationManager(IClaimsTransformer claimsTransformer)
{
this._claimsTransformer = claimsTransformer;
}
#endregion
public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
if (incomingPrincipal != null && !incomingPrincipal.Identity.IsAuthenticated)
{
return base.Authenticate(resourceName, incomingPrincipal);
}
return this._claimsTransformer.TransformIdentity(incomingPrincipal);
}
}
public class ClaimsTransformer : IClaimsTransformer
{
public ClaimsPrincipal TransformIdentity(IPrincipal principal)
{
if (!(principal is ClaimsPrincipal))
{
throw new Exception("The provided IPrincipal object is not of type ClaimsPrincipal.");
}
var user = (ClaimsPrincipal)principal;
var claims = user.Claims.ToList();
if (claims.All(x => x.Type != ClaimTypes.Email))
{
var upnClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.Upn);
if (upnClaim != null)
{
claims.Add(new Claim(ClaimTypes.Email, upnClaim.Value));
}
}
return new ClaimsPrincipal(new ClaimsIdentity(claims, principal.Identity.AuthenticationType));
}
}
Here is the way how to SignOut using Owin context, app is "IAppBuilder":
app.Map("/signout", map =>
{
map.Run(ctx =>
{
ctx.Authentication.SignOut();
return Task.CompletedTask;
});
});
More details here: https://leastprivilege.com/2014/02/21/test-driving-the-ws-federation-authentication-middleware-for-katana/
UseWsFederationAuthentication will not work in AspNetCore MVC 1.0.0. UseWsFederationAuthentication used non-standard OWIN keys that are not supported by UseOwin, so it cannot communicate with MVC.
I'm using ASP.NET Web API 2 with attribute routing but i can't seem to get the versioning using media types application/vnd.company[.version].param[+json] to work.
I get the following error:
The given key was not present in the dictionary.
which originates from testing the key _actionParameterNames[descriptor] in FindActionMatchRequiredRouteAndQueryParameters() method.
foreach (var candidate in candidatesFound)
{
HttpActionDescriptor descriptor = candidate.ActionDescriptor;
if (IsSubset(_actionParameterNames[descriptor], candidate.CombinedParameterNames))
{
matches.Add(candidate);
}
}
Source: ApiControllerActionSelector.cs
After further debugging I've realized that if you have two controllers
[RoutePrefix("api/people")]
public class PeopleController : BaseApiController
{
[Route("")]
public HttpResponseMessage GetPeople()
{
}
[Route("identifier/{id}")]
public HttpResponseMessage GetPersonById()
{
}
}
[RoutePrefix("api/people")]
public class PeopleV2Controller : BaseApiController
{
[Route("")]
public HttpResponseMessage GetPeople()
{
}
[Route("identifier/{id}")]
public HttpResponseMessage GetPersonById()
{
}
}
you can't use your custom ApiVersioningSelector : DefaultHttpControllerSelector because it will test the keys,as stated above, from all controllers having the same [RoutePrefix("api/people")] and obviously an exception will be thrown.
Just to be sure the right controller was selected
I don't know if this is a bug, but using route [RoutePrefix("api/v1/people")] to version API makes me sad.
NOTE: This works great without attribute routing.
UPDATE
public class ApiVersioningSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _HttpConfiguration;
public ApiVersioningSelector(HttpConfiguration httpConfiguration)
: base(httpConfiguration)
{
_HttpConfiguration = httpConfiguration;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
var attributedRoutesData = request.GetRouteData().GetSubRoutes();
var subRouteData = attributedRoutesData.LastOrDefault(); //LastOrDefault() will get PeopleController, FirstOrDefault will get People{version}Controller which we don't want
var actions = (ReflectedHttpActionDescriptor[])subRouteData.Route.DataTokens["actions"];
var controllerName = actions[0].ControllerDescriptor.ControllerName;
//For controller name without attribute routing
//var controllerName = (string)routeData.Values["controller"];
HttpControllerDescriptor oldControllerDescriptor;
if (controllers.TryGetValue(controllerName, out oldControllerDescriptor))
{
var apiVersion = GetVersionFromMediaType(request);
var newControllerName = String.Concat(controllerName, "V", apiVersion);
HttpControllerDescriptor newControllerDescriptor;
if (controllers.TryGetValue(newControllerName, out newControllerDescriptor))
{
return newControllerDescriptor;
}
return oldControllerDescriptor;
}
return null;
}
private string GetVersionFromMediaType(HttpRequestMessage request)
{
var acceptHeader = request.Headers.Accept;
var regularExpression = new Regex(#"application\/vnd\.mycompany\.([a-z]+)\.v([0-9]+)\+json",
RegexOptions.IgnoreCase);
foreach (var mime in acceptHeader)
{
var match = regularExpression.Match(mime.MediaType);
if (match != null)
{
return match.Groups[2].Value;
}
}
return "1";
}
}
Thanks for the sharing your code. I have modified your version controller selector like below and tried some scenarios and it seems to work well. Can you try updating your controller selector like below and see if it works?
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
HttpControllerDescriptor controllerDescriptor = null;
// get list of all controllers provided by the default selector
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//check if this route is actually an attribute route
IEnumerable<IHttpRouteData> attributeSubRoutes = routeData.GetSubRoutes();
var apiVersion = GetVersionFromMediaType(request);
if (attributeSubRoutes == null)
{
string controllerName = GetRouteVariable<string>(routeData, "controller");
if (controllerName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
string newControllerName = String.Concat(controllerName, "V", apiVersion);
if (controllers.TryGetValue(newControllerName, out controllerDescriptor))
{
return controllerDescriptor;
}
else
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
else
{
// we want to find all controller descriptors whose controller type names end with
// the following suffix(ex: CustomersV1)
string newControllerNameSuffix = String.Concat("V", apiVersion);
IEnumerable<IHttpRouteData> filteredSubRoutes = attributeSubRoutes.Where(attrRouteData =>
{
HttpControllerDescriptor currentDescriptor = GetControllerDescriptor(attrRouteData);
bool match = currentDescriptor.ControllerName.EndsWith(newControllerNameSuffix);
if (match && (controllerDescriptor == null))
{
controllerDescriptor = currentDescriptor;
}
return match;
});
routeData.Values["MS_SubRoutes"] = filteredSubRoutes.ToArray();
}
return controllerDescriptor;
}
private HttpControllerDescriptor GetControllerDescriptor(IHttpRouteData routeData)
{
return ((HttpActionDescriptor[])routeData.Route.DataTokens["actions"]).First().ControllerDescriptor;
}
// Get a value from the route data, if present.
private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
{
object result = null;
if (routeData.Values.TryGetValue(name, out result))
{
return (T)result;
}
return default(T);
}