keeehlan
keeehlan

Reputation: 8054

Compare two properties with custom DataAnnotation Attribute?

I have a custom model that holds some DateTime values, and a custom DataAnnotation that's built to compare these values.

Here's the properties with their annotations:

[Required]
[DataType(System.ComponentModel.DataAnnotations.DataType.Date)]
[Display(Name = "Start Date")]
public DateTime StartTime { get; set; }

[DataType(System.ComponentModel.DataAnnotations.DataType.Date)]
[Display(Name = "End Date")]
[CompareTo(this.StartTime, CompareToAttribute.CompareOperator.GreaterThanEqual)]
public DateTime? EndTime { get; set; }

The CompareTo attribute is the one in question. I get an error:

Keyword 'this' is not available in the current context

I've tried placing only StartTime in the annotation with no luck. How can I pass in a property value from the same model class?

Upvotes: 2

Views: 10805

Answers (4)

ASHOK MANGHAT
ASHOK MANGHAT

Reputation: 35

public class StartDateValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var model = (CostCenterAllocationHeader)validationContext.ObjectInstance;
        if (model.EndEffectiveDate == null)  // Abort if no End Date
            return ValidationResult.Success;

        DateTime EndDate = model.EndEffectiveDate.GetValueOrDefault();
        DateTime StartDate = Convert.ToDateTime(value);  // value = StartDate

        if (StartDate > EndDate)
            return new ValidationResult("The start date must be before the end date");
        else
            return ValidationResult.Success;
    }
}

In the above example there is one issue , This solution can't be used as a common solution for validating the dates . Because the type casting of below line is not generic

var model = (CostCenterAllocationHeader)validationContext.ObjectInstance;

It means that the validation can only be applied to the specific model "costCenterAllocationHeader" . What needs to be done by passing the member name to the constructor of the validator and get the value from the ValidationContext using reflection. By this method we can use this attribute as a generic solution and can be applied in any ViewModels.

Upvotes: 0

Eric Wood
Eric Wood

Reputation: 361

I like Hassen's answer.

Same as Hassen's example, but suggest: 1) aborting if no end date if end date is optional. 2) putting a validator on end date in case user only changes end date

Data Annotation:

[Required]
[Display(Name = "Start Effective Date", Description = "Start Date")]
[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
[DataType(DataType.Date)]
[StartDateValidator]
public DateTime StartEffectiveDate { get; set; }

[Display(Name = "End Effective Date", Description = "End Date")]
[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
[DataType(DataType.Date)]
[EndDateValidator]
public DateTime? EndEffectiveDate { get; set; }

Code:

public class StartDateValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var model = (CostCenterAllocationHeader)validationContext.ObjectInstance;
        if (model.EndEffectiveDate == null)  // Abort if no End Date
            return ValidationResult.Success;

        DateTime EndDate = model.EndEffectiveDate.GetValueOrDefault();
        DateTime StartDate = Convert.ToDateTime(value);  // value = StartDate

        if (StartDate > EndDate)
            return new ValidationResult("The start date must be before the end date");
        else
            return ValidationResult.Success;
    }
}

public class EndDateValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var model = (CostCenterAllocationHeader)validationContext.ObjectInstance;
        if (model.EndEffectiveDate == null)  // Abort if no End Date
            return ValidationResult.Success;

        DateTime EndDate = Convert.ToDateTime(value); // value = EndDate
        DateTime StartDate = model.StartEffectiveDate; 

        if (StartDate > EndDate)
            return new ValidationResult("The start date must be before the end date");
        else
            return ValidationResult.Success;
    }
}

Would have commented on Hassen's answer, but don't have enough reputation.

Upvotes: 1

Hassen Ch.
Hassen Ch.

Reputation: 1751

If anyone is still wondering how to compare two dates and use that in a validation DataAnnotation, you can simply add an extension method that will compare the start date and the end date like the following.

Assuming that this is your class:

using System;
using System.ComponentModel.DataAnnotations;

namespace Entities.Models
{
    public class Periode
    {
        [Key]
        public int PeriodeID { get; set; }

        public string Name { get; set; }

        [DataType(DataType.Date)]
        [Display(Name ="Start Date")]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime StartDate { get; set; }

        [DataType(DataType.Date)]
        [Display(Name = "End Date")]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EndDate { get; set; }
    }
}

You simply add the following class as a validator:

namespace Entities.Models
{

    public class StartEndDateValidator : ValidationAttribute
    {
        protected override ValidationResult
                IsValid(object value, ValidationContext validationContext)
        {
            var model = (Models.Periode)validationContext.ObjectInstance;
            DateTime EndDate = Convert.ToDateTime(model.EndDate);
            DateTime StartDate = Convert.ToDateTime(value);

            if (StartDate > EndDate)
            {
                return new ValidationResult
                    ("The start date must be anterior to the end date");
            }
            else
            {
                return ValidationResult.Success;
            }
        }
    }
}

And then you need to add that DataAnnotation on the StartDate as following

namespace Entities.Models
{
    public class Periode
    {
        [Key]
        public int PeriodeID { get; set; }

        public string Name { get; set; }

        [DataType(DataType.Date)]
        [Display(Name ="Start Date")]
        // You need to add the following line
        [StartEndDateValidator]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime StartDate { get; set; }

        [DataType(DataType.Date)]
        [Display(Name = "End Date")]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EndDate { get; set; }
    }
}

Upvotes: 5

Darin Dimitrov
Darin Dimitrov

Reputation: 1038730

I've tried placing only StartTime in the annotation with no luck. How can I pass in a property value from the same model class?

That's impossible because attributes are metadata that is baked into the assembly at compile-time. This means that you can pass only CONSTANT parameters to an attribute. Yeah, that's a hell of a limitation because in order to perform such an obvious validation thing as comparing 2 values in your model you will have to write gazzilion of plumbing code such as what I have illustrated here for example: https://stackoverflow.com/a/16100455/29407 I mean, you will have to use reflection! Come on Microsoft! Are you serious?

Or just cut the crap of data annotations and start doing validation the right way: using FluentValidation.NET. It allows you to express your validation rules in a very elegant way, it greatly integrates with ASP.NET MVC and allows you to unit test your validation logic in isolation. It also doesn't rely on reflection so it is super fast. I have benchmarked it and using it in very heavy traffic production applications.

Data annotations just don't cut the mustard compared to imperative validation rules when you start writing applications that are a little more complicated than a Hello World and which require a little more complex validation logic than you would have in a Hello World application.

Upvotes: 3

Related Questions