in MVC3 you can add validation to models to check if properties match like so:
public string NewPassword { get; set; }
[Compare("NewPassword",
ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
Is there a way to check that two properties differ like in the following make-believe code?
[CheckPropertiesDiffer("OldPassword",
ErrorMessage = "Old and new passwords cannot be the same")]
public string OldPassword { get; set; }
public string ConfirmPassword { get; set; }
I would do checking in the controller.
In controller:
if(model.ConfirmPassword == model.OldPassword ){
ModelState.AddModelError("ConfirmPassword", "Old and new passwords cannot be the same");
}
In View:
#Html.ValidationMessage("ConfirmPassword")
Hope this helps
Here is what you could use in the model:
public string OldPassword
[NotEqualTo("OldPassword", ErrorMessage = "Old and new passwords cannot be the same.")]
public string NewPassword { get; set; }
And then define the following custom attribute:
public class NotEqualToAttribute : ValidationAttribute
{
private const string defaultErrorMessage = "{0} cannot be the same as {1}.";
private string otherProperty;
public NotEqualToAttribute(string otherProperty) : base(defaultErrorMessage)
{
if (string.IsNullOrEmpty(otherProperty))
{
throw new ArgumentNullException("otherProperty");
}
this.otherProperty = otherProperty;
}
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, name, otherProperty);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null)
{
PropertyInfo otherPropertyInfo = validationContext.ObjectInstance.GetType().GetProperty(otherProperty);
if (otherPropertyInfo == null)
{
return new ValidationResult(string.Format("Property '{0}' is undefined.", otherProperty));
}
var otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
if (otherPropertyValue != null && !string.IsNullOrEmpty(otherPropertyValue.ToString()))
{
if (value.Equals(otherPropertyValue))
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
}
}
return ValidationResult.Success;
}
}
You could also implement class level validation like in the description here: http://weblogs.asp.net/scottgu/archive/2010/12/10/class-level-model-validation-with-ef-code-first-and-asp-net-mvc-3.aspx
Basically you implement the Validate method of IValidatableObject and can access any properties you want.
public class MyClass : IValidateableObject
{
public string NewPassword { get; set; }
public string OldPassword { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (NewPassword == OldPassword)
yield return new ValidationResult("Passwords should not be the same");
}
}
I don't think that there is already a built-in attribute providing this functionality.
The best approach would be to create your own custom attribute like described in detail there:
http://www.codeproject.com/KB/aspnet/CustomValidation.aspx
Related
Some of my actions accept models like:
public class PaymentRequest
{
public decimal Amount { get; set; }
public bool? SaveCard { get; set; }
public int? SmsCode { get; set; }
public BankCardDetails Card { get; set; }
}
public class BankCardDetails
{
public string Number { get; set; }
public string HolderName { get; set; }
public string ExpiryDate { get; set; }
public string ValidationCode { get; set; }
}
And the action method looks like:
[HttpPost]
[Route("api/v1/payment/pay")]
public Task<BankCardActionResponse> Pay([FromBody] PaymentRequest request)
{
if (request == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
return _paymentService.PayAsync(DataUserHelper.PhoneNumber, request);
}
I use Nlog. I think it's clear this is a bad idea to log all this bank data. My log config file contained the following line:
<attribute name="user-requestBody" layout="${aspnet-request-posted-body}"/>
I logged the request. I decided to refactor that and planned the following strategy. Actions that contain sensitive data into their requests I will mark with an attribute like
[RequestMethodFormatter(typeof(PaymentRequest))]
then take a look at my custom renderer:
[LayoutRenderer("http-request")]
public class NLogHttpRequestLayoutRenderer : AspNetRequestPostedBody
{
protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
{
base.DoAppend(builder, logEvent);
var body = builder.ToString();
// Get attribute of the called action.
var type = ... // How can I get "PaymentRequest" from the [RequestMethodFormatter(typeof(PaymentRequest))]
var res = MaskHelper.GetMaskedJsonString(body, type);
// ... and so on
}
}
I think you understand the idea. I need the type from the method's RequestMethodFormatter attribute. Is it even possible to get it into the renderer? I need it because I'm going to deserialize request JSON into particular models (it's gonna be into the MaskHelper.GetMaskedJsonString), work with the models masking the data, serialize it back into JSON.
So, did I choose a wrong approach? Or it's possible to get the type from the attribute into the renderer?
After some research, I ended up with the following solution:
namespace ConsoleApp7
{
internal class Program
{
private static void Main()
{
var sourceJson = GetSourceJson();
var userInfo = JsonConvert.DeserializeObject(sourceJson, typeof(User));
Console.WriteLine("----- Serialize without Resolver-----");
Console.WriteLine(JsonConvert.SerializeObject(userInfo));
Console.WriteLine("----- Serialize with Resolver-----");
Console.WriteLine(JsonConvert.SerializeObject(userInfo, new JsonSerializerSettings
{
ContractResolver = new MaskPropertyResolver()
}));
}
private static string GetSourceJson()
{
var guid = Guid.Parse("3e92f0c4-55dc-474b-ae21-8b3dac1a0942");
return JsonConvert.SerializeObject(new User
{
UserId = guid,
Age = 19,
Name = "John",
BirthDate = new DateTime(1990, 5, 12),
Hobbies = new[]
{
new Hobby
{
Name = "Football",
Rating = 5,
DurationYears = 3,
},
new Hobby
{
Name = "Basketball",
Rating = 7,
DurationYears = 4,
}
}
});
}
}
public class User
{
[MaskGuidValue]
public Guid UserId { get; set; }
[MaskStringValue("***")] public string Name { get; set; }
public int Age { get; set; }
[MaskDateTimeValue]
public DateTime BirthDate { get; set; }
public Hobby[] Hobbies { get; set; }
}
public class Hobby
{
[MaskStringValue("----")]
public string Name { get; set; }
[MaskIntValue(replacement: 11111)]
public int Rating { get; set; }
public int DurationYears { get; set; }
}
public class MaskPropertyResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var props = base.CreateProperties(type, memberSerialization);
var allowedPropertyTypes = new Type[]
{
typeof(Guid),
typeof(DateTime),
typeof(string),
typeof(int),
};
foreach (var prop in props.Where(p => allowedPropertyTypes.Contains(p.PropertyType)))
{
if (prop.UnderlyingName == null)
continue;
var propertyInfo = type.GetProperty(prop.UnderlyingName);
var attribute =
propertyInfo?.GetCustomAttributes().FirstOrDefault(x => x is IMaskAttribute) as IMaskAttribute;
if (attribute == null)
{
continue;
}
if (attribute.Type != propertyInfo.PropertyType)
{
// Log this case, cause somebody used wrong attribute
continue;
}
prop.ValueProvider = new MaskValueProvider(propertyInfo, attribute.Replacement, attribute.Type);
}
return props;
}
private class MaskValueProvider : IValueProvider
{
private readonly PropertyInfo _targetProperty;
private readonly object _replacement;
private readonly Type _type;
public MaskValueProvider(PropertyInfo targetProperty, object replacement, Type type)
{
_targetProperty = targetProperty;
_replacement = replacement;
_type = type;
}
public object GetValue(object target)
{
return _replacement;
}
public void SetValue(object target, object value)
{
_targetProperty.SetValue(target, value);
}
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskStringValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(string);
public object Replacement { get; }
public MaskStringValueAttribute(string replacement)
{
Replacement = replacement;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskIntValueAttribute : Attribute, IMaskAttribute
{
public object Replacement { get; }
public Type Type => typeof(int);
public MaskIntValueAttribute(int replacement)
{
Replacement = replacement;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskGuidValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(Guid);
public object Replacement => Guid.Empty;
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskDateTimeValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(DateTime);
public object Replacement => new DateTime(1970, 1, 1);
}
public interface IMaskAttribute
{
Type Type { get; }
object Replacement { get; }
}
}
I hope somebody will find it helpful.
You can try nuget package https://www.nuget.org/packages/Slin.Masking and https://www.nuget.org/packages/Slin.Masking.NLog.
It can easily be integrated with DotNet projects with slight changes, and you can define your rules for it. But the document needs some improvement.
As a suggestion, you can use two files:
masking.json (can be a generic one, that shared across all projects)
masking.custom.json (can be used with particular rules for specific projects)
I have a route in a controller with some model transferred. This model (DTO) contains a custom type property (eg. Password, zip-code, ...). I would like to add a ModelBinder like one described here (https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-6.0) to the Property Type (Password). !Not to the whole DTO!
The resulting model (DTO) definition should look like:
namespace UserService.Models.DTO
{
public class UserRegisterDto
{
public string Firstname { get; set; }
public string Lastname { get; set; }
public string Email { get; set; }
public Password Password { get; set; }
}
}
When a JSON (eg. see below) is now transferred the Password property shall be automatically validated.
{
"firstname": "John",
"lastname": "Doe",
"email": "johndoe#mail.com",
"password": "Test12345!"
}
The route implementation should look like that:
namespace UserService.Controllers
{
[ApiController]
[Route("/api/user/")]
public class AuthController : ControllerBase
{
[HttpPost("register")]
public async Task<ActionResult> Register(UserRegisterDto request)
{
User? user = await this._userService.Register(request);
if (user == null)
{
return BadRequest();
}
return Ok(user);
}
}
}
At first I thought I could implement something like the following but the validation does not work at all...
namespace UserService.Models
{
[ModelBinder(typeof(PasswordEntityBinder))]
public class Password
{
private const string PasswordRegex = "(?=^.{8,}$)(?=.*\\d)(?=.*[!##$%&?*\\\"§$\\/()=~]+)(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$";
#region Properties
public string Value { get; set; }
public override string ToString() => Value;
public static implicit operator string(Password e) => e.Value;
public static bool TryParse(ReadOnlySpan<char> s, out Password? result)
{
result = null;
if (string.IsNullOrWhiteSpace(s.ToString()))
return false;
if (!Regex.IsMatch(s.ToString(), PasswordRegex))
return false;
result = new Password()
{
Value = s.ToString(),
};
return true;
}
}
}
Does anyone know if there is any possibility to achieve a modelbinding to a type which is used as a property in a model (DTO)?
I was having a further look for my question since I was not satisfied with using some 3rd party lib. I found a proper solution which allows me to use a custom complex type as for example int with validation and conversion at the same time. This solution works for asp.net net6.0. I use:
A System.Text.Json JSON custom converter
IValidatableObject
The json password is at first converted with the JSON converter and then validated.
The following shows the example, which can be applied to any type:
// Password.cs
namespace UserService.Models
{
[JsonConverter(typeof(PasswordJsonConverter))]
public class Password : IValidatableObject
{
private const string PasswordRegex = "(?=^.{8,}$)(?=.*\\d)(?=.*[!##$%&?*\\\"§$\\/()=~]+)(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$";
public string Value { get; set; } = "";
public override string ToString() => Value;
public static implicit operator string(Password e) => e.Value;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(this.Value))
{
yield return new ValidationResult("Password may not be empty");
yield break;
}
if (!Regex.IsMatch(this.Value, PasswordRegex))
yield return new ValidationResult("Password invalid formatted");
}
}
}
// PasswordJsonConverter.cs
namespace UserService.Converter
{
public class PasswordJsonConverter : JsonConverter<Password>
{
public override bool CanConvert(Type typeToConvert)
{
return (typeToConvert == typeof(Password));
}
public override Password? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Password pw = new Password();
string? pw_string;
try
{
pw_string = reader.GetString();
}catch(InvalidOperationException)
{
return null;
}
if (pw_string == null)
return null;
pw.Value = pw_string;
return pw;
}
public override void Write(Utf8JsonWriter writer, Password value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
}
This allows me to use the Password object in a DTO like any basic type. For example:
// RegisterDto.cs
namespace UserService.Models.DTO
{
public class UserRegisterDto
{
public string Firstname { get; set; }
public string Lastname { get; set; }
public string Email { get; set; }
public Password Password { get; set; }
}
}
The input json would look like:
{
"firstname": "John",
"lastname": "Doe",
"email": "johndoe#mail.com",
"password": "Test12345!"
}
======Model.cs=======
[MyCustomDoubleMessage(ErrorMessage = "Road length must be a number.")]
public double road_lenth {get; set;}
Without my custom atttribute, Default error message => "The road_length must a number."
Can I change default message like above? Thanks.
Use below attribute
[Display(Name = "Road length")]
[MyCustomDoubleMessage(ErrorMessage = "Road length must be a number.")]
public double road_lenth {get; set;}
Write new custom attribute as below .Change logic for double instead of not null
public class RequiredAttribute : System.ComponentModel.DataAnnotations.RequiredAttribute
{
private String displayName;
public RequiredAttribute()
{
this.ErrorMessage = "{0} is required";
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var attributes = validationContext.ObjectType.GetProperty(validationContext.MemberName).GetCustomAttributes(typeof(DisplayNameAttribute), true);
if (attributes != null)
this.displayName = (attributes[0] as DisplayNameAttribute).DisplayName;
else
this.displayName = validationContext.DisplayName;
return base.IsValid(value, validationContext);
}
public override string FormatErrorMessage(string name)
{
return string.Format(this.ErrorMessageString, displayName);
}
}
and model must be like below
[DisplayName("Road length")]
[Required]
public string road_lenth { get; set; }
Based On this Article: Manual Validation with Data Annotations
I write this code:
public class Person04
{
[CustomValidation(typeof(AWValidation), "ValidateSalesAmount")]
public int SalesAmout { get; set; }
[DataType(DataType.EmailAddress, ErrorMessage = "Invalid E-mail")]
public string EmailAddress { get; set; }
[Range(0, 99, ErrorMessage = "Age should be in range 0 to 99")]
public int Age { get; set; }
[Required(ErrorMessage="Name is required")]
public string Name { get; set; }
[StringLength(10, ErrorMessage = "Invalid Last Name")]
public string LastName { get; set; }
}
public class AWValidation
{
public static ValidationResult ValidateSalesAmount(int salesAmount)
{
if (salesAmount < 0)
{
return new ValidationResult("Invalid Sales Amount");
}
return ValidationResult.Success;
}
}
and
var person = new Person04() { SalesAmout = -1, Age = 100, EmailAddress = "nima", LastName = "Arian The Great" };
var context = new ValidationContext(person, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(person, context, results);
if (!isValid)
{
foreach (var validationResult in results)
{
Console.WriteLine(validationResult.ErrorMessage);
}
}
but this Code just write 1 error:
Name is required
Why other errors not specified?
thanks
var isValid = Validator.TryValidateObject(person, context, results, true);
You were missing the last boolean parameter which indicates that you want all properties to be validated:
validateAllProperties: true to validate all properties; if false, only required attributes are validated.
I think i found solution.You just missed an object of class ValidationContext.
public class AWValidation
{
public static ValidationResult ValidateSalesAmount(int salesAmount,ValidationContext validationContext)
{
if (salesAmount < 0)
{
return new ValidationResult("Invalid Sales Amount");
}
return ValidationResult.Success;
}
}
I have a tricky requirement where i have to compare two model properties which are of custom class types. Server side validation is straight forward but achieving client side validation using jquery unobtrusive is not clear . When the mvc renders the html i am not seeing data-val attributes appended
public class CommunicationViewModel
{
public PhoneViewModel MobilePhone { get; set; }
[ComparePhoneLocalized("CommunicationView.MobilePhone")]
public PhoneViewModel ConfirmMobilePhoneNumber { get; set; }
........
........
}
public class PhoneViewModel
{
public string Area { get; set; }
public string Number { get; set; }
public string Extension { get; set; }
public string CountryCode { get; set; }
}
Custom validator
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class CompareConditionalLocalizedAttribute : CompareAttribute, IClientValidatable
{
private readonly object _typeId = new object();
private const string ErrorMsg = "Rentered mobile number does not match the original";
public string FieldName { get; set; }
//public MessageManagement.MessageContext MessageContext { get; set; }
//public MessageCode MessageCode { get; set; }
public new string OtherProperty { get; set; }
public CompareConditionalLocalizedAttribute(string otherProperty)
: base(otherProperty)
{
OtherProperty = otherProperty;
}
public override string FormatErrorMessage(string name)
{
//** This error message will eventually be driven by resource provider factory **//
return ErrorMsg;
//var msg = Message.Create(MessageCode, MessageStatusType.Error, FieldName);
//var messageRepository = ServeContext.Current.ResolveInstance<IMessageRepository>();
//msg = messageRepository.MapMessage(msg, MessageContext);
//return msg.MessageText;
}
protected override ValidationResult IsValid(object value, ValidationContext
validationContext)
{
if (String.IsNullOrEmpty(OtherProperty)) return null;
var otherProperty = validationContext.ObjectType.GetProperty(OtherProperty);
var compareTo = (string)otherProperty.GetValue(validationContext.ObjectInstance, null);
if (value == null) return null;
if (compareTo.Equals(value.ToString(), StringComparison.Ordinal))
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return ValidationResult.Success;
}
public new IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata
metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(FieldName),
ValidationType = "compareemail"
};
rule.ValidationParameters.Add("otherproperty", OtherProperty);
yield return rule;
}
}
JQuery Wire up
jQuery.validator.unobtrusive.adapters.add("comparephone", "[otherproperty]", function
(options) {
options.rules["comparephone"] = options.params.otherproperty;
options.messages["comparephone"] = options.message;
});
jQuery.validator.addMethod("comparephone", function (value, element, params) {
var compareTo = $('[name="' + params + '"]').val();
if (value == compareTo) {
return true;
}
return false;
});