I'm trying to implement HATEOAS in my ASP rest API, changing the ReferenceResolverProvider.
The problem is, that depending on which controller I use, I'd like to use different ReferenceResolvers, because I need to behave differently for each Controller.
Now I have universal options:
services.AddMvc()
.AddJsonOptions(option => option.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver())
.AddJsonOptions(options => options.SerializerSettings.ReferenceResolverProvider = () => new RoomsReferenceResolver<Room>())
.AddJsonOptions(options => options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects);
And I want to have something like this:
services.AddMvc()
.AddJsonOptions(option => option.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver())
.AddJsonOptions<RoomsController>(options => options.SerializerSettings.ReferenceResolverProvider = () => new RoomsReferenceResolver<Room>())
.AddJsonOptions(options => options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects);
You seem to be wanting to create a per-controller specific formatters. This can be achieved by using a filter called IResourceFilter. A quick example:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CamelCaseJsonFormatterResourceFilter : Attribute, IResourceFilter
{
private readonly JsonSerializerSettings serializerSettings;
public CamelCaseJsonFormatterResourceFilter()
{
// Since the contract resolver creates the json contract for the types it needs to deserialize/serialize,
// cache it as its expensive
serializerSettings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
// remove existing input formatter and add a new one
var camelcaseInputFormatter = new JsonInputFormatter(serializerSettings);
var inputFormatter = context.InputFormatters.FirstOrDefault(frmtr => frmtr is JsonInputFormatter);
if (inputFormatter != null)
{
context.InputFormatters.Remove(inputFormatter);
}
context.InputFormatters.Add(camelcaseInputFormatter);
// remove existing output formatter and add a new one
var camelcaseOutputFormatter = new JsonOutputFormatter(serializerSettings);
var outputFormatter = context.OutputFormatters.FirstOrDefault(frmtr => frmtr is JsonOutputFormatter);
if (outputFormatter != null)
{
context.OutputFormatters.Remove(outputFormatter);
}
context.OutputFormatters.Add(camelcaseOutputFormatter);
}
}
// Here I am using the filter to indicate that only the Index action should give back a camelCamse response
public class HomeController : Controller
{
[CamelCaseJsonFormatterResourceFilter]
public Person Index()
{
return new Person() { Id = 10, AddressInfo = "asdfsadfads" };
}
public Person Blah()
{
return new Person() { Id = 10, AddressInfo = "asdfsadfads" };
}
If you are curious about the filter execution order, following is an example of the sequence of them:
Inside TestAuthorizationFilter.OnAuthorization
Inside TestResourceFilter.OnResourceExecuting
Inside TestActionFilter.OnActionExecuting
Inside Home.Index
Inside TestActionFilter.OnActionExecuted
Inside TestResultFilter.OnResultExecuting
Inside TestResultFilter.OnResultExecuted
Inside TestResourceFilter.OnResourceExecuted
Interesting problem.
What about making the ReferenceResolver a facade:
class ControllerReferenceResolverFacade : IReferenceResolver
{
private IHttpContextAccessor _context;
public ControllerReferenceResolverFacade(IHttpContextAccessor context)
{
_context = context;
}
public void AddReference(object context, string reference, object value)
{
if ((string)_context.HttpContext.RequestServices.GetService<ActionContext>().RouteData.Values["Controller"] == "HomeController")
{
// pass off to HomeReferenceResolver
}
throw new NotImplementedException();
}
Then you should be able to do:
services.AddMvc()
.AddJsonOptions(options => options.SerializerSettings.ReferenceResolverProvider = () => {
return new ControllerReferenceResolverFacade(
services.BuildServiceProvider().GetService<IHttpContextAccessor>());
});
This might not be exactly what you need but it might help you get started?
Related
I'm working on an multilingual website. earlier, i used to use QueryStringRequestProvider for RequestLocalizationOptions. Finally, based on some SEO-friendly best practices i decided to migrate to RouteDataRequestCultureProvider. I have many problems in this way. i've already checked more than 100 github issues and stackoverflow posts to find a working solution, but still nothing.
Currently i used these codes to partial reach the goal
Startup.cs
services.Configure<RequestLocalizationOptions>(options =>
{
options.ConfigureRequestLocalization();
options.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider
{
RouteDataStringKey = LanguageRouteConstraint.Key,
Options = options
});
});
services.AddRazorPages()
.AddRazorPagesOptions(options =>
{
options.Conventions.Add(new LocalizedPageRouteModelConvention());
});
LanguageRouteConstraint.cs
public class LanguageRouteConstraint : IRouteConstraint
{
public const string Key = "culture";
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.ContainsKey(Key))
return false;
var lang = values[routeKey] as string;
if (string.IsNullOrWhiteSpace(lang))
return false;
return lang == "tr" || lang == "en" || lang == "fa";
}
}
Cultures.cs
public static class Cultures
{
public static readonly string[] Supported = { "tr", "en", "fa" };
public static readonly string Default = Supported[0];
public static RequestLocalizationOptions ConfigureRequestLocalization(this RequestLocalizationOptions options)
{
options ??= new RequestLocalizationOptions();
var supportedCultures = Supported
.Select(culture => new CultureInfo(culture))
.ToList();
options.DefaultRequestCulture = new RequestCulture(Default, Default);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
return options;
}
}
LocalizedPageRouteModelConvention.cs
public class LocalizedPageRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
foreach (var selector in model.Selectors.ToList())
{
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = -1,
Template = AttributeRouteModel
.CombineTemplates("{" + LanguageRouteConstraint.Key + "?}",
selector.AttributeRouteModel.Template),
}
});
}
}
}
The major problem of mine, is currently i dont have a working way to put a middleware between IUrlHelper and RazorPage link generator
Now, i can use as below:
var currentCulture = Request.RouteValues[LanguageRouteConstraint.Key];
Url.Page("/Index/Contact", new {culture= currentCulture});
but it doesn't make sense to trace codes in project from the scratch to change code from Url.Page("") to Url.Page("", new{culture=ANY})
As i said before, i checked many sites, but still i got nothing.
The only thing i need for now, is a WORKING WAY to use standard Url.Page(), that to be translated to Url.Page("", new{culture=ANY}) in background.
Thanks in advance
My problem is only about RazorPages, not Controllers
Is it possible to hide the 'api-version' and 'x-api-version' parameters?
services.AddApiVersioning(config =>
{
config.ReportApiVersions = true;
config.DefaultApiVersion = new ApiVersion(1, 0);
config.AssumeDefaultVersionWhenUnspecified = true;
config.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader(),
new HeaderApiVersionReader()
{
HeaderNames = { "x-api-version" }
});
});
services.AddVersionedApiExplorer(
options =>
{
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
options.DefaultApiVersionParameterDescription = "Do NOT modify api-version!";
});
I already checked how-to-set-up-swashbuckle-vs-microsoft-aspnetcore-mvc-versioning which implements a 'RemoveVersionFromParameter' method, but in that case the Swagger page would loose the api version and always uses the default v1.0. As shown in the code snippet, I am using the QueryStringApiVersionReader and HeaderApiVersionReader, but I don't want to support the url api versioning.
Note: The API does have multiple swagger json pages for all versions (e.g. V1, V1.1, V2.0)
You can try an operation filter. This is similar to Helder's solution, but the implementation doesn't have to be at the document level, so it seems simpler:
public void Configure(SwaggerGenOptions options)
{
// Filter out `api-version` parameters globally
options.OperationFilter<ApiVersionFilter>();
}
internal class ApiVersionFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var parametersToRemove = operation.Parameters.Where(x => x.Name == "api-version").ToList();
foreach (var parameter in parametersToRemove)
operation.Parameters.Remove(parameter);
}
}
Have you looked into IDocumentFilter with that you can remove stuff from the final swagger.json and that will remove it from the UI
Here is an example me removing some properties from the definitions:
private class HideStuffDocumentFilter : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry s, IApiExplorer a)
{
foreach (var definition in swaggerDoc.definitions)
{
foreach (var prop in definition.Value.properties.ToList())
{
if (prop.Value.maxLength == 9999)
definition.Value.properties.Remove(prop);
}
}
}
}
I have a few more samples here:
https://github.com/heldersepu/Swagger-Net-Test/blob/e701b1d20d0b42c1287c3da2641ca521a0a7b592/Swagger_Test/App_Start/SwaggerConfig.cs#L766
You can add your own custom CSS
and use it to hide those elements (and do any other customisation you want).
app.UseSwaggerUI(c =>
{
...
c.InjectStylesheet("/swagger-ui/custom.css");
...
});
Edit - example:
Suppose you're trying to hide - in my example; you can easily adapt it to yours - the tenantId parameter in this "Remove Basket" operation:
This would do that:
div#operations-baskets-remove tr[data-param-name="tenantId"] {
display: none;
}
This can be done by setting ApiExplorerOption SubstituteApiVersionInUrl = true . In your case:
services.AddVersionedApiExplorer(
options =>
{
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
options.DefaultApiVersionParameterDescription = "Do NOT modify api-version!";
options.SubstituteApiVersionInUrl = true;
});
this worked for me.
public class SwaggerOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (operation?.Parameters == null
|| !operation.Parameters.Any())
{
return;
}
var parametersWithPropertiesToIgnore = context.ApiDescription
.ActionDescriptor.Parameters.Where(p =>
p.ParameterType.GetProperties()
.Any(t => t.GetCustomAttribute<IgnoreDataMemberAttribute>() != null));
foreach (var parameter in parametersWithPropertiesToIgnore)
{
var ignoreDataMemberProperties = parameter.ParameterType.GetProperties()
.Where(t => t.GetCustomAttribute<IgnoreDataMemberAttribute>() != null)
.Select(p => p.Name);
operation.Parameters = operation.Parameters.Where(p => !ignoreDataMemberProperties.Contains(p.Name))
.ToList();
}
}
}
On Startup
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Api", Version = "v1" });
c.SchemaFilter<SwaggerSchemaFilter>();
c.OperationFilter<SwaggerOperationFilter>();
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var filePath = Path.Combine(System.AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(filePath);
});
Just add the data annotation [IgnoreDataMember] in the property to hide it.
public class ExampleRequest
{
[IgnoreDataMember]
public string? HiddenProp { get; set; }
public string OtherProp { get; set; }
}
I have based on this article to create this solution.
Do you really want to hide the API version parameters or just make them read-only? The API version parameter is required when invoking the API so that should not be hidden from the documentation or user.
Your question implies that you're more concerned that the user not be able to change the API version default value for a particular documented API. That's fair, but has a different solution.
While you can mark a parameter as read-only, I have not been able to get the UI to honor that. It might be a bug or something else is misconfigured. You can definitely get read-only like behavior by using an enumeration with a single value. For example:
public class SwaggerDefaultValues : IOperationFilter
{
public void Apply( OpenApiOperation operation, OperationFilterContext context )
{
var apiDescription = context.ApiDescription;
operation.Deprecated |= apiDescription.IsDeprecated();
if ( operation.Parameters == null )
{
return;
}
foreach ( var parameter in operation.Parameters )
{
var description = apiDescription.ParameterDescriptions
.First( p => p.Name == parameter.Name );
parameter.Description ??= description.ModelMetadata?.Description;
if ( parameter.Schema.Default == null &&
description.DefaultValue != null &&
description.DefaultValue is not DBNull &&
description.ModelMetadata is ModelMetadata modelMetadata )
{
var json = JsonSerializer.Serialize(
description.DefaultValue,
modelMetadata.ModelType );
// this will set the API version, while also making it read-only
parameter.Schema.Enum = new[] { OpenApiAnyFactory.CreateFromJson( json ) };
}
parameter.Required |= description.IsRequired;
}
}
You can added startup.cs file.
services.AddApiVersioning(options =>
{
// reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;
});
services.AddVersionedApiExplorer(options =>
{
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
// can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
});
And then,you can added top on the controller. But I try to without this([ApiVersion("1.0")]),it could run.I had successfully hide version parametre.
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/Check")]
[ApiController]
[Authorize]
public class CheckController : ControllerBase {}
I'm looking to have all OkObjectResult responses coming out of my api get run through a custom JSON resolver that I have. The resolver relies on some request-specific data - namely, the user's roles. It's effectively like the Authorize attribute on a controller, but for data transfer objects passed from the API to the UI.
I can add the resolver in Configure Services via AddJsonOptions, but it doesn't have access to that user info there.
How can I pass values that are based on the request to this resolver? Am I looking at some sort of custom middleware, or something else?
As a sample, if I have an object with some custom attribute decorators, like so:
public class TestObject
{
public String Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public String Field2 => "ViewRequiresAdmin";
}
And call my custom serializer with different roles, like so:
var test = new TestObject();
var userRoles = GetRoles(); // "User" for the sake of this example
var outputJson = JsonConvert.SerializeObject(test,
new JsonSerializerSettings {
ContractResolver = new MyCustomResolver(userRoles)
});
Then the output JSON will skip anything the user can't access, like so:
{
"Field1":"NoRestrictions",
// Note the absence of Field2, since it has [RequireRoleView("Admin")]
}
Suppose you have an custom RequireRoleViewAttribute:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequireRoleViewAttribute : Attribute
{
public string Role;
public RequireRoleViewAttribute(string role){
this.Role = role;
}
}
How can I pass values that are based on the request to this resolver?
You can have a IServiceProvider injected in your custom resolver :
public class RoleBasedContractResolver : DefaultContractResolver
{
public IServiceProvider ServiceProvider { get; }
public RoleBasedContractResolver( IServiceProvider sp)
{
this.ServiceProvider = sp;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you're using the Identity, you can get the userManager :
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
// ...
}
}
thus we can get the HttpContext and User as we like. If you're using the Identity, you can also get the UserManager service and roles.
and now we can follow #dbc's advice to control the ShouldSerialize:
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you use the Identitiy, you can get the usermanager
//UserManager<IdentityUser>
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
JsonProperty property = base.CreateProperty(member, memberSerialization);
// get the attributes
var attrs=member.GetCustomAttributes<RequireRoleViewAttribute>();
// if no [RequireResoveView] decorated, always serialize it
if(attrs.Count()==0) {
property.ShouldDeserialize = instance => true;
return property;
}
// custom your logic to dertermine wether should serialize the property
// I just use check if it can statisify any the condition :
var roles = this.GetIdentityUserRolesAsync(context,userManager).Result;
property.ShouldSerialize = instance => {
var resource = new { /* any you need */ };
return attrs.Any(attr => {
var rolename = attr.Role;
return roles.Any(r => r == rolename ) ;
}) ? true : false;
};
return property;
}
The function GetIdentityUserRolesAsync here is helper method to retrieve roles using the current HttpContext and the UserManger service :
private async Task<IList<string>> GetIdentityUserRolesAsync(HttpContext context, UserManager<IdentityUser> userManager)
{
var rolesCached= context.Items["__userRoles__"];
if( rolesCached != null){
return (IList<string>) rolesCached;
}
var identityUser = await userManager.GetUserAsync(context.User);
var roles = await userManager.GetRolesAsync(identityUser);
context.Items["__userRoles__"] = roles;
return roles;
}
How to inject the IServiceProvider in details :
The trick is all about how to configure the default MvcJwtOptions with an IServiceProvider.
Don't configure the JsonOptions by :
services.AddMvc().
.AddJsonOptions(o =>{
// o.
});
as it doesn't allow us add a IServiceProvider parameter.
We can custom a subclass of MvcJsonOptions:
// in .NET 3.1 and above, change this from MvcJsonOptions to MvcNewtonsoftJsonOptions
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
IServiceProvider ServiceProvider;
public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcJsonOptions options)
{
options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
}
}
and register the services by :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// don't forget to add the IHttpContextAccessor
// in .NET 3.1 and above, change this from MvcJsonOptions to MvcNewtonsoftJsonOptions
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();
Test Case :
Let's say you have a custom POCO :
public class TestObject
{
public string Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public string Field2 => "ViewRequiresAdmin";
[RequireRoleView("HR"),RequireRoleView("OP")]
public string Field3 => "ViewRequiresHROrOP";
[RequireRoleView("IT"), RequireRoleView("HR")]
public string Field4 => "ViewRequiresITOrHR";
[RequireRoleView("IT"), RequireRoleView("OP")]
public string Field5 => "ViewRequiresITOrOP";
}
And the Current User has roles : Admin and HR:
The result will be :
{"Field1":"NoRestrictions","Field2":"ViewRequiresAdmin","Field3":"ViewRequiresHROrOP","Field4":"ViewRequiresITOrHR"}
A screenshot of testing with an action method :
Itminus's answer covers everything that's needed, but for anyone interested, I've extended it a little for easy reuse.
First, in a class library
My RequireRoleViewAttribute, which allows multiple roles (OR, not AND):
[AttributeUsage(AttributeTargets.Property)]
public class RequireRoleViewAttribute : Attribute
{
public List<String> AllowedRoles { get; set; }
public RequireRoleViewAttribute(params String[] AllowedRoles) =>
this.AllowedRoles = AllowedRoles.Select(ar => ar.ToLower()).ToList();
}
My resolver is almost identical to Itminus's, but CreateProperty is adjusted to
IEnumerable<String> userRoles = this.GetIdentityUserRoles();
property.ShouldSerialize = instance =>
{
// Check if every attribute instance has at least one role listed in the user's roles.
return attrs.All(attr =>
userRoles.Any(ur =>
attr.AllowedRoles.Any(ar =>
String.Equals(ar, ur, StringComparison.OrdinalIgnoreCase)))
);
};
And GetIdentityUserRoles doesn't use UserManager
private IEnumerable<String> GetIdentityUserRoles()
{
IHttpContextAccessor contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext context = contextAccessor.HttpContext;
ClaimsPrincipal user = context.User;
Object rolesCached = context.Items["__userRoles__"];
if (rolesCached != null)
{
return (List<String>)rolesCached;
}
var roles = ((ClaimsIdentity)user.Identity).Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
context.Items["__userRoles__"] = roles;
return roles;
}
And I have an extensions class which contains:
public static IServiceCollection AddRoleBasedContractResolver(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcJsonOptions>, RoleBasedContractResolverOptions>();
return services;
}
Then in my API
I reference that class library. In Startup.cs -> ConfigureServices, I call:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddRoleBasedContractResolver();
...
}
And my DTOs are tagged with the attribute:
public class Diagnostics
{
public String VersionNumber { get; set; }
[RequireRoleView("admin")]
public Boolean ViewIfAdmin => true;
[RequireRoleView("hr")]
public Boolean ViewIfHr => true;
[RequireRoleView("hr", "admin")]
public Boolean ViewIfHrOrAdmin => true;
}
And the return value as an admin is:
{
"VersionNumber": "Debug",
"ViewIfAdmin": true,
"ViewIfHrOrAdmin": true
}
I have made a claims filter
public class ClaimRequirementAttribute : TypeFilterAttribute
{
public ClaimRequirementAttribute(string claimType, ClaimRoles claimValue) : base(typeof(ClaimRequirementFilter))
{
Arguments = new object[] {new Claim(claimType, claimValue.ToString()) };
}
}
public class ClaimRequirementFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var headers = context.HttpContext.Request.Headers;
var tokenSuccess = headers.TryGetValue("Token", out var token);
var emailSuccess = headers.TryGetValue("Email", out var email);
var deviceNameSuccess = headers.TryGetValue("DeviceName", out var deviceName);
if (tokenSuccess && emailSuccess && deviceNameSuccess)
{
var accountLogic = context.HttpContext.RequestServices.GetService<IAccountLogic>();
var hasClaim = accountLogic.ValidateLogin(email, token, deviceName).Result.Success;
if (!hasClaim)
{
context.HttpContext.ForbidAsync();
}
}
else
{
context.HttpContext.ForbidAsync();
}
}
}
I have registered the filter in my startup
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ConnectionStringsSettings>(Configuration.GetSection("ConnectionStrings"));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<ClaimRequirementFilter>();
But I get this error when I navigate to an action that uses the filter
[HttpPost]
[ClaimRequirement("Permission", ClaimRoles.Admin)]
public async Task ResetLeaderboard()
InvalidOperationException: A suitable constructor for type 'Foosball.Logic.ClaimRequirementFilter' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor
github: https://github.com/Mech0z/Foosball/tree/core2.1/Foosball
As your code has
Arguments = new object[] {new Claim(claimType, claimValue.ToString()) };
you need to add the following constructor:
public ClaimRequirementFilter(Claim claim)
{
}
That is because the internal constructor resolving logic uses TypeFilterAttribute.Argument property to decide what constructor to use for instantiation.
I'm trying to use FluentValidation in a WebApi project (not asp.net Core).
I have the following code:
public static class UnityConfig
{
public static void RegisterComponents(UnityContainer container)
{
// Register validators
RegisterValidators(container);
// Mediatr
container.RegisterType<IMediator, Mediator>();
container.RegisterTypes(AllClasses.FromAssemblies(true, Assembly.GetExecutingAssembly()), WithMappings.FromAllInterfaces, GetName, GetLifetimeManager);
container.RegisterInstance<SingleInstanceFactory>(t => container.Resolve(t));
container.RegisterInstance<MultiInstanceFactory>(t => container.ResolveAll(t));
// Automapper profiles
var profileTypes = typeof(BaseProfile).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(BaseProfile)));
var config = new MapperConfiguration(cfg => new MapperConfiguration(x =>
{
foreach (var type in profileTypes)
{
var profile = (BaseProfile)Activator.CreateInstance(type);
cfg.AddProfile(profile);
}
}));
container.RegisterInstance<IConfigurationProvider>(config);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
}
static LifetimeManager GetLifetimeManager(Type type)
{
return IsNotificationHandler(type) ? new ContainerControlledLifetimeManager() : null;
}
static string GetName(Type type)
{
return IsNotificationHandler(type) ? string.Format("HandlerFor" + type.Name) : string.Empty;
}
private static void RegisterValidators(IUnityContainer container)
{
var validators = AssemblyScanner.FindValidatorsInAssembly(Assembly.GetExecutingAssembly());
validators.ForEach(validator => container.RegisterType(validator.InterfaceType, validator.ValidatorType));
}
}
I'm scanning the assemblies and registrering the validators, of which there's only one right now, it sits here: (don't mind the weird validations, I'm trying to have it fail)
public class Query : IRequest<Result>
{
public Guid? Id { get; set; }
}
public class QueryValidator : AbstractValidator<Query>
{
public QueryValidator()
{
RuleFor(q => q.Id).Empty();
RuleFor(q => q.Id).Equal(Guid.NewGuid());
}
}
My Application_start looks like this:
protected void Application_Start()
{
var container = new UnityContainer();
UnityConfig.RegisterComponents(container);
GlobalConfiguration.Configure(WebApiConfig.Register);
var factory = new UnityValidatorFactory2(GlobalConfiguration.Configuration);
FluentValidationModelValidatorProvider.Configure(GlobalConfiguration.Configuration, x => x.ValidatorFactory = factory);
}
And I have the following validatorFactory:
public class UnityValidatorFactory2 : ValidatorFactoryBase
{
private readonly HttpConfiguration _configuration;
public UnityValidatorFactory2(HttpConfiguration configuration)
{
_configuration = configuration;
}
public override IValidator CreateInstance(Type validatorType)
{
var validator = _configuration.DependencyResolver.GetService(validatorType) as IValidator;
return validator;
}
}
Now; when I call the action on the controller, 'CreateInstance' tries to resolve a validatorType of the type:
IValidator<Guid>
instead of:
IValidator<Query>
and of course finds nothing, this means that my validations does not run.
Does anyone have an ideas as to why this is? it seems faily straight forward, so I have trouble seeing what goes wrong.
After having slept on it, I found the answer myself.
I was posting a Guid to my controller instead of the model I was trying to validate (which only contains a guid)
After posting the right model, it now validates correctly.