.NET Core MVC Appliction with RESTful Service

    0
    30

    Introduction

    There is a lot of information out there about how to build a web API or RESTful service, but much less about how to consume it, so I decided to build one myself and share it with you. In this blog, I explain how you can create a RESTful service combined with a MVC web application that consumes this service. Security is covered in the next blog. The web application has the standard CRUD (Create Retrieve Update and Delete) operations and a table view for browsing the data.

    Overview

    Figure 1 shows the application is divided in several layers. Each layer has its own responsibility.

    The GUI layer renders webpages for the user interface and uses the RESTful service for receiving and storing data. The RESTful service offers the CRUD functions for the resource model. In our case, the resource model represents a country. The business logic is located at the resource service. It receives the CRUD calls and knows how to validate the business rules. The database service manages the storage and is implemented with Entity Framework. Some business rules are also implemented in this layer. For example, it checks if the primary key constraint is not violated. In this example, MySQL is the database but it could also be another one like Oracle or SQL server. In this example, the GUI talks to the RESTful service, but could also talk directly to the Resource service. I skipped this because one of the goals was to create and consume a RESTful service. Each layer is created with a separate C# project.

    Resource Model

    The resource model is simple POCO (Plain Old CLR Object) and represents a country. It travels between the different application layers.

    public class CountryResource : Resource<Int32>
 { 
 [Required]
 [Range(1, 999)]
 public override Int32 Id { get; set; }
    
 [Required]
 [StringLength(2, MinimumLength = 2, ErrorMessage = "Must be 2 chars long")]
 public String Code2 { get; set; }
    
 [Required]
 [StringLength(3, MinimumLength = 3, ErrorMessage = "Must be 3 chars long")]
 public String Code3 { get; set; }
    
 [Required] 
 [StringLength(50, MinimumLength = 2, ErrorMessage = "Name must be 2 to 50 chars long")]
 public String Name { get; set; }
 }

    It moved common fields to the base class Resource. Please note this class has a generic type for its Id, an Int32 for the CountryResource class.

    public class Resource<TKey> where TKey : IEquatable<TKey>
 {
 virtual public TKey Id { get; set; }
 public String CreatedBy { get; set; }
 public DateTime CreatedAt { get; set; }
 public DateTime? ModifiedAt { get; set; }
 public String ModifiedBy { get; set; }
 public String RowVersion { get; set; }
 }

    Resource Service

    I liked to start with the key Layer, the Resource service. It hosts all the core resource functions by implementing the IResourceService<TResource, TKey> interface.

    public interface IResourceService<TResource, TKey> 
    where TResource : Resource<TKey> where TKey : IEquatable<TKey>
 {
 TResource Create();
    
 Task<TResource> FindAsync(TKey id);
    
 IQueryable<TResource> Items();
    
 LoadResult<TResource> Load(String sortBy, String sortDirection, 
 Int32 skip, Int32 take, String search, String searchFields);
    
 Task<ResourceResult<TResource>> InsertAsync(TResource resource);
 Task<ResourceResult<TResource>> UpdateAsync(TResource resource);
 Task<ResourceResult<TResource>> DeleteAsync(TKey id);
 }

    The generic TKey sets the primary key type like String, Int, Guid, etc. This makes the interface more flexible for other resources. This design has no support for composite keys and the key name is always Id. Based on interface IResourceService, I create the specific interface ICountryResourceService .

    public interface ICountryResourceService : IResourceService<CountryResource, Int32>
 {
 }

    The interface approach will allow us to create later on the RESTful controller with the DI Dependency Injection pattern and make it easier testable. Now it’s time to get our hands dirty and implement the ICountryResourceService.

    public class CountryResourceService : ICountryResourceService, IDisposable
 {
 private readonly IMapper Mapper;
 protected ServiceDbContext ServiceContext { get; private set; }
    
 public CountryResourceService(ServiceDbContext serviceContext)
 {
 ServiceContext = serviceContext;
    
 
 var config = new AutoMapper.MapperConfiguration(cfg =>
 {
 cfg.AddProfiles(typeof(CountryMapping).GetTypeInfo().Assembly);
 });
    
 Mapper = config.CreateMapper();
 }

    The class CountryResourceServer gets the parameter ServiceDbContext serviceContext as dependency injection. The context parameter is a Database Service instance and knows how to store and to retrieve data.

    Resource Mapping

    In this simple example, the CountryResource model and the Country Entity Model are the same. With more complex resources, it’s highly likely the resource model and the entity model will differ and create the need for mapping or conversion between the two types. Mapping the two types in the Resource Service layer also drops the need for the Database Service to have a reference to the resource model and makes the design easier to maintain. Because the mapping only occurs the Resource Service layer, it’s OK to setup the mapper instance in the constructor, e.g. no need for DI. AutoMapper handles conversion between the two types. The CountryMapping class defines the mapping between the two types.

    public class CountryMapping : Profile
 {
 public CountryMapping()
 {
 
 CreateMap<Resources.CountryResource, Country>();
 CreateMap<Country, Resources.CountryResource>();
 }
 }

    Everything is now prepared to use the AutoMapper, the FindAsync function gives a good example how to use AutoMapper.

    public async Task<CountryResource> FindAsync(Int32 id)
 {
 
 var entity = await ServiceContext.FindAsync<Country>(id);
    
 
 var result = Mapper.Map<CountryResource>(entity);
    
 return result;
 }

    Business Rules

    Business Rules sets the resources constrains. The Resource Service layer enforces these rules.

    The CountryResource business rules are:

    • Id, unique and range from 1-999
    • Code2 unique, Length must be 2 upper chars ranging from A-Z
    • Code3 unique, Length must be 3 upper chars ranging from A-Z
    • Name, length ranging from 2 – 50 chars.

    Validation during Create or Update

    Before a resource is saved, the Resource Service layer fires three calls for business rules validation.

    BeautifyResource(resource);
    ValidateAttributes(resource, result.Errors); 
    ValidateBusinessRules(resource, result.Errors);

    BeautifyResource gives the opportunity to clean the resource from unwanted user input and alter the resource content. Beautifying is the only task, it does not validate in any way. Beautifying enhances the success rate for valid user input. During beautifying, all none letters are removed and the rest is converted to upper case.

    protected virtual void BeautifyResource(CountryResource resource)
 {
 
 resource.Code2 = resource.Code2?.ToUpperInvariant()?.ToLetter();
 resource.Code3 = resource.Code3?.ToUpperInvariant()?.ToLetter();
    
 resource.Name = resource.Name?.Trim();
 }

    ValidateAttributes enforces simple business rules at property level. Validation attributes are set by adding attributes to properties. The StringLength attribute in the CountryResource model is an example of validation attributes. One property can have multiple validation attributes. It’s up to the developer that these constraints don’t violate each other.

    protected void ValidateAttributes(CountryResource resource, IList<ValidationError> errors)
 {
 var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(resource);
 var validationResults = new List<ValidationResult>(); ;
    
 Validator.TryValidateObject(resource, validationContext, validationResults, true);
    
 foreach (var item in validationResults)
 errors.Add(new ValidationError(item.MemberNames?.FirstOrDefault() ?? "", item.ErrorMessage));
 }

    ValidateBussinesRules enforces complex business rules. Not all constraints can be covered by validation attributes. The unique constraints for several fields, for example, cannot be done by attributes. The ValidateBussinesRules is the place where such rules can be coded.

    protected virtual void ValidateBusinessRules(CountryResource resource, IList<ValidationError> errors)
 {
 var code2Check = Items().Where(r => r.Code2 == resource.Code2);
 var code3Check = Items().Where(r => r.Code3 == resource.Code3);
    
 
 if (resource.RowVersion.IsNullOrEmpty())
 {
 if (Items().Where(r => r.Id == resource.Id).Count() > 0)
 errors.Add(new ValidationError($"{resource.Id} is already taken", nameof(resource.Id)));
 }
 else
 {
 code2Check = code2Check.Where(r => r.Code2 == resource.Code2);
 code3Check = code3Check.Where(r => r.Code3 == resource.Code3);
 }
    
 if (code2Check.Count() > 0)
 errors.Add(new ValidationError($"{resource.Code2} already exist", nameof(resource.Code2)));
    
 if (code3Check.Count() > 0)
 errors.Add(new ValidationError($"{resource.Code3} already exist", nameof(resource.Code3)));
 }

    If the constraints are not met, errors are returned to the caller. These errors are shown in the GUI.

    Validation during Delete

    Business rules apply also on delete operations. Suppose a resource has a special status in a workflow and is there forbidden to delete. During deletion, the ValidateDelete method is called.

    protected virtual void ValidateDelete(CountryResource resource, IList<ValidationError> errors)
 {
 if (resource.Code2.EqualsEx("NL"))
 {
 errors.Add(new ValidationError("It's not allowed to delete the Low Lands! ;-)"));
 }
 }

    If errors are set, deletion is canceled and errors are shown:

    Database Service

    • Database agnostic
    • Audit
    • Concurrency exception

    RESTful Service

    • No mapping
    • Status codes

    LEAVE A REPLY