I have an object which has properties decorated with Validation attributes. My validation is working correctly, however I need an additional method which performs string manipulation based on those attributes.
Consider the following object:
public class Pupil
{
/// <summary>
///
/// </summary>
public Pupil()
{
}
[NotNullValidator]
[StringLengthValidator(0, 54, MessageTemplate = "Invalid value '{0}' for {1}, Max length: {5}")]
public string Name{ get; set; }
[NotNullValidator]
[StringLengthValidator(0, 2, MessageTemplate = "Invalid value '{0}' for {1}, Max length: {5}")]
public string Gender{ get; set; }
}
I want to be able to manipulate the "Name" based on the StringLengthValidator attribute and its arguments. For example:
///Takes a Pupil object in
public static void DoManipulation(object value)
{
foreach(var property in value.GetType().GetProperties())
{
if(property.Name == "Name")
{
var att = property.GetCustomAttributes(typeof(StringLengthValidator), false);
var length = ((StringLengthValidator)att[0]).UpperBound;
}
}
}
The value of "length" is coming up as null, rather than 54. How do I get the value out?
Hopefully this makes sense, thanks.
A
This works for me, are you getting the same StringLengthValidator attribute that you think you are? (is this your custom class or the one from Enterprise Lib?
In my case, I created a custom class StringLengthValidator
The idea behind all this is that the value 54 can be changed, right? Otherwise you could just hard code 54.
Take a look on how to contol validation in the web.config with the tag so you can add the 54 to the web.config and read it from your application
Here is an example, look for the first approach, Rule sets in Configuration
As I was looking for it today with my co-worker and we did not find a proper solution to our problem, here is the answer, for the next one who is in trouble.
The attribute name is StringLengthValidator, but if you check at the class name it is StringLengthValidatorAttribute, so in order to get the proper attribute, you need to call the function GetCustomAttributes this way :
property.GetCustomAttributes(typeof(StringLengthValidatorAttribute), false)
This will fetch correctly the attribute and then you will be able to get the value of UpperBound
Related
I am working with Post endpoint in C# and I want to make all fields optional, but also want to update it to null/empty list. I know this is very weird requirement. I can set int and string data types to -1 or some default strings, but I am finding difficulties for List or any list of data.
I can put any extra flag for conditional update, but this is increasing number of properties in request body.
Could anyone suggest me solution for this case, if anyone has done similar problem. I know solution will be going to tricky one.
Sorry for the delay.
Let me try to explain it with more code.
First of all, we do a generic method that will fill a generic object with data extracted from a JSON string.
public static void FillObjectFromJson<T>(T objectToFill, string objectInJson)
where T : class
{
// Check parameters
if (objectToFill == null) throw new ArgumentNullException(nameof(objectToFill));
if (string.IsNullOrEmpty(objectInJson)) throw new ArgumentNullException(nameof(objectInJson));
// Deserialize in a typed object (helpful for the type converter)
var typed = JsonConvert.DeserializeObject<T>(objectInJson);
// Deserialize in an expando object (for check properties)
var expando = JsonConvert.DeserializeObject<ExpandoObject>(objectInJson);
// Converts the expando to dictionary for check all given properties
var dictionary = (IDictionary<string, object>)expando;
// Read all properties of the given object (the only one you can assign)
foreach (var property in typeof(T).GetProperties().Where(x => x.CanWrite))
{
// If dictionary contains the property, it was in the JSON string,
// so we have to replace it
if (dictionary.ContainsKey(property.Name))
{
var propValue = property.GetValue(typed);
property.SetValue(objectToFill, propValue);
}
}
}
For the performance it's not the best, since we deserialize the same JSON twice:
The first time into a typed object
The second time to an ExpandoObject type
I did it because we can have some problems with property types. In this example, in the ExpandoObject the Age property will be in of type int64 instead of int32. So, if we want to deserialize the JSON only once, then we need to cast properties.
Feel free to change this code as you prefer.
Now that we have a generic method, try to use it in an example.
First of all, we need a class to use:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
// This method is used only for this example, just for write data to the console
public override string ToString()
{
return $"Name: {Name} - Age: {Age}";
}
}
Then we need a method that will extract the data to update from storage. In this example we will simply back an object:
public static Person ReadPersonFromStorage()
{
return new Person
{
Name = "Name from storage",
Age = 44
};
}
Finally, we can write our test method:
// Example of JSON with explicit name (so we have to set it as null)
var personWithNameJson = "{ \"Name\" : null, \"Age\" : 24 }";
// Read the original value from the storage
var person = ReadPersonFromStorage();
Console.WriteLine(person); // We have the name
// Fills from JSON (it will replace the Name since it's in the JSON)
FillObjectFromJson(person, personWithNameJson);
Console.WriteLine(person); // The name is set to null
// Example of JSON without explicit name (so you have to leave it with the original value)
var personWithoutNameJson = "{ \"Age\" : 24 }";
// Read the original value from the storage
var otherPerson = ReadPersonFromStorage();
Console.WriteLine(otherPerson); // We have the name
// Fills from JSON (it won't replace the Name since it's NOT in the JSON)
FillObjectFromJson(otherPerson, personWithoutNameJson);
Console.WriteLine(otherPerson); // We still have the name since it's not in the JSON
I hope it's more clear what I mean and, even better, that i would help you.
PS: For this sample I used Newtonsoft for the JSON deserialization.
If you're in control of both client and server, you can manage it using the format of the request.
Suppose you have an object like this:
public class Customer {
public string Name { get; set; }
public int? Age { get; set; }
}
Then you can manage two kind of request. I will explain it using JSON format.
{
"Name": null,
"Age": 12
}
In this case, the request contains the value "Name", so you will set it to null in the database (or any other storage)
{
"Age": 12
}
In this case, the request does not contain the value "Name", so ti means it hasn't to be changed.
For the JSON format, both request are the same (Name = null). But in your code they will work in different way.
Of course:
It's not a standard, so you have to do it by yourself (JSON serializer/deserializer won't manage this scenario)
Since it's not standard, better if you manage both client and server side. Ask people to use your server if you don't follow standard rules can be a trick
Can someone tell about passing input for WebApi for number data types i.e int, long etc.
public class Emp
{
public int Id { get; set; }
public long Volume { get; set; }
}
Input set 1:
{
"Id" : "1",
"Volume" : "200"
}
this is working without model validation error.
Input set 2:
{
"Id" : "1.2",
"Volume" : "200.5"
}
Model validation failing as not able to convert values.
Error : {"Error converting value \"200.5\" to type 'System.Int64'. Path 'Volume', line 2, position 14."}
Input set 3:
{
"Id" : 1.2,
"Volume" : 200.5
}
It not gives any model validation failure, but the values mapped to C# object is Id = 1, Volume = 200.
But here also, I want model validation error as I'm passing decimal input. It should allow only whole numbers.
So what is the right pattern to send api input?
What is the right way to get model validation error when passing decimal point values to int and long data type.
Using int & long will make those values rounded to nearest integer value
How to override this to give model validation errors?
You can use the JsonConverter attribute on properties to define a custom json converter for it.
public class Emp
{
[JsonConverter(typeof(MyCustomIntConverter))
public int Id { get; set; }
public long Volume { get; set; }
}
public class MyCustomIntConverter : JsonConverter<int>
{
//implement here
}
There is a way to archive what you want, but isn't a easy way.
#Saif answer is pointing to the right way.
You must change you model to avoid unwanted framework behaviors and get the raw values, for example, change the type to double or string.
Then you have to validade the arguments for yourself inside your method. If some argument is not right, you must create a model validation errro.
In mvc core, you can create a model validation error like this (in mvc should be something similar):
ModelState.AddModelError(nameof(emp.Volume), $"Error converting value {emp.Volume} to long");
This is the spiritual successor to my previous question Web API attribute routing and validation - possible?, which I think was too general to answer. Most of those issues are solved, but the default value question remains.
Basically I have solved many pieces of the puzzle. I have this:
[HttpGet]
[Route("test/{id}"]
public IHttpActionResult RunTest([FromUri]TestRequest request)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
return Ok();
}
My TestRequest class:
public class TestRequest
{
public string id { get; set; }
[DefaultValue("SomethingDefault")]
public string something { get; set; }
}
The problem is that if no parameter is in the query string for something, the model is "valid" and yet something is null.
If I specify a blank value for something (i.e. GET test/123?something=), then the default value comes into play, and the model is valid again.
Why is this? How can I get a default value into my model here? As a bonus, why is it when a parameter is not specified, the default value is not used, but when a blank string is explicitly specific, the default value is used?
(I've been trawling through the ASP.NET stack source code and am knee-deep in model binders and binding contexts. But my best guess can't be right - it looks like the DefaultValueAttribute is used only if the parameter value is null. But that's not the case here)
You need to initialize the default value in the constructor for your Model:
public class TestRequest
{
public TestRequest()
{
this.something = "SomethingDefault";
}
public string id { get; set; }
[DefaultValue("SomethingDefault")]
public string something { get; set; }
}
Update:
With C# 6, you don't need to initialize it in the constructor anymore. You can assign the default value to the property directly:
public class TestRequest
{
public string id { get; set; }
[DefaultValue("SomethingDefault")]
public string something { get; set; } = "SomethingDefault";
}
As documentation of the DefaultValueAttribute states:
Note
A DefaultValueAttribute will not cause a member to be
automatically initialized with the attribute's value. You must set the
initial value in your code.
In the case where you're providing no value for your something property, the property is initialized and the ModelBinder doesn't have a value to assign to it and thus the property defaults to its default value.
Specifying the default in the constructor works for when no parameter is specified at all, but when a blank string is specified, null is put into the field instead.
As such, adding [DefaultValue("")] actually worked the best - when a blank string was specified, a blank string was passed in. Then the constructor can specify default values for when the parameter is missing.
To get around this, I've created PreserveBlankStringAttribute, derives from DefaultValueAttribute which is equivalent to [DefaultValue("")].
I would very much welcome a better answer than this, please.
I'm facing little stranger issue with Web API controller. I have a collection which is being passed in an action of api controller. Object being used is collection is having 4 properties.
My action is able to accept collection parameter when it's properties are in specific order. See below :-
[HttpPost]
public ForexRates UpdateRates([FromBody] Rates rates)
{
// TODO: Obviously code :)
return rates;
}
This code is being place in API controller & calling from Postman. See below:-
<rates>
<rate>
<id>fefef</id>
<rate>35353.333</rate>
<series>dfefge</series>
<series-order>sfefefef</series-order>
</rate></rates>
If I change the order of the properties I started getting null value in my action. Can some one please explain this :)
Models
public class Rate
{
public string Id { get; set; }
public string Date { get; set; }
public double Rate { get; set; }
}
public class Rates : Collection<ForexRate>
{
}
You will need to control the order with which your XML is serialized. Use XmlElementAttribute and specify the Order.
There is a similar question here
FYI, I suppose there is no way for you to change the order of the properties, while you supply from PostMan to your WebApi service. You will need to follow the exact order.
If you don't wanna do that, then pass this Xml as a string parameter and then parse it inside a method.
The default binder can have issues when the same name is used in different places during binding.
In your case you've got Rate.Rate - both class name and property name. Try changing your class to (and corresponding xml for the post) :
public class Rate
{
public string Id { get; set; }
public string Date { get; set; }
public double Value { get; set; }
}
and then try changing the order.
While I don't have a definitive reason why it works in one order and not another, it's likely that when it gets to the Rate(double) value it tries to create a new Rate(object) but doesn't have the correct properties (as its just a double).
A more complicated solution would be to write a specific model binder for the Rate object.
The issue has to do with the DataContractSerializer which expects the elements to occur in a specific order (alphabetical with some consideration given to inheritance). That's the default serializer used when creating a Web API project.
You can override this and specify a different serializer during API Configuration like this:
GlobalConfiguration.Configuration.Formatters.XmlFormatter
.SetSerializer<SomeType>(new XmlSerializer(typeof(SomeType)));
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"
};
}
}