Best ASP.MVC Model usage (Part 2)

Hi everyone,

In previous article I made a rant about recommended software templates and techniques (A rant about Microsoft’s ASP.NET MVC templates).
And I made a promise to tell you how to create ASP.NET MVC application better.

In this article we are going to address first requirement:

Where to put business/validation logic for an entity??

Let’s asume we’ve got a shopping application and we need an Order entity. It has primary key (OrderId), some user readable number (OrderCode), id of customer (FK to customer table), SentDate to tell when the order was sent to the customer, and a Status indicating at what stage of business processes this order is situated. So a new order has Open status. Then we assign a person who is going to process this order, which changes status to Assigned. Then it’s Sent to the customer.

Because I hate using string literals I’ve inserted Statuses enumerable, so statuses have strong naming across the entire system. I did the same for Actions which basically are actions in ASP controller (like button clicks, display of some view etc).

public class Order
{
     public enum Statuses { Open, Assigned, Sent, Cancelled}
     public enum Actions { Edit, Delete, Details, Cancel, Index, Create, Assign, Send}

     public int OrderId { get; set; }

     [Required]
     public String OrderCode { get; set; }

     [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm}", ApplyFormatInEditMode = true)]
     public DateTime? SentDate { get; set; }

     [Required]
     public int CustomerId { get; set; }
     public virtual Customer Customer { get; set; }

     [Required]
     public String Status { get; set; }
}

I tend to use Status as string because if you use enumerable instead, then Entity Framework Code First mechanism will convert it into integer in SQL. Which is not very developer friendly when you browse data. Also if you change order of statuses in enumerable then states change their internal integer index and you end up with a mess in database. I only want to make sure that Status field is used as intended, meaning that it always contains one of allowed states. I do it by using custom attribute.

    [Required]
    [Status(typeof(Statuses))]
    public String Status { get; set; }

What it does is that it checks whether Status string value corresponds to one of statuses in enumerable.

 public class StatusAttribute : ValidationAttribute
    {
        Type EnumType;

        public StatusAttribute(Type EnumType)
        {
            this.EnumType = EnumType;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            System.Reflection.PropertyInfo pi = validationContext.ObjectInstance.GetType().GetProperty("Status");
            String StrStatus = pi.GetValue(validationContext.ObjectInstance) as String;
            try
            {
                Object result = Enum.Parse(EnumType, StrStatus);
                if (result == null)
                    return new ValidationResult("Status parsed cannot be null");
                if (result.GetType() == EnumType)
                    return ValidationResult.Success;
                return new ValidationResult("Status type does not match expected type");

This is our first business condition. Then I add a functin that converts from string status value into Statuses enumerable value. It can be used in the application easier than string value. String Status value is only for database basically. The new StatusEnum field is marked ‘NotMapped’ as we don’t want it to be mapped in database.

[NotMapped]
public Statuses StatusEnum 
{
 get {
    if (String.IsNullOrEmpty(Status)) return Statuses.Open;
    else
    {
      Statuses statOut;
      if (Enum.TryParse(Status, out statOut))
         return (Statuses)Enum.Parse(typeof(Statuses), Status);
      throw new Exceptions.MyGeneralException($"Status {Status} not recognized, for Order entity: {OrderId} ");
     }
    }       
 set { Status = value.ToString(); }
}

Also I would like to make sure that when status is Sent then we require SendDate to be filled with some real date. Custom attribute is again best for this job. Important: this has nothing to do with standard Required attribute which is then converted to database’s NOT NULL condition.

public class ConditionalRequiredAttribute : ValidationAttribute
    {
        Order.Statuses RequiredInThisStatus;
        public ConditionalRequiredAttribute(Order.Statuses RequiredInThisStatus)
        {
            this.RequiredInThisStatus = RequiredInThisStatus;            
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            Order order = validationContext.ObjectInstance as Order;
            if (order.StatusEnum == RequiredInThisStatus && value == null)
            {
                var result = new ValidationResult($"Value of field { validationContext.MemberName} required when Status={RequiredInThisStatus.ToString()}");
                return result;
            }
            return ValidationResult.Success ;
        }
    }

As you can see, most business requirements for data in our system are checked at model level, by using builtin and custom attributes. It allowed the system to filter out all incoming data from user or external api’s input, so we make sure that everything inside the system is correct and according to our standards. It means that whatever happens and whoever and in whatever way tries to input data into the system it has to follow some basic standards/requirements for data purity.

Also we have one place to check/change in case requirements for Order change.

In the next article I will show you how to make sure that certain actions are allowed only in certain states of an Order. For example that we can cancel an order only if it is Open or Assigned, not when it’s Sent.

Thanks for reading 🙂

Dominik Steinhauf

CEO, .Net developer, software architect at Creative Yellow Solutions (formerly Indesys)

If you need help with your software project, or need customized software for your company, contact me at: dominik.steinhauf (at) cys.biz.pl

Leave a Reply

Your email address will not be published. Required fields are marked *