.Net Core RESTful or WebAPI MVC Web Application

0
77

Introduction

There is a lot of information out there 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 it’s 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 the it checks if the primary key constrain 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 an 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 it’s 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 an 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 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() { 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)
 {
 
 var entity = await ServiceContext.FindAsync<Country>(id);

 
 var result = mapper.Map<CountryResource>(entity);

 return result;
 }

Business Rules

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

The CountryResoure businesrules 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 remove and the remaining string 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 constrains 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 who cannot be covered by validation attributes.  The complex rules can be coded in ValidateBussinesRules. The unique constrains for fields Code2 and Code3 cannot be done by attributes.

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 && r.Id != resource.Id); code3Check = code3Check.Where(r => r.Code3 == resource.Code3 && r.Id != resource.Id); } 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 constrains 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 build 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 an 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()
 {
 
 foreach (var entry in this.ChangeTracker.Entries())
 {
 var entity = entry.Entity as Entity;

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

 if (entity != null)
 {
 
 entity.RowVersion = Guid.NewGuid().ToString();

 
 if (Entry(entity).State == EntityState.Added)
 {
 entity.CreatedBy = currentUserName;
 entity.CreatedAt = DateTime.UtcNow;
 }

 
 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 and 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 an 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 is no reason the 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 { using (var transaction = Database.BeginTransaction()) { try { var entityState = String.IsNullOrEmpty(entity.RowVersion) ? EntityState.Added : EntityState.Modified; if (entityState == EntityState.Modified) { var keyValues = GetKeyValues(entity); var existingEntity = await FindAsync<TEntity>(keyValues); var existingRowVersion = existingEntity?.RowVersion ?? null; 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 to 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 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 improves 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. An other 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:

Code Description Example
2xx Success
 200 OK Resource updated
 201 Created Resource created
 204 No Content Resource deleted
 4xx Client Errors
 400 Bad Request POST/PUT a resource with invalid business rules
 404 Not Found Resource not found for GET command
409 Conflict Concurrency error

URI and API

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

http:
{
 "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 an useful error message. It’s up to 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 requires 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 Dot Net Core an REST controller is the same as a 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",
 
 "DbConnection": "server=localhost;Database=DemoCountries;User Id=root;password=masterkey"

 
 
 },

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)
 {
 
 var connectionString = Configuration.GetConnectionString("DbConnection");

 
 var databaseDriver = Configuration.GetConnectionString("DatabaseDriver");

 
 if (databaseDriver.EqualsEx("MySQL"))
 services.AddDbContext<EntityContext>(options => options.UseMySql(connectionString));
 else
 services.AddDbContext<EntityContext>(options => options.UseSqlServer(connectionString));

 
 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 and 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 some where down the process property names are changed and can cause errors. Fortunately the default behavior can be set during, of course the startup.


 public void ConfigureServices(IServiceCollection services)
 {
 ...
 
 services.AddMvc()
 .AddJsonOptions(options =>
 {
 
 options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver();
 });

POST method

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

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

 
 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 an 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) other wise 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 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.


[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);
 }


[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 makes 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. Its already a lot to cover and I explain the security in a next blog.

You can find more info 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)
 {
 ...
 
 services.Configure<HttpClientConfig>(Configuration.GetSection("HttpClient"));

 
 services.AddTransient<HttpClient, HttpRestClient>();
 ...

HttpClient implements no interface we can pass to the GUI constructor. I created a custom HttpRestClient to get 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)
 {
 
 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 ?? "";

 
 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);

 
 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 an resource based on Id and is mapped to an view model. The view model is rendered in the modal dialog. The first steps happens 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 who 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 therefor remain open.

[HttpPost]
public async Task<IActionResult> Edit([FromForm]CountryModel model)
{ if (!ModelState.IsValid) PartialView(); var resource = mapper.Map<CountryResource>(model); var resourceDocument = JsonConvert.SerializeObject(resource); using (var content = new StringContent(resourceDocument, Encoding.UTF8, "application/json")) { Upsert upsert = apiClient.PutAsync; if (model.RowVersion.IsNullOrEmpty()) upsert = apiClient.PostAsync; using (var response = await upsert(apiUrl, content)) { var result = new ResourceResult<CountryResource>(resource); var responseDocument = await response.Content.ReadAsStringAsync(); if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Created) { result.Resource = JsonConvert.DeserializeObject<CountryResource>(responseDocument); ; } else { result = JsonConvert.DeserializeObject<ResourceResult<CountryResource>>(responseDocument); } 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); foreach (var error in result.Errors) ModelState.AddModelError(error.MemberName ?? "", error.Message); 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); } 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 are 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(); 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

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

Dependency Injection in ASP.NET Core

Swagger and Visual Studio

10 RESTful tips

Create REST with ASP.NET Core

LEAVE A REPLY