Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Web API in ASP.NET Core 1x

0.00/5 (No votes)
24 Oct 2018 3  
Creating Web API in ASP.NET Core 1x

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

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:

C#
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);
        }

        // 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> 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();
        }

        // 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> 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();
        }

        // POST Production/Product/
        /// <summary>
        /// Creates a new product on Production catalog
        /// </summary>
        /// <param name="request">Product entry</param>
        /// <returns>Single response</returns>
        [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();
        }

        // PUT Production/Product/5
        /// <summary>
        /// Updates an existing product
        /// </summary>
        /// <param name="id">Product ID</param>
        /// <param name="request">Product entry</param>
        /// <returns>Single response</returns>
        [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();
        }

        // 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> 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:

C#
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:

C#
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:

C#
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:

C#
using System;

namespace AdventureWorksAPI.Models
{
    public class AppSettings
    {
        public String ConnectionString { get; set; }
    }
}

Product class code:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
using System;

namespace AdventureWorksAPI.Responses
{
    public interface IResponse
    {
        String Message { get; set; }

        Boolean DidError { get; set; }

        String ErrorMessage { get; set; }
    }
}

ISingleModelResponse interface code:

C#
namespace AdventureWorksAPI.Responses
{
    public interface ISingleModelResponse<TModel> : IResponse
    {
        TModel Model { get; set; }
    }
}

ListModelResponse class code:

C#
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:

C#
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:

C#
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.

C#
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:

C#
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:

C#
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()
        {
            // Arrange
            var repository = RepositoryMocker.GetAdventureWorksRepository();
            var controller = new ProductionController(repository);

            // Act
            var response = await controller.GetProductsAsync() as ObjectResult;
            var value = response.Value as IListModelResponse<ProductViewModel>;

            controller.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

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

            // Act
            var response = await controller.GetProductAsync(id) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

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

            // Act
            var response = await controller.GetProductAsync(id) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestPostProductAsync()
        {
            // Arrange
            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)
            };

            // Act
            var response = await controller.PostProductAsync(request) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

        [Fact]
        public async Task TestPutProductAsync()
        {
            // Arrange
            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"
            };

            // Act
            var response = await controller.PutProductAsync(id, request) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

            // Assert
            Assert.False(value.DidError);
        }

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

            // Act
            var response = await controller.DeleteProductAsync(id) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

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

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

CHECK THE NEW VERSION FOR THIS GUIDE! CLICK HERE!

  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
  • 10th November, 2017: Code Review
  • 23th October, 2018: Addition of link for new version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here