This is the second part in a series of posts that will demonstrate how to implement an inventory system for the Skinet app we build in the course – β€œLearn to build an e-commerce app with .Net Core and Angular”.   

Since we are adding the ability to create/edit/delete products and will be using the same GetProducts method to retrieve the products in the admin module we create in Angular we will need to turn off caching as otherwise we will have to wait 5 minutes to see the changes to the updates we make when we edit a product, so comment out this line from the GetProduts method in the ProductsController:

        // [Cached(600)]
        [HttpGet]
        public async Task<ActionResult<Pagination<ProductToReturnDto>>> GetProducts(
            [FromQuery]ProductSpecParams productParams)
        {

In this next part we will focus on implementing the CRUD operations for the Products in our store.   We will refactor the ProductsController to accommodate the CreateProduct, EditProduct and DeleteProduct methods, and we will also update this controller to use the Unit Of Work.   To start with though we will need to add a new DTO for the Creation and Editing of the product (we will call it ProductCreateDTO but we can use the same for the edit):

ProductCreateDto.cs

using System.ComponentModel.DataAnnotations;

namespace API.Dtos
{
    public class ProductCreateDto
    {
        [Required]
        public string Name { get; set; }
        
        [Required]
        public string Description { get; set; }
        
        [Required]
        [RegularExpression(@"^\$?([0-9]{1,3},([0-9]{3},)*[0-9]{3}|[0-9]+)(.[0-9][0-9])?$", 
            ErrorMessage = "Price must be a decimal (e.g 20.30)")]
        public decimal Price { get; set; }
        
        public string PictureUrl { get; set; }
        
        [Required]
        public int ProductTypeId { get; set; }
        
        [Required]
        public int ProductBrandId { get; set; }    
    }
}

We will also need a new AutoMapper Profile for this so update the MappingProfiles.cs in the Helpers folder in the API:

CreateMap<ProductCreateDto, Product>();

We will make use of the UnitOfWork interface we created earlier, so if you still have the repositories injected into the ProductController you can remove these and replace with the following in ProductsController.cs:

    public class ProductsController : BaseApiController
    {
        private readonly IUnitOfWork _unitOfWork;
        private readonly IMapper _mapper;

        public ProductsController(IUnitOfWork unitOfWork, IMapper mapper)
        {
            _unitOfWork = unitOfWork;
            _mapper = mapper;
        }

We will also need to refactor the GetProducts, GetBrands, GetTypes and GetProduct methods to use the _unitOfWork now instead of using the Repositories directly

        [Cached(600)]
        [HttpGet]
        public async Task<ActionResult<Pagination<ProductToReturnDto>>> GetProducts(
            [FromQuery]ProductSpecParams productParams)
        {
            var spec = new ProductsWithTypesAndBrandsSpecification(productParams);

            var countSpec = new ProductWithFiltersForCountSpecificication(productParams);

            var totalItems = await _unitOfWork.Repository<Product>().CountAsync(countSpec);

            var products = await _unitOfWork.Repository<Product>().ListAsync(spec);

            var data = _mapper
                .Map<IReadOnlyList<Product>, IReadOnlyList<ProductToReturnDto>>(products);

            return Ok(new Pagination<ProductToReturnDto>(productParams.PageIndex, productParams.PageSize, totalItems, data));
        }

        [Cached(600)]
        [HttpGet("{id}")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
        public async Task<ActionResult<ProductToReturnDto>> GetProduct(int id)
        {
            var spec = new ProductsWithTypesAndBrandsSpecification(id);

            var product = await _unitOfWork.Repository<Product>().GetEntityWithSpec(spec);

            if (product == null) return NotFound(new ApiResponse(404));

            return _mapper.Map<Product, ProductToReturnDto>(product);
        }

        [HttpGet("brands")]
        public async Task<ActionResult<IReadOnlyList<ProductBrand>>> GetProductBrands()
        {
            return Ok(await _unitOfWork.Repository<ProductBrand>().ListAllAsync());
        }

        [Cached(1000)]
        [HttpGet("types")]
        public async Task<ActionResult<IReadOnlyList<ProductType>>> GetProductTypes()
        {
            return Ok(await _unitOfWork.Repository<ProductType>().ListAllAsync());
        }

Now we can create 3 new methods to Create, Update and Delete the products. Note that for the photo we are keeping things very simple here and are just hardcoding this to a placeholder image at the moment. The goal of this is to set up an inventory/admin system for the shop and we can always add an image upload system later:

        [HttpPost]
        public async Task<ActionResult<Product>> CreateProduct(ProductCreateDto productToCreate)
        {
            var product = _mapper.Map<ProductCreateDto, Product>(productToCreate);
            product.PictureUrl = "images/products/placeholder.png";

            _unitOfWork.Repository<Product>().Add(product);

            var result = await _unitOfWork.Complete();

            if (result <= 0) return BadRequest(new ApiResponse(400, "Problem creating product"));

            return Ok(product);
        }
        
        [HttpPut("{id}")]
        public async Task<ActionResult<Product>> UpdateProduct(int id, ProductCreateDto productToUpdate)
        {
            var product = await _unitOfWork.Repository<Product>().GetByIdAsync(id);

            _mapper.Map(productToUpdate, product);

            _unitOfWork.Repository<Product>().Update(product);

            var result = await _unitOfWork.Complete();

            if (result <= 0) return BadRequest(new ApiResponse(400, "Problem updating product"));

            return Ok(product);
        }
        
        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteProduct(int id)
        {
            var product = await _unitOfWork.Repository<Product>().GetByIdAsync(id);
            
            _unitOfWork.Repository<Product>().Delete(product);

            var result = await _unitOfWork.Complete();
            
            if (result <= 0) return BadRequest(new ApiResponse(400, "Problem deleting product"));

            return Ok();
        }

Also, lets add the placeholder image into the Content/images/products folder in the API (right-click and select save as then copy into the folder):

Now if we test these methods in Postman we should be able to successfully create, edit and delete a product as the following screen grabs show:

Adding a new product
Editing the product with Id of 19
Successfully deleting the product

So we now have some very basic CRUD functionality for our products – we haven’t touched on security yet, that will be added later – but coming up next we will add the UI so we can do this from the Angular app.

You can see the changes in this commit here.