Passing data from custom validation attribute to page/view - asp.net-core

Let's say I have a custom validation attribute:
public class CustomAttribute : ValidationAttribute
{
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var customErrorString = "You did something wrong!"; //How to pass this to the localized string defined in the resource file?
if(SomethingIsInvalid())
return new ValidationResult(FormatErrorMessage(validationContext.MemberName));
return ValidationResult.Success;
}
}
public class CustomAttributeAdapter : AttributeAdapterBase<CustomAttribute>
{
public CustomAttributeAdapter(CustomAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}
public override void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
}
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
}
How can I pass, let's say, a string from the data annotation to the validation tag? For example:
[Custom(ErrorMessage = "CustomValidationMessage")]
public string ValidateThis { get; set; }
CustomValidationMessage is defined in a resource file and results to "This is invalid:"
Now my question is, how can I pass customErrorString from the validation attribute to the localized string so it shows on the validation tag like this:
<span id="validation-span" asp-validation-for="#Model.ValidateThis" class="text-danger">This is invalid: You did something wrong!</span>
I hope my question is clear. If not, feel free to ask for more details.
EDIT: I got it to work:
public class CustomAttribute : ValidationAttribute
{
//Create property to hold our custom error string
public string CustomErrorString { get; set; }
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//Set the custom error string property.
CustomErrorString = "You did something wrong!";
if(SomethingIsInvalid())
return new ValidationResult(FormatErrorMessage(validationContext.MemberName));
return ValidationResult.Success;
}
}
public class CustomAttributeAdapter : AttributeAdapterBase<CustomAttribute>
{
//Declare class variable to hold the attribute's custom error string.
private string _customErrorString = string.empty;
public CustomAttributeAdapter(CustomAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
//Set the adapter's custom error string according to the attribute's custom error string
if(!string.IsNullOrEmpty(attribute.CustomErrorString))
_customErrorString = attribute.CustomErrorString;
}
public override void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
}
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
//Pass the custom error string instead of member name
return GetErrorMessage(validationContext.ModelMetadata, _customErrorString);
}
}
After you do all of this, you can then set your resource string like this:
[Custom(ErrorMessage = "CustomValidationMessage")]
public string ValidateThis { get; set; }
Where CustomValidationMessage results in "This is invalid: {0}"
Normally, {0} would result in the localized member name of the property. But because we passed the custom error string in the adapter, it will be set to the custom error message.
It might be a little dirty, but it gets the job done.

You can't pass it back. Anything set on the attribute is static, because attributes are instantiated in place, i.e. there's no opportunity to modify anything there later. Typically, the error message would be passed as a format string ("This is invalid: {0}"), and then the validation code would use that along with something like string.Format to fill in the member name.
Localization is not an issue the attribute needs to worry about. You simply need to add the data annotations localizer:
services.AddControllers()
.AddDataAnnotationsLocalization();

Related

JsonConverterAttribute is not working for Deserialization in ASP.NET Core 3.1 / 5.0

I want set property names at runtime. I already achieve this for serialization.
For example. I have a simple model like as below:
[JsonConverter(typeof(DataModelConverter))]
public class DataModel
{
public string Name { get; set; }
public int Age { get; set; }
}
And I have a simple DataModelConverter, that inherited from JsonConverter:
public class DataModelConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Type type = value.GetType();
JObject jo = new JObject();
foreach (PropertyInfo prop in type.GetProperties())
{
jo.Add(prop.Name == "Name" ? "FullName" : prop.Name, new JValue(prop.GetValue(value)));
}
jo.WriteTo(writer);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DataModel);
}
}
And I have a simple controller like as below:
[Route("api/[controller]")]
[ApiController]
public class NewtonController : ControllerBase
{
public IEnumerable<DataModel> GetNewtonDatas([FromBody] DataModel input)
{
return new List<DataModel>()
{
new DataModel
{
Name="Ramil",
Age=25
},
new DataModel
{
Name="Yusif",
Age=26
}
};
}
}
If I call this API, result will like as below (Showing FullName Instead of Name):
[
{
"FullName": "Ramil",
"Age": 25
},
{
"FullName": "Yusif",
"Age": 26
}
]
But I have a problem. This is not working for deserialization.
For example: If I call this API with this body, then Name will null.
{
"FullName":"Ramil"
}
My attribute is not working for deserialization. I want set property name via attribute for deserialization at runtime .
I don't want use some middleware, I want to achieve this only by using the any attribute at runtime. I must read JSON property names from my appsettings.json file.
Thanks for help!
You have overridden CanRead to return false:
public override bool CanRead
{
get { return false; }
}
This causes Json.NET not to call your your converter's DataModelConverter.ReadJson() method during deserialization, and instead use default deserialization. Since "FullName" does not have the same (case-invariant) name as the Name property, it never gets set, and remains null.
To fix this, remove the override for CanRead (the default implementation returns true) and implement ReadJson(), e.g. as follows:
public class DataModelConverter : NameRemappingConverterBase
{
static string AlternateName => "FullName";
static string OriginalName => "Name";
public override bool CanConvert(Type objectType) => objectType == typeof(DataModel);
// Replace the below logic with name mappings from appsettings.json
protected override string ToJsonPropertyName(JsonProperty property) =>
string.Equals(property.UnderlyingName, OriginalName, StringComparison.OrdinalIgnoreCase) ? AlternateName : base.ToJsonPropertyName(property);
protected override string FromJsonPropertyName(string name) =>
string.Equals(name, AlternateName, StringComparison.OrdinalIgnoreCase) ? OriginalName : base.FromJsonPropertyName(name);
}
public abstract class NameRemappingConverterBase : JsonConverter
{
protected virtual string ToJsonPropertyName(JsonProperty property) => property.PropertyName;
protected virtual string FromJsonPropertyName(string name) => name;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);
var value = existingValue ?? contract.DefaultCreator();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
if (reader.TokenType != JsonToken.PropertyName)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
var name = FromJsonPropertyName((string)reader.Value);
reader.ReadToContentAndAssert();
var property = contract.Properties.GetProperty(name, StringComparison.OrdinalIgnoreCase);
if (!ShouldDeserialize(property))
{
reader.Skip();
}
else
{
var propertyValue = serializer.Deserialize(reader, property.PropertyType);
property.ValueProvider.SetValue(value, propertyValue);
}
}
return value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
writer.WriteStartObject();
foreach (var property in contract.Properties.Where(p => ShouldSerialize(p, value)))
{
var propertyValue = property.ValueProvider.GetValue(value);
if (propertyValue == null && serializer.NullValueHandling == NullValueHandling.Ignore)
continue;
var name = ToJsonPropertyName(property);
writer.WritePropertyName(name);
serializer.Serialize(writer, propertyValue);
}
writer.WriteEndObject();
}
protected virtual bool ShouldDeserialize(JsonProperty property) =>
property != null && property.Writable;
protected virtual bool ShouldSerialize(JsonProperty property, object value) =>
property.Readable && !property.Ignored && (property.ShouldSerialize == null || property.ShouldSerialize(value));
}
public static partial class JsonExtensions
{
public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
Demo fiddle here.

Display Name is problem on Data Annotation ErrorMessage (The {0} field is required.) with Localization

My aim is very simple. I wanna get "Display Name" in Required Error Message. So I used it '{0}' that string format
Example: sqlLocalization
[Required(ErrorMessage="The {0} field is required.")]
public class AttributeField
{
[Display(Name = "AttributeFeatureCode")]
[Required(ErrorMessage = "The {0} field is required.")]
public string AttributeFeatureCode { get; set; }
...
}
Result: The true field is required
data-val-required="The {0} field is required"
So I researched on web and I see that there is an error on GetErrorMessage
I guesss something is wrong in here... And I have to write override on my project.
public class RequiredAttributeAdapter : AttributeAdapterBase<RequiredAttribute>
{
public RequiredAttributeAdapter(RequiredAttribute attribute, IStringLocalizer stringLocalizer): base(attribute, stringLocalizer)
{
}
public override void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context));
}
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
var errorMessage = GetErrorMessage(validationContext.ModelMetadata);
return string.Format(errorMessage, validationContext.ModelMetadata.GetDisplayName());
}
}
When I use it similar like this
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
result is The {0} field is required
If I change it with override, that's working
var errorMessage = GetErrorMessage(validationContext.ModelMetadata);
return string.Format(errorMessage, validationContext.ModelMetadata.GetDisplayName());
What do you think ? is there any error on GetErrorMessage ?
Why I can get {0} ? and
why cant I get display name for {0}?
thanks a lot
there is a problem on SqlStringLocalizer.cs and line 40
so I changed it
public LocalizedString this[string name, params object[] arguments]
{
get
{
var str = this[name];
if (arguments.Length > 0)
str = this[string.Format(str, arguments.Select(x => x.ToString()).ToArray())];
return str;
}
}
if string contain string format I mean this {} so it will work for string format. I think my logic will work

FluentValidator and JsonPatchDocument

I have WebAPI (.NET Core) and use FluentValidator to validate model, including updating.
I use PATCH verb and have the following method:
public IActionResult Update(int id, [FromBody] JsonPatchDocument<TollUpdateAPI> jsonPatchDocument)
{
also, I have a validator class:
public class TollUpdateFluentValidator : AbstractValidator<TollUpdateAPI>
{
public TollUpdateFluentValidator ()
{
RuleFor(d => d.Date)
.NotNull().WithMessage("Date is required");
RuleFor(d => d.DriverId)
.GreaterThan(0).WithMessage("Invalid DriverId");
RuleFor(d => d.Amount)
.NotNull().WithMessage("Amount is required");
RuleFor(d => d.Amount)
.GreaterThanOrEqualTo(0).WithMessage("Invalid Amount");
}
}
and map this validator in Startup class:
services.AddTransient<IValidator<TollUpdateAPI>, TollUpdateFluentValidator>();
but it does not work. How to write valid FluentValidator for my task?
You will need to trigger the validation manually.
Your action method will be somthing like this:
public IActionResult Update(int id, [FromBody] JsonPatchDocument<TollUpdateAPI> jsonPatchDocument)
{
// Load your db entity
var myDbEntity = myService.LoadEntityFromDb(id);
// Copy/Map data to the entity to patch using AutoMapper for example
var entityToPatch = myMapper.Map<TollUpdateAPI>(myDbEntity);
// Apply the patch to the entity to patch
jsonPatchDocument.ApplyTo(entityToPatch);
// Trigger validation manually
var validationResult = new TollUpdateFluentValidator().Validate(entityToPatch);
if (!validationResult.IsValid)
{
// Add validation errors to ModelState
foreach (var error in validationResult.Errors)
{
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
// Patch failed, return 422 result
return UnprocessableEntity(ModelState);
}
// Map the patch to the dbEntity
myMapper.Map(entityToPatch, myDbEntity);
myService.SaveChangesToDb();
// So far so good, patch done
return NoContent();
}
You can utilize a custom rule builder for this. It might not be the most elegant way of handling it but at least the validation logic is where you expect it to be.
Say you have the following request model:
public class CarRequestModel
{
public string Make { get; set; }
public string Model { get; set; }
public decimal EngineDisplacement { get; set; }
}
Your Validator class can inherit from the AbstractValidator of JsonPatchDocument instead of the concrete request model type.
The fluent validator, on the other hand, provides us with decent extension points such as the Custom rule.
Combining these two ideas you can create something like this:
public class Validator : AbstractValidator<JsonPatchDocument<CarRequestModel>>
{
public Validator()
{
RuleForEach(x => x.Operations)
.Custom(HandleInternalPropertyValidation);
}
private void HandleInternalPropertyValidation(JsonPatchOperation property, CustomContext context)
{
void AddFailureForPropertyIf<T>(
Expression<Func<T, object>> propertySelector,
JsonPatchOperationType operation,
Func<JsonPatchOperation, bool> predicate, string errorMessage)
{
var propertyName = (propertySelector.Body as MemberExpression)?.Member.Name;
if (propertyName is null)
throw new ArgumentException("Property selector must be of type MemberExpression");
if (!property.Path.ToLowerInvariant().Contains(propertyName.ToLowerInvariant()) ||
property.Operation != operation) return;
if (predicate(property)) context.AddFailure(propertyName, errorMessage);
}
AddFailureForPropertyIf<CarRequestModel>(x => x.Make, JsonPatchOperationType.remove,
x => true, "Car Make cannot be removed.");
AddFailureForPropertyIf<CarRequestModel>(x => x.EngineDisplacement, JsonPatchOperationType.replace,
x => (decimal) x.Value < 12m, "Engine displacement must be less than 12l.");
}
}
In some cases, it might be tedious to write down all the actions that are not allowed from the domain perspective but are defined in the JsonPatch RFC.
This problem could be eased by defining none but rules which would define the set of operations that are valid from the perspective of your domain.
Realization bellow allow use IValidator<Model> inside IValidator<JsonPatchDocument<Model>>, but you need create model with valid properties values.
public class ModelValidator : AbstractValidator<JsonPatchDocument<Model>>
{
public override ValidationResult Validate(ValidationContext<JsonPatchDocument<Model>> context)
{
return _validator.Validate(GetRequestToValidate(context));
}
public override Task<ValidationResult> ValidateAsync(ValidationContext<JsonPatchDocument<Model>> context, CancellationToken cancellation = default)
{
return _validator.ValidateAsync(GetRequestToValidate(context), cancellation);
}
private static Model GetRequestToValidate(ValidationContext<JsonPatchDocument<Model>> context)
{
var validModel = new Model()
{
Name = nameof(Model.Name),
Url = nameof(Model.Url)
};
context.InstanceToValidate.ApplyTo(validModel);
return validModel;
}
private class Validator : AbstractValidator<Model>
{
/// <inheritdoc />
public Validator()
{
RuleFor(r => r.Name).NotEmpty();
RuleFor(r => r.Url).NotEmpty();
}
}
private static readonly Validator _validator = new();
}
You may try the below generic validator - it validates only updated properties:
public class JsonPatchDocumentValidator<T> : AbstractValidator<JsonPatchDocument<T>> where T: class, new()
{
private readonly IValidator<T> _validator;
public JsonPatchDocumentValidator(IValidator<T> validator)
{
_validator = validator;
}
private static string NormalizePropertyName(string propertyName)
{
if (propertyName[0] == '/')
{
propertyName = propertyName.Substring(1);
}
return char.ToUpper(propertyName[0]) + propertyName.Substring(1);
}
// apply path to the model
private static T ApplyPath(JsonPatchDocument<T> patchDocument)
{
var model = new T();
patchDocument.ApplyTo(model);
return model;
}
// returns only updated properties
private static string[] CollectUpdatedProperties(JsonPatchDocument<T> patchDocument)
=> patchDocument.Operations.Select(t => NormalizePropertyName(t.path)).Distinct().ToArray();
public override ValidationResult Validate(ValidationContext<JsonPatchDocument<T>> context)
{
return _validator.Validate(ApplyPath(context.InstanceToValidate),
o => o.IncludeProperties(CollectUpdatedProperties(context.InstanceToValidate)));
}
public override async Task<ValidationResult> ValidateAsync(ValidationContext<JsonPatchDocument<T>> context, CancellationToken cancellation = new CancellationToken())
{
return await _validator.ValidateAsync(ApplyPath(context.InstanceToValidate),
o => o.IncludeProperties(CollectUpdatedProperties(context.InstanceToValidate)), cancellation);
}
}
it has to be registered manually:
builder.Services.AddScoped<IValidator<JsonPatchDocument<TollUpdateAPI>>, JsonPatchDocumentValidator<TollUpdateAPI>>();

Use IMarkupExtension together with StringFormat

I'm using the TranslateExtension from Xamarin. Is it possible to add a StringFormat to the call?
Currently, I have
<Label Text="{i18n:Translate User}" />
but I would need something like this
<Label Text="{i18n:Translate User, StringFormat='{0}:'}" />
If I do the latter, I get
Xamarin.Forms.Xaml.XamlParseException: Cannot assign property "StringFormat": Property does not exists, or is not assignable, or mismatching type between value and property
I know I could add another translation with a colon, but it would be nice to have a different option.
A bit late to the party, but doing it with the standard extension and just XAML, go like this:
<Label Text="{Binding YourDynamicValue, StringFormat={i18n:Translate KeyInResources}}"/>
Your translation should look something like: Static text {0}. Where {0} is replaced by the value you bind to.
The problem is that the Translate extension just gets your string out of the resources, and doesn't have a StringFormat property etc. But you can assign the retrieved resource value to the StringFormat of the Binding.
You can add a parameter property to TranslateExtension.
My TranslateExtension looks like this. You can take the Parameter parts and add it to the one from the Xamarin sample.
[ContentProperty("Text")]
public class TranslateExtension : IMarkupExtension
{
public string Text { get; set; }
public string Parameter { get; set; }
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
{
try
{
if (Text == null)
return null;
var culture = new CultureInfo(CultureHelper.CurrentIsoLanguage);
var result = LocalizationResources.ResourceManager.GetString(Text, culture);
if (string.IsNullOrWhiteSpace(Parameter))
{
return string.IsNullOrWhiteSpace(result) ? "__TRANSLATE__" : result;
}
return string.IsNullOrWhiteSpace(result) ? "__TRANSLATE__" : string.Format(result, Parameter);
}
catch (Exception ex)
{
TinyInsights.TrackErrorAsync(ex);
return "__TRANSLATE__";
}
}
}
Here I have updated the Xamarin sample:
[ContentProperty("Text")]
public class TranslateExtension : IMarkupExtension
{
readonly CultureInfo ci = null;
const string ResourceId = "UsingResxLocalization.Resx.AppResources";
static readonly Lazy<ResourceManager> ResMgr = new Lazy<ResourceManager>(() => new ResourceManager(ResourceId, IntrospectionExtensions.GetTypeInfo(typeof(TranslateExtension)).Assembly));
public string Text { get; set; }
public string StringFormat {get;set;}
public TranslateExtension()
{
if (Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.Android)
{
ci = DependencyService.Get<ILocalize>().GetCurrentCultureInfo();
}
}
public object ProvideValue(IServiceProvider serviceProvider)
{
if (Text == null)
return string.Empty;
var translation = ResMgr.Value.GetString(Text, ci);
if (translation == null)
{
#if DEBUG
throw new ArgumentException(
string.Format("Key '{0}' was not found in resources '{1}' for culture '{2}'.", Text, ResourceId, ci.Name),
"Text");
#else
translation = Text; // HACK: returns the key, which GETS DISPLAYED TO THE USER
#endif
}
if(!string.IsNullOrWhitespace(StringFormat)
return string.Format(StringFormat, translation);
return translation;
}
}

MVC WebAPI Data Annotation Error Message Empty String

I have implemented an OWIN self-hosted webapi and am trying to use data annotations and an ActionFilterAttribute to return formatted errors to the user. I have set custom error messages on the data annotation but when I try to retrieve the message from the ModelState it is always an empty string (shown in image below).
Model:
public class JobPointer
{
[Required(ErrorMessage = "JobId Required")]
public Guid JobId { get; set; }
}
Filter:
public class ModelValidationFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid) return;
string errors = actionContext.ModelState.SelectMany(state => state.Value.Errors).Aggregate("", (current, error) => current + (error.ErrorMessage + ". "));
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, errors);
}
}
Endpoint:
[HttpPost]
public HttpResponseMessage DescribeJob(JobPointer jobId)
{
Job job = _jobhelper.GetJob(jobId.JobId);
return Request.CreateResponse(HttpStatusCode.OK, job);
}
Request Body:
{
}
Response:
Status Code: 400
{
"Message": ". "
}
If I change error.Message in ModelValidationFilter to error.Exception.Message I get back the default validation error:
Status Code: 400
{
"Message": "Required property 'JobId' not found in JSON. Path '', line 3, position 2.. "
}
I know this is an old question, but I just had this problem and found the solution myself.
As you no doubt discovered, as Guid is a non-nullable type [Required] produces an unfriendly error message (I assume because the JSON parser picks it up before actually getting the model validation).
You can get around this by making the Guid nullable...
public class JobPointer
{
[Required(ErrorMessage = "JobId Required")]
public Guid? JobId { get; set; }
}
... however, this is not a viable option in all cases (as in my case), so I ended up writing my own validation attribute that would check the property against it's Empty declaration...
public class IsNotEmptyAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (value == null) return false;
var valueType = value.GetType();
var emptyField = valueType.GetField("Empty");
if (emptyField == null) return true;
var emptyValue = emptyField.GetValue(null);
return !value.Equals(emptyValue);
}
}
You could then implement like...
public class JobPointer
{
[IsNotEmpty(ErrorMessage = "JobId Required")]
public Guid JobId { get; set; }
}