I'm building GraphQL API in Asp.Net Core 2.2 using GraphQL.Net 2.4.0
I've created Controller to handle GraphQL Queries:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class GraphQLController : Controller
{
private readonly ISchema _schema;
private readonly IDocumentExecuter _executer;
private readonly ILogger _logger;
IEnumerable<IValidationRule> _validationRules;
private readonly DataLoaderDocumentListener _dataLoaderDocumentListener;
//private readonly IDocumentWriter _writer;
//private readonly IHttpContextAccessor _accessor;
public GraphQLController(
ILogger<GraphQLController> logger,
IEnumerable<IValidationRule> validationRules,
IDocumentExecuter executer,
DataLoaderDocumentListener dataLoaderDocumentListener,
//IDocumentWriter writer,
ISchema schema)
{
_executer = executer;
_schema = schema;
_logger = logger;
_validationRules = validationRules;
_dataLoaderDocumentListener = dataLoaderDocumentListener;
//_writer = writer;
}
[Route("graphql")]
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody]GraphQLQuery query)
{
if (!ModelState.IsValid || query == null)
{
return Json("Проблем в формата на изпратената на заявка!");
}
var inputs = query.Variables.ToInputs();
var result = await _executer.ExecuteAsync(o =>
{
o.Schema = _schema;
o.Query = query.Query;
o.OperationName = query.OperationName;
o.Inputs = inputs;
o.ExposeExceptions = true;
o.EnableMetrics = true;
o.ComplexityConfiguration = new GraphQL.Validation.Complexity.ComplexityConfiguration { MaxDepth = 15 };
//o.FieldMiddleware.Use<InstrumentFieldsMiddleware>();
o.UserContext = new GraphQLUserContext
{
// this is the User on your controller
// which is populated from your jwt
User = User
};
o.ValidationRules = DocumentValidator.CoreRules().Concat(_validationRules).ToList();
o.Listeners.Add(_dataLoaderDocumentListener);
}).ConfigureAwait(false);
if (result.Errors?.Count > 0)
{
_logger.LogWarning($"Errors: {JsonConvert.SerializeObject(result.Errors)}");
return BadRequest(result);
}
return Ok(result);
}
}
The problem arises when i want to validate some of the Fields in my InputObjectGraphType , I've implemented IValidationRule interface.
Prior this im adding Metadata to the field i want to validate so i can easily find it. I;m getting the fieldType but cant fetch the Value to Validate it.
This is the Implementation of the IValidationRule i Use:
public class PhoneNumberValidationRule : IValidationRule
{
public INodeVisitor Validate(ValidationContext context)
{
return new EnterLeaveListener(_ =>
{
_.Match<Argument>(argAst =>
{
var inputType = context.TypeInfo.GetInputType().GetNamedType() as InputObjectGraphType;
var argDef = context.TypeInfo.GetArgument();
if (argDef == null) return;
var type = argDef.ResolvedType;
if (type.IsInputType())
{
var fields = ((type as NonNullGraphType)?.ResolvedType as IComplexGraphType)?.Fields;
if (fields != null)
{
foreach (var field in fields)
{
if (field.ResolvedType is NonNullGraphType)
{
if ((field.ResolvedType as NonNullGraphType).ResolvedType is IComplexGraphType)
{
fields = fields.Union(((field.ResolvedType as NonNullGraphType).ResolvedType as IComplexGraphType)?.Fields);
}
}
if (field.ResolvedType is IComplexGraphType)
{
fields = fields.Union((field.ResolvedType as IComplexGraphType)?.Fields);
}
}
//let's look for fields that have a specific metadata
foreach (var fieldType in fields.Where(f => f.HasMetadata(nameof(EmailAddressValidationRule))))
{
//now it's time to get the value
context.Inputs.GetValue(argAst.Name, fieldType.Name);
if (value != null)
{
if (!value.IsValidPhoneNumber())
{
context.ReportError(new ValidationError(context.OriginalQuery
, "Invalid Phone Number"
, "The supplied phone number is not valid."
, argAst
));
}
}
}
}
}
});
});
}
}
But here the context.Inputs property is always null.
In the controller this line var inputs = query.Variables.ToInputs();
also produces null. Is this query Variable field and Document Executer's Input Field anything to do with this ?
You probably figured it out by now, but for anyone finding this question in the future:
context.Inputs is only used with variables.
As far as I could understand argAst.GetValue could be either VariableReference or ObjectValue depending on whether the query was using a variable or not.
In case variables were not used context.Inputs.GetValue will return null.
I have created a ModelBinder, which only gets triggered if an object has a [Decimal] attribute assigned, yet for some reason, despite it actually sanitising the data it does not seem to update the posted model.
I wonder if someone could see from my code below, where I maybe going wrong.
Startup.cs
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddMvc(config => config.ModelBinderProviders.Insert(0, new DecimalModelBinderProvider()));
}
DecimalModelBinderProvider.cs
public class DecimalModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext modelBinderProviderContext)
{
if (modelBinderProviderContext == null)
{
throw new ArgumentNullException(nameof(modelBinderProviderContext));
}
if (!modelBinderProviderContext.Metadata.IsComplexType)
{
try
{
var propertyName = modelBinderProviderContext.Metadata.PropertyName;
var property = modelBinderProviderContext.Metadata.ContainerType.GetProperty(propertyName);
if (property != null)
{
var attribute = property.GetCustomAttributes(typeof(DecimalAttribute), false).FirstOrDefault();
if (attribute != null)
{
return new DecimalModelBinder(modelBinderProviderContext.Metadata.ModelType, attribute as IDecimalAttribute);
}
}
}
catch (Exception exception)
{
var message = exception.Message;
return null;
}
}
return null;
}
}
DecimalModelBinder.cs
public class DecimalModelBinder : IModelBinder
{
private readonly IDecimalAttribute _decimalAttribute;
private readonly SimpleTypeModelBinder _simpleTypeModelBinder;
public DecimalModelBinder(Type type, IDecimalAttribute decimalAttribute)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
_decimalAttribute = decimalAttribute;
_simpleTypeModelBinder = new SimpleTypeModelBinder(type);
}
public Task BindModelAsync(ModelBindingContext modelBindingContext)
{
if (modelBindingContext == null)
{
throw new ArgumentNullException(nameof(modelBindingContext));
}
var valueProviderResult = modelBindingContext.ValueProvider.GetValue(modelBindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
modelBindingContext.ModelState.SetModelValue(modelBindingContext.ModelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
bool success;
var result = _decimalAttribute.Decimal(value, out success);
if (success)
{
modelBindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return _simpleTypeModelBinder.BindModelAsync(modelBindingContext);
}
}
IDecimalAttribute.cs
public interface IDecimalAttribute
{
object Decimal(string value, out bool success);
}
DecimalAttribute.cs
[AttributeUsage(AttributeTargets.Property)]
public class DecimalAttribute : Attribute, IDecimalAttribute
{
public object Decimal(string value, out bool success)
{
var tryModelValue = string.IsNullOrEmpty(value) ? "0.00" : value.Replace("£", "").Replace("%", "");
decimal #decimal;
success = decimal.TryParse(tryModelValue, out #decimal);
return #decimal;
}
}
Test.cs
public class Test
{
[Display(Name = "Name", Description = "Name")]
public string Name { get; set; }
[Decimal]
[Display(Name = "Amount", Description = "Amount")]
public double Amount { get; set; }
}
HomeController
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult Index(Test test)
{
if (ModelState.IsValid)
{
}
return View(test);
}
For the purpose of testing I will enter the value £252.83 into the Amount field and submit the form.
If I then place a brakepoint on the line var value = valueProviderResult.FirstValue; I can see that value is £252.83 and if I place a breakpoint on the line modelBindingContext.Result = ModelBindingResult.Success(result); I can see that the result is 252.83M.
However if I step through the code further and place a breakpoint on the line if (ModelState.IsValid) the valid state is false and if I inspect the model test the object Amount is 0.
If anyone can help it would be much appreciated :-)
Try inspect further on the ModelState Error, the Amount property should be invalid and there must be an exception.
I am guessing it should be InvalidCastException. I notice that the Amount property in Test class is Double while you are producing Decimal in your DecimalAttribute.
So the build-in Model Binder that processing the Test class (should be ComplexTypeModelBinder) unable to set the Amount property as it is different type.
I'm trying to edit an enum value in a class instance based on whether that instance appears in a dictionary of type <string, myClass>. What seems logical to me is to do the code snippets below:
if (pumpDict.ContainsKey(ID))
{
foreach(KeyValuePair<string, PumpItem> kvp in pumpDict)
{
if(kvp.Key == ID)
{
kvp.Value.state = kvp.Value.state.Available; //error here
kvp.Value.fuelPumped = fuelPumped;
kvp.Value.fuelCost = fuelCost;
break;
}
}
}
else
{
PumpItem pump = new PumpItem();
pumpDict.Add(ID, pump);
}
And my PumpItems class is such:
namespace PoSClientWPF
{
public enum pumpState
{
Available,
customerWaiting,
Pumping,
customerPaying
};
public enum fuelSelection
{
Petrol,
Diesel,
LPG,
Hydrogen,
None
};
class PumpItem
{
public double fuelPumped;
public double fuelCost;
public fuelSelection selection;
public pumpState state;
public PumpItem()//intialize constructor
{
this.fuelPumped = 0;
this.fuelCost = 0;
this.selection = fuelSelection.None;
this.state = pumpState.Available;
}
}
}
I was led to believe that to have an enum value in a constructor, they have to be set up as above, with a new instance of those enums declared in the class body.
It seems to me, that what I'm trying to do is logical but I am getting an error on the right hand side of the assignation which states:
"member PoSClientWPF.pumpState.Available cannot be accessed with an instance reference; qualify is with a type name instead"
I've searched for this error among several forums but only seem to find errors involving calling static variables incorrectly. Can anyone point me in the direction of a solution?
Thanks in advance.
You are incorrectly accessing the Enum member:
// this is incorrect
kvp.Value.state = kvp.Value.state.Available; //error here
// this is the correct way
kvp.Value.state = PoSClientWPF.pumpState.Available;
You know you have a dictionary?
PumpItem pumpItem = pumpDict[ID];
pumpItem.state = PoSClientWPF.pumpState.Available;
or
PumpItem pumpItem;
if (pumpDict.TryGetValue(ID, out pumpItem))
{
pumpItem.state = PoSClientWPF.pumpState.Available;
}
else
{
pumpItem = new PumpItem();
pumpDict.Add(ID, pumpItem);
}
Could just add ID to PumpItem and use a List
PumpItem pumpItem = pumpList.FirstOrDefualt(x => x.ID == ID)
if (pumpItem == null)
pumpList.Add(new PumpItem(ID));
else
pumpItem.state = PoSClientWPF.pumpState.Available;
class PumpItem
{
public double fuelPumped = 0;
public double fuelCost = 0;
public fuelSelection selection = fuelSelection.None;
public pumpState state = pumpState.Available;
public Int32? ID = null;
public PumpItem()//intialize constructor
{ }
public PumpItem(Int32? ID)
{
this.ID = ID;
}
}
I'm writing a plugin for resharper which I want to use to navigate from a ConcreteCommand -> ConcreteCommandHandler where those types look like this
public class ConcreteCommand : ICommand
public class ConcreteCommandHandler : ICommandHandler<ConcreteCommand>
I've got as far as adding my navigation menu option when the cursor is on a ICommand instance/definition (currently only by checking if the name contains 'Command' and not 'CommandHandler'), and I think I have the code necessary to actually search for a type which inherits something, but my issue is that the only thing I actually have a type for is my ConcereteCommand and I need to create (or get a reference to) the generic type ICommandHandler<T> with T being the type the cursor is currently on.
So I have 2 things I still want to know:
How can I check if my IDeclaredElement is an implementation of a particular interface (ideally by specifying the full name in a string from config)?
How can I create a ITypeElement which is a generic type of a specific interface where I can set the generic type from my existing IDeclaredElements type, so I can then find classes which inherit this?
My existing code looks like this:
[ContextNavigationProvider]
public class CommandHandlerNavigationProvider : INavigateFromHereProvider
{
public IEnumerable<ContextNavigation> CreateWorkflow(IDataContext dataContext)
{
ICollection<IDeclaredElement> declaredElements = dataContext.GetData(DataConstants.DECLARED_ELEMENTS);
if (declaredElements != null || declaredElements.Any())
{
IDeclaredElement declaredElement = declaredElements.First();
if (IsCommand(declaredElement))
{
var solution = dataContext.GetData(JetBrains.ProjectModel.DataContext.DataConstants.SOLUTION);
yield return new ContextNavigation("This Command's &handler", null, NavigationActionGroup.Other, () => { GotToInheritor(solution,declaredElement); });
}
}
}
private void GotToInheritor(ISolution solution, IDeclaredElement declaredElement)
{
var inheritorsConsumer = new InheritorsConsumer();
SearchDomainFactory searchDomainFactory = solution.GetComponent<SearchDomainFactory>();
//How can I create the ITypeElement MyNameSpace.ICommandHandler<(ITypeElement)declaredElement> here?
solution.GetPsiServices().Finder.FindInheritors((ITypeElement)declaredElement, searchDomainFactory.CreateSearchDomain(solution, true), inheritorsConsumer, NullProgressIndicator.Instance);
}
private bool IsCommand(IDeclaredElement declaredElement)
{
//How can I check if my declaredElement is an implementation of ICommand here?
string className = declaredElement.ShortName;
return className.Contains("Command")
&& !className.Contains("CommandHandler");
}
}
Ok managed to work this out with a fair bit of pushing in the right direction from #CitizenMatt.
basically my solution looks like this (still needs some tidying up)
private static readonly List<HandlerMapping> HandlerMappings = new List<HandlerMapping>
{
new HandlerMapping("HandlerNavigationTest.ICommand", "HandlerNavigationTest.ICommandHandler`1", "HandlerNavigationTest"),
new HandlerMapping("HandlerNavTest2.IEvent", "HandlerNavTest2.IEventHandler`1", "HandlerNavTest2")
};
public IEnumerable<ContextNavigation> CreateWorkflow(IDataContext dataContext)
{
ICollection<IDeclaredElement> declaredElements = dataContext.GetData(DataConstants.DECLARED_ELEMENTS);
if (declaredElements != null && declaredElements.Any())
{
IDeclaredElement declaredElement = declaredElements.First();
ISolution solution = dataContext.GetData(JetBrains.ProjectModel.DataContext.DataConstants.SOLUTION);
ITypeElement handlerType = GetHandlerType(declaredElement);
if (handlerType != null)
{
yield return new ContextNavigation("&Handler", null, NavigationActionGroup.Other, () => GoToInheritor(solution, declaredElement as IClass, dataContext, handlerType));
}
}
}
private static ITypeElement GetHandlerType(IDeclaredElement declaredElement)
{
var theClass = declaredElement as IClass;
if (theClass != null)
{
foreach (IPsiModule psiModule in declaredElement.GetPsiServices().Modules.GetModules())
{
foreach (var handlerMapping in HandlerMappings)
{
IDeclaredType commandInterfaceType = TypeFactory.CreateTypeByCLRName(handlerMapping.HandledType, psiModule, theClass.ResolveContext);
ITypeElement typeElement = commandInterfaceType.GetTypeElement();
if (typeElement != null)
{
if (theClass.IsDescendantOf(typeElement))
{
IDeclaredType genericType = TypeFactory.CreateTypeByCLRName(handlerMapping.HandlerType, psiModule, theClass.ResolveContext);
ITypeElement genericTypeElement = genericType.GetTypeElement();
return genericTypeElement;
}
}
}
}
}
return null;
}
private static void GoToInheritor(ISolution solution, IClass theClass, IDataContext dataContext, ITypeElement genericHandlerType)
{
var inheritorsConsumer = new InheritorsConsumer();
var searchDomainFactory = solution.GetComponent<SearchDomainFactory>();
IDeclaredType theType = TypeFactory.CreateType(theClass);
IDeclaredType commandHandlerType = TypeFactory.CreateType(genericHandlerType, theType);
ITypeElement handlerTypeelement = commandHandlerType.GetTypeElement();
solution.GetPsiServices().Finder.FindInheritors(handlerTypeelement, searchDomainFactory.CreateSearchDomain(solution, true),
inheritorsConsumer, NullProgressIndicator.Instance);
var potentialNavigationPoints = new List<INavigationPoint>();
foreach (ITypeElement inheritedInstance in inheritorsConsumer.FoundElements)
{
IDeclaredType[] baseClasses = inheritedInstance.GetAllSuperTypes();
foreach (IDeclaredType declaredType in baseClasses)
{
if (declaredType.IsInterfaceType())
{
if (declaredType.Equals(commandHandlerType))
{
var navigationPoint = new DeclaredElementNavigationPoint(inheritedInstance);
potentialNavigationPoints.Add(navigationPoint);
}
}
}
}
if (potentialNavigationPoints.Any())
{
NavigationOptions options = NavigationOptions.FromDataContext(dataContext, "Which handler do you want to navigate to?");
NavigationManager.GetInstance(solution).Navigate(potentialNavigationPoints, options);
}
}
public class InheritorsConsumer : IFindResultConsumer<ITypeElement>
{
private const int MaxInheritors = 50;
private readonly HashSet<ITypeElement> elements = new HashSet<ITypeElement>();
public IEnumerable<ITypeElement> FoundElements
{
get { return elements; }
}
public ITypeElement Build(FindResult result)
{
var inheritedElement = result as FindResultInheritedElement;
if (inheritedElement != null)
return (ITypeElement) inheritedElement.DeclaredElement;
return null;
}
public FindExecution Merge(ITypeElement data)
{
elements.Add(data);
return elements.Count < MaxInheritors ? FindExecution.Continue : FindExecution.Stop;
}
}
And this allows me no navigate to multiple handlers if they exist. This currently relies on the interfaces for the handled type and the handler type being in the same assembly. But this seems reasonable enough for me at the moment.
How can I use Same Custom Validation Attribute Multiple Times on Same Field or simply enable AllowMultiple=true, for both server side and client side validation??
I have a following Custom Validation Attribute:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,
AllowMultiple = true, Inherited = true)]
public class RequiredIfAttribute : ValidationAttribute,IClientValidatable
{
public RequiredIfAttribute(string dependentProperties,
string dependentValues = "",
string requiredValue = "val")
{
}
}
Where in dependentProperties I can specify multiple dependant properties seperated by comma, in dependentValues I can specify for which values of dependant properties validation should process and finally in requiredValue I can specify expected value for the field to be validated.
In my model there are two properties LandMark, PinCode and I want to use validation as follows:
public string LandMark { get; set; }
[RequiredIf("LandMark","XYZ","500500")]
[RequiredIf("LandMark", "ABC", "500505")]
public string PinCode { get; set; }
The values here are just for example, as per it seems I can add the attribute multiple times and don't get any compile error, I have implemented TypeID in attribute and it works well from serverside if I remove client validation from it. But when I am implementing IClientValidatable on the attribute, it gives me an error:
"Validation type names in unobtrusive client validation rules must be unique."
Any help how can I solve it??
The Problem
Validation Attributes have two environments they can validate against:
Server
Client
Server Validation - Multiple Attributes Easy
If you have any attribute with:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class RequiredIfAttribute : ValidationAttribute
And have put it on your class property like this:
public class Client
{
public short ResidesWithCd { get; set; };
[RequiredIf(nameof(ResidesWithCd), new[] { 99 }, "Resides with other is required.")]
public string ResidesWithOther { get; set; }
}
Then anytime the Server goes to validate an object (ex. ModelState.IsValid), it will check every ValidationAttribute on each property and call .IsValid() to determine validity. This will work fine, even if AttributeUsage.AllowMultiple is set to true.
Client Validation - HTML Attribute Bottleneck
If you enable client side by implementing IClientValidatable like this:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "requiredif",
ErrorMessage = ErrorMessageString
};
modelClientValidationRule.ValidationParameters.Add("target", prop.PropName);
modelClientValidationRule.ValidationParameters.Add("values", prop.CompValues);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
Then ASP.NET will emit the following HTML when generated:
(As long as ClientValidationEnabled &
UnobtrusiveJavaScriptEnabled are enabled)
<input class="form-control" type="text" value=""
id="Client_CommunicationModificationDescription"
name="Client.CommunicationModificationDescription"
data-val="true"
data-val-requiredif="Communication Modification Description is required."
data-val-requiredif-target="CommunicationModificationCd"
data-val-requiredif-values="99" >
Data Attributes are the only vehicle we have for dumping rules into the client side validation engine which will search for any attributes on the page via a built in or custom adapter. And once part of the set of client side rules, it'll be able to determine the validity of each parsed rule with a built in or custom method.
So we can call jQuery Validate Unobtrusive to look for and parse these attributes by adding a custom adapter which will add a validation rule to the engine:
// hook up to client side validation
$.validator.unobtrusive.adapters.add('requiredif', ['target', 'values'], function (options) {
options.rules["requiredif"] = {
id: '#' + options.params.target,
values: JSON.parse(options.params.values)
};
options.messages['requiredif'] = options.message;
});
We can then tell that rule how function and determine validity by adding a custom method like this which will add a custom way to evaluate requiredif rules (as opposed to date rules or regex rules) which will rely on the parameters we loaded earlier through the adapter:
// test validity
$.validator.addMethod('requiredif', function (value, element, params) {
var targetHasCondValue = targetElHasValue(params.id, params.value);
var requiredAndNoValue = targetHasCondValue && !value; // true -> :(
var passesValidation = !requiredAndNoValue; // true -> :)
return passesValidation;
}, '');
Which all operates like this:
Solution
So, what have we learned? Well, if we want the same rule to appear multiple times on the same element, the adapter would have to see the exact set of rules multiple times per element, with no way to differentiate between each instance within multiple sets. Further, ASP.NET won't render the same attribute name multiple times since it's not valid html.
So, we either need to:
Collapse all the client side rules into a single mega attribute with all the info
Rename attributes with each instance number and then find a way to parse them in sets.
I'll explore Option One (emitting a single client side attribute), which you could do a couple ways:
Create a single Attribute that takes in multiple elements to validate on the server client
Keep multiple distinct server side attributes and then merge all attributes via reflection before emitting to the client
In either case you will have to re-write the client side logic (adapter/method) to take an array of values, instead of a single value at a time.
To we'll build/transmit a JSON serialized object that looks like this:
var props = [
{
PropName: "RoleCd",
CompValues: ["2","3","4","5"]
},
{
PropName: "IsPatient",
CompValues: ["true"]
}
]
Scripts/ValidateRequiredIfAny.js
Here's how we'll handle that in client side adapter / method:
// hook up to client side validation
$.validator.unobtrusive.adapters.add("requiredifany", ["props"], function (options) {
options.rules["requiredifany"] = { props: options.params.props };
options.messages["requiredifany"] = options.message;
});
// test validity
$.validator.addMethod("requiredifany", function (value, element, params) {
var reqIfProps = JSON.parse(params.props);
var anytargetHasValue = false;
$.each(reqIfProps, function (index, item) {
var targetSel = "#" + buildTargetId(element, item.PropName);
var $targetEl = $(targetSel);
var targetHasValue = elHasValue($targetEl, item.CompValues);
if (targetHasValue) {
anytargetHasValue = true;
return ;
}
});
var valueRequired = anytargetHasValue;
var requiredAndNoValue = valueRequired && !value; // true -> :(
var passesValidation = !requiredAndNoValue; // true -> :)
return passesValidation;
}, "");
// UTILITY METHODS
function buildTargetId(currentElement, targetPropName) {
// https://stackoverflow.com/a/39725539/1366033
// we are only provided the name of the target property
// we need to build it's ID in the DOM based on a couple assumptions
// derive the stacking context and depth based on the current element's ID/name
// append the target property's name to that context
// currentElement.name i.e. Details[0].DosesRequested
var curId = currentElement.id; // get full id i.e. Details_0__DosesRequested
var context = curId.replace(/[^_]+$/, ""); // remove last prop i.e. Details_0__
var targetId = context + targetPropName; // build target ID i.e. Details_0__OrderIncrement
// fail noisily
if ($("#" + targetId).length === 0)
console.error(
"Could not find id '" + targetId +
"' when looking for '" + targetPropName +
"' on originating element '" + curId + "'");
return targetId;
}
function elHasValue($el, values) {
var isCheckBox = $el.is(":checkbox,:radio");
var isChecked = $el.is(":checked");
var inputValue = $el.val();
var valueInArray = $.inArray(String(inputValue), values) > -1;
var hasValue = (!isCheckBox || isChecked) && valueInArray;
return hasValue;
};
Models/RequiredIfAttribute.cs
On the server side, we'll validate attributes like normal, but when we got to build the client side attributes, we'll look for all attributes and build one mega attribute
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Web.Helpers;
using System.Web.Mvc;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{
public PropertyNameValues TargetProp { get; set; }
public RequiredIfAttribute(string compPropName, string[] compPropValues, string msg) : base(msg)
{
this.TargetProp = new PropertyNameValues()
{
PropName = compPropName,
CompValues = compPropValues
};
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo compareProp = validationContext.ObjectType.GetProperty(TargetProp.PropName);
var compPropVal = compareProp.GetValue(validationContext.ObjectInstance, null);
string compPropValAsString = compPropVal?.ToString().ToLower() ?? "";
var matches = TargetProp.CompValues.Where(v => v == compPropValAsString);
bool needsValue = matches.Any();
if (needsValue)
{
if (value == null || value.ToString() == "" || value.ToString() == "0")
{
return new ValidationResult(FormatErrorMessage(null));
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
// at this point, who cares that we're on this particular instance - find all instances
PropertyInfo curProp = metadata.ContainerType.GetProperty(metadata.PropertyName);
RequiredIfAttribute[] allReqIfAttr = curProp.GetCustomAttributes<RequiredIfAttribute>().ToArray();
// emit validation attributes from all simultaneously, otherwise each will overwrite the last
PropertyNameValues[] allReqIfInfo = allReqIfAttr.Select(x => x.TargetProp).ToArray();
string allReqJson = Json.Encode(allReqIfInfo);
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "requiredifany",
ErrorMessage = ErrorMessageString
};
// add name for jQuery parameters for the adapter, must be LOWERCASE!
modelClientValidationRule.ValidationParameters.Add("props", allReqJson);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
}
public class PropertyNameValues
{
public string PropName { get; set; }
public string[] CompValues { get; set; }
}
Then we can bind that to our model by applying multiple attributes simultaneously:
[RequiredIf(nameof(RelationshipCd), new[] { 1,2,3,4,5 }, "Mailing Address is required.")]
[RequiredIf(nameof(IsPatient), new[] { "true" },"Mailing Address is required.")]
public string MailingAddressLine1 { get; set; }
Further Reading
ASP.NET MVC custom multiple fields validation by Stephen Muecke
Unobtrusive Client Validation in ASP.NET MVC 3 by Brad Wilson
Finally here I found the answer my-self.
Look at following article for solution
http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx
The link in the accepted answer (http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx) is buggy, and someone else has written an errata here which I would recommend reading first. The answer above does not handle inheritance.
I believe this alternate solution has some advantages (including support of inheritance), but remains far from perfect code - improvements appreciated.
this C# uses Json.NET and Stuart Leeks HTML Attribute provider
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using Newtonsoft.Json;
namespace DabTrial.Infrastructure.Validation
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public abstract class MultipleValidationAttribute : ValidationAttribute, IMetadataAware
{
private class Validation
{
public ICollection<string> ErrorMessage { get; set; }
public IDictionary<string, ICollection<object>> Attributes { get; set; }
}
private object _typeId = new object();
public const string attributeName = "multipleValidations";
public MultipleValidationAttribute()
{
}
public override object TypeId
{
get
{
return this._typeId;
}
}
public void OnMetadataCreated(ModelMetadata metadata)
{
Dictionary<string, Validation> allMultis;
if (metadata.AdditionalValues.ContainsKey(attributeName))
{
allMultis = (Dictionary<string, Validation>)metadata.AdditionalValues[attributeName];
}
else
{
allMultis = new Dictionary<string, Validation>();
metadata.AdditionalValues.Add(attributeName, allMultis);
}
foreach (var result in GetClientValidationRules(metadata))
{
if (allMultis.ContainsKey(result.ValidationType))
{
var thisMulti = allMultis[result.ValidationType];
thisMulti.ErrorMessage.Add(result.ErrorMessage);
foreach (var attr in result.ValidationParameters)
{
thisMulti.Attributes[attr.Key].Add(attr.Value);
}
}
else
{
var thisMulti = new Validation
{
ErrorMessage = new List<string>(),
Attributes = new Dictionary<string, ICollection<object>>()
};
allMultis.Add(result.ValidationType, thisMulti);
thisMulti.ErrorMessage.Add(result.ErrorMessage);
foreach (var attr in result.ValidationParameters)
{
var newList = new List<object>();
newList.Add(attr.Value);
thisMulti.Attributes.Add(attr.Key, newList);
}
}
}
}
public static IEnumerable<KeyValuePair<string, object>> GetAttributes(ModelMetadata metadata)
{
if (!metadata.AdditionalValues.ContainsKey(attributeName))
{
return null;
}
var returnVar = new List<KeyValuePair<string, object>>();
returnVar.Add(new KeyValuePair<string,object>("data-val", true));
var allMultis = (Dictionary<string, Validation>)metadata.AdditionalValues[attributeName];
foreach (var multi in allMultis)
{
string valName = "data-val-" + multi.Key;
returnVar.Add(new KeyValuePair<string,object>(valName, JsonConvert.SerializeObject(multi.Value.ErrorMessage)));
returnVar.AddRange(multi.Value.Attributes.Select(a=>new KeyValuePair<string,object>(valName + '-' + a.Key, JsonConvert.SerializeObject(a.Value))));
}
return returnVar;
}
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
ControllerContext context)
{
throw new NotImplementedException("This function must be overriden");
}
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata)
{
return GetClientValidationRules(metadata, null);
}
}
}
the Global.asax contains the code
HtmlAttributeProvider.Register((metadata) =>
{
return MultipleValidationAttribute.GetAttributes(metadata);
});
and the JavaScript (within a custom validators function)
function setMultiValidationValues(options, ruleName, values) {
var i = 0, thisRule;
for (; i < values.length; i++) {
thisRule = (i == 0) ? ruleName : ruleName + i;
options.messages[thisRule] = values[i].message;
delete values[i].message;
options.rules[thisRule] = values[i];
if (ruleName !== thisRule) {
(function addValidatorMethod() {
var counter = 0;
if (!$.validator.methods[ruleName]) {
if (++counter > 10) { throw new ReferenceError(ruleName + " is not defined"); }
setTimeout(addValidatorMethod, 100);
return;
}
if (!$.validator.methods[thisRule]) { $.validator.addMethod(thisRule, $.validator.methods[ruleName]); }
})();
}
}
}
function transformValidationValues(options) {
var rules = $.parseJSON(options.message),
propNames = [], p, utilObj,i = 0,j, returnVar=[];
for (p in options.params) {
if (options.params.hasOwnProperty(p)) {
utilObj = {};
utilObj.key = p;
utilObj.vals = $.parseJSON(options.params[p]);
propNames.push(utilObj);
}
}
for (; i < rules.length; i++) {
utilObj = {};
utilObj.message = rules[i];
for (j=0; j < propNames.length; j++) {
utilObj[propNames[j].key] = propNames[j].vals[i];
}
returnVar.push(utilObj);
}
return returnVar;
}
An example of its use is below:
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using System.Web.Mvc;
namespace DabTrial.Infrastructure.Validation
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class RegexCountAttribute : MultipleValidationAttribute
{
# region members
private string _defaultErrorMessageFormatString;
protected readonly string _regexStr;
protected readonly RegexOptions _regexOpt;
private int _minimumCount=0;
private int _maximumCount=int.MaxValue;
#endregion
#region properties
public int MinimumCount
{
get { return _minimumCount; }
set
{
if (value < 0) { throw new ArgumentOutOfRangeException(); }
_minimumCount = value;
}
}
public int MaximumCount
{
get { return _maximumCount; }
set
{
if (value < 0) { throw new ArgumentOutOfRangeException(); }
_maximumCount = value;
}
}
private string DefaultErrorMessageFormatString
{
get
{
if (_defaultErrorMessageFormatString == null)
{
_defaultErrorMessageFormatString = string.Format(
"{{0}} requires a {0}{1}{2} match(es) to regex {3}",
MinimumCount>0?"minimum of "+ MinimumCount:"",
MinimumCount > 0 && MaximumCount< int.MaxValue? " and " : "",
MaximumCount<int.MaxValue?"maximum of "+ MaximumCount:"",
_regexStr);
}
return _defaultErrorMessageFormatString;
}
set
{
_defaultErrorMessageFormatString = value;
}
}
#endregion
#region instantiation
public RegexCountAttribute(string regEx, string defaultErrorMessageFormatString = null, RegexOptions regexOpt = RegexOptions.None)
{
#if debug
if (minimumCount < 0) { throw new ArgumentException("the minimum value must be non-negative"); }
#endif
_regexStr = regEx;
DefaultErrorMessageFormatString = defaultErrorMessageFormatString;
_regexOpt = regexOpt;
}
#endregion
#region methods
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var instr = (string)value;
int matchCount = 0;
if (MinimumCount > 0 && instr != null)
{
Match match = new Regex(_regexStr,_regexOpt).Match(instr);
while (match.Success && ++matchCount < MinimumCount)
{
match = match.NextMatch();
}
if (MaximumCount != int.MaxValue)
{
while (match.Success && ++matchCount <= MaximumCount)
{
match = match.NextMatch();
}
}
}
if (matchCount >= MinimumCount && matchCount <=MaximumCount)
{
return ValidationResult.Success;
}
string errorMessage = GetErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
protected string GetErrorMessage(string displayName)
{
return ErrorMessage ?? string.Format(DefaultErrorMessageFormatString,
displayName,
MinimumCount);
}
private bool HasFlag(RegexOptions options, RegexOptions flag)
{
return ((options & flag) == flag);
}
private string RegexpModifier
{
get
{
string options = string.Empty;
if (HasFlag(_regexOpt, RegexOptions.IgnoreCase)) { options += 'i'; }
if (HasFlag(_regexOpt, RegexOptions.Multiline)) { options += 'm'; }
return options;
}
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata)
{
var returnVal = new ModelClientValidationRule {
ErrorMessage = GetErrorMessage(metadata.DisplayName),
ValidationType = "regexcount",
};
returnVal.ValidationParameters.Add("min",MinimumCount);
returnVal.ValidationParameters.Add("max",MaximumCount);
returnVal.ValidationParameters.Add("regex",_regexStr);
returnVal.ValidationParameters.Add("regexopt", RegexpModifier);
yield return returnVal;
}
#endregion
}
public class MinNonAlphanum : RegexCountAttribute
{
public MinNonAlphanum(int minimum) : base("[^0-9a-zA-Z]", GetDefaultErrorMessageFormatString(minimum))
{
this.MinimumCount = minimum;
}
private static string GetDefaultErrorMessageFormatString(int min)
{
if (min == 1)
{
return "{0} requires a minimum of {1} character NOT be a letter OR number";
}
return "{0} requires a minimum of {1} characters NOT be a letter OR number";
}
}
public class MinDigits : RegexCountAttribute
{
public MinDigits(int minimum) : base(#"\d", GetDefaultErrorMessageFormatString(minimum))
{
this.MinimumCount = minimum;
}
private static string GetDefaultErrorMessageFormatString(int min)
{
if (min == 1)
{
return "{0} requires a minimum of {1} character is a number";
}
return "{0} requires a minimum of {1} characters are numbers";
}
}
}
JavaScript:
$.validator.addMethod("regexcount", function (value, element, params) {
var matches = (value.match(params.regex)||[]).length
return matches >= params.min && matches <= params.max;
});
$.validator.unobtrusive.adapters.add("regexcount", ["min", "max", "regex", "regexopt"], function (options) {
var args = transformValidationValues(options), i=0;
for (; i < args.length; i++) {
args[i].regex = new RegExp(args[i].regex, args[i].regexopt);
delete args[i].regexopt;
}
setMultiValidationValues(options, "regexcount", args);
});