Click here to Skip to main content
13,252,065 members (57,119 online)
Click here to Skip to main content
Add your own
alternative version

Stats

8.1K views
405 downloads
22 bookmarked
Posted 31 Jul 2017

.Net Core RESTful or WebAPI MVC Web Application

, 7 Aug 2017
Rate this:
Please Sign up or sign in to vote.
Modern design example for .NET Core MVC with RESTful service

Introduction

There is a lot of information out there about how to build a WebAPI 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. 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 and SqlServer are supported. 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 a 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;

      // Setup AutoMapper between Resource and Entity
      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 creates the need for mapping 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 in the Resource Service layer, it's OK to setup the mapper instance in the constructor, e.g., no need for DI. AutoMapper handles the conversion between the two types based on the CountryMapping class.

public class CountryMapping : Profile
 {
   public CountryMapping()
   {
     // 2 way mapping resource <==> entity model
     CreateMap<Resources.CountryResource, Country>();
     CreateMap<Country, Resources.CountryResource>();
   }
 }

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

public async Task<CountryResource> FindAsync(Int32 id)
    {
      // Fetch entity from storage
      var entity = await ServiceContext.FindAsync<Country>(id);

      // Convert emtity to resource
      var result = mapper.Map<CountryResource>(entity);

      return result;
    }

Business Rules

Business Rules sets the resources constraints and are enforced in the Resource Service layer.

The CountryResource business rules are:

  • Id, unique and range from 1-999
  • Code2 unique, Length must be 2 upper case chars ranging from A-Z
  • Code3 unique, Length must be 3 upper case 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. 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 remaining string is converted to upper case.

protected virtual void BeautifyResource(CountryResource resource)
   {
     // Only letter are allowed in codes
     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 that cannot be covered by validation attributes. The complex rules can be coded in ValidateBusinessRules. The unique constraints for fields Code2 and Code3 cannot be done by attributes.

    protected virtual void ValidateBusinessRules
       (CountryResource resource, IList<ValidationError> errors)
    {
      // Check if Code2 and Code3 are unique 
      var code2Check = Items().Where(r => r.Code2 == resource.Code2);
      var code3Check = Items().Where(r => r.Code3 == resource.Code3);

      // Check if Id is unique for new resource
      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
      {
        // Existing resource, skip resource itself in unique check
        code2Check = code2Check.Where(r => r.Code2 == resource.Code2 && r.Id != resource.Id);
        code3Check = code3Check.Where(r => r.Code3 == resource.Code3 && r.Id != resource.Id);
      }

      // set error message
      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

The Database service stores the resource data and is built with Entity Framework. It receives the only calls from the Resource Service layer. This reduces the effort if you want to replace the Entity Frame work with an OM (Object Mapper) of your own choice. The service has a few handy features:

Database Agnostic

The database service has no knowledge on which database is actually used. The database configuration is set with DI (Dependency Injection) in the RESTful service layer. I explain this later in more detail.

Audit Support

Just before an entity is saved (insert or update), the audit trail fields are set. The current implementation is simple but it gives an good start point to extend the trail. The SaveChangesAsync method is overridden and a small hook AddAuditInfo is added.

public override Int32 SaveChanges(Boolean acceptAllChangesOnSuccess)
    {
      AddAuditInfo();

      var result = base.SaveChanges(acceptAllChangesOnSuccess);

      return result;
    }

    public override Task<Int32> SaveChangesAsync(Boolean acceptAllChangesOnSuccess, 
                       CancellationToken cancellationToken = default(CancellationToken))
    {
      AddAuditInfo();

      return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

The audit fields are set in AuditInfo.

private void AddAuditInfo()
    {
      // process all pending items
      foreach (var entry in this.ChangeTracker.Entries())
      {
        var entity = entry.Entity as Entity;

        var currentUserName = Identity?.Name ?? "Unknown";

        if (entity != null)
        {
          // Create new row version for concurrency support
          entity.RowVersion = Guid.NewGuid().ToString();

          // set audit info new entity
          if (Entry(entity).State == EntityState.Added)
          {
            entity.CreatedBy = currentUserName;
            entity.CreatedAt = DateTime.UtcNow;
          }

          // set audit info existing entity
          if (Entry(entity).State == EntityState.Modified)
          {
            entity.ModifiedBy = currentUserName;
            entity.ModifiedAt = DateTime.UtcNow;
          }
        }
      }
    }

Concurrency Detection

The Database Service supports optimistic concurrency. This means there is no lock set on an entity before editing. Optimistic locking has an easy approach. Get a entity with version info, start editing and save. During the save process, the version info is compared with the latest version from the database. If they are the same, all is fine. If the versions differ, someone else has made an update and a concurrency error has occurred. How this error is handled and presented to the user is not up to Database Service. On deletion, there is no concurrency detection. The user wants to drop the resource and if it's already gone or the content is changed, there is no reason to cancel the deletion. The UpsertAsync method inserts or updates an entity and performs the concurrency check.

public async Task<TEntity> UpsertAsync<TEntity>(TEntity entity) where TEntity : Entity
   {
     // transaction is required for concurrency check
     using (var transaction = Database.BeginTransaction())
     {
       try
       {
         // Detect Insert or Update
         var entityState = String.IsNullOrEmpty(entity.RowVersion) ?
                                   EntityState.Added : EntityState.Modified;

         // Check for concurrency error before update
         if (entityState == EntityState.Modified)
         {
           var keyValues = GetKeyValues(entity);

           // Find existing entity based on keyvale(s)
           var existingEntity = await FindAsync<TEntity>(keyValues);

           var existingRowVersion = existingEntity?.RowVersion ?? null;

           // If the rowversion does not match with the entity
           // the entity is updated by an other user or process and concurrency error has occurred
           if (existingRowVersion != entity.RowVersion)
             throw new ConcurrencyException("Concurrency Error");
         }

         if (entityState == EntityState.Added)
           Add(entity);
         else
           Attach(entity);

         Entry(entity).State = entityState;

         var ra = await SaveChangesAsync();

         Database.CommitTransaction();
       }
       catch (Exception ex)
       {
         Database.RollbackTransaction();

         throw ex;
       }

       return entity;
     }
   }

RESTful Service

Before we dive into the details, first a small primer about REST (Representational State Transfer) service. REST is an architectural style for exchanging resources between computers over the internet. In the last few years, REST has become a dominant design for building web services. Most developers find REST easier to use than SOAP or WDSL based services. REST has a few design principles:

  • HTTP verbs Get, Post, Delete, Update
  • Stateless
  • Transfer data in JSON
  • Status Codes
  • URI and API design
  • Self describing error messages

HTTP Methods

HTTP is designed around resource and verbs. HTTP verbs specify the operation type:

  • GET retrieves a resource.
  • POST creates a resource.
  • PUT updates a resource.
  • PATCH updates a small part of a resource.
  • DELETE deletes a resource (you already guessed that).

PATCH can be useful when you want to update only one field, for example, the status in a workflow application. PATCH is not used in my example.

Stateless

REST is aimed to be fast. Stateless services improve performance and are easier to design and implement.

JSON

JSON is the weapon of choice for serializing resources between the server and client. It can also be done in XML, however XML is more chatty than JSON and will result in bigger transfer documents. Another reason is the availability of very good JSON parsers for web client development, for example jQuery JSON.parse(...).

Status Codes

The status code of a response indicates the result. In this way, there is no need to add some kind of status result in the response message itself. The most common status codes are:

CodeDescriptionExample
2xxSuccess-
200OKResource updated
201CreatedResource created
204No ContentResource deleted
4xxClient Errors-
400Bad RequestPOST/PUT a resource with invalid business rules
404Not FoundResource not found for GET command
409ConflictConcurrency error

URI and API

URI (Uniform Resource Identifier) plays a major role in a well designed API. The URIs must be consistent, intuitive and easy to guess. The API for fetching a country could be:

http://localhost:50385/api/Country/528
{
  "Id": 528,
  "Code2": "NL",
  "Code3": "NLD",
  "Name": "Netherlands",
  "CreatedBy": "Unknown",
  "CreatedAt": "2017-06-08T11:56:16.187606",
  "ModifiedAt": null,
  "ModifiedBy": null,
  "RowVersion": "60985dce-f4c1-41a4-9d92-28cb62048ed8"
}
200

Self Describing Error Messages

A good REST service returns a useful error message. It's up to the client if or how to show the error message. Suppose we want to update a resource with this PUT request.

{
  "Id": 528,
  "Code2": "NL",
  "Code3": "NLD",
  "Name": null,
  "CreatedBy": "Me",
  "CreatedAt": "2030-01-25T03:15:21",
  "ModifiedAt": null,
  "ModifiedBy": null,
  "RowVersion": "60985dce-f4c1-41a4-9d92-28cb62048ed8"
}

The business rules require the Name is mandatory and is not set the request and will result in an error:

{
  "Resource": {
    "Id": 528,
    "Code2": "NL",
    "Code3": "NLD",
    "Name": null,
    "CreatedBy": "Me",
    "CreatedAt": "2030-01-25T03:15:21",
    "ModifiedAt": null,
    "ModifiedBy": null,
    "RowVersion": "60985dce-f4c1-41a4-9d92-28cb62048ed8"
  },
  "Errors": [
    {
      "Message": "Name",
      "MemberName": "The Name field is required."
    }
  ],
  "Exceptions": []
}

Swagger

Swagger UI is a free plugging that is extremely helpful during RESTful development. With Swagger, you can easily develop and test your solution.

Fig. 2 General Swagger screen

The overview shows the available API, and can easily be tested.

Fig.3 Testing API call

RESTful Controller

In .NET Core, a REST controller is the same as an MVC controller. It only differs in routing attributes.

[Route("api/[controller]")]
 public class CountryController : Controller
 {
   private readonly ICountryResourceService ResourceService;

   public CountryController(ICountryResourceService resourceService)
   {
     ResourceService = resourceService;
   }

Database Dependency Injection

In this example, the RESTful service connects to either MySQL or SqlServer.

The controller gets the ResourceService interface as DI and is configured during startup. The setting is located in the appsettings.json file.

"ConnectionStrings": {
    DatabaseDriver: "MySql",
     // MySql connection
    "DbConnection": "server=localhost;Database=DemoCountries;User Id=root;password=masterkey"

    // SqlServer connection
    // "DbConnection": "server=localhost;Database=DemoCountries;Trusted_Connection=True"
  },

Only if the DatabaseDriver points to "MySQL" (case insensitive), the service connects to a MySQL database, any other setting will connect to SqlServer.

public void ConfigureServices(IServiceCollection services)
    {
      // Get Database connection config
      var connectionString = Configuration.GetConnectionString("DbConnection");

      // Connect by default SqlServer other wise to MySql 
      var databaseDriver = Configuration.GetConnectionString("DatabaseDriver");

      // Setup Database Service layer used in CountryResourceService
      if (databaseDriver.EqualsEx("MySQL"))
        services.AddDbContext<EntityContext>(options => options.UseMySql(connectionString));
      else
        services.AddDbContext<EntityContext>(options => options.UseSqlServer(connectionString));

      // Setup ResourceService
      services.AddTransient<ICountryResourceService, CountryResourceService>();

GET Method

[HttpGet("{id:int}")]
    public async Task<IActionResult> Get(Int32 id)
    {
      var resource = await ResourceService.FindAsync(id);

      return (resource == null) ? NotFound() as IActionResult : Json(resource);
    }

The Resource Service fetches the resource, if found, a Json structure with the resource is returned, if not an empty message is return with status code 404 (Not Found).

Camel or Pascal Case

By default, the returned Json structure is camel cased. I find this inconvenient, because somewhere down the process, property names are changed and can cause errors. Fortunately, the default behavior can be set during, of course, the startup.

// This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
       ...
           // Add framework services.
      services.AddMvc()
        .AddJsonOptions(options =>
        {
          // Override default camelCase style 
          // (yes its strange the default configuration results in camel case)
          options.SerializerSettings.ContractResolver = 
                   new Newtonsoft.Json.Serialization.DefaultContractResolver();
        });

POST Method

[HttpPost]
    public async Task<IActionResult> Post([FromBody]CountryResource resource)
    {
      try
      {
        // Create resource
        var serviceResult = await ResourceService.InsertAsync(resource);

        // if return error message if needed
        if (serviceResult.Errors.Count > 0)
          return BadRequest(serviceResult);

        // On success return URL with id and newly created resource  
        return CreatedAtAction(nameof(Get), 
               new { id = serviceResult.Resource.Id }, serviceResult.Resource);
      }
      catch (Exception ex)
      {
        var result = new ResourceResult<CountryResource>(resource);

        while (ex != null)
          result.Exceptions.Add(ex.Message);

        return BadRequest(result);
      }
    }

The [HttpPost] attribute tells the controller only to react on POST request and ignore all other types. The [FromBody] attribute ensures the CountryResource is read from the message body and not the URI or another source. Swagger does not care about the [FromBody] attribute but the C# web client fails without it. Please note that on success, a URI and resource is returned.

PUT Method

[HttpPut]
   public async Task<IActionResult> Put([FromBody]CountryResource resource)
   {
     try
     {
       var currentResource = await ResourceService.FindAsync(resource.Id);

       if (currentResource == null)
         return NotFound();

       var serviceResult = await ResourceService.UpdateAsync(resource);

       if (serviceResult.Errors.Count > 0)
         return BadRequest(serviceResult);

       return Ok(serviceResult.Resource);
     }
     catch (Exception ex)
     {
       var result = new ResourceResult<CountryResource>(resource);

       while (ex != null)
       {
         result.Exceptions.Add(ex.Message);

         if (ex is ConcurrencyException)
           return StatusCode(HttpStatusCode.Conflict.ToInt32(), result);

         ex = ex.InnerException;
       }

       return BadRequest(result);
     }
   }

The PUT implementation looks a lot like the POST function. On success, the updated resource is returned with status code 200 (OK) otherwise an error message.

DELETE Method

[HttpDelete("{id}")]
    public async Task<IActionResult> Delete(Int32 id)
    {
      try
      {
        var serviceResult = await ResourceService.DeleteAsync(id);

        if (serviceResult.Resource == null)
          return NoContent();

        if (serviceResult.Errors.Count > 0)
          return BadRequest(serviceResult);

        return Ok();
      }
      catch (Exception ex)
      {
        var result = new ResourceResult<CountryResource>();

        while (ex != null)
          result.Exceptions.Add(ex.Message);

        return BadRequest(result);
      }
    }

The DELETE method has the same pattern as POST and PUT, delegate the actual work to the resource service and report success or failure with errors.

GET Revised

REST supports function overloading, you can have the "same" function with other parameters. In the first GET example, a country is returned based on the incoming Id. You can also fetch a country based on its Code field.

// Extra GET, based on String type Code 
[HttpGet("{code}")]
    public IActionResult Get(String code)
    {
      if (code.IsNullOrEmpty())
        return BadRequest();

      code = code.ToUpper();

      CountryResource result = null;

      switch (code.Length)
      {
        case 2:
          result = ResourceService.Items().Where(c => c.Code2 == code).FirstOrDefault();
          break;

        case 3:
          result = ResourceService.Items().Where(c => c.Code3 == code).FirstOrDefault();
          break;
      }

      return (result == null) ? NotFound() as IActionResult : Json(result);
    }

// Original GET based in Int32 type Id
[HttpGet("{id:int}")]
    public async Task<IActionResult> Get(Int32 id)
    {
      var resource = await ResourceService.FindAsync(id);

      return (resource == null) ? NotFound() as IActionResult : Json(resource);
    }

Now Get has code as a string parameter. Depending on its length, 2 or 3 chars, the corresponding country is returned. In order to make this work, the original function must have the int type in the HttpGet attribute. If left out there 2 get functions who both have a string as parameter and the routing will fail to resolve this. No additional type info is required when the parameter count resolves the routing. The more complex Get function demonstrates this:

[HttpGet]
public IActionResult Get(String sortBy, String sortDirection, Int32 skip,
         Int32 take, String search, String searchFields)
{
   var result = ResourceService.Load(sortBy, sortDirection, skip, take, search, searchFields);

  return Json(result);
}

GUI

Now we have all the required services for building the GUI. The GUI is a straight forward Dot Net Core MVC project with Bootstrap styling. I left out the security part intentionally. It's already a lot to cover and I explain the security in the next blog.

You can find more information about bootstrap-table grid in one of my previous posts. The modal dialogs are created with the excellent Dante nakupanda Bootstrap dialog library. This library removes the verbose Bootstrap model dialog HTML. The grid and dialogs are glued together with jQuery (of course what else), see the file cruddialog.js for more details.

GUI Controller

The GUI Controller connects with an HttpClient to the RESTful service. The HttpClient is setup outside the controller and passed with DI as an parameter in the constructor because it is not the controllers concern where the RESTful service is hosted.

public CountryController(HttpClient client)
 {
   apiClient = client;

   apiUrl = "/api/country/";
   ...

The apiUrl is set in the constructor and not by DI because the Controller is tightly coupled to this url.

Setup HttpClient

The HttpClient base address is set in the configuration file appsettings.json.

...
 "HttpClient": {
    "BaseAddress": "http://localhost:50385",
  },
...

The dependency injection is setup during ConfigureServices (startup.cs).

public void ConfigureServices(IServiceCollection services)
    {
      ...
      // Read config file for HttpClient settings
      services.Configure<HttpClientConfig>(Configuration.GetSection("HttpClient"));

      // Setup Dependency Injection HttpClient
      services.AddTransient<HttpClient, HttpRestClient>();
      ...

HttpClient implements no interface we can pass to the GUI constructor. I created a custom HttpRestClient to get a grip on the HttpClient setup.

namespace System.Config
{
  public class HttpClientConfig
  {
    public String BaseAddress { get; set; }

    public String UserId { get; set; }
    public String UserPassword { get; set; }

  }
}

namespace System.Net.Http
{
  public class HttpRestClient : HttpClient
  {
    public HttpRestClient(IOptions<HttpClientConfig> config) : base()
    {
      BaseAddress = new Uri(config.Value.BaseAddress);

      DefaultRequestHeaders.Accept.Clear();
      DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }
  }
}

With this approach, the controller receives an HttpRestClient instance as client parameter in the constructor.

Load Grid Data

The Bootstrap-Table calls the Load function with a bunch of parameters. These parameters must be added to the client URL and passed to RESTful service. The service result must be converted into a format the Bootstrap-Table can read.

[HttpGet]
    public async Task<IActionResult> Load(String sort, String order, 
              Int32 offset, Int32 limit, String search, String searchFields)
    {
      //  setup url with query parameters
      var queryString = new Dictionary<String, String>();
      queryString["sortBy"] = sort ?? "";
      queryString["sortDirection"] = order ?? "";
      queryString["skip"] = offset.ToString();
      queryString["take"] = limit.ToString();
      queryString[nameof(search)] = search ?? "";
      queryString[nameof(searchFields)] = searchFields ?? "";

      // convert dictionary to query params
      var uriBuilder = new UriBuilder(apiClient.BaseAddress + apiUrl)
      {
        Query = QueryHelpers.AddQueryString("", queryString)
      };

      using (var response = await apiClient.GetAsync(uriBuilder.Uri))
      {
        var document = await response.Content.ReadAsStringAsync();

        var loadResult = JsonConvert.DeserializeObject<LoadResult<CountryResource>>(document);

        // Convert loadResult into Bootstrap-Table compatible format
        var result = new
        {
          total = loadResult.CountUnfiltered,
          rows = loadResult.Items
        };

        return Json(result);
      }
    }

Insert or Edit Dialog

The Insert or Edit dialog is a bit more complicated. It has two stages. In the first stage, the controller get a resource based on Id and is mapped to a view model. The view model is rendered in the modal dialog. The first steps happen in the Edit method with the Get attribute.

[HttpGet]
    public async Task<IActionResult> Edit(Int32 id)
    {
      String url = apiUrl + ((id == 0) ? "create" : $"{id}");

      using (var response = await apiClient.GetAsync(url))
      {
        var document = await response.Content.ReadAsStringAsync();

        if (response.StatusCode == HttpStatusCode.OK)
        {
          var resource = JsonConvert.DeserializeObject<CountryResource>(document);

          var result = mapper.Map<CountryModel>(resource);

          return PartialView(nameof(Edit), result);
        }

        else
        {
          var result = new ResourceResult<CountryResource>();

          if (response.StatusCode == HttpStatusCode.NotFound)
            result.Errors.Add(new ValidationError($"Record with id {id} is not found"));

          return StatusCode(response.StatusCode.ToInt32(), result);
        }
      }
    }

It is the RESTful service that creates a new resource when the Id is empty and not the GUI controller. The RESTful service has the knowledge how to initialize a new resource and this is not a concern for the GUI controller. jQuery code in the webpage handles the edit response.

First stage Edit Get

Submit Insert or Edit dialog

The second stage submits the dialog to the controller. The view model is mapped to a resource. The controller makes a POST call for a new resource or PUT call for an existing one. The controller parses the RESTful service result. On success, the dialog is closed and table grid shows the new resource data. Errors are shown to the dialog and will therefore remain open.

    [HttpPost]
    public async Task<IActionResult> Edit([FromForm]CountryModel model)
    {
      if (!ModelState.IsValid)
        PartialView();

      // Map model to resource
      var resource = mapper.Map<CountryResource>(model);

      // save resource to Json
      var resourceDocument = JsonConvert.SerializeObject(resource);

      using (var content = new StringContent(resourceDocument, Encoding.UTF8, "application/json"))
      {
        // determine call update or insert
        Upsert upsert = apiClient.PutAsync;

        // no RowVersion indicates insert
        if (model.RowVersion.IsNullOrEmpty())
          upsert = apiClient.PostAsync;

        using (var response = await upsert(apiUrl, content))
        {
          // init result
          var result = new ResourceResult<CountryResource>(resource);

          // read result from RESTful service
          var responseDocument = await response.Content.ReadAsStringAsync();

          if (response.StatusCode == HttpStatusCode.OK ||
              response.StatusCode == HttpStatusCode.Created)
          {
            // Fetch created or updated resource from response
            result.Resource = JsonConvert.DeserializeObject<CountryResource>(responseDocument); ;
          }
          else
          {
            // fetch errors and or exceptions
            result = JsonConvert.DeserializeObject<ResourceResult<CountryResource>>(responseDocument);
          }

          // Set error message for concurrency error
          if (response.StatusCode == HttpStatusCode.Conflict)
          {
            result.Errors.Clear();
            result.Errors.Add(new ValidationError("This record is modified by another user"));
            result.Errors.Add(new ValidationError
                ("Your work is not saved and replaced with new content"));
            result.Errors.Add(new ValidationError
                ("Please review the new content and if required edit and save again"));
          }

          if (response.StatusCode.IsInSet(HttpStatusCode.OK, 
                    HttpStatusCode.Created, HttpStatusCode.Conflict))
            return StatusCode(response.StatusCode.ToInt32(), result);

          // copy errors so they will be rendered in edit form
          foreach (var error in result.Errors)
            ModelState.AddModelError(error.MemberName ?? "", error.Message);

          // Update model with Beautify effect(s) and make it visible in the partial view
          IEnumerable<PropertyInfo> properties = 
          model.GetType().GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance);

          foreach (var property in properties)
          {
            var rawValue = property.GetValue(model);
            var attemptedValue = rawValue == null ? "" : 
                 Convert.ToString(rawValue, CultureInfo.InvariantCulture);

            ModelState.SetModelValue(property.Name, rawValue, attemptedValue);
          }
          
          // No need to specify model here, it has no effect on the render process :-(
          return PartialView();
        }
      }
    }

Edit dialog at work:

Submit Edit dialog

Updated grid after successful save

Updated grid After successful save

Delete Resource

Before a resource is deleted, the user receives a confirmation dialog. This is the same as the edit dialog, only now the edit controls are in read only modus and the dialog title and buttons are adjusted.

Create or Edit dialog

If the user confirms the delete, the GUI controller gets a call with the resource Id. The controller calls the RESTful service with the Id and reads the return result. The dialog is always closed after confirmation. On success is removed from the table grid.

[HttpPost]
public async Task<IActionResult> Delete(Int32 id)
{
  String url = apiUrl + $"{id}";

  using (var response = await apiClient.DeleteAsync(url))
  {
    var responseDocument = await response.Content.ReadAsStringAsync();

    // create only response if something off has happened
    if (response.StatusCode != HttpStatusCode.OK)
    {
      var result =
      JsonConvert.DeserializeObject<ResourceResult<CountryResource>>(responseDocument);

      return StatusCode(response.StatusCode.ToInt32(), result);
    }

    return Content(null);
  }
}

Errors are shown in a new dialog.

Delete error dialog

Get Source Code started

The solution works with MySQL or SqlServer.  You can configure the database of your choice in appsettings.json

{
  "ConnectionStrings": {
    // MySql connection
    //DatabaseDriver: "MySql",
    //"DbConnection": "server=localhost;Database=DemoCountries;User Id=root;password=<yourkey>"

    // SqlServer connection
     "DbConnection": "server=localhost;Database=DemoCountries;Trusted_Connection=True"
  },

  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

Make sure the database the account has sufficient rights to create the database. ConfigureServices reads the configuration and adds a DbContext.

 public void ConfigureServices(IServiceCollection services)
    {
      // Get Database connection config
      var connectionString = Configuration.GetConnectionString("DbConnection");

      // Connect by default SqlServer other wise to MySql 
      var databaseDriver = Configuration.GetConnectionString("DatabaseDriver");

      // Setup Database Service layer used in CountryResourceService
      if (databaseDriver.EqualsEx("MySQL"))
        services.AddDbContext<EntityContext>(options => options.UseMySql(connectionString));
      else
        services.AddDbContext<EntityContext>(options => options.UseSqlServer(connectionString));
...

Initialize Database content

The EntityContext constructor checks if the database exists. If a new database is created, countries are added from the embedded countries.json file.

public partial class EntityContext : DbContext
  {
    protected readonly IIdentity Identity;

    public DbSet<Country> Countries { get; set; }

    public EntityContext(DbContextOptions<EntityContext> options, IIdentity identity) : base(options)
    {
      Identity = identity;

      ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

      // Check if database exists, if created add content
      if (Database.EnsureCreated())
        InitDatabaseContent();
    }

    public void InitDatabaseContent()
    {
      // Extract Countries Json file from embedded resource
      var assembly = GetType().GetTypeInfo().Assembly;

      var fileName = assembly.GetManifestResourceNames().FirstOrDefault();

      using (var resourceStream = assembly.GetManifestResourceStream(fileName))
      {
        using (var reader = new StreamReader(resourceStream, Encoding.UTF8))
        {
          var document = reader.ReadToEnd();

          var countries = JsonConvert.DeserializeObject<List<Country>>(document);

          foreach (var country in countries)
          {
            Add(country);
            Entry(country).State = EntityState.Added;

            var ra = SaveChanges();

            Entry(country).State = EntityState.Detached;
          }
        }
      }
    }
...

Versions

1.0.02017 JulyInitial version
 1.0.1 2017 August Section Get Source Code Started Added

Conclusion

Thanks that you made it this far! In this blog, I showed Dot Net Core is very capable for creating RESTful services. Swagger, the open source plugging is a big help during RESTful service development. The service can be consumed with a MVC application or third party apps. RESTful design offers several benefits like performance, easy to develop, and a centralized repository. Please download the code and play with it. I hope it may be helpful for you.

Further Reading

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Bart-Jan Brouwer
Technical Lead
Netherlands Netherlands
I graduated as Bachelor of Mechanical Engineering. Soon I moved from mechanical to software engineering. With more than 20 years of experience in software design, development, and architecture I love building software that users enjoy en suit their needs.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionWebApi and One-Many Relationships Pin
Kieron6-Nov-17 17:15
memberKieron6-Nov-17 17:15 
SuggestionInstructions to run application Pin
Member 15931452-Aug-17 21:09
memberMember 15931452-Aug-17 21:09 
GeneralRe: Instructions to run application Pin
Bart-Jan Brouwer3-Aug-17 2:54
memberBart-Jan Brouwer3-Aug-17 2:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171114.1 | Last Updated 7 Aug 2017
Article Copyright 2017 by Bart-Jan Brouwer
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid