Ensure sensitive data is removed from response [closed] - asp.net-core-webapi

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.

Related

Adding new Entity generates negative id

I'm trying to add a new Container entity, but when I add the entity, I'm getting a negative Id being displayed in the resulting dataset. However, inside of my SQL database, I end up getting the correct next Id of the Container that was just inserted.
I've looked around, and I can see where this same issue occurs if you were to Update the entity before adding it - but I can't quite see anything related to the error occuring when you're first Adding the entity.
I'm literally just passing a new'ed up Container object, and adding it to the Containers DbSet on the ProductContext.
public class ProductService : IProductService
public ProductService(IProductContext productContext)
{
_productContext = productContext;
}
public Container CreateContainer(Container container, int currentUserId)
{
_productContext.Containers.Add(container);
_productContext.SaveChangesAsync();
return container;
}
}
However, when stepping through, I can see that I start off with a zero'd out Id...
And after the save is processed, the container has an Id of -2147482645.
But if I check the database, I end up with the correct next value after it's been saved. However, this value does not update on the entity, instead it stays as the -2147482645 value.
Here's my Container entity...
public class Container : BaseEntity
{
public Container()
{
this.ChildContainers = new HashSet<Container>();
this.InventoryItems = new HashSet<InventoryItem>();
}
public ICollection<Container> ChildContainers { get; set; }
public ICollection<InventoryItem> InventoryItems { get; set; }
public Location Location { get; set; }
public int? LocationId { get; set; }
public string Name { get; set; }
[ForeignKey("ParentContainerId ")]
public Container ParentContainer { get; set; }
public int? ParentContainerId { get; set; }
}
and the value of the BaseEntity class
public abstract class BaseEntity
{
[Key]
public virtual int Id { get; set; }
[DefaultValue(false)]
public virtual bool IsDeleted { get; set; }
protected virtual object Actual => this;
public override bool Equals(object obj)
{
if (!(obj is BaseEntity other))
return false;
if (ReferenceEquals(this, other))
return true;
if (Actual.GetType() != other.Actual.GetType())
return false;
if (Id == 0 || other.Id == 0)
return false;
return Id == other.Id;
}
public static bool operator ==(BaseEntity a, BaseEntity b)
{
if (a is null && b is null)
return true;
if (a is null || b is null)
return false;
return a.Equals(b);
}
public static bool operator !=(BaseEntity a, BaseEntity b)
{
return !(a == b);
}
public override int GetHashCode()
{
return (Actual.GetType().ToString() + Id).GetHashCode();
}
public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; }
}
And the implementation of my SaveChangesAsync method - however, I don't believe this really could affect it, as the Id is getting updated before it hits this point.
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
var entries = ChangeTracker
.Entries()
.Where(e => e.Entity is BaseEntity && (
e.State == EntityState.Added
|| e.State == EntityState.Modified));
foreach (var entityEntry in entries)
{
((BaseEntity)entityEntry.Entity).UpdatedDate = DateTime.Now;
if (entityEntry.State == EntityState.Added)
{
((BaseEntity)entityEntry.Entity).CreatedDate = DateTime.Now;
}
}
return await base.SaveChangesAsync();
}
EDIT 1: SaveChanges syncronously
It was mentioned that the SaveChanges could affect it as I was running the asynchronous version of the method in a synchronous way. I don't believe this would be causing the error, as the Id is getting set/updated inside the Add method.

How skip NullReferenceException in Get API

Here create an API to get the records, in my entity relation table there are twice start date and end date. Here my compulsion is one of them need to keep Null able type.
Here is ER that is SchoolCourses:
public class SchoolCourses
{
public Guid ID { get; set; }
public DateTime StartCourseDate { get; set; }
public DateTime EndCourseDate { get; set; }
public DateTime? StartSemDate { get; set; } // Null able type
public DateTime? EndSemDate { get; set; } // Null able type
}
I creates a repository for getting the value:
public async Task<ICollection<SchoolCourses>> GetcourseBySchoolId(Guid SchoolId)
{
List<SchoolCourses> schoolCourses = null;
schoolCourses = await _GpsContext.SchoolCourses.AsNoTracking()
.Where(x => x.SchoolsID == SchoolId)
.ToListAsync();
return schoolCourses;
}
And the Controller are like this:
public async Task<IActionResult> GetforSchoolCourse(string SchoolId)
{
var result = await _schoolCoursesRepository.GetcourseBySchoolId(Guid.Parse(SchoolId));
List<GetSchoolCourseBySchoolIdVm> getSchoolCourseBySchoolIdVms = new List<GetSchoolCourseBySchoolIdVm>();
foreach (SchoolCourses schoolCourse in result)
{
getSchoolCourseBySchoolIdVms.Add(new GetSchoolCourseBySchoolIdVm
{
id = schoolCourse.ID.ToString(),
StarCoursetDate = schoolCourse.StartCourseDate.ToString(),
EndCourseDate = schoolCourse.EndCourseDate.ToString(),
StartSemDate = schoolCourse.StartSemDate.ToString(),
EndSemDate = schoolCourse.EndSemDate.ToString(),
});
}
return Ok(getSchoolCourseBySchoolIdVms);
}
Here is View Model for reference:
public class GetSchoolCourseBySchoolIdVm
{
public string id { get; set; }
public string StarCoursetDate { get; set; }
public string EndCourseDate { get; set; }
public string StartSemDate { get; set; }
public string EndSemDate { get; set; }
}
After doing all the above staff it is getting exception error in swagger is following:
System.NullReferenceException: Object reference not set to an instance of an object.;
In your SchoolCourses model StartSemDate and EndSemDate are nullable types, so it must be possible that values of those fields are null. That should have been checked before using it, unlike you have used
StartSemDate = schoolCourse.StartSemDate.ToString(),
EndSemDate = schoolCourse.EndSemDate.ToString(),
here if any of the date is null then calling .ToString() method on it will throw NullReferenceException. Use safe navigation operator to check
schoolCourse.StartSemDate?.ToString()
or
schoolCourse.StartSemDate != null ? schoolCourse.StartSemDate.ToString() : string.Empty

i want to disable my custom validation in edit action in asp.net mvc

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.
}

Add checking in controller

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.

ASP.NET MVC 3 Data Annotation: Add validation dynamically

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

Resources