Click here to Skip to main content
13,146,395 members (49,463 online)
Click here to Skip to main content
Add your own
alternative version

Stats

146.4K views
166 bookmarked
Posted 17 Jul 2016

Creating Web API in ASP.NET Core

, 22 Feb 2017
Rate this:
Please Sign up or sign in to vote.
How-To for Web API in ASP.NET Core

Introduction

Let's create a Web API with ASP.NET Core and latest version of the Entity Framework.

Background

Related to data access from any organization, nowadays, we need to share information across platforms and RESTful APIs are part of enterprise solutions.

Skills Prerequisites

  • C#
  • ORM (Object Relational Mapping)
  • RESTful services

Software Prerequisites

  • Visual Studio 2015 with Update 3
  • AdventureWorks database download

Using the Code

There is a github repository for this how-to.

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

New project

Set the project name AdventureWorksAPI and click OK.

Select template

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:

First API run

Additionally, we'll add the connection string in appsettings.json file:

App settings

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:

Entity framework packages

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.

Add controller

ProductionController class code:

using System;
using System.Linq;
using System.Threading.Tasks;
using AdventureWorksAPI.Core.DataLayer;
using AdventureWorksAPI.Core.EntityLayer;
using AdventureWorksAPI.Extensions;
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);
        }

        // GET Production/Product
        /// <summary>
        /// Retrieves a list of products
        /// </summary>
        /// <param name="pageSize">Page size</param>
        /// <param name="pageNumber">Page number</param>
        /// <param name="name">Name</param>
        /// <returns>List response</returns>
        [HttpGet]
        [Route("Product")]
        public async Task<IActionResult> GetProducts(Int32? pageSize = 10, Int32? pageNumber = 1, String name = null)
        {
            var response = new ListModelResponse<ProductViewModel>() as IListModelResponse<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();
        }

        // GET Production/Product/5
        /// <summary>
        /// Retrieves a specific product by id
        /// </summary>
        /// <param name="id">Product ID</param>
        /// <returns>Single response</returns>
        [HttpGet]
        [Route("Product/{id}")]
        public async Task<IActionResult> GetProduct(Int32 id)
        {
            var response = new SingleModelResponse<ProductViewModel>() as ISingleModelResponse<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();
        }

        // POST Production/Product/
        /// <summary>
        /// Creates a new product on Production catalog
        /// </summary>
        /// <param name="value">Product entry</param>
        /// <returns>Single response</returns>
        [HttpPost]
        [Route("Product")]
        public async Task<IActionResult> CreateProduct([FromBody]ProductViewModel value)
        {
            var response = new SingleModelResponse<ProductViewModel>() as ISingleModelResponse<ProductViewModel>;

            try
            {
                var entity = await AdventureWorksRepository.AddProductAsync(value.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();
        }

        // PUT Production/Product/5
        /// <summary>
        /// Updates an existing product
        /// </summary>
        /// <param name="value">Product entry</param>
        /// <returns>Single response</returns>
        [HttpPut]
        [Route("Product")]
        public async Task<IActionResult> UpdateProduct([FromBody]ProductViewModel value)
        {
            var response = new SingleModelResponse<ProductViewModel>() as ISingleModelResponse<ProductViewModel>;

            try
            {
                var entity = await AdventureWorksRepository.UpdateProductAsync(value.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();
        }

        // DELETE Production/Product/5
        /// <summary>
        /// Delete an existing product
        /// </summary>
        /// <param name="id">Product ID</param>
        /// <returns>Single response</returns>
        [HttpDelete]
        [Route("Product/{id}")]
        public async Task<IActionResult> DeleteProduct(Int32 id)
        {
            var response = new SingleModelResponse<ProductViewModel>() as ISingleModelResponse<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)
            {
                if (DbContext != null)
                {
                    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:

  1. POCO: represents the table as a CRL object
  2. Mapping: configuration for a POCO object inside of DbContext
  3. 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 entity == null ? null : new ProductViewModel
            {
                ProductID = entity.ProductID,
                ProductName = entity.Name,
                ProductNumber = entity.ProductNumber
            };
        }
        
        public static Product ToEntity(this ProductViewModel viewModel)
        {
            return viewModel == null ? null : 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 AdventureWorksAPI.Responses;
using Microsoft.AspNetCore.Mvc;

namespace AdventureWorksAPI.Extensions
{
    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 Products.

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

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc().AddJsonOptions
            (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);
        }

        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        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:

Solution structure

For creating the structure above, follow these steps:

  1. Right click on solution's name > Open Command Line > Default (cmd)
  2. Create "test" directory (mkdir test)
  3. Enter in "test" directory (cd test)
  4. Create "AdventureWorksAPI.Tests" directory (mkdir AdventureWorksAPI.Tests)
  5. Enter in "AdventureWorksAPI.Tests" directory (cd AdventureWorksAPI.Tests)
  6. Create unit test project (dotnet new -t xunittest)
  7. Back to Visual Studio and create a new solution folder and name test
  8. Add existing project for test solution folder (AdventureWorksAPI.Tests)
  9. 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 AdventureWorksRepository
        {
            get
            {
                var appSettings = Options.Create(new AppSettings
                {
                    ConnectionString = "server=(local);database=AdventureWorks2012;integrated security=yes;"
                });

                var entityMapper = new AdventureWorksEntityMapper() as IEntityMapper;

                return new AdventureWorksRepository(new AdventureWorksDbContext(appSettings, entityMapper)) as IAdventureWorksRepository;
            }
        }
    }
}

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 Production_GetProductsAsync()
        {
            using (var repository = RepositoryMocker.AdventureWorksRepository)
            {
                // Arrange
                var controller = new ProductionController(repository);

                // Act
                var response = await controller.GetProducts() as ObjectResult;

                // Assert
                var value = response.Value as IListModelResponse<ProductViewModel>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task Production_GetProductAsync()
        {
            using (var repository = RepositoryMocker.AdventureWorksRepository)
            {
                // Arrange
                var controller = new ProductionController(repository);
                var id = 1;

                // Act
                var response = await controller.GetProduct(id) as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<ProductViewModel>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task Production_GetNonExistingProductAsync()
        {
            using (var repository = RepositoryMocker.AdventureWorksRepository)
            {
                // Arrange
                var controller = new ProductionController(repository);
                var id = 0;

                // Act
                var response = await controller.GetProduct(id) as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<ProductViewModel>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task Production_CreateProductAsync()
        {
            using (var repository = RepositoryMocker.AdventureWorksRepository)
            {
                // Arrange
                var controller = new ProductionController(repository);

                var viewModel = 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)
                };

                // Act
                var response = await controller.CreateProduct(viewModel) as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<ProductViewModel>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task Production_UpdateProductAsync()
        {
            using (var repository = RepositoryMocker.AdventureWorksRepository)
            {
                // Arrange
                var controller = new ProductionController(repository);

                var id = 1;

                var viewModel = new ProductViewModel
                {
                    ProductName = "New product test II",
                    ProductNumber = "XYZ"
                };

                // Act
                var response = await controller.UpdateProduct(id, viewModel) as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<ProductViewModel>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task Production_DeleteProductAsync()
        {
            using (var repository = RepositoryMocker.AdventureWorksRepository)
            {
                // Arrange
                var controller = new ProductionController(repository);
                var id = 1000;

                // Act
                var response = await controller.DeleteProduct(id) as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<ProductViewModel>;

                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:

Unit Test Output

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/?name=a&pageSize=5&pageNumber=1
  • api/Production/Product/?pageSize=12&pageNumber=1

Default list output: api/Production/Product/

API list result

List output with page size and page number parameters: api/Production/Product/?pageSize=12&pageNumber=1

API list result with page size and page number parameters

Single output: api/Production/Product/4

API single result

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:

  1. Right click on solution's name
  2. Add > New Project > .NET Core
  3. Set project's name to AdventureWorksAPI.Core
  4. OK

Now we add the entity framework core packages for new project.

This is the structure for AdventureWorksAPI.Core project:

  • DataLayer
  • EntityLayer

Use the following image and refactor all classes to individual files:

Project refactoring structure

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

  1. Add of integration tests
  2. Logging on Web API methods
  3. 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

License

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

Share

About the Author

Carlo Hans H.
United States United States
Developer focused in Web Applications (ASPNET Core and Angular)

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionVoted 5 for this. Can you write about deploying Core Web API to IIS? Pin
shielo11-Aug-17 0:23
membershielo11-Aug-17 0:23 
AnswerRe: Voted 5 for this. Can you write about deploying Core Web API to IIS? Pin
Carlo Hans H.15-Aug-17 16:10
memberCarlo Hans H.15-Aug-17 16:10 
GeneralRe: Voted 5 for this. Can you write about deploying Core Web API to IIS? Pin
shielo15-Aug-17 21:17
membershielo15-Aug-17 21:17 
SuggestionDifferent article with very less code. Pin
lawrine31-Jul-17 2:40
memberlawrine31-Jul-17 2:40 
GeneralRe: Different article with very less code. Pin
Carlo Hans H.1-Aug-17 11:52
memberCarlo Hans H.1-Aug-17 11:52 
QuestionNull reference Pin
Member 1117239322-May-17 4:24
professionalMember 1117239322-May-17 4:24 
AnswerRe: Null reference Pin
Carlo Hans H.1-Aug-17 11:33
memberCarlo Hans H.1-Aug-17 11:33 
QuestionDot net core with Entity framework for the DB which we do not have Primary key Pin
Member 1099479810-May-17 5:49
memberMember 1099479810-May-17 5:49 
AnswerRe: Dot net core with Entity framework for the DB which we do not have Primary key Pin
Carlo Hans H.10-May-17 14:13
memberCarlo Hans H.10-May-17 14:13 
QuestionI'm to stupid for my...code Pin
Claudiu Schiopu12-Mar-17 9:56
memberClaudiu Schiopu12-Mar-17 9:56 
AnswerRe: I'm to stupid for my...code Pin
Carlo Hans H.20-Mar-17 15:49
memberCarlo Hans H.20-Mar-17 15:49 
QuestionNice article but you should not use Task.Run Pin
Philipos Sakellaropoulos9-Jan-17 9:57
memberPhilipos Sakellaropoulos9-Jan-17 9:57 
AnswerRe: Nice article but you should not use Task.Run Pin
Carlo Hans H.22-Feb-17 7:24
memberCarlo Hans H.22-Feb-17 7:24 
PraiseMy vote of 5!! Pin
JohannQ20-Dec-16 14:22
memberJohannQ20-Dec-16 14:22 
QuestionModels... controllers.... Pin
Thornik8-Dec-16 7:23
memberThornik8-Dec-16 7:23 
AnswerRe: Models... controllers.... Pin
Carlo Hans H.8-Dec-16 7:46
memberCarlo Hans H.8-Dec-16 7:46 
GeneralMy vote of 5 Pin
A-Yik8-Nov-16 18:56
memberA-Yik8-Nov-16 18:56 
GeneralRe: My vote of 5 Pin
Carlo Hans H.5-Dec-16 20:08
memberCarlo Hans H.5-Dec-16 20:08 
PraiseOne to word to say it all Pin
HamzaZia4-Nov-16 21:27
memberHamzaZia4-Nov-16 21:27 
GeneralRe: One to word to say it all Pin
Carlo Hans H.6-Nov-16 21:25
memberCarlo Hans H.6-Nov-16 21:25 
QuestionDB Migration Pin
Member 117543353-Nov-16 15:50
memberMember 117543353-Nov-16 15:50 
AnswerRe: DB Migration Pin
Carlo Hans H.11-Nov-16 20:04
memberCarlo Hans H.11-Nov-16 20:04 
QuestionWhy .NET Core? Pin
netizenk3-Nov-16 13:44
membernetizenk3-Nov-16 13:44 
AnswerRe: Why .NET Core? Pin
Florian Rappl3-Nov-16 21:14
professionalFlorian Rappl3-Nov-16 21:14 
AnswerRe: Why .NET Core? Pin
Carlo Hans H.4-Nov-16 10:20
memberCarlo Hans H.4-Nov-16 10:20 

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

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

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.170915.1 | Last Updated 22 Feb 2017
Article Copyright 2016 by Carlo Hans H.
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid