C# Attribute AppSettings - c#

I am writing an app using .NET Web API (4.6 Framework)
I have an attribute that I use: [ApiExplorerSettings(IgnoreApi = true)] to hide certain controllers from my Swagger.
This attribute is a part of : System.Web.Http.Description
Basically I want to create an AppSetting in my web.config file so when I publish to Development, the controllers show up (IgnoreApi = false) and when I publish to Production, the controllers are hidden (IgnoreApi = true).
I've tried accessing ConfigurationManager.AppSettings directly in the attribute, but that seems to not work as expected.
Perhaps I need to find a way to override that attribute so that on the getter/setter of IgnoreApi, it can pull the correct value from my web.config?

Extending the "ApiExplorerSettingsAttribute" class seems straightforward but its sealed. So ended up with the following workaround;
A custom attribute which inherits from the base calss "Attribute".
IncludeInApiExplorerAttribute.cs class
public class IncludeInApiExplorerAttribute : Attribute
{
private readonly bool value;
public IncludeInApiExplorerAttribute(string IsInAPI=null)
{
if (!string.IsNullOrEmpty(IsInAPI))
{
value = Convert.ToBoolean(ConfigurationManager.AppSettings[IsInAPI]); //Reads the app config value
}
else
{
value = true;
}
}
public bool Value { get { return value; } }
}
Then we can implement a custom ApiExplorer as below.
OptApiExplorer.cs Class
public class OptApiExplorer : ApiExplorer
{
public OptApiExplorer(HttpConfiguration configuration)
: base(configuration)
{
}
//Overrides the method from the base class
public override bool ShouldExploreAction(string actionVariableValue, HttpActionDescriptor actionDescriptor, IHttpRoute route)
{
var includeAttribute = actionDescriptor.GetCustomAttributes<IncludeInApiExplorerAttribute>().FirstOrDefault(); //Get the given custom attribute from the action
if (includeAttribute != null)
{
return includeAttribute.Value && MatchRegexConstraint(route, "action", actionVariableValue); //If it is not null read the includeAttribute.Value which is set in app.config and return true or false based on the includeAttribute.Value and MatchRegexConstraint return value
}
var includeControlAttribute = actionDescriptor.ControllerDescriptor.GetCustomAttributes<IncludeInApiExplorerAttribute>().FirstOrDefault(); //If the action does not have any given type of custom attribute then chekc it in the controller level
if (includeControlAttribute != null)
{
return includeControlAttribute.Value && MatchRegexConstraint(route, "action", actionVariableValue);//Similar to action level
}
return true && MatchRegexConstraint(route, "action", actionVariableValue);
}
//This method is as it is in the base class
private static bool MatchRegexConstraint(IHttpRoute route, string parameterName, string parameterValue)
{
IDictionary<string, object> constraints = route.Constraints;
if (constraints != null)
{
object constraint;
if (constraints.TryGetValue(parameterName, out constraint))
{
string constraintsRule = constraint as string;
if (constraintsRule != null)
{
string constraintsRegEx = "^(" + constraintsRule + ")$";
return parameterValue != null && Regex.IsMatch(parameterValue, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
}
}
}
return true;
}
}
Web config settings
This is the value read by our custom attribute. Add this to web.config file
<appSettings>
<add key="IsInAPI" value="false"/>
</appSettings>
In WebAPI.config.cs file add the following
There we have replaced the IApiExplorer with the custom class.
config.Services.Replace(typeof(IApiExplorer), new OptApiExplorer(config));
Then in your Controller or in the Action you can add the custom
attribute as below.
[IncludeInApiExplorer("IsInAPI")]
IsInApi is the web.config value which we can set to true or false. If it is not set then it will default set to true as we have implemented in IncludeInApiExplorerAttribute class.
Refer this post for more insight on this.

Related

Setting default value in example for enum based on attribute for Swagger definitions

We are building a WebAPI in C# and .NET core 6 and are using SwaggerUI for exposing end user documentation and a demo environment. In some API request models we are using enums for which we want to change the default value for the examples shown in SwaggerUI.
The issue is that it does not seem possible to do that using:
Using DefaultValue attribute from System.ComponentModel on the property.
The example documentation tag on the property.
Edit: The following (example) code should set the Example and the Default, for either an attribute specified on the type or on a property. Which works but it does not show up in the generated API specs:
public class DefaultPropertyValueAttribute : Attribute
{
public DefaultPropertyValueAttribute(string propertyName, string defaultValue) {
PropertyName = propertyName;
DefaultValue = defaultValue;
}
public string PropertyName { get; }
public string DefaultValue { get; }
}
/// <summary>
/// Adds the possibility to use custom examples for Swagger documentation generation.
/// </summary>
public class CustomExampleSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema.Properties == null)
{
return;
}
var defaultPropertyValueAttribute = context.Type.GetCustomAttribute<DefaultPropertyValueAttribute>();
if (defaultPropertyValueAttribute != null)
{
var relatedProperty = context.Type.GetProperties().SingleOrDefault(property => string.Equals(property.Name, defaultPropertyValueAttribute.PropertyName, StringComparison.Ordinal));
foreach (KeyValuePair<string, OpenApiSchema> property in schema.Properties)
{
if (string.Equals(relatedProperty.Name, property.Key, StringComparison.InvariantCultureIgnoreCase))
{
property.Value.Example = new OpenApiString(defaultPropertyValueAttribute.DefaultValue);
property.Value.Default = new OpenApiString(defaultPropertyValueAttribute.DefaultValue);
break;
}
}
}
foreach (PropertyInfo propertyInfo in context.Type.GetProperties())
{
DefaultValueAttribute defaultAttribute = propertyInfo
.GetCustomAttribute<DefaultValueAttribute>();
if (defaultAttribute != null)
{
foreach (KeyValuePair<string, OpenApiSchema> property in schema.Properties)
{
if (string.Equals(propertyInfo.Name, property.Key, StringComparison.InvariantCultureIgnoreCase))
{
property.Value.Example = new OpenApiString(defaultAttribute.Value.ToString());
property.Value.Default = new OpenApiString(defaultAttribute.Value.ToString());
break;
}
}
}
}
}
}
I see a lot of examples to do this using a custom filter but all examples use the context of the model instead of the property. We have complete control over request model, so I want to have the DefaultValue attribute on the model itself. But I got stuck finding the appropriate properties using the OperationFilterContext used in the Apply(OpenApiOperation operation, OperationFilterContext context) method.
So how to do this? For the record, I do want to have this attribute on the relevant properties and not on the class.
Sources:
https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/575 does not suffice, seems to work either on .NET framework or a older version of Swagger.
https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/69#issuecomment-70295372 is not what I want because the attribute is used on method level, not on model level.

Override Message “the value {0} is invalid for {1}” in case of int in WebAPI

I have a variable name CountryId(Integer Type).
If user provides a string or any random input to CountryId, the In-built DefaultBindingModel in ASP.Net throws an error :
The value '<script>gghghg</script>' is not valid for CountryId.
I want to override this message and provide my own text if the ModelState fails. I want a generic solution.
I've searched and tried many solutions, but they only worked for MVC applications, not webAPI.
public class IntegerModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == null)
{
return base.BindModel(controllerContext, bindingContext);
}
int i;
return !int.TryParse(valueProviderResult.AttemptedValue.ToString(), out i) ? new ValidationResult("Failed") : ValidationResult.Success;
}
}
And in my WebAPI.config :
ModelBinders.Binders.Add(typeof(int), new IntegerModelBinder());
Expected :
The value is not valid for CountryId.
Result :
The value '<script>gghghg</script>' is not valid for CountryId.
Web API
For Web API you can replace the TypeConversionErrorMessageProvider to provide a custom message.
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ModelBinderConfig.TypeConversionErrorMessageProvider = CustomTypeConversionErrorMessageProvider;
// rest of init code
}
private string CustomTypeConversionErrorMessageProvider(HttpActionContext actionContext, System.Web.Http.Metadata.ModelMetadata modelMetadata, object incomingValue)
{
return $"The value is not valid for {modelMetadata.PropertyName}";
}
}
Note the full qualification of the modelMetadata parameter of CustomTypeConversionErrorMessageProvider; if you don't do this, then the ModelMetadata class of System.Web.Mvc is referenced (due to the default usings in Global.asax.cs), instead of the one in System.Web.Http.Metadata, and you get an error:-
Error CS0123 No overload for 'CustomTypeConversionErrorMessageProvider' matches delegate 'ModelBinderErrorMessageProvider'
MVC
For MVC, you can use the localization capability of MVC to replace those validation messages.
Basically, you create your own resource file, point MVC to that resource file using DefaultModelBinder.ResourceClassKey and in that resource file, specify your own text for the PropertyValueInvalid key.
There is a guide on how to do this here.
I think this link will helps you, and Option #3: Use a custom model binder could be to the point solution.
public class LocationModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
string key = bindingContext.ModelName;
ValueProviderResult val = bindingContext.ValueProvider.GetValue(key);
if (val != null)
{
string s = val.AttemptedValue as string;
if (s != null)
{
return Location.TryParse(s);
}
}
return null;
}
}
now we need to wire up the model binder.
public object MyAction2(
[ModelBinder(typeof(LocationModelBinder))]
Location loc) // Use model binding to convert
{
// use loc...
}
https://blogs.msdn.microsoft.com/jmstall/2012/04/20/how-to-bind-to-custom-objects-in-action-signatures-in-mvcwebapi/
Thanks Everyone! But I got the solution.
To Overrride this message for Int Validation in WebAPI, you just need to add the following snippet in Application_Start method in Global.asax.cs
ModelBinderConfig.TypeConversionErrorMessageProvider = (context, metadata, value) => {
if (!typeof(int?).IsAssignableFrom(value.GetType()))
{
return "The Value is not valid for " + metadata.PropertyName;
}
return null;
};

Get version when using IPageRouteModelConvention

I sometime ago asked how to add some kind of localized url's, were IPageRouteModelConvention came into play in a, for me, perfect way.
With that I'm able to have routes in different languages/names.
If I use www.domain.com/nyheter (swedish) or www.domain.com/sistenytt (norwegian) I still only find, in RouteData, that the News route were used (RouteData.Values["page"]).
How do I get which version?
I know I can check/parse the context.Request.Path but am wondering if there is a built-in property that will give me it instead.
In startup
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2).AddRazorPagesOptions(options =>
{
options.Conventions.Add(new LocalizedPageRouteModelConvention(new LocalizationService(appsettings.Routes)));
});
appsettings.Routes is read from appsettings.json
"Routes": [
{
"Page": "/Pages/News.cshtml",
"Versions": [ "nyheter", "sistenytt" ]
},
and so on....
]
The class
public class LocalizedPageRouteModelConvention : IPageRouteModelConvention
{
private ILocalizationService _localizationService;
public LocalizedPageRouteModelConvention(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
public void Apply(PageRouteModel model)
{
var route = _localizationService.LocalRoutes().FirstOrDefault(p => p.Page == model.RelativePath);
if (route != null)
{
foreach (var option in route.Versions)
{
model.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel
{
Template = option
}
});
}
}
}
}
To retrieve a RouteData value, you can specify a token within the template for a route. For example, the route {version} would add a RouteData value of version that would be taken from the URL's first segment. In your example, you don't specify a token for version and so there will be no RouteData value for it, as you've described.
The solution for your specific problem is two-part:
Instead of using specific values when creating new SelectorModels, use a token as described above.
With this in place, you will now be able to access a version value from RouteData, but the new problem is that any value can be provided, whether or not it was specified in your configuration.
To solve the second problem, you can turn to IActionConstraint. Here's an implementation:
public class VersionConstraint : IActionConstraint
{
private readonly IEnumerable<string> allowedValues;
public VersionConstraint(IEnumerable<string> allowedValues)
{
this.allowedValues = allowedValues;
}
public int Order => 0;
public bool Accept(ActionConstraintContext ctx)
{
if (!ctx.RouteContext.RouteData.Values.TryGetValue("version", out var routeVersion))
return false;
return allowedValues.Contains((string)routeVersion);
}
}
VersionConstraint takes a list of allowed values (e.g. nyheter, sistenytt) and checks whether or not the version RouteData value matches. If it doesn't match, the "action" (it's really a page at this point) won't be a match and will end up with a 404.
With that implementation in place, you can update your implementation of LocalizedPageRouteModelConvention's Apply to look like this:
var route = _localizationService.LocalRoutes().FirstOrDefault(p => p.Page == model.RelativePath);
if (route != null)
{
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "{version}"
},
ActionConstraints =
{
new VersionConstraint(route.Versions)
}
});
}
This implementation adds a single new SelectorModel that's set up with a Version RouteData value and is constrained to only allow the values specified in configuration.

How to do custom validation with DataAnnotation and get value from config appsettings.json?

I am doing custom validation on a input field in ASP.NET Core MVC (2.1).
I would like to add a simple Captcha field asking user to enter some numbers which can be easily reconfigured in appsettings.json file. I know there are plenty of libraries out there doing Captcha's but this is not what I want for this particular case.
I having trouble getting the value from the appsettings.json. The below code compiles perfectly but I don't know how to get the value from appsettings.json file in the CaptchaCustomAttribute class.
Here is my code:
// appsettings.json
{
"GeneralConfiguration": {
"Captcha": "123456"
}
}
// GeneralConfiguration.cs
public class GeneralConfiguration
{
public string Captcha { get; set; }
}
// startup.cs / dependency injection
public void ConfigureServices(IServiceCollection services)
{
services.Configure<GeneralConfiguration>(Configuration.GetSection("GeneralConfiguration"));
}
// form model
public class ContactFormModel {
... simplified
[Display(Name = "Captcha")]
[Required(ErrorMessage = "Required")]
[CaptchaCustom]
public string Captcha { get; set; }
}
// CaptchaCustomAttribute.cs
public sealed class CaptchaCustomAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("value cannot be null");
if (value.GetType() != typeof(string)) throw new InvalidOperationException("can only be used on string properties.");
// get value of Captcha here. How?
// this will fail for obvious reasons
//var service = (GeneralConfiguration)validationContext
// .GetService(typeof(GeneralConfiguration));
//if ((string)value == service.Captcha)
//{
// return ValidationResult.Success;
//}
return new ValidationResult("unspecified error");
}
}
The code you've commented-out in your question is very close to working, except for one small detail. When you use IServiceCollection.Configure<T>, you are adding (amongst other things) a registration for IOptions<T> to the DI container, rather than a registration for T itself. This means you need to ask for an IOptions<GeneralConfiguration> in your ValidationAttribute implementation, like so:
var serviceOptions = (IOptions<GeneralConfiguration>)
validationContext.GetService(typeof(IOptions<GeneralConfiguration>));
var service = serviceOptions.Value;

Customize attribute names with Web API default model binder?

I have a request model class that I'm trying to use the default Web API 2 model binding (.NET 4.6.1). Some of the query string parameters match the model properties, but some do not.
public async Task<IHttpActionResult> Get([FromUri]MyRequest request) {...}
Sample query string:
/api/endpoint?country=GB
Sample model property:
public class MyRequest
{
[JsonProperty("country")] // Did not work
[DataMember(Name = "country")] // Also did not work
public string CountryCode { get; set; }
// ... other properties
}
Is there a way to use attributes on my model (like you might use [JsonProperty("country")]) to avoid implementing a custom model binding? Or is best approach just to use create a specific model for the QueryString to bind, and then use AutoMapper to customize for the differences?
Late answer but I bumped into this issue recently also. You could simply use the BindProperty attribute:
public class MyRequest
{
[BindProperty(Name = "country")]
public string CountryCode { get; set; }
}
Tested on .NET Core 2.1 and 2.2
Based on further research, the default model binding behavior in Web API does not support JsonProperty or DataMember attributes, and most likely solutions seem to be either (1) custom model binder or (2) maintaining 2 sets of models and a mapping between them.
JSON to Model property binding using JsonProperty
Changing the parameter name Web Api model binding
I opted for the custom model binder (implementation below) so I could re-use this and not have to duplicate all my models (and maintain mappings between every model).
Usage
The implementation below allows me to let any model optionally use JsonProperty for model binding, but if not provided, will default to just the property name. It supports mappings from standard .NET types (string, int, double, etc). Not quite production ready, but it meets my use cases so far.
[ModelBinder(typeof(AttributeModelBinder))]
public class PersonModel
{
[JsonProperty("pid")]
public int PersonId { get; set; }
public string Name { get; set; }
}
This allows the following query string to be mapped in a request:
/api/endpoint?pid=1&name=test
Implementation
First, the solution defines a mapped property to track the source property of the model and the target name to use when setting the value from the value provider.
public class MappedProperty
{
public MappedProperty(PropertyInfo source)
{
this.Info = source;
this.Source = source.Name;
this.Target = source.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? source.Name;
}
public PropertyInfo Info { get; }
public string Source { get; }
public string Target { get; }
}
Then, a custom model binder is defined to handle the mapping. It caches the reflected model properties to avoid repeating the reflection on subsequent calls. It may not be quite production ready, but initial testing has been promising.
public class AttributeModelBinder : IModelBinder
{
public static object _lock = new object();
private static Dictionary<Type, IEnumerable<MappedProperty>> _mappings = new Dictionary<Type, IEnumerable<MappedProperty>>();
public IEnumerable<MappedProperty> GetMapping(Type type)
{
if (_mappings.TryGetValue(type, out var result)) return result; // Found
lock (_lock)
{
if (_mappings.TryGetValue(type, out result)) return result; // Check again after lock
return (_mappings[type] = type.GetProperties().Select(p => new MappedProperty(p)));
}
}
public object Convert(Type target, string value)
{
try
{
var converter = TypeDescriptor.GetConverter(target);
if (converter != null)
return converter.ConvertFromString(value);
else
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
catch (NotSupportedException)
{
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
}
public void SetValue(object model, MappedProperty p, IValueProvider valueProvider)
{
var value = valueProvider.GetValue(p.Target)?.AttemptedValue;
if (value == null) return;
p.Info.SetValue(model, this.Convert(p.Info.PropertyType, value));
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
try
{
var model = Activator.CreateInstance(bindingContext.ModelType);
var mappings = this.GetMapping(bindingContext.ModelType);
foreach (var p in mappings)
this.SetValue(model, p, bindingContext.ValueProvider);
bindingContext.Model = model;
return true;
}
catch (Exception ex)
{
return false;
}
}
}

Categories