I'm new with data annotation. I'd like to know if it possible (and how) to add some validation dynamically. It is very extensive to explain why, but I've a ViewModel that receives and object when created. In that object I must check for some property and depending its value I should have or not some validations.
An example:
public class ProfileViewModel
{
[Required(ErrorMessage = "The field {0} is required")]
[Display(Name = "Client Code")]
public int ClientCode { get; set; }
[Required(ErrorMessage = "The field {0} is required")]
[StringLength(100, ErrorMessage = "The field {0} must have up to 100 characters.")]
[Display(Name = "Company")]
public string Company { get; set; }
[StringLength(50, ErrorMessage = "The field {0} must have up to 50 characters.")]
[Display(Name = "Name")]
public string Name { get; set; }
[StringLength(50, ErrorMessage = "The field {0} must have up to 50 characters.")]
[Display(Name = "LastName")]
public string LastName { get; set; }
public ProfileViewModel(User usr)
{
if (usuario.ClientCode != null)
{
ClientCode = Convert.ToInt32(usr.ClientCode);
}
else
{
//ClientCode and Company are not yet required.
//Name and LastName are now required.
}
Company = usr.Company;
Name = usr.Name;
LastName = usr.LastName;
}
}
I think that the simplest way of doing what I wanted is implementing IValidatableObject:
public class Product : IValidatableObject
{
public int Prop1 { get; set; }
public int Prop2 { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Prop1 < Prop2)
yield return new ValidationResult("Property 1 can't be less than Property 2");
}
}
See also: Class-Level Model Validation with ... ASP.NET MVC 3
Custom Attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class CustomRequiredIfAttribute : CustomAttribute
{
private RequiredAttribute innerAttribute = new RequiredAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public CustomRequiredIfAttribute()
{
}
public CustomRequiredIfAttribute(string dependentProperty, object targetValue)
: base()
{
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
public override bool IsValid(object value)
{
return innerAttribute.IsValid(value);
}
}
Custom RequiredIfValidator
using System;
using System.Collections.Generic;
using System.Web.Mvc;
namespace Custom.Web.Validation
{
public class RequiredIfValidator : DataAnnotationsModelValidator<CustomRequiredIfAttribute>
{
public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, CustomRequiredIfAttribute attribute)
: base(metadata, context, attribute)
{
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
return base.GetClientValidationRules();
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
// get a reference to the property this validation depends upon
var field = Metadata.ContainerType.GetProperty(Attribute.DependentProperty);
if (field != null)
{
// get the value of the dependent property
object value = field.GetValue(container, null);
// compare the value against the target value
if (this.IsEqual(value) || (value == null && Attribute.TargetValue == null))
{
// match => means we should try validating this field
if (!Attribute.IsValid(Metadata.Model))
{
// validation failed - return an error
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
}
}
private bool IsEqual(object dependentPropertyValue)
{
bool isEqual = false;
if (Attribute.TargetValue != null && Attribute.TargetValue.GetType().IsArray)
{
foreach (object o in (Array)Attribute.TargetValue)
{
isEqual = o.Equals(dependentPropertyValue);
if (isEqual)
{
break;
}
}
}
else
{
isEqual = Attribute.TargetValue.Equals(dependentPropertyValue);
}
return isEqual;
}
}
}
Register custom DataAnnotationsModelValidatorProvider
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(CustomRequiredIfAttribute), typeof(RequiredIfValidator));
Use this CustomRequiredIf in the ViewModel
[CustomRequiredIf("CategoryId", 3, ErrorMessageResourceName = GlobalResourceLiterals.AccountGroup_Required)]
public string AccountGroup { get; set; }
Heres the updated MVC 3 version of that blog post http://blogs.msdn.com/b/simonince/archive/2011/02/04/conditional-validation-in-asp-net-mvc-3.aspx
Related
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question does not appear to be about programming within the scope defined in the help center.
Closed 11 months ago.
Improve this question
I have some dtos returned by my API that have sensitive data fields like Createdby, CreatedDate, LastModifiedBy and LastModifiedDate. These fields should only be in the response if the user is authenticated and/or have allowed roles in his claims. My current running solution is to use my dto mappers that have a dependency on IUserIdentity (custom interface).
UserDto record
public record UserDto {
public string? CreatedBy { get; init; }
public DateTime? CreatedDate { get; init; }
public string? Email { get; set; }
public string? ExternalProviderUserId { get; set; }
public string? GivenName { get; set; }
public Guid? Id { get; set; }
public string? LastModifiedBy { get; init; }
public DateTime? LastModifiedDate { get; init; }
public string? Name { get; set; }
public string? Surname { get; set; }
}
UserDtoMapper class
public class UserDtoMapper : IUserDtoMapper {
private readonly IUserIdentity _userIdentity;
public UserDtoMapper(IUserIdentity userIdentity) {
_userIdentity = userIdentity;
}
public UserDto ToDto(User user) => new() {
CreatedBy = _userIdentity.IsAuthenticated ? user.CreatedBy : null,
CreatedDate = _userIdentity.IsAuthenticated ? user.CreatedDate : null,
Email = user.Email,
ExternalProviderUserId = user.ExternalProviderUserId,
GivenName = user.GivenName,
Id = user.Id,
LastModifiedBy = _userIdentity.IsAuthenticated ? user.LastModifiedBy : null,
LastModifiedDate = _userIdentity.IsAuthenticated ? user.LastModifiedDate : null,
Name = user.Name,
Surname = user.Surname
};
public List<UserDto> ToDtos(IEnumerable<User> users) {
return users.Select(o => ToDto(o)).ToList();
}
}
It work as entended but I would like to have a global and easier way to set these fields as senstitive and let the api filter them if the user is not authenticed and/or doesn't have allowed roles in his claims. After searching online for many days and did try and error solutions, I finally comes with my own solution and hope to have feedbacks on potential issues I could have. It works great so far.
My solution is to use the Filters in ASP.NET Core and Reflection (C#).
I've created this custom attribute SensitiveDataAttribute
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class SensitiveDataAttribute : Attribute {
public SensitiveDataAttribute() {
AllowedRoles = Array.Empty<string>();
}
public SensitiveDataAttribute(params string[] allowedRoles)
: this((IEnumerable<string>)allowedRoles) { }
public SensitiveDataAttribute(IEnumerable<string> allowedRoles) {
if (allowedRoles == null) {
throw new ArgumentNullException(nameof(allowedRoles));
}
if (!allowedRoles.Any()) {
throw new InvalidOperationException("At least one role must be specified.");
}
AllowedRoles = allowedRoles;
}
/// <summary>
/// Gets the collection of allowed roles.
/// </summary>
public IEnumerable<string> AllowedRoles { get; }
public Task<bool> IsValidAsync(ClaimsPrincipal user) {
bool isUserAuthenticated = user.Identity?.IsAuthenticated ?? false;
if (!isUserAuthenticated) {
return Task.FromResult(false);
}
if (!AllowedRoles.Any()) {
return Task.FromResult(true);
}
bool found = AllowedRoles.Any(r => user.IsInRole(r));
return Task.FromResult(found);
}
public override string ToString() {
if (!AllowedRoles.Any()) {
return $"{nameof(SensitiveDataAttribute)}:User must be authenticated";
}
string roles = string.Join("|", AllowedRoles);
var stringValue = $"User must be authenticated and User.IsInRole must be true for one of the following roles:({roles})";
return $"{nameof(SensitiveDataAttribute)}: {stringValue}";
}
}
And created this action filter SensitiveDataActionFilter
public class SensitiveDataActionFilter : IAsyncActionFilter {
private readonly ILogger<SensitiveDataActionFilter> _logger;
public SensitiveDataActionFilter(ILogger<SensitiveDataActionFilter> logger) {
_logger = logger;
}
public async Task OnActionExecutionAsync(ActionExecutingContext _, ActionExecutionDelegate next) {
ActionExecutedContext executedContext = await next();
if (executedContext.Exception != null) {
return;
}
if (executedContext.Result is not OkObjectResult result) {
return;
}
if (result.Value == null) {
return;
}
_logger.LogDebug("Filtering action result sensitive data of type {Type} started.", result.Value.GetType());
Stopwatch stopwatch = Stopwatch.StartNew();
if (result.Value is IEnumerable elements) {
int index = 0;
foreach (var element in elements) {
await FilterResultAsync(executedContext, element, $"[{index++}].");
}
_logger.LogDebug("Filtering action result sensitive data of type {Type} finished in {Elapsed} ms.", result.Value.GetType(), stopwatch.ElapsedMilliseconds);
return;
}
await FilterResultAsync(executedContext, result.Value, null);
_logger.LogDebug("Filtering action result sensitive data of type {Type} finished in {Elapsed} ms.", result.Value.GetType(), stopwatch.ElapsedMilliseconds);
}
private async Task FilterResultAsync(ActionExecutedContext context, object? source, string? propPath) {
if (source == null) {
return;
}
PropertyInfo[] properties = source.GetType()
.GetProperties(BindingFlags.Instance
| BindingFlags.Public)
.Where(p => p.GetMethod != null
&& p.GetMethod.IsPublic
&& p.GetMethod.IsStatic == false)
.ToArray();
foreach (var propertyInfo in properties) {
if (propertyInfo == null) {
continue;
}
object? propertyValue = propertyInfo.GetValue(source);
string propertyName = propertyInfo.Name;
string path = $"{propPath}{propertyName}";
if (propertyValue == null) {
_logger.LogDebug("Property {Path}: {Value}", path, propertyValue);
continue;
}
SensitiveDataAttribute? sensitiveDataAttribute = propertyInfo
.GetCustomAttribute<SensitiveDataAttribute>(true);
if (sensitiveDataAttribute != null) {
_logger.LogDebug("Property {Path} is sensitive: {Value}", path, propertyValue);
bool isValid = await sensitiveDataAttribute.IsValidAsync(context.HttpContext.User);
if (!isValid) {
_logger.LogDebug("Property {Path} to be cleared: {Reason}", path, sensitiveDataAttribute);
propertyInfo.SetValue(source, default);
continue;
}
}
if (propertyValue is DateTime or string) {
_logger.LogDebug("Property {Path}: {Value}", path, propertyValue);
continue;
}
if (propertyValue is IEnumerable elements) {
int index = 0;
foreach (var element in elements) {
await FilterResultAsync(context, element, $"{path}.[{index++}].");
}
continue;
}
_logger.LogDebug("Property {Path}: {Value}", path, propertyValue);
await FilterResultAsync(context, propertyValue, $"{path}.");
}
}
}
And register the filter SensitiveDataActionFilter like this:
services.AddScoped<SensitiveDataActionFilter>();
services.AddControllers(options => options.Filters.AddService<SensitiveDataActionFilter>())
And add the attribute SensitiveDataAttribute to sensitive field:
public record UserDto {
[SensitiveData]
public string? CreatedBy { get; init; }
[SensitiveData]
public DateTime? CreatedDate { get; init; }
public string? Email { get; set; }
[SensitiveData("SYS_ADMIN")]
public string? ExternalProviderUserId { get; set; }
public string? GivenName { get; set; }
public Guid? Id { get; set; }
[SensitiveData]
public string? LastModifiedBy { get; init; }
[SensitiveData]
public DateTime? LastModifiedDate { get; init; }
public string? Name { get; set; }
public string? Surname { get; set; }
}
Circular reference is one problem I have have with my code. I might need to have a max-depth setting somewhere. I had problem with DateTime and string. Datetime gave me a circular reference issue and I needed to check if propertyValue is DateTime then stopped to go deeper. I needed to do the same with string because string implements IEnumerable.
Any feedbacks is appreciated. :)
My main argument against this solution is that in case of a bug you're revealing too much information. From a security point of view it much better to structure code so that in case of a bug not enough information is returned.
Model
public partial class MemberModel
{
[Key]
public int MemberID { get; set; }
[Required]
[Unique_Member]
[StringLength(255)]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[Unique_Member]
[StringLength(255)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name="Name")]
public string FullName { get { return string.Format(FirstName + " " + LastName); } }
[Required]
[StringLength(355)]
public string Address { get; set; }
[Required(ErrorMessage="The City field is Required")]
public int CityID { get; set; }
[Required(ErrorMessage = "The Country field is Required")]
public int CountryID { get; set; }
[Required]
[RegularExpression(#"^((0092))-{0,1}\d{3}-{0,1}\d{7}$|^\d{4}$|^\d{4}-\d{7}$", ErrorMessage = "Invalid Phone number")]
[Unique_Member]
public string Pin { get; set; }
[Display(Name="Mobile No.")]
[Required(ErrorMessage="Mobile No. Required")]
[RegularExpression(#"^((\+92)|(0092))-{0,1}\d{3}-{0,1}\d{7}$|^\d{11}$|^\d{4}-\d{7}$",ErrorMessage="Invalid Phone number")]
public string Phone { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
public virtual List<Order_SummeryModel> Order_Summeries { get; set; }
public virtual CountryModel Country { get; set; }
public virtual CityModel City { get; set; }
}
Custom Validation [Unique_Member]
its a custom validation for three properties "Pin","FirstName" and "LastName" which i made for create new member. It checks whether fullname and pin of new member is unique or not.
its works perfectly for create action but in edit action this restrict me to update the member model, i want to disable it for edit action, or there is another way to update the model with disable it.
public class Unique_MemberAttribute : ValidationAttribute
{
private static int count;
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
Context_getway db = new Context_getway();
string membervalue = value.ToString();
//var count = db.Members.Where((x => x.Name == membervalue || x.Pin == membervalue || x.Email == membervalue)).Count();
var count_fname = db.Members.Where(x => x.FirstName == membervalue).Count();
var count_lname = db.Members.Where(x => x.LastName == membervalue).Count();
var count_pin = db.Members.Where(x => x.Pin == membervalue).Count();
if ((count_fname != 0)||(count_lname != 0))
{
count++;
if (count == 2)
{
return new ValidationResult("Member Already Exist with the same Full Name (Change First Name OR Last Name)!");
}
}
if (count_pin != 0)
{
return new ValidationResult("Member Already Exist with the same Pin!");
}
return ValidationResult.Success;
}
}
[MetadataType(typeof(MemberModel))]
public partial class MemberModel
{
}
Member Controller (edit action)
[HttpGet]
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
else
{
var member = db.Members.Find(id);
ViewBag.CountryID = new SelectList(db.CountryModels.ToList(), "CountryID", "Country",member.CountryID);
ViewBag.CityID = new SelectList(db.CityModels.ToList(), "CityID", "City",member.CityID);
if (member != null)
{
return View(member);
}
else
return HttpNotFound();
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(MemberModel member)
{
try
{
if (ModelState.IsValid)
{
db.Entry(member).State = System.Data.Entity.EntityState.Modified;
db.SaveChanges();
TempData["Msg"] = "Update Successfully";
return RedirectToAction("Index");
}
else
{
ViewBag.CountryID = new SelectList(db.CountryModels.ToList(), "CountryID", "Country",member.CountryID);
ViewBag.CityID = new SelectList(db.CityModels.ToList(), "CityID", "City",,member.CityID);
return View(member);
}
}
catch(Exception e)
{
TempData["Msg"] = "Update Unsuccessfully: "+ e.Message;
return View();
}
}
Try this:
ModelState.Remove("PropertyNameInModel");
You should still do the validation on Edit action method. Otherwise user can edit a record and select a unique combination already used by another record. You should simply use the Id property in your where clause to check any record other than the currently editing record.
So the first step is to get the Id property value of the current entity/view model you are validating. Then use the value in your where clauses.
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var idProperty = validationContext.ObjectType.GetProperty("MemberID");
var idValueObj = idProperty.GetValue(validationContext.ObjectInstance, null);
var id = 0;
if (idValueObj != null)
id = (int) idValueObj;
var db = new Context_getway();
string membervalue = value.ToString();
var count_fname = db.Members.Count(x => x.FirstName == membervalue && x.UserId!=id);
//Your existing code goes here. Make sure to use the id value in your WHERE clauses
}
I just hardcoded the property name ("MemberID") in the answer to give you the idea. But if you want more flexibility, you can pass that when you use the attribute as mentioned in this answer.
Also you should double check your conditions. I find issues with your code. What if the value of your count variable valus is more than 1(ex:2)), then your if (count == 2) will not return true (because your count is more than 2. I am not sure your business requirements/rules. But if you are looking for unique full names, you can create a single LINQ statement to do that ( Use Any method instead of getting Count as needed)
One way is to remove the error from ModelState in the Edit Controller action, right before checking if the model is valid.
But the better way is to separate your Edit and Insert models.
The Edit Model will have all validation rules for edit; and the Insert model will be inherited from the Edit model and overriding some properties with additional validation rules.
public partial class EditMemberModel
{
[Key]
public int MemberID { get; set; }
[Required]
[StringLength(255)] // Removed the Unique_Member rule**
[Display(Name = "First Name")]
public virtual string FirstName { get; set; }
/// etc.
}
public partial class InsertMemberModel : EditMemberModel
{
[Required]
[Unique_Member]
[StringLength(255)]
[Display(Name = "First Name")]
public override string FirstName { get; set; }
/// etc.
}
I use ASP.NET MVC. How can i validate string array in my view model. Because "Required" attribute doesn't work with string array.
[DisplayName("Content Name")]
[Required(ErrorMessage = "Content name is required")]
public string[] ContentName { get; set; }
You can create a custom validation attribute : http://www.codeproject.com/Articles/260177/Custom-Validation-Attribute-in-ASP-NET-MVC
public class StringArrayRequiredAttribute : ValidationAttribute
{
protected override ValidationResult IsValid (object value, ValidationContext validationContext)
{
string[] array = value as string[];
if(array == null || array.Any(item => string.IsNullOrEmpty(item)))
{
return new ValidationResult(this.ErrorMessage);
}
else
{
return ValidationResult.Success;
}
}
}
Then you can use like this :
[DisplayName("Content Name")]
[StringArrayRequired(ErrorMessage = "Content name is required")]
public string[] ContentName { get; set; }
You should use custom validate
[HttpPost]
public ActionResult Index(TestModel model)
{
for (int i = 0; i < model.ContentName.Length; i++)
{
if (model.ContentName[i] == "")
{
ModelState.AddModelError("", "Fill string!");
return View(model);
}
}
return View(model);
}
My model is defined as follows:
namespace Project.Models
{
public enum LogType
{
Login = 0,
Login_Fail = 1
}
[Table("UserLog")]
public class UserLog
{
public long Id { get; set; }
public int UserId { get; set; }
public DateTime Date { get; set; }
public string Des { get; set; }
public LogType Type { get; set; }
public virtual User User { get; set; }
}
}
Base type of Type field in the UserLog table is tinyint.
Login controller code as follows:
[HttpPost]
public virtual JsonResult Login(UserViewModel model)
{
if (userRepository.CheckUserLogin(model.UserName, model.Password))
{
UserLog log = new UserLog();
log.Date = DateTime.Now;
log.Des = "";
log.Type = LogType.Login;
userRepository.AddUserLog(model.UserName, log);
userRepository.Save();
Session["LoginUser"] = model.UserName;
}
}
And Login Repository code as follows:
public void AddUserLog(string username, UserLog log)
{
User user = GetUserByUserName(username);
if (user != null)
user.UserLogs.Add(log);
}
The problem is that information is properly stored in UserLog table, but the Type field remains Null!
I've used this solution:
public int Type { get; set; }
[NotMapped]
public LogType UserLogType
{
get { return (LogType)Type; }
set { Type = (int)value; }
}
I have class User in my project and have model UserRow (for showing user in view)
it's UserRow
using System;
namespace Argussite.SupplierServices.ViewModels
{
public class UserRow
{
public Guid Id { get; set; }
public string FullName { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Status { get; set; }
public int Role { get; set; }
public Guid SupplierId { get; set; }
public bool ActionsAllowed { get; set; }
public bool MailResendRequired { get; set; }
}
}
and I need to add in my controller checking if ActionsAllowed
[HttpPost]
public ActionResult Unlock(Guid id)
{
var user = Context.Users.Find(id);
if (user == null)
{
return Json(CommandResult.Failure("User was not found. Please, refresh the grid and try again."));
}
var checkActionsAllowed = Context.Users.AsNoTracking()
.Select(e => new UserRow
{
Id = e.Id,
ActionsAllowed = e.ActionsAllowed
};
if (checkActionsAllowed == true)
{
user.Status = UserStatus.Active;
return Json(CommandResult.Success(string.Format("User {0} has been unlocked.", user.FullName)));
}
else return;
}
but I got error with ActionsAllowed = e.ActionsAllowed and
in else return;
Help me please to solve this problem.
You have two problems:
Context.Users.AsNoTracking()
.Select(e => new UserRow
{
ActionsAllowed = e.ActionsAllowed
};
returns a list of objects, not a single object.
You have queried the user above, so i guess you can write simply:
if (user.ActionsAllowed) {
user.Status = UserStatus.Active;
return Json(CommandResult.Success...);
}
The second problem is the return; statement.
Your method returns an action result, so you have to return something.
For example
return Json(CommandResult.Failure(
"ActionsAllowed = false"));
First error sounds like you User class doesn't provide a ActionsAllowed Boolean property, while the second error happens because you need to return something from the method that can be interpreted as an ActionResult.
EDIT:
Hmm, I didn't notice this the first time, but this:
var checkActionsAllowed = Context.Users.AsNoTracking()
.Select(e => new UserRow
{
Id = e.Id,
ActionsAllowed = e.ActionsAllowed
};
followed by this:
if (checkActionsAllowed == true)
makes no sense - you're not returning a boolean result from a Select method, but rather an IEnumerable. Perhaps you should add your User schema to your question so that it's more obvious what you're trying to accomplish.