Introduction
Let's create a Web API with the latest version of ASP.NET Core and Entity Framework Core.\
REST APIs provide at
least the following operations:
- GET
- POST
- PUT
- DELETE
There are other
operations for RESTful, but they aren't necessary for this guide.
Those operations allow
clients to perform actions through RESTful API, so our Web API must contain
those operations.
Sample database contains
4 schemas:
- Application
- Purchasing
- Sales
- Storagehouse
In this guide, We'll
work with Storagehouse.StockItems table. We'll add code to
work with this entity: allow to retrieve stock items, retrieve stock item by
id, create, update and delete stock items from database.
The version for this API
is 1.
This is the route table for API:
VERB |
URL |
DESCRIPTION |
GET |
api/v1/Storagehouse/StockItem |
Retrieves stock items |
GET |
api/v1/Storagehouse/StockItem/id |
Retrieves a stock item
by id |
POST |
api/v1/Storagehouse/StockItem |
Creates a new stock
item |
PUT |
api/v1/Storagehouse/StockItem/id |
Updates an existing
stock item |
DELETE |
api/v1/Storagehouse/StockItem/id |
Deletes an existing
stock item |
Keep these routes in
mind because API must implement these routes.
Prerequisites :
Software
- · .NET Core
- · Visual Studio 2017 with
last update or Visual Studio 2019
- · SQL Server
- · Sample database
Skills
- · C#
- · ORM (Object Relational
Mapping)
- · RESTful services
- · TDD (Test Driven Development)
Using the Code
For this guide, the
working directory for source code is C:\Projects.
Also, This is the GitHub repository for this guide.
Step 01 - Create Project
Open Visual Studio and
follow these steps:
Go to File > New > Project -->Go to Installed > Visual C# > .NET Core
Set the name for project as Sample.API -->Click OK
In the next window, select API and the latest version for .ASP.NET Core, in this case is 2.1:
Once Visual Studio has finished with creation for solution, we'll see this window:
Step 02 - Install Nuget Packages
In this step, We need to
install the following NuGet packages:
*
EntityFrameworkCore.SqlServer
*
Swashbuckle.AspNetCore
Now, we'll proceed to
install EntityFrameworkCore.SqlServer package from Nuget, right click on Sample.API
project:
Change to Browse tab and type Microsoft.EntityFrameworkCore.SqlServer:
Next, install Swashbuckle.AspNetCore package:
This is the structure for project.
Now run the project to
check if solution is ready, press F5 and Visual Studio will show this browser
window:
By default, Visual Studio adds a file with name ValuesController in Controllers directory, remove it from project.
Step 03 - Add Models
Now, create a directory
with name Models and add the following files:
- Domain.cs
- Extensions.cs
- Requests.cs
- Responses.cs
- Domain.cs will contains all
code related to Entity Framework Core.
- Extensions.cs will contain the
extension methods for DbContext.
- Requests.cs will contain
definitions for request models.
- Responses.cs will contain
definitions for response models.
Code for Domain.cs file:
using System;
using Microsoft.EntityFrameworkCore;
using
Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Sample.API.Models
{
#pragma
warning disable CS1591
public partial class StockItem
{
public StockItem() { }
public StockItem(int? stockItemID) { StockItemID = stockItemID; }
public int? StockItemID { get; set; }
public string StockItemName { get; set; }
public int? SupplierID { get; set; }
public int? ColorID { get; set; }
public int? UnitPackageID { get; set; }
public int? OuterPackageID { get; set; }
public string Brand { get; set; }
public string Size { get; set; }
public int? LeadTimeDays { get; set; }
public int? QuantityPerOuter { get; set; }
public bool? IsChillerStock { get; set; }
public string Barcode { get; set; }
public decimal? TaxRate { get; set; }
public decimal? UnitPrice { get; set; }
public decimal? RecommendedRetailPrice { get; set; }
public decimal? TypicalWeightPerUnit { get; set; }
public string MarketingComments { get; set; }
public string InternalComments { get; set; }
public string CustomFields { get; set; }
public string Tags { get; set; }
public string SearchDetails { get; set; }
public int? LastEditedBy { get; set; }
public DateTime? ValidFrom { get; set; }
public DateTime? ValidTo { get; set; }
}
public class StockItemsConfiguration : IEntityTypeConfiguration<StockItem>
{
public void Configure(EntityTypeBuilder<StockItem>
builder)
{
// Set configuration for entity
builder.ToTable("StockItems", "Storagehouse");
// Set key for entity
builder.HasKey(p =>
p.StockItemID);
// Set configuration for columns
builder.Property(p => p.StockItemName).HasColumnType("nvarchar(200)").IsRequired();
builder.Property(p =>
p.SupplierID).HasColumnType("int").IsRequired();
builder.Property(p =>
p.ColorID).HasColumnType("int");
builder.Property(p =>
p.UnitPackageID).HasColumnType("int").IsRequired();
builder.Property(p => p.OuterPackageID).HasColumnType("int").IsRequired();
builder.Property(p =>
p.Brand).HasColumnType("nvarchar(100)");
builder.Property(p =>
p.Size).HasColumnType("nvarchar(40)");
builder.Property(p =>
p.LeadTimeDays).HasColumnType("int").IsRequired();
builder.Property(p =>
p.QuantityPerOuter).HasColumnType("int").IsRequired();
builder.Property(p =>
p.IsChillerStock).HasColumnType("bit").IsRequired();
builder.Property(p =>
p.Barcode).HasColumnType("nvarchar(100)");
builder.Property(p =>
p.TaxRate).HasColumnType("decimal(18, 3)").IsRequired();
builder.Property(p =>
p.UnitPrice).HasColumnType("decimal(18, 2)").IsRequired();
builder.Property(p => p.RecommendedRetailPrice).HasColumnType("decimal(18,
2)");
builder.Property(p =>
p.TypicalWeightPerUnit).HasColumnType("decimal(18, 3)").IsRequired();
builder.Property(p => p.MarketingComments).HasColumnType("nvarchar(max)");
builder.Property(p =>
p.InternalComments).HasColumnType("nvarchar(max)");
builder.Property(p =>
p.CustomFields).HasColumnType("nvarchar(max)");
builder.Property(p => p.LastEditedBy).HasColumnType("int").IsRequired();
// Columns with default value
builder
.Property(p =>
p.StockItemID)
.HasColumnType("int")
.IsRequired()
.HasDefaultValueSql("NEXT VALUE FOR
[Sequences].[StockItemID]");
// Computed columns
builder
.Property(p => p.Tags)
.HasColumnType("nvarchar(max)")
.HasComputedColumnSql("json_query([CustomFields],N'$.Tags')");
builder
.Property(p => p.SearchDetails)
.HasColumnType("nvarchar(max)")
.IsRequired()
.HasComputedColumnSql("concat([StockItemName],N'
',[MarketingComments])");
// Columns with generated value on add or update
builder
.Property(p => p.ValidFrom)
.HasColumnType("datetime2")
.IsRequired()
.ValueGeneratedOnAddOrUpdate();
builder
.Property(p => p.ValidTo)
.HasColumnType("datetime2")
.IsRequired()
.ValueGeneratedOnAddOrUpdate();
}
}
public class SampleDbContext : DbContext
{
public SampleDbContext(DbContextOptions<SampleDbContext> options) : base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply configurations for
entity
modelBuilder.ApplyConfiguration(new StockItemsConfiguration());
base.OnModelCreating(modelBuilder);
}
public DbSet<StockItem> StockItems { get; set; }
}
#pragma
warning restore CS1591
}
Code for Extensions.cs file:
using System.Linq;
using System.Threading.Tasks;
using
Microsoft.EntityFrameworkCore;
namespace Sample.API.Models
{
#pragma
warning disable CS1591
public static class SampleDbContextExtensions
{
public static
IQueryable<StockItem> GetStockItems(this SampleDbContext dbContext, int pageSize = 10, int pageNumber = 1,
int? lastEditedBy = null, int? colorID = null, int? outerPackageID = null, int?
supplierID = null, int? unitPackageID = null)
{
// Get query from DbSet
var query =
dbContext.StockItems.AsQueryable();
// Filter by: 'LastEditedBy'
if (lastEditedBy.HasValue)
query = query.Where(item =>
item.LastEditedBy == lastEditedBy);
// Filter by: 'ColorID'
if (colorID.HasValue)
query = query.Where(item =>
item.ColorID == colorID);
// Filter by: 'OuterPackageID'
if (outerPackageID.HasValue)
query = query.Where(item =>
item.OuterPackageID == outerPackageID);
// Filter by: 'SupplierID'
if (supplierID.HasValue)
query = query.Where(item =>
item.SupplierID == supplierID);
// Filter by: 'UnitPackageID'
if (unitPackageID.HasValue)
query = query.Where(item =>
item.UnitPackageID == unitPackageID);
return query;
public static async Task<StockItem> GetStockItemsAsync(this SampleDbContext
dbContext, StockItem entity)
=> await dbContext.StockItems.FirstOrDefaultAsync(item
=> item.StockItemID == entity.StockItemID);
public static async Task<StockItem> GetStockItemsByStockItemNameAsync(this SampleDbContext dbContext, StockItem entity)
=> await
dbContext.StockItems.FirstOrDefaultAsync(item => item.StockItemName ==
entity.StockItemName);
}
public static class IQueryableExtensions
{
public static IQueryable<TModel> Paging<TModel>(this
IQueryable<TModel> query, int pageSize = 0, int pageNumber = 0) where TModel : class
=> pageSize > 0 && pageNumber
> 0 ?
query.Skip((pageNumber - 1) * pageSize).Take(pageSize) : query;
}
#pragma
warning restore CS1591
}
Code for Requests.cs file:
using System;
using System.ComponentModel.DataAnnotations;
namespace Sample.API.Models
{
#pragma
warning disable CS1591
public class PostStockItemsRequest
{
[Key]
public int? StockItemID { get; set; }
[Required]
[StringLength(200)]
public string StockItemName { get; set; }
[Required]
public int? SupplierID { get; set; }
public int? ColorID { get; set; }
[Required]
public int? UnitPackageID { get; set; }
[Required]
public int? OuterPackageID { get; set; }
[StringLength(100)]
public string Brand { get; set; }
[StringLength(40)]
public string Size { get; set; }
[Required]
public int? LeadTimeDays { get; set; }
[Required]
public int? QuantityPerOuter { get; set; }
[Required]
public bool? IsChillerStock { get; set; }
[StringLength(100)]
public string Barcode { get; set; }
[Required]
public decimal? TaxRate { get; set; }
[Required]
public decimal? UnitPrice { get; set; }
public decimal? RecommendedRetailPrice { get; set; }
[Required]
public decimal? TypicalWeightPerUnit {
get; set; }
public string MarketingComments { get; set; }
public string InternalComments { get; set; }
public string CustomFields { get; set; }
public string Tags { get; set; }
[Required]
public string SearchDetails { get; set; }
[Required]
public int? LastEditedBy { get; set; }
public DateTime? ValidFrom { get; set; }
public DateTime? ValidTo { get; set; }
}
public class PutStockItemsRequest
{
[Required]
[StringLength(200)]
public string StockItemName { get; set; }
[Required]
public int? SupplierID { get; set; }
public int? ColorID { get; set; }
[Required]
public decimal? UnitPrice { get; set; }
}
public static class Extensions
{
public static StockItem ToEntity(this PostStockItemsRequest request) => new StockItem
{
StockItemID =
request.StockItemID,
StockItemName =
request.StockItemName,
SupplierID =
request.SupplierID,
ColorID = request.ColorID,
UnitPackageID =
request.UnitPackageID,
OuterPackageID =
request.OuterPackageID,
Brand = request.Brand,
Size = request.Size,
LeadTimeDays =
request.LeadTimeDays,
QuantityPerOuter =
request.QuantityPerOuter,
IsChillerStock =
request.IsChillerStock,
Barcode = request.Barcode,
TaxRate = request.TaxRate,
UnitPrice = request.UnitPrice,
RecommendedRetailPrice =
request.RecommendedRetailPrice,
TypicalWeightPerUnit =
request.TypicalWeightPerUnit,
MarketingComments =
request.MarketingComments,
InternalComments =
request.InternalComments,
CustomFields =
request.CustomFields,
Tags = request.Tags,
SearchDetails =
request.SearchDetails,
LastEditedBy =
request.LastEditedBy,
ValidFrom = request.ValidFrom,
ValidTo = request.ValidTo
};
}
#pragma
warning restore CS1591
}
Code for Responses.cs file:
using
System.Collections.Generic;
using System.Net;
using
Microsoft.AspNetCore.Mvc;
namespace Sample.API.Models
{
#pragma
warning disable CS1591
public interface IResponse
{
string Message { get; set; }
bool DidError { get; set; }
string ErrorMessage { get; set; }
}
public interface ISingleResponse<TModel> : IResponse
{ TModel Model { get; set; } }
public interface IListResponse<TModel> : IResponse
{ IEnumerable<TModel> Model { get; set; } }
public interface IPagedResponse<TModel> : IListResponse<TModel>
{
int ItemsCount { get; set; }
double PageCount { get; }
}
public class Response : IResponse
{
public string Message { get; set; }
public bool DidError { get; st; }
public string ErrorMessage { get; set; }
}
public class SingleResponse<TModel> : ISingleResponse<TModel>
{
public string Message { get; set; }
public bool DidError { get; set; }
public string ErrorMessage { get; set; }
public TModel Model { get; set; }
}
public class ListResponse<TModel> : IListResponse<TModel>
{
public string Message { get; set; }
public bool DidError { get; set; }
public string ErrorMessage { get; set; }
public IEnumerable<TModel> Model { get; set; }
}
public class PagedResponse<TModel> : IPagedResponse<TModel>
{
public string Message { get; set; }
public bool DidError { get; set; }
public string ErrorMessage { get; set; }
public IEnumerable<TModel> Model { get; set; }
public int PageSize { get; set; }
public int PageNumber { get; set; }
public int ItemsCount { get; set; }
public double PageCount
=> ItemsCount < PageSize ? 1 : (int)(((double)ItemsCount / PageSize)
+ 1);
}
public static class ResponseExtensions
{
public static IActionResult ToHttpResponse(this IResponse response)
{
var status =
response.DidError ? HttpStatusCode.InternalServerError : HttpStatusCode.OK;
return new ObjectResult(response) { statusCode = (int)status };
}
public static IActionResult ToHttpResponse<TModel>(this ISingleResponse<TModel> response)
{
var status = HttpStatusCode.OK;
if (response.DidError)
status =
HttpStatusCode.InternalServerError;
else if (response.Model == null)
status =
HttpStatusCode.NotFound;
return new ObjectResult(response) { StatusCode = (int)status };
}
public static IActionResult ToHttpResponse<TModel>(this IListResponse<TModel> response)
{
var status = HttpStatusCode.OK;
if (response.DidError)
status =
HttpStatusCode.InternalServerError;
else if (response.Model == null)
status =
HttpStatusCode.NoContent;
return new ObjectResult(response) { StatusCode = (int)status };
}
}
#pragma
warning restore CS1591
}
Understanding Models
DOMAIN
- StockItems class is the representation for Storagehouse.StockItems table.
- StockItemsConfiguration class contains the mapping for StockItems class.
- SampleDbContext class is the link between database and C# code, this class handles queries and commits the changes in database and of course, another things.
EXTENSIONS
- SampleDbContextExtensions contains extension methods to provide linq queries.
- IQueryableExtensions contains extension methods to allow paging in IQueryable instances.
REQUESTS
We have the following
definitions:
- PostStockItemsRequest
- PutStockItemsRequest
- PostStockItemsRequestModel represents the model to create a new stock item, contains all required properties to save in database.
- PutStockItemsRequestModel represents the model to update an existing stock item, in this case contains only 4 properties: StockItemName, SupplierID, ColorID and UnitPrice. This class doesn't contain StockItemID property because id is in route for controller's action.
The models for requests
do not require to contain all properties like entities, because we don't need
to expose full definition in a request or response, it's a good practice to
limit data using models with few properties.
Extensions class
contains an extension method for PostStockItemsRequestModel, to return an
instance of StockItem class from request model.
RESPONSES
These are the
interfaces:
- IResponse
- ISingleResponse<TModel>
- IListResponse<TModel>
- IPagedResponse<TModel>
Each one of these
interfaces has implementations, why do we need these definitions if it's more
simple to return objects without wrapping them in these models? Keep in mind
this Web API will provide operations for clients, with UI or without UI and
it's more easy to have properties to send message, to have a model or send information
if an error occurs, in addition, we set Http status code in response to
describe the result from request.
These classes are
generic, because in this way, we save time to define responses in future, this
Web API only returns a response for a single entity, a list and a paged list.
- ISingleResponse represents a response for a single entity.
- IListResponse represents a response with a list, for example all shipping to existing order without paging.
- IPagedResponse represents a response with pagination, for example all orders in a date range.
- ResponseExtensions class contains extension methods to convert a response in a Http response, these methods return InternalServerError (500) status if an error occurs, OK (200) if it's OK and NotFound (404) if an entity does not exist in database or NoContent (204) for list responses without model.
Step 04 - Add Controller
Now, inside of
Controllers directory, add a code file with name StoragehouseController.cs and
add this code:
using System;
using System.Threading.Tasks;
using
Microsoft.AspNetCore.Mvc;
using
Microsoft.EntityFrameworkCore;
using
Microsoft.Extensions.Logging;
using Sample.API.Models;
namespace Sample.API.Controllers
{
#pragma
warning disable CS1591
[ApiController]
[Route("api/v1/[controller]")]
public class StoragehouseController : ControllerBase
{
protected readonly ILogger Logger;
protected readonly SampleDbContext
DbContext;
public StoragehouseController(ILogger<StoragehouseController> logger, SampleDbContext dbContext)
{
Logger = logger;
DbContext = dbContext;
}
#pragma
warning restore CS1591
// GET
// api/v1/Storagehouse/StockItem
/// <summary>
/// Retrieves stock items
/// </summary>
/// <param
name="pageSize">Page size</param>
/// <param
name="pageNumber">Page number</param>
/// <param
name="lastEditedBy">Last edit by (user id)</param>
/// <param
name="colorID">Color id</param>
/// <param
name="outerPackageID">Outer package id</param>
/// <param name="supplierID">Supplier
id</param>
/// <param
name="unitPackageID">Unit package id</param>
/// <returns>A response
with stock items list</returns>
/// <response
code="200">Returns the stock items list</response>
/// <response code="500">If
there was an internal server error</response>
[HttpGet("StockItem")]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public async
Task<IActionResult> GetStockItemsAsync(int pageSize = 10, int pageNumber
= 1, int? lastEditedBy = null, int? colorID = null, int? outerPackageID = null,
int? supplierID = null, int? unitPackageID = null)
{
Logger?.LogDebug("'{0}' has been
invoked", nameof(GetStockItemsAsync));
var response = new PagedResponse<StockItem>();
try
{
// Get the "proposed"
query from repository
var query =
DbContext.GetStockItems();
// Set paging values
response.PageSize = pageSize;
response.PageNumber =
pageNumber;
// Get the total rows
response.ItemsCount = await query.CountAsync();
// Get the specific page from database
response.Model = await query.Paging(pageSize,
pageNumber).ToListAsync();
response.Message = string.Format("Page {0} of {1}, Total of products: {2}.", pageNumber, response.PageCount, response.ItemsCount);
Logger?.LogInformation("The stock items have been retrieved successfully.");
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an
internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(GetStockItemsAsync), ex);
}
return response.ToHttpResponse();
}
// GET
// api/v1/Storagehouse/StockItem/5
/// <summary>
/// Retrieves a stock
item by ID
/// </summary>
/// <param
name="id">Stock item id</param>
/// <returns>A response
with stock item</returns>
/// <response code="200">Returns
the stock items list</response>
/// <response
code="404">If stock item is not exists</response>
/// <response
code="500">If there was an internal server error</response>
[HttpGet("StockItem/{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async
Task<IActionResult> GetStockItemAsync(int id)
{
Logger?.LogDebug("'{0}' has been
invoked", nameof(GetStockItemAsync));
var response = new SingleResponse<StockItem>();
try
{
// Get the stock item by id
response.Model = await
DbContext.GetStockItemsAsync(new StockItem(id));
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an
internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(GetStockItemAsync), ex);
}
return response.ToHttpResponse();
}
// POST
// api/v1/Storagehouse/StockItem/
/// <summary>
/// Creates a new stock item
/// </summary>
/// <param
name="request">Request model</param>
/// <returns>A response
with new stock item</returns>
/// <response
code="200">Returns the stock items list</response>
/// <response
code="201">A response as creation of stock item</response>
/// <response
code="400">For bad request</response>
/// <response
code="500">If there was an internal server error</response>
[HttpPost("StockItem")]
[ProducesResponseType(200)]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public async
Task<IActionResult> PostStockItemAsync([FromBody]PostStockItemsRequest
request)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(PostStockItemAsync));
var response = new SingleResponse<StockItem>();
try
{
var existingEntity = await DbContext
.GetStockItemsByStockItemNameAsync(new StockItem { StockItemName
= request.StockItemName });
if (existingEntity != null)
ModelState.AddModelError("StockItemName", "Stock item name
already exists");
if (!ModelState.IsValid)
return BadRequest();
// Create entity from request model
var entity = request.ToEntity();
// Add entity to repository
DbContext.Add(entity);
// Save entity in database
await
DbContext.SaveChangesAsync();
// Set the entity to response model
response.Model = entity;
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an
internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(PostStockItemAsync), ex);
}
return response.ToHttpResponse();
}
// PUT
// api/v1/Storagehouse/StockItem/5
/// <summary>
/// Updates an existing stock
item
/// </summary>
/// <param
name="id">Stock item ID</param>
/// <param
name="request">Request model</param>
/// <returns>A response as
update stock item result</returns>
/// <response
code="200">If stock item was updated successfully</response>
/// <response
code="400">For bad request</response>
/// <response
code="500">If there was an internal server error</response>
[HttpPut("StockItem/{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public async Task<IActionResult>
PutStockItemAsync(int id,
[FromBody]PutStockItemsRequest request)
{
Logger?.LogDebug("'{0}' has been
invoked", nameof(PutStockItemAsync));
var response = new Response();
try
{
// Get stock item by id
var entity = await
DbContext.GetStockItemsAsync(new StockItem(id));
// Validate if entity exists
if (entity == null)
return NotFound();
// Set changes to entity
entity.StockItemName =
request.StockItemName;
entity.SupplierID = request.SupplierID;
entity.ColorID =
request.ColorID;
entity.UnitPrice =
request.UnitPrice;
// Update entity in repository
DbContext.Update(entity);
// Save entity in database
await
DbContext.SaveChangesAsync();
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an
internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(PutStockItemAsync), ex);
}
return response.ToHttpResponse();
}
// DELETE
// api/v1/Storagehouse/StockItem/5
/// <summary>
/// Deletes an existing stock
item
/// </summary>
/// <param
name="id">Stock item ID</param>
/// <returns>A response as
delete stock item result</returns>
/// <response
code="200">If stock item was deleted successfully</response>
/// <response
code="500">If there was an internal server error</response>
[HttpDelete("StockItem/{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public async Task<IActionResult>
DeleteStockItemAsync(int id)
{
Logger?.LogDebug("'{0}' has been
invoked", nameof(DeleteStockItemAsync));
var response = new Response();
try
{
// Get stock item by id
var entity = await
DbContext.GetStockItemsAsync(new StockItem(id));
// Validate if entity exists
if (entity == null)
return NotFound();
// Remove entity from repository
DbContext.Remove(entity);
// Delete entity in database
await DbContext.SaveChangesAsync();
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = "There was an
internal error, please contact to technical support.";
Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(DeleteStockItemAsync), ex);
}
return response.ToHttpResponse();
}
}
}
The process for all
controller's actions is:
- Log the invocation for method.
- Create the instance for response according to action (Paged, list or single).
- Perform access to database through DbContext instance.
- If invocation for DbContext extension method fails, set DidError property as true and set ErrorMessage property with: There was an internal error, please contact to technical support., because it isn't recommended to expose error details in response, it's better to save all exception details in log file.
- Return result as Http response.
- Keep in mind all names for methods that end with Async sufix because all operations are async but in Http attributes, we don't use this suffix.
Step 05 - Setting Up
Dependency Injection
- ASP.NET Core enables dependency injection in native way, this means we don't need any 3rd party framework to inject dependencies in controllers.
- This is a great challenge because we need to change our mind from Web Forms and ASP.NET MVC, for those technologies use a framework to inject dependencies it was a luxury, now in ASP.NET Core dependency injection is a basic aspect.
- Project template for ASP.NET Core has a class with name Startup, in this class we must to add the configuration to inject instances for DbContext, Repositories, Loggers, etc.
Modify the code of
Startup.cs file to look like this:
using System;
using System.IO;
using System.Reflection;
using
Microsoft.AspNetCore.Builder;
using
Microsoft.AspNetCore.Hosting;
using
Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using
Microsoft.Extensions.Configuration;
using
Microsoft.Extensions.DependencyInjection;
using
Microsoft.Extensions.Logging;
using
Swashbuckle.AspNetCore.Swagger;
using Sample.API.Controllers;
using Sample.API.Models;
namespace Sample.API
{
#pragma
warning disable CS1591
public class Startup
{
public Startup(IConfiguration
configuration)
{ Configuration = configuration; }
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection
services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// Add configuration for DbContext
// Use connection string from
appsettings.json file
services.AddDbContext<SampleDbContext>(options
=>
{ options.UseSqlServer(Configuration["AppSettings:ConnectionString"]); });
// Set up dependency injection for controller's logger
services.AddScoped<ILogger,
Logger<StoragehouseController>>();
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Info { Title = "Sample API", Version = "v1" });
// Get xml comments path
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath =
Path.Combine(AppContext.BaseDirectory, xmlFile);
// Set xml path
options.IncludeXmlComments(xmlPath);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{ c.SwaggerEndpoint("/swagger/v1/swagger.json", "Sample API V1"); });
app.UseMvc();
}
}
#pragma
warning restore CS1591
}
The ConfigureServices
method specifies how dependencies will be resolved, in this method. We need to
set up DbContext, Repositories and Logging.
The Configure method
adds the configuration for Http request runtime.
Step 06 - Running Web
API
Before you run Web API
project, add the connection string in appsettings.json file:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"AppSettings": {
"ConnectionString": "server=(local);database=Sample;integrated
security=yes;"
}
}
In order to show
descriptions in help page, enable XML documentation for your Web API project:
1.
Right click on Project > Properties
2.
Go to Build > Output
3.
Enable XML documentation file
4.
Save changes
Now, press F5 to start debugging for Web API project, if everything it's OK, we'll get the following output in the browser:
Also, We can load help page in ahother tab:
Related Links
· Tutorials for
ASP.NET Web API (Courtesy of Jennifer Cai)
Code Improvements
- · Explain how to use
command line for .NET Core
- · Add Security
(Authentication and authorization) for API
- · Split models definitions
in files
- · Refact models outside of
Web API project
- · Anything else? Let me
know in the comments :)
Points of Interest
- In this article, We're working with Entity Framework Core.
- Entity Framework Core has in memory database.
- We can adjust all repositories to expose specific operations, in some cases, we don't want to have GetAll, Add, Update or Delete operations.
- Unit tests perform testing for Assemblies. Integration tests perform testing for Http request
- All tests have been created with xUnit framework.