I have an Address class that is used for both a MailingAddress and BillingAddress property in my Model. I want the MailingAddress to be required, but not the BillingAddress, but am not seeing a way to do this with DataAnnotations.
If I were able to set the [Required] attribute on the MailingAddress property and somehow define the logic for how the Address class is supposed to handle the required logic, I feel like that would be a simple solution.
Any ideas?
If your question is how to use the Required attribute in your own logic, the answer is by use of reflection. Forgive me if that is not your question.
Get all properties from the type in question, then see if it is decorated with a RequiredAttribute or not.
class ParentClass
{
[Required]
public Address MailingAddress { get; set; }
public Address BillingAddress { get; set; }
}
(...)
Type t = typeof(ParentClass);
foreach (PropertyInfo p in t.GetProperties())
{
Attribute a = Attribute.GetCustomAttribute(p, typeof(RequiredAttribute));
if (a != null)
{
// The property is required, apply your logic
}
else
{
// The property is not required, apply your logic
}
}
Edit: Fixed a typo in code
Edit 2: Extended code example
This is just an odd quirk which popped into my head:
A simple solution might be to subclass Address to OptionalAddress.
I don't think the Required attributes would be inherited to the child class.
[AttributeUsage (Inherited = False)] also comes to mind if needed.
A more MVCish solution might be to implement a custom model binder (completely untested):
public override object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var address = base.BindModel(controllerContext, bindingContext) as Address;
if (bindingContext.ModelName.EndsWith("BillingAddress"))
{
foreach (PropertyInfo p in address.GetType().GetProperties())
{
Attribute a = Attribute.GetCustomAttribute(p, typeof(RequiredAttribute));
if (a != null
&& propertyInfo.GetValue(address, null) == null
&& bindingContext.ModelState[bindingContext.ModelName
+ "." + p.Name].Errors.Count == 1)
{
bindingContext.ModelState[bindingContext.ModelName + "." + p.Name].Errors.Clear();
}
}
return address;
}
Many options available at this previously asked question:
ASP.NET MVC Conditional validation
Do you need this validation done on the client side or not?
IValidateableObject will be used in conjunction with any of your existing attributes and can provide for the additional custom validation.
Related
I'm developing ASP.NET MVC appliation. I've found Fluent Validation great validation tool and it works, but with my current architecture it has one drawback. The validator does not care about Metadata. I'm using Metadata on seperate class for clarity.
Model
[MetadataType(typeof(DocumentEditMetadata))]
[Validator(typeof(DocumentValidator))]
public class DocumentEditModel
{
public string DocumentNumber { get; set; }
(etc...)
}
Metadata Model
public class DocumentEditMetadata
{
[Required]
[StringLength(50)]
[Display(ResourceType = typeof(Label), Name = "DocumentNumber")]
public string DocumentNumber { get; set; }
(etc...)
}
Can anyone point a solution? I need data annotations for localization of labels (hence the DisplayAttribute).
Think you need to write your own Display name resolver for fluent validation (guess this should be placed in your global.asax).
Caution
This solution is only trying to resolve the display name.
Your other "validation" attributes (Required, StringLength) should no more be used, as you will manage that with FluentValidation.
ValidatorOptions.DisplayNameResolver = (type, memberInfo, expression) =>
{
//this will get in this case, "DocumentNumber", the property name.
//If we don't find anything in metadata / resource, that what will be displayed in the error message.
var displayName = memberInfo.Name;
//we try to find a corresponding Metadata type
var metadataType = type.GetCustomAttribute<MetadataTypeAttribute>();
if (metadataType != null)
{
var metadata = metadataType.MetadataClassType;
//we try to find a corresponding property in the metadata type
var correspondingProperty = metadata.GetProperty(memberInfo.Name);
if (correspondingProperty != null)
{
//we try to find a display attribute for the property in the metadata type
var displayAttribute = correspondingProperty.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null)
{
//finally we got it, try to resolve the name !
displayName = displayAttribute.GetName();
}
}
}
return displayName ;
};
Personal point of view
By the way, if you just use Metadata classes for clarity, don't use them !
It may be a solution if you have no choice (when entity classes are generated from an edmx and you really want to manage the display names this way), but I would really avoid them if it's not necessary.
public class CreateHireViewModel
{
[Display(Name = nameof(CreateHireViewModel.Title), ResourceType = typeof(Resource.HireResource.Hire))]
public string Title { get; set; }
}
public class CreateHireViewModelValidator : AbstractValidator<CreateHireViewModel>
{
public CreateHireViewModelValidator(IStringLocalizer<Resource.HireResource.Hire> l)
{
RuleFor(x => x.Title).NotEmpty().WithName(l[nameof(CreateHireViewModel.Title)]);
RuleFor(x => x.Title).Length(3, 50).WithName(l[nameof(CreateHireViewModel.Title)]);
}
}
I have a model where I am using DataAnnotations to perform validation, such as
public class OrderDTO
{
[Required]
public int Id { get; set; }
[Required]
public Decimal Amount { get; set; }
}
Then I am checking the ModelState in each request to make sure that the JSON is valid.
However, I am having trouble for number properties such as Amount above. Even though it is set as [Required], if it's not included in the JSON it will skip the ModelState validation because it is automatically defaulted to 0 instead of null, so the model will seem valid even though it isn't.
An easy way to 'fix' this is to set all the number properties as nullable (int?, Decimal?). If I do this, the defaulting to 0 doesn't happen, but I don't like this as a definitive solution as I need to change my model.
Is there a way to set the properties to null if they are not part of the JSON?
Because Decimal is a non-nullable type so you cannot do that.
You need Decimal? to bind null value.
You have to use a nullable type. Since a non-nullable value, as you know, cannot be null then it will use 0 as a default value and therefore appear to have a value and always pass the validation.
As you have said it has to be null for the validation to work and therefore be nullable. Another option could be to write your own validation attribute but this could then cause a problem as you would most likely be saying if is null or 0 then not valid, a big issue when you want to have 0 as an accepted value because you then need another way of deciding when 0 is and isn't valid.
Example for custom validation, not specific to this case.
Web API custom validation to check string against list of approved values
A further option could be to add another property that is nullable and provides the value to the non-nullable property. Again, this could cause issues with the 0 value. Here is an example with the Id property, your json will now need to send NullableId rather than Id.
public class OrderDTO
{
//Nullable property for json and validation
[Required]
public int? NullableId {
get {
return Id == 0 ? null : Id; //This will always return null if Id is 0, this can be a problem
}
set {
Id = value ?? 0; //This means Id is 0 when this is null, another problem
}
}
//This can be used as before at any level between API and the database
public int Id { get; set; }
}
As you say another option is to change the model to nullable values through the whole stack.
Finally you could look at having an external model coming into the api with nullable properties and then map it to the current model, either manually or using something like AutoMapper.
I agree with others that Decimal being a non Nullable type cannot be assigned with a null value. Moreover, Required attribute checks for only null, empty string and whitespaces. So for your specific requirement you can use CustomValidationAttribute and you can create a custom Validation Type to do the "0" checking on Decimal properties.
There is no way for an int or Decimal to be null. That is why the nullables where created.
You have several options [Edit: I just realized that you are asking for Web-API specifically and in this case I believe the custom binder option would be more complex from the code I posted.]:
Make the fields nullable in your DTO
Create a ViewModel with nullable types, add the required validation attributes on the view model and map this ViewModel to your DTO (maybe using automapper or something similar).
Manually validate the request (bad and error prone thing to do)
public ActionResult MyAction(OrderDTO order)
{
// Validate your fields against your possible sources (Request.Form,QueryString, etc)
if(HttpContext.Current.Request.Form["Ammount"] == null)
{
throw new YourCustomExceptionForValidationErrors("Ammount was not sent");
}
// Do your stuff
}
Create a custom binder and do the validation there:
public class OrderModelBinder : DefaultModelBinder
{
protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor, object value)
{
if ((propertyDescriptor.PropertyType == typeof(DateTime) && value == null) ||
(propertyDescriptor.PropertyType == typeof(int) && value == null) ||
(propertyDescriptor.PropertyType == typeof(decimal) && value == null) ||
(propertyDescriptor.PropertyType == typeof(bool) && value == null))
{
var modelName = string.IsNullOrEmpty(bindingContext.ModelName) ? "" : bindingContext.ModelName + ".";
var name = modelName + propertyDescriptor.Name;
bindingContext.ModelState.AddModelError(name, General.RequiredField);
}
return base.OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, value);
}
}
And register your binder to your model using one of the techniques described in the following answer: https://stackoverflow.com/a/13749124/149885
For example:
[ModelBinder(typeof(OrderBinder))]
public class OrderDTO
{
[Required]
public int Id { get; set; }
[Required]
public Decimal Amount { get; set; }
}
I am using HttpPatch to partially update an object. To get that working I am using Delta and Patch method from OData (mentioned here: What's the currently recommended way of performing partial updates with Web API?). Everything seems to be working fine but noticed that mapper is case sensitive; when the following object is passed the properties are getting updated values:
{
"Title" : "New title goes here",
"ShortDescription" : "New text goes here"
}
But when I pass the same object with lower or camel-case properties, Patch doesn't work - new value is not going through, so it looks like there is a problem with deserialisation and properties mapping, ie: "shortDescription" to "ShortDescription".
Is there a config section that will ignore case sensitivity using Patch?
FYI:
On output I have camel-case properties (following REST best practices) using the following formatter:
//formatting
JsonSerializerSettings jss = new JsonSerializerSettings();
jss.ContractResolver = new CamelCasePropertyNamesContractResolver();
config.Formatters.JsonFormatter.SerializerSettings = jss;
//sample output
{
"title" : "First",
"shortDescription" : "First post!"
}
My model classes however are follwing C#/.NET formatting conventions:
public class Entry {
public string Title { get; set;}
public string ShortDescription { get; set;}
//rest of the code omitted
}
Short answer, No there is no config option to undo the case sensitiveness (as far as i know)
Long answer: I had the same problem as you today, and this is how i worked around it.
I found it incredibly annoying that it had to be case sensitive, thus i decided to do away with the whole oData part, since it is a huge library that we are abusing....
An example of this implementation can be found at my github github
I decided to implement my own patch method, since that is the muscle that we are actually lacking. I created the following abstract class:
public abstract class MyModel
{
public void Patch(Object u)
{
var props = from p in this.GetType().GetProperties()
let attr = p.GetCustomAttribute(typeof(NotPatchableAttribute))
where attr == null
select p;
foreach (var prop in props)
{
var val = prop.GetValue(this, null);
if (val != null)
prop.SetValue(u, val);
}
}
}
Then i make all my model classes inherit from *MyModel*. note the line where i use *let*, i will excplain that later. So now you can remove the Delta from you controller action, and just make it Entry again, as with the put method. e.g.
public IHttpActionResult PatchUser(int id, Entry newEntry)
You can still use the patch method the way you used to:
var entry = dbContext.Entries.SingleOrDefault(p => p.ID == id);
newEntry.Patch(entry);
dbContext.SaveChanges();
Now, let's get back to the line
let attr = p.GetCustomAttribute(typeof(NotPatchableAttribute))
I found it a security risk that just any property would be able to be updated with a patch request. For example, you might now want the an ID to be changeble by the patch. I created a custom attribute to decorate my properties with. the NotPatchable attribute:
public class NotPatchableAttribute : Attribute {}
You can use it just like any other attribute:
public class User : MyModel
{
[NotPatchable]
public int ID { get; set; }
[NotPatchable]
public bool Deleted { get; set; }
public string FirstName { get; set; }
}
This in this call the Deleted and ID properties cannot be changed though the patch method.
I hope this solve it for you as well. Do not hesitate to leave a comment if you have any questions.
I added a screenshot of me inspecting the props in a new mvc 5 project. As you can see the Result view is populated with the Title and ShortDescription.
It can be done quite easily with a custom contract resolver that inherits CamelCasePropertyNamesContractResolver and implementing CreateContract method that look at concrete type for delta and gets the actual property name instead of using the one that comes from json. Abstract is below:
public class DeltaContractResolver : CamelCasePropertyNamesContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
// This class special cases the JsonContract for just the Delta<T> class. All other types should function
// as usual.
if (objectType.IsGenericType &&
objectType.GetGenericTypeDefinition() == typeof(Delta<>) &&
objectType.GetGenericArguments().Length == 1)
{
var contract = CreateDynamicContract(objectType);
contract.Properties.Clear();
var underlyingContract = CreateObjectContract(objectType.GetGenericArguments()[0]);
var underlyingProperties =
underlyingContract.CreatedType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in underlyingContract.Properties)
{
property.DeclaringType = objectType;
property.ValueProvider = new DynamicObjectValueProvider()
{
PropertyName = this.ResolveName(underlyingProperties, property.PropertyName),
};
contract.Properties.Add(property);
}
return contract;
}
return base.CreateContract(objectType);
}
private string ResolveName(PropertyInfo[] properties, string propertyName)
{
var prop = properties.SingleOrDefault(p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
if (prop != null)
{
return prop.Name;
}
return propertyName;
}
}
Update (21st Sept 2016) - Thanks to Digbyswift for commenting that this solution still works in MVC5 also.
Update (30th April 2012) - Note to people stumbling across this question from searches etc - the accepted answer is not how I ended up doing this - but I left it accepted because it might have worked in some cases. My own answer contains the final solution I used, which is reusable and will apply to any project.
It's also confirmed to work in v3 and v4 of the MVC framework.
I have the following model type (the names of the class and its properties have been changed to protect their identities):
public class MyExampleModel
{
public string[] LongPropertyName { get; set; }
}
This property is then bound to a bunch (>150) of check boxes, where each one's input name is of course LongPropertyName.
The form submits to url with an HTTP GET, and say the user selects three of those checkboxes - the url will have the query string ?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c
Big problem then is that if I select all (or even just over half!) the checkboxes, I exceed the maximum query string length enforced by the request filter on IIS!
I do not want to extend that - so I want a way to trim down this query string (I know I can just switch to a POST - but even so I still want to minimize the amount of fluff in the data sent by the client).
What I want to do is have the LongPropertyName bound to simply 'L' so the query string would become ?L=a&L=b&L=c but without changing the property name in code.
The type in question already has a custom model binder (deriving from DefaultModelBinder), but it's attached to its base class - so I don't want to put code in there for a derived class. All the property binding is currently performed by the standard DefaultModelBinder logic, which I know uses TypeDescriptors and Property Descriptors etc from System.ComponentModel.
I was kinda hoping that there might be an attribute I could apply to the property to make this work - is there? Or should I be looking at implementing ICustomTypeDescriptor?
In response to michaelalm's answer and request - here's what I've ended up doing. I've left the original answer ticked mainly out of courtesy since one of the solutions suggested by Nathan would have worked.
The output of this is a replacement for DefaultModelBinder class which you can either register globally (thereby allowing all model types to take advantage of aliasing) or selectively inherit for custom model binders.
It all starts, predictably with:
/// <summary>
/// Allows you to create aliases that can be used for model properties at
/// model binding time (i.e. when data comes in from a request).
///
/// The type needs to be using the DefaultModelBinderEx model binder in
/// order for this to work.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class BindAliasAttribute : Attribute
{
public BindAliasAttribute(string alias)
{
//ommitted: parameter checking
Alias = alias;
}
public string Alias { get; private set; }
}
And then we get this class:
internal sealed class AliasedPropertyDescriptor : PropertyDescriptor
{
public PropertyDescriptor Inner { get; private set; }
public AliasedPropertyDescriptor(string alias, PropertyDescriptor inner)
: base(alias, null)
{
Inner = inner;
}
public override bool CanResetValue(object component)
{
return Inner.CanResetValue(component);
}
public override Type ComponentType
{
get { return Inner.ComponentType; }
}
public override object GetValue(object component)
{
return Inner.GetValue(component);
}
public override bool IsReadOnly
{
get { return Inner.IsReadOnly; }
}
public override Type PropertyType
{
get { return Inner.PropertyType; }
}
public override void ResetValue(object component)
{
Inner.ResetValue(component);
}
public override void SetValue(object component, object value)
{
Inner.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return Inner.ShouldSerializeValue(component);
}
}
This proxies a 'proper' PropertyDescriptor that is normally found by the DefaultModelBinder but presents its name as the alias.
Next we have the new model binder class:
UPDATED WITH #jsabrooke's suggestion below
public class DefaultModelBinderEx : DefaultModelBinder
{
protected override System.ComponentModel.PropertyDescriptorCollection
GetModelProperties(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var toReturn = base.GetModelProperties(controllerContext, bindingContext);
List<PropertyDescriptor> additional = new List<PropertyDescriptor>();
//now look for any aliasable properties in here
foreach (var p in
this.GetTypeDescriptor(controllerContext, bindingContext)
.GetProperties().Cast<PropertyDescriptor>())
{
foreach (var attr in p.Attributes.OfType<BindAliasAttribute>())
{
additional.Add(new AliasedPropertyDescriptor(attr.Alias, p));
if (bindingContext.PropertyMetadata.ContainsKey(p.Name)
&& !string.Equals(p.Name, attr.Alias, StringComparison.OrdinalIgnoreCase)))
{
bindingContext.PropertyMetadata.Add(
attr.Alias,
bindingContext.PropertyMetadata[p.Name]);
}
}
}
return new PropertyDescriptorCollection
(toReturn.Cast<PropertyDescriptor>().Concat(additional).ToArray());
}
}
And, then technically, that's all there is to it. You can now register this DefaultModelBinderEx class as the default using the solution posted as the answer in this SO: Change the default model binder in asp.net MVC, or you can use it as a base for your own model binder.
Once you've selected your pattern for how you want the binder to kick in, you simply apply it to a model type as follows:
public class TestModelType
{
[BindAlias("LPN")]
//and you can add multiple aliases
[BindAlias("L")]
//.. ad infinitum
public string LongPropertyName { get; set; }
}
The reason I chose this code was because I wanted something that would work with custom type descriptors as well as being able to work with any type. Equally, I wanted the value provider system to be used still in sourcing the model property values. So I've changed the meta data that the DefaultModelBinder sees when it starts binding. It's a slightly more long-winded approach - but conceptually it's doing at the meta data level exactly what you want it to do.
One potentially interesting, and slightly annoying, side effect will be if the ValueProvider contains values for more than one alias, or an alias and the property by it's name. In this case, only one of the retrieved values will be used. Difficult to think of a way of merging them all in a type-safe way when you're just working with objects though. This is similar, though, to supplying a value in both a form post and query string - and I'm not sure exactly what MVC does in that scenario - but I don't think it's recommended practise.
Another problem is, of course, that you must not create an alias that equals another alias, or indeed the name of an actual property.
I like to apply my model binders, in general, using the CustomModelBinderAttribute class. The only problem with this can be if you need to derive from the model type and change it's binding behaviour - since the CustomModelBinderAttribute is inherited in the attribute search performed by MVC.
In my case this is okay, I'm developing a new site framework and am able to push new extensibility into my base binders using other mechanisms to satisfy these new types; but that won't be the case for everybody.
You can use the BindAttribute to accomplish this.
public ActionResult Submit([Bind(Prefix = "L")] string[] longPropertyName) {
}
Update
Since the 'longPropertyName' parameter is part of the model object, and not an independent parameter of the controller action, you have a couple of other choices.
You could keep the model and the property as independent parameters to your action and then manually merge the data together in the action method.
public ActionResult Submit(MyModel myModel, [Bind(Prefix = "L")] string[] longPropertyName) {
if(myModel != null) {
myModel.LongPropertyName = longPropertyName;
}
}
Another option would be implementing a custom Model Binder that performs the parameter value assignment (as above) manually, but that is most likely overkill. Here's an example of one, if you're interested: Flags Enumeration Model Binder.
would this be a solution similar to yours Andras? i hope you could post your answer as well.
controller method
public class MyPropertyBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
for (int i = 0; i < propertyDescriptor.Attributes.Count; i++)
{
if (propertyDescriptor.Attributes[i].GetType() == typeof(BindingNameAttribute))
{
// set property value.
propertyDescriptor.SetValue(bindingContext.Model, controllerContext.HttpContext.Request.Form[(propertyDescriptor.Attributes[i] as BindingNameAttribute).Name]);
break;
}
}
}
}
Attribute
public class BindingNameAttribute : Attribute
{
public string Name { get; set; }
public BindingNameAttribute()
{
}
}
ViewModel
public class EmployeeViewModel
{
[BindingName(Name = "txtName")]
public string TestProperty
{
get;
set;
}
}
then to use the Binder in the controller
[HttpPost]
public ActionResult SaveEmployee(int Id, [ModelBinder(typeof(MyPropertyBinder))] EmployeeViewModel viewModel)
{
// do stuff here
}
the txtName form value should be set to the TestProperty.
This should probably be a shorter comment on Andras Zoltan's answer but don't have enough reputation, sorry.
Thanks for the solution, I've just used it and it still works great! However, some of my properties have an alias with the same name, but different case e.g.
[BindAlias("signature")]
public string Signature { get; set; }
These throw an error when the custom model binder tries to add the aliases to the
PropertyMetadata dictionary, as their main property name versions have already been added by the base model binder, and the model binding is case-insensitive.
To solve this, just do a case insensitive check -
replace
if (bindingContext.PropertyMetadata.ContainsKey(p.Name))
with
if (bindingContext.PropertyMetadata.ContainsKey(p.Name)
&& !string.Equals(p.Name, attr.Alias, StringComparison.OrdinalIgnoreCase))
So I've spent most of the day trying to figure out why I couldn't get this to work. Since I'm making my calls from a System.Web.Http.ApiController turns out that you can't use the DefaultPropertyBinder solution as mentioned above but instead must us an IModelBinder class.
the class that I've wound up writing to replace #AndreasZoltan's foundational work as written above is as follows:
using System.Reflection;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using QueryStringAlias.Attributes;
namespace QueryStringAlias.ModelBinders
{
public class AliasModelBinder : IModelBinder
{
private bool TryAdd(PropertyInfo pi, NameValueCollection nvc, string key, ref object model)
{
if (nvc[key] != null)
{
try
{
pi.SetValue(model, Convert.ChangeType(nvc[key], pi.PropertyType));
return true;
}
catch (Exception e)
{
Debug.WriteLine($"Skipped: {pi.Name}\nReason: {e.Message}");
}
}
return false;
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Type bt = bindingContext.ModelType;
object model = Activator.CreateInstance(bt);
string QueryBody = actionContext.Request.Content.ReadAsStringAsync().Result;
NameValueCollection nvc = HttpUtility.ParseQueryString(QueryBody);
foreach (PropertyInfo pi in bt.GetProperties())
{
if (TryAdd(pi, nvc, pi.Name, ref model))
{
continue;
};
foreach (BindAliasAttribute cad in pi.GetCustomAttributes<BindAliasAttribute>())
{
if (TryAdd(pi, nvc, cad.Alias, ref model))
{
break;
}
}
}
bindingContext.Model = model;
return true;
}
}
}
In order to ensure that this runs as part of a WebAPI call you must also add config.BindParameter(typeof(TestModelType), new AliasModelBinder()); in the Regiser portion of your WebApiConfig.
If you are using this method, you also must remove [FromBody] from your method signature.
[HttpPost]
[Route("mytestendpoint")]
[System.Web.Mvc.ValidateAntiForgeryToken]
public async Task<MyApiCallResult> Signup(TestModelType tmt) // note that [FromBody] does not appear in the signature
{
// code happens here
}
Note that this work builds on the answer above, using the QueryStringAlias samples.
At the moment this would likely fail in the case where TestModelType had complex nested types. Ideally there are a few other things:
handle complex nested types robustly
enable an attribute on the class to activate the IModelBuilder as opposed to in the registration
enable the same IModelBuilder to work in both Controllers and ApiControllers
But for now I'm satisfied with this for my own needs. Hopefully someone finds this piece useful.
I'm writing a PropertiesMustMatch validation attribute that can take a string property name as a parameter. I'd like it to find the corresponding property by name on that object and do a basic equality comparison. What's the best way to access this through reflection?
Also, I checked out the Validation application block in the Enterprise Library and decided its PropertyComparisonValidator was way too intense for what we need.
UPDATE: For further clarification (to provide some context), the goal is simply validation that enforces field matching (e.g., password verification). We'd like it to work with property-level attribute data annotations that inherit from the ValidationAttribute class, if possible.
UPDATE: In case anyone is curious, I ended up solving the actual business problem through tweaking code provided as an answer to this question
You can't, basically. The code that checks the object for the presence of the attribute must also assume responsibility for telling any code which type/object it was looking at. You can't obtain any additional metadata from within an attribute.
You cannot do that. See also this question. Try to change the logic to work with the object, checking its attributes, not vice versa. You can also provide more information about your task, not just this narrow question.
You can something like this.
//target class
public class SomeClass{
[CustomRequired(ErrorMessage = "{0} is required", ProperytName = "DisplayName")]
public string Link { get; set; }
public string DisplayName { get; set; }
}
//custom attribute
public class CustomRequiredAttribute : RequiredAttribute, IClientValidatable
{
public string ProperytName { get; set; }
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var propertyValue = "Value";
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, context.Controller.ViewData.Model.GetType());
var property = parentMetaData.FirstOrDefault(p => p.PropertyName == ProperytName);
if (property != null)
propertyValue = property.Model.ToString();
yield return new ModelClientValidationRule
{
ErrorMessage = string.Format(ErrorMessage, propertyValue),
ValidationType = "required"
};
}
}