Introduction
Let's create a Web API with ASP.NET Core and Entity Framework Core 1x.
Background
Related to data access from any organization, nowadays, we need to share information across platforms and RESTful APIs are part of enterprise solutions.
Prerequisites
Skills
- C#
- ORM (Object Relational Mapping)
- RESTful services
Software Prerequisites
- Visual Studio 2015 with Update 3
AdventureWorks
database download
Using the Code
CHECK THE NEW VERSION FOR THIS GUIDE! CLICK HERE!
Step 01 - Create Project in Visual Studio
Open Visual Studio, and select menu File > New > Project > Visual C# - Web > ASP.NET Core Web Application (.NET Core).
Set the project name AdventureWorksAPI
and click OK.
Select Web API in templates, set "No Authentication", uncheck "Host in the cloud" options and click OK.
Once we have project created, we can run the project and we'll get the following output:
Additionally, we'll add the connection string in appsettings.json file:
Step 02 - Add API Related Objects
We need to add Entity Framework packages for our project, open project.json file and add Entity Framework packages as we can see in the following image, lines number 7 and 8:
Save changes and rebuild your project, if everything is OK, build woudn't have any compilation error.
Also, we need to create the following directories for project:
- Extensions: Placeholder for extension methods
- Models: Placeholder for objects related for database access, modeling and configuration
- Responses: Placeholder for objects that represent Http responses
- ViewModels: Placeholder for objects that represent Http outputs
Now, we'll create a new controller inside of Controllers directory.
ProductionController
class code:
using System;
using System.Linq;
using System.Threading.Tasks;
using AdventureWorksAPI.Core.DataLayer;
using AdventureWorksAPI.Core.EntityLayer;
using AdventureWorksAPI.Responses;
using AdventureWorksAPI.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace AdventureWorksAPI.Controllers
{
[Route("api/[controller]")]
public class ProductionController : Controller
{
private IAdventureWorksRepository AdventureWorksRepository;
public ProductionController(IAdventureWorksRepository repository)
{
AdventureWorksRepository = repository;
}
protected override void Dispose(Boolean disposing)
{
AdventureWorksRepository?.Dispose();
base.Dispose(disposing);
}
[HttpGet]
[Route("Product")]
public async Task<IActionResult> GetProductsAsync(Int32? pageSize = 10, Int32? pageNumber = 1, String name = null)
{
var response = new ListModelResponse<ProductViewModel>();
try
{
response.PageSize = (Int32)pageSize;
response.PageNumber = (Int32)pageNumber;
response.Model = await AdventureWorksRepository
.GetProducts(response.PageSize, response.PageNumber, name)
.Select(item => item.ToViewModel())
.ToListAsync();
response.Message = String.Format("Total of records: {0}", response.Model.Count());
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = ex.Message;
}
return response.ToHttpResponse();
}
[HttpGet]
[Route("Product/{id}")]
public async Task<IActionResult> GetProductAsync(Int32 id)
{
var response = new SingleModelResponse<ProductViewModel>();
try
{
var entity = await AdventureWorksRepository.GetProductAsync(new Product { ProductID = id });
response.Model = entity?.ToViewModel();
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = ex.Message;
}
return response.ToHttpResponse();
}
[HttpPost]
[Route("Product")]
public async Task<IActionResult> PostProductAsync([FromBody]ProductViewModel request)
{
var response = new SingleModelResponse<ProductViewModel>();
try
{
var entity = await AdventureWorksRepository.AddProductAsync(request.ToEntity());
response.Model = entity?.ToViewModel();
response.Message = "The data was saved successfully";
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = ex.ToString();
}
return response.ToHttpResponse();
}
[HttpPut]
[Route("Product/{id}")]
public async Task<IActionResult> PutProductAsync(Int32 id, [FromBody]ProductViewModel request)
{
var response = new SingleModelResponse<ProductViewModel>();
try
{
var entity = await AdventureWorksRepository.UpdateProductAsync(request.ToEntity());
response.Model = entity?.ToViewModel();
response.Message = "The record was updated successfully";
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = ex.Message;
}
return response.ToHttpResponse();
}
[HttpDelete]
[Route("Product/{id}")]
public async Task<IActionResult> DeleteProductAsync(Int32 id)
{
var response = new SingleModelResponse<ProductViewModel>();
try
{
var entity = await AdventureWorksRepository.DeleteProductAsync(new Product { ProductID = id });
response.Model = entity?.ToViewModel();
response.Message = "The record was deleted successfully";
}
catch (Exception ex)
{
response.DidError = true;
response.ErrorMessage = ex.Message;
}
return response.ToHttpResponse();
}
}
}
For enterprise implementations, we need to implement big code files. In this case, we are working on Production scheme, that means all entities related to Production namespace will be required, avoid to have a big code file in C# we can split in different code files with partial
keyword on class' definition.
Inside of Models directory, we need to have the following files:
- AdventureWorksDbContext.cs: Database access through Entity Framework
- AdventureWorksRepository.cs: Implementation of repository
- AppSettings.cs: Typed appsettings
- IAdventureWorksRepository.cs: Contract (interface)
- Product.cs: Poco
- ProductMap.cs: Poco class mapping
All of them are part of Models
namespace because they represent the database connection in our API.
IAdventureWorksRepository
interface code:
using System;
using System.Linq;
using System.Threading.Tasks;
using AdventureWorksAPI.Core.EntityLayer;
namespace AdventureWorksAPI.Core.DataLayer
{
public interface IAdventureWorksRepository : IDisposable
{
IQueryable<Product> GetProducts(Int32 pageSize, Int32 pageNumber, String name);
Task<Product> GetProductAsync(Product entity);
Task<Product> AddProductAsync(Product entity);
Task<Product> UpdateProductAsync(Product changes);
Task<Product> DeleteProductAsync(Product changes);
}
}
AdventureWorksRepository
class code:
using System;
using System.Linq;
using System.Threading.Tasks;
using AdventureWorksAPI.Core.EntityLayer;
using Microsoft.EntityFrameworkCore;
namespace AdventureWorksAPI.Core.DataLayer
{
public class AdventureWorksRepository : IAdventureWorksRepository
{
private readonly AdventureWorksDbContext DbContext;
private Boolean Disposed;
public AdventureWorksRepository(AdventureWorksDbContext dbContext)
{
DbContext = dbContext;
}
public void Dispose()
{
if (!Disposed)
{
DbContext?.Dispose();
Disposed = true;
}
}
public IQueryable<Product> GetProducts(Int32 pageSize, Int32 pageNumber, String name)
{
var query = DbContext.Set<Product>().Skip((pageNumber - 1) * pageSize).Take(pageSize);
if (!String.IsNullOrEmpty(name))
{
query = query.Where(item => item.Name.ToLower().Contains(name.ToLower()));
}
return query;
}
public Task<Product> GetProductAsync(Product entity)
{
return DbContext.Set<Product>().FirstOrDefaultAsync(item => item.ProductID == entity.ProductID);
}
public async Task<Product> AddProductAsync(Product entity)
{
entity.MakeFlag = false;
entity.FinishedGoodsFlag = false;
entity.SafetyStockLevel = 1;
entity.ReorderPoint = 1;
entity.StandardCost = 0.0m;
entity.ListPrice = 0.0m;
entity.DaysToManufacture = 0;
entity.SellStartDate = DateTime.Now;
entity.rowguid = Guid.NewGuid();
entity.ModifiedDate = DateTime.Now;
DbContext.Set<Product>().Add(entity);
await DbContext.SaveChangesAsync();
return entity;
}
public async Task<Product> UpdateProductAsync(Product changes)
{
var entity = await GetProductAsync(changes);
if (entity != null)
{
entity.Name = changes.Name;
entity.ProductNumber = changes.ProductNumber;
await DbContext.SaveChangesAsync();
}
return entity;
}
public async Task<Product> DeleteProductAsync(Product changes)
{
var entity = await GetProductAsync(changes);
if (entity != null)
{
DbContext.Set<Product>().Remove(entity);
await DbContext.SaveChangesAsync();
}
return entity;
}
}
}
AdventureWorksDbContext
class code:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace AdventureWorksAPI.Models
{
public class AdventureWorksDbContext : Microsoft.EntityFrameworkCore.DbContext
{
public AdventureWorksDbContext(IOptions<AppSettings> appSettings)
{
ConnectionString = appSettings.Value.ConnectionString;
}
public String ConnectionString { get; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(ConnectionString);
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.MapProduct();
base.OnModelCreating(modelBuilder);
}
}
}
AppSettings
class code:
using System;
namespace AdventureWorksAPI.Models
{
public class AppSettings
{
public String ConnectionString { get; set; }
}
}
Product
class code:
using System;
namespace AdventureWorksAPI.Models
{
public class Product
{
public Int32? ProductID { get; set; }
public String Name { get; set; }
public String ProductNumber { get; set; }
public Boolean? MakeFlag { get; set; }
public Boolean? FinishedGoodsFlag { get; set; }
public Int16? SafetyStockLevel { get; set; }
public Int16? ReorderPoint { get; set; }
public Decimal? StandardCost { get; set; }
public Decimal? ListPrice { get; set; }
public Int32? DaysToManufacture { get; set; }
public DateTime? SellStartDate { get; set; }
public Guid? rowguid { get; set; }
public DateTime? ModifiedDate { get; set; }
}
}
ProductMap
class code:
using Microsoft.EntityFrameworkCore;
namespace AdventureWorksAPI.Models
{
public static class ProductMap
{
public static ModelBuilder MapProduct(this ModelBuilder modelBuilder)
{
var entity = modelBuilder.Entity<Product>();
entity.ToTable("Product", "Production");
entity.HasKey(p => new { p.ProductID });
entity.Property(p => p.ProductID).UseSqlServerIdentityColumn();
return modelBuilder;
}
}
}
As we can see, we have different classes for each table:
- POCO: represents the table as a CRL object
- Mapping: configuration for a POCO object inside of DbContext
- Mapper: logic for match properties' values according to properties' names
There is a big question, if we have 200 mapped tables, that means we need to have 200 code files for each type? The answer is YES!!!. There are options to solve this issue, we can search for code generation tool or we can write all of them, please check CatFactory on links section to know more about code generation for EF Core, anyway the fact is we need to define this object because at design time, it's very useful to know the types we going to use inside of our API.
Inside of Extensions directory, we have the following files:
- ProductViewModelMapper: Extensions for map
Product
poco class to ProductViewModel
class - ResponseExtensions: Extension methods for creating Http responses
ProductViewModelMapper
class code:
using AdventureWorksAPI.Models;
using AdventureWorksAPI.ViewModels;
namespace AdventureWorksAPI.Extensions
{
public static class ProductViewModelMapper
{
public static ProductViewModel ToViewModel(this Product entity)
{
return new ProductViewModel
{
ProductID = entity.ProductID,
ProductName = entity.Name,
ProductNumber = entity.ProductNumber
};
}
public static Product ToEntity(this ProductViewModel viewModel)
{
return new Product
{
Name = viewModel.ProductName,
ProductNumber = viewModel.ProductNumber
};
}
}
}
Why we don't use a mapper framework? At this point, we can change the mapper according to our preferences, if you want to improve your C# skills, you can add a dynamic way for mapping. :)
ResponseExtensions
class code:
using System;
using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace AdventureWorksAPI.Responses
{
public static class ResponseExtensions
{
public static IActionResult ToHttpResponse<TModel>(this IListModelResponse<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 = (Int32)status
};
}
public static IActionResult ToHttpResponse<TModel>(this ISingleModelResponse<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 = (Int32)status
};
}
}
}
Inside of Responses directory, we need to have the following files:
- IListModelResponse.cs: Interface for representing a list response
- IResponse.cs: Generic interface for responses
- ISingleModelResponse.cs: Interface for representing a single response (one entity)
- ListModelResponse.cs: Implementation of list response
- SingleModelResponse.cs: Implementation of single response
IListModelResponse
interface code:
using System;
using System.Collections.Generic;
namespace AdventureWorksAPI.Responses
{
public interface IListModelResponse<TModel> : IResponse
{
Int32 PageSize { get; set; }
Int32 PageNumber { get; set; }
IEnumerable<TModel> Model { get; set; }
}
}
IResponse
interface code:
using System;
namespace AdventureWorksAPI.Responses
{
public interface IResponse
{
String Message { get; set; }
Boolean DidError { get; set; }
String ErrorMessage { get; set; }
}
}
ISingleModelResponse
interface code:
namespace AdventureWorksAPI.Responses
{
public interface ISingleModelResponse<TModel> : IResponse
{
TModel Model { get; set; }
}
}
ListModelResponse
class code:
using System;
using System.Collections.Generic;
namespace AdventureWorksAPI.Responses
{
public class ListModelResponse<TModel> : IListModelResponse<TModel>
{
public String Message { get; set; }
public Boolean DidError { get; set; }
public String ErrorMessage { get; set; }
public Int32 PageSize { get; set; }
public Int32 PageNumber { get; set; }
public IEnumerable<TModel> Model { get; set; }
}
}
SingleModelResponse
class code:
using System;
namespace AdventureWorksAPI.Responses
{
public class SingleModelResponse<TModel> : ISingleModelResponse<TModel>
{
public String Message { get; set; }
public Boolean DidError { get; set; }
public String ErrorMessage { get; set; }
public TModel Model { get; set; }
}
}
Inside of ViewModels directory, we have the following files:
- ProductViewModelr: View Model for represent information about
Product
s.
ProductViewModel
class code:
using System;
namespace AdventureWorksAPI.ViewModels
{
public class ProductViewModel
{
public Int32? ProductID { get; set; }
public String ProductName { get; set; }
public String ProductNumber { get; set; }
}
}
View models contain only the properties we want to expose for client, in this case, we handle all default values for Product
entity inside of repository's implementation, we need to make sure all requests will use repository implementation for set values in default properties.
Step 03 - Setting Up All Services Together
One of the main changes in ASP.NET Core is its dependency injection, now is "native" and we don't need to install additional packages.
At this point, we need to configure all services in Startup
class, in ConfigureServices
method, we need to setup the dependencies that will be injected for controllers, also contract's name resolver and typed settings.
using AdventureWorksAPI.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
namespace AdventureWorksAPI
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddJsonOptions
(a => a.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
services.AddEntityFrameworkSqlServer().AddDbContext<AdventureWorksDbContext>();
services.AddScoped<IAdventureWorksRepository, AdventureWorksRepository>();
services.AddOptions();
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
services.AddSingleton<IConfiguration>(Configuration);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
}
}
}
Step 04 - Adding Unit Tests
Testing if required in these days, because with unit tests, it is easy to test a feature before publishing, Test Driven Development (TDD) is the way to define unit tests and validate the behavior in our code.
ASP.NET Core includes a lot of changes, about testing, there is a command line for running unit tests, we need to change the current code for add unit tests.
We need to have the following structure:
For creating the structure above, follow these steps:
- Right click on solution's name > Open Command Line > Default (cmd)
- Create "test" directory (
mkdir test
) - Enter in "test" directory (
cd test
) - Create "AdventureWorksAPI.Tests" directory (
mkdir AdventureWorksAPI.Tests
) - Enter in "AdventureWorksAPI.Tests" directory (
cd AdventureWorksAPI.Tests
) - Create unit test project (
dotnet new -t xunittest
) - Back to Visual Studio and create a new solution folder and name test
- Add existing project for test solution folder (
AdventureWorksAPI.Tests
) - Remove Tests.cs file and add a new file: ProductionControllerTest.cs
Code for RepositoryMocker
class:
using AdventureWorksAPI.Core.DataLayer;
using Microsoft.Extensions.Options;
namespace AdventureWorksAPI.Tests
{
public static class RepositoryMocker
{
public static IAdventureWorksRepository GetAdventureWorksRepository()
{
var appSettings = Options.Create(new AppSettings
{
ConnectionString = "server=(local);database=AdventureWorks2012;integrated security=yes;"
});
return new AdventureWorksRepository(new AdventureWorksDbContext(appSettings, new AdventureWorksEntityMapper()));
}
}
}
Code for ProductionControllerTest
class:
using System;
using System.Threading.Tasks;
using AdventureWorksAPI.Controllers;
using AdventureWorksAPI.Responses;
using AdventureWorksAPI.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Xunit;
namespace AdventureWorksAPI.Tests
{
public class ProductionControllerTest
{
[Fact]
public async Task TestGetProductsAsync()
{
var repository = RepositoryMocker.GetAdventureWorksRepository();
var controller = new ProductionController(repository);
var response = await controller.GetProductsAsync() as ObjectResult;
var value = response.Value as IListModelResponse<ProductViewModel>;
controller.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestGetProductAsync()
{
var repository = RepositoryMocker.GetAdventureWorksRepository();
var controller = new ProductionController(repository);
var id = 1;
var response = await controller.GetProductAsync(id) as ObjectResult;
var value = response.Value as ISingleModelResponse<ProductViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestGetNonExistingProductAsync()
{
var repository = RepositoryMocker.GetAdventureWorksRepository();
var controller = new ProductionController(repository);
var id = 0;
var response = await controller.GetProductAsync(id) as ObjectResult;
var value = response.Value as ISingleModelResponse<ProductViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestPostProductAsync()
{
var repository = RepositoryMocker.GetAdventureWorksRepository();
var controller = new ProductionController(repository);
var request = new ProductViewModel
{
ProductName = String.Format("New test product {0}{1}{2}", DateTime.Now.Minute, DateTime.Now.Second, DateTime.Now.Millisecond),
ProductNumber = String.Format("{0}{1}{2}", DateTime.Now.Minute, DateTime.Now.Second, DateTime.Now.Millisecond)
};
var response = await controller.PostProductAsync(request) as ObjectResult;
var value = response.Value as ISingleModelResponse<ProductViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestPutProductAsync()
{
var repository = RepositoryMocker.GetAdventureWorksRepository();
var controller = new ProductionController(repository);
var id = 1;
var request = new ProductViewModel
{
ProductID = id,
ProductName = "New product test II",
ProductNumber = "XYZ"
};
var response = await controller.PutProductAsync(id, request) as ObjectResult;
var value = response.Value as ISingleModelResponse<ProductViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
[Fact]
public async Task TestDeleteProductAsync()
{
var repository = RepositoryMocker.GetAdventureWorksRepository();
var controller = new ProductionController(repository);
var id = 1000;
var response = await controller.DeleteProductAsync(id) as ObjectResult;
var value = response.Value as ISingleModelResponse<ProductViewModel>;
repository.Dispose();
Assert.False(value.DidError);
}
}
}
As we can see until now, we have added unit tests for our Web API project, now we can run the unit tests from command line, open a command line window and change directory for AdventureWorksAPI.Tests directory and type this command: dotnet test
, we will get an output like this:
Step 05 - Running Code
Once we have built the project without any compilation error, we can run our project from Visual Studio, later with any browser, we can access API.
Please remember in my machine, IIS Express uses the port number 38126, this will be changed on your machine, also in ProductionController
we have Route
attribute for route definition, if we need the API resolves with another name, we must change the values for Route
attribute.
As we can see, we can build different urls for products search:
- api/Production/Product/
- api/Production/Product/?pageSize=12&pageNumber=1
- api/Production/Product/?pageSize=5&pageNumber=1&name=a
Default list output: api/Production/Product/
List output with page size and page number parameters: api/Production/Product/?pageSize=12&pageNumber=1
Single output: api/Production/Product/4
If you can't see the json in a pretty way, there is a viewer extension on Chrome JSON Viewer.
Please remember, you can test your Web API with other tools, like Postman download.
Refactor your Back-end code
As we can see at this point, we have many objects into AdventureWorksAPI project, as part of enterprice applications development it's not recommended to have all objects into API project, we going to split our API project follow these steps:
- Right click on solution's name
- Add > New Project > .NET Core
- Set project's name to AdventureWorksAPI.Core
- OK
Now we add the entity framework core packages for new project.
This is the structure for AdventureWorksAPI.Core project:
Use the following image and refactor all classes to individual files:
Take this task as a challenge for you, one you have refactor all your code, add reference to AdventureWorksAPI.Core project to AdventureWorksAPI project, save all changes and build your solution, you'll get errors on unit tests project, so add namespaces and reference in unit tests project, now save all changes and build your solution.
If everything it's fine, we can run without errors our application.
Code Improvements
CHECK THE NEW VERSION FOR THIS GUIDE! CLICK HERE!
- Add of integration tests
- Logging on Web API methods
- Another improvement according to your point of view, please let me know in the comments :)
Points of Interest
- Entity Framework now is "
Microsoft.EntityFrameworkCore
". - Why do we need to have typed responses? For design purposes, it's more flexible to have typed responses to avoid common mistakes in development phase such as to know if a search result is empty or not and avoid unexpected behavior. Also with typed response, we can know if one request has error from server side (database connection, casting, etc.)
- Why we should to have
ViewModels
if we already have models (POCOs)? Imagine that we have a table with 100 columns that represent customer information and for specific requirement; we just need to return customer id, contact's name, company's name and country; we can solve this issue using anonymous type but as we can see above, we need a structure that allow us to know how many fields have a response, anyway with anonymous type or not, we need return an object that contains specific fields and do not expose unnecessary data (phone, email, etc.)
Related Links
History
- 18th July, 2016: Initial version
- 24th July, 2016: CRUD operations for controller
- 24th October, 2016: Unit Tests
- 10th November, 2017: Code Review
- 23th October, 2018: Addition of link for new version