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;
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()
{
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 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)
{
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)
{
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 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()
{
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 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
{
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 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:
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 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",
"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 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.
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, 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.
[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 char
s, 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)
{
...
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 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)
{
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 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();
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 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
Get Source Code started
The solution works with MySQL or SqlServer. You can configure the database of your choice in appsettings.json
{
"ConnectionStrings": {
"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)
{
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));
...
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;
if (Database.EnsureCreated())
InitDatabaseContent();
}
public void InitDatabaseContent()
{
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.0 | 2017 July | Initial 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