Web API – Custom Data Annotation

By | 06/10/2021

In this post, we will see how to create a Data Annotation custom to use in a .NET 5 Web API project.
First of all, what is a Data Annotation?
From Microsoft web site:
“The advantage of using the Data Annotation validators is that they enable you to perform validation simply by adding one or more attributes – such as the Required or StringLength attribute – to a class property.”

We start creating a .NET 5 Web API project called CustomDataAnnotaion_Post, where we will add these files:

[Domain\Entities\Document.cs]

using System;

namespace OData_Post.Domain.Entities
{
    public class Document
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string Tag { get; set; }
        public DateTime? Expiration { get; set; }
    }
}



[Application\Interfaces\IDocumentControllerService]

using OData_Post.Domain.Entities;
using System.Collections.Generic;

namespace OData_Post.Application.Interfaces
{
    public interface IDocumentControllerService
    {
        List<Document> GetAllDocuments();

        void InsertDocument(Document inputDocument);
    }
}



[Application\ControllerServices\DocumentControllerService]

using OData_Post.Application.Interfaces;
using OData_Post.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;

namespace OData_Post.Application.ControllerServices
{
    public class DocumentControllerService : IDocumentControllerService
    {
        private List<Document> _lstDocuments = new List<Document>();

        public DocumentControllerService()
        {
            FeedLstDocument();
        }

        public List<Document> GetAllDocuments()
        {
            return _lstDocuments;
        }

        public void InsertDocument(Document inputDocument)
        {
            inputDocument.Id = _lstDocuments.Max(x => x.Id) + 1;
            _lstDocuments.Add(inputDocument);
        }

        private void FeedLstDocument()
        {
            for (int i = 1; i <= 5; i++)
            {
                Document objDocument = new Document();
                objDocument.Description = $"Description_{i}";
                objDocument.Id = i;
                objDocument.Tag = $"Tag{i + 1};Tag{i + 2}";
                objDocument.Title = $"Title{i}";
                objDocument.Expiration = i % 2 == 0 ? DateTime.Now.AddDays(-(i * 3)) : null;
                _lstDocuments.Add(objDocument);
            }
        }
    }
}



Then, we will add a new Controller called DocumentController:

[Controllers\DocumentController.cs]

using Microsoft.AspNetCore.Mvc;
using OData_Post.Application.Interfaces;
using OData_Post.Domain.Entities;

namespace OData_Post.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class DocumentController : ControllerBase
    {
        IDocumentControllerService _documentControllerService;

        public DocumentController(IDocumentControllerService documentControllerService)
        {
            _documentControllerService = documentControllerService;
        }

        [HttpGet]
        public IActionResult GetAllDocument()
        {
            return Ok(_documentControllerService.GetAllDocuments());
        }

        [HttpPost]
        public IActionResult NewDocument(Document inputDocument)
        {
            try
            {
                _documentControllerService.InsertDocument(inputDocument);
                return Ok();
            }
            catch
            {
                return BadRequest();
            }
            
        }
    }
}



Finally, we will add the dependency injection for the DocumentControllerService in Startup file:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDocumentControllerService, DocumentControllerService>();
    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "OData_Post", Version = "v1" });
    });
}



Now, using Postman, we will check everything works fine:

LIST

INSERT NEW DOCUMENT

Now, we will use Data Annotation in order to check values in Input.
For example, if we want the parameter Title mandatory, we have just to modify the Document entity in this way:

using System;
using System.ComponentModel.DataAnnotations;

namespace OData_Post.Domain.Entities
{
    public class Document
    {
        public int Id { get; set; }
        [Required]
        public string Title { get; set; }
        public string Description { get; set; }
        public string Tag { get; set; }
        public DateTime? Expiration { get; set; }
    }
}



If we try to insert a Document without the Title, this will be the result:

By design we have many Data Annotation that help us to create our application but, it is very easy create own Data Annotation custom.

For example, if we want to verify that the expiration date is greater than today’s date in case the Expiration parameter is not null, we should add this code in the method NewDocument of DocumentController:

[HttpPost]
public IActionResult NewDocument(Document inputDocument)
{
    if (inputDocument.Expiration.HasValue && inputDocument.Expiration.Value > DateTime.Now)
    {
         return BadRequest("The expiration Date must be greater than today");
    }

    try
    {
         _documentControllerService.InsertDocument(inputDocument);
          return Ok();
    }
    catch
    {
         return BadRequest();
    }
}



If we try to insert a Document with a Expiration parameter greater the today (today is 25/09/2021), this will be the result:

We see that it works but, we could create a custom Data Annotation for avoiding to modify our controller.

We start creating a new Class called Document_Expiration where we will define our Data Annotation:

[Domain\CustomValidation\Document_Expiration.cs]

using OData_Post.Domain.Entities;
using System;
using System.ComponentModel.DataAnnotations;

namespace CustomDataAnnotaion_Post.Domain.CustomValidation
{
    // we have to inherit ValidationAttribute
    public class Document_Expiration: ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            // we take the Entity to check
            var objDocument = validationContext.ObjectInstance as Document;

            if (objDocument != null && objDocument.Expiration.HasValue)
            {
                // we check the parameter
                if(objDocument.Expiration.Value > DateTime.Now)
                {
                    // creation of error message
                    return new ValidationResult("The Expiration date must be greater than today");
                }
            }
            // no error
            return ValidationResult.Success;
        }
    }
}



Then, we have to add the attribute Document_Expiration in our Document class:

using CustomDataAnnotaion_Post.Domain.CustomValidation;
using System;
using System.ComponentModel.DataAnnotations;

namespace OData_Post.Domain.Entities
{
    public class Document
    {
        public int Id { get; set; }
        [Required]
        public string Title { get; set; }
        public string Description { get; set; }
        public string Tag { get; set; }

        [Document_Expiration]
        public DateTime? Expiration { get; set; }
    }
}



Finally, we have to delete the code we added in DocumentController:

using Microsoft.AspNetCore.Mvc;
using OData_Post.Application.Interfaces;
using OData_Post.Domain.Entities;

namespace OData_Post.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class DocumentController : ControllerBase
    {
        IDocumentControllerService _documentControllerService;

        public DocumentController(IDocumentControllerService documentControllerService)
        {
            _documentControllerService = documentControllerService;
        }

        [HttpGet]
        public IActionResult GetAllDocument()
        {
            return Ok(_documentControllerService.GetAllDocuments());
        }

        [HttpPost]
        public IActionResult NewDocument(Document inputDocument)
        {
            try
            {
                _documentControllerService.InsertDocument(inputDocument);
                return Ok();
            }
            catch
            {
                return BadRequest();
            }
            
        }
    }
}



We have done and now, if we try to run the Web API, this will be the result:

WITHOUT EXPIRATION VALUE => OK

WITH A CORRECT EXPIRATION VALUE => OK

WITH A NOT CORRECT EXPIRATION VALUE => ERROR

WITH A NOT CORRECT EXPIRATION VALUE AND NOT TITLE VALUE => ERROR



Leave a Reply

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