Skip to content

BuildCore

Mehmet Özkaya edited this page Mar 25, 2019 · 2 revisions

Step by step extention of code base and apply your custom use cases over the aspnetrun-core repository. This tutorial includes step by step development of aspnetrun-core repository.

Use Cases

We will track on example of Product-Category use cases. Similar to e-commerce domain, we have Product entity and Category entity. In our example Product has only 1 category.

Creating Layers

As per previous chapters you can see clean architecture and we will create 4 layer;

  • Core
  • Application
  • Infrastructure
  • Web

Developing Core Layer

The first point of implementation is definition of Entity. Because we are choosing code-first approach of Entity Framework Core and this seperate this entities to Core layer in order to write only one place.

public class Product : BaseEntity
    {        
        public string ProductName { get; set; }
        public string QuantityPerUnit { get; set; }
        public decimal? UnitPrice { get; set; }
        public short? UnitsInStock { get; set; }
        public short? UnitsOnOrder { get; set; }
        public short? ReorderLevel { get; set; }
        public bool Discontinued { get; set; }
        public int CategoryId { get; set; }
        public Category Category { get; set; }        
    }

As you can see that, any entity class should inherit from BaseEntity.cs. This class include Id field with int type. If you want to change Id field type, you should change only BaseEntity.cs.

Also Category entity should be created;

 public class Category : BaseEntity
    {
        public Category()
        {
            Products = new HashSet<Product>();
        }

        public string CategoryName { get; set; }
        public string Description { get; set; }        
        public ICollection<Product> Products { get; private set; }        
    }

Product-Category Interfaces

Core layer has Interfaces folder which basically include abstraction of dependencies. So when we create new 2 entity if we need to operate some custom db operation we should create repository classes ;

public interface IProductRepository : IAsyncRepository<Product>
    {
        Task<IEnumerable<Product>> GetProductListAsync();
        Task<IEnumerable<Product>> GetProductByNameAsync(string productName);
        Task<IEnumerable<Product>> GetProductByCategoryAsync(int categoryId);
    }

  public interface ICategoryRepository : IAsyncRepository<Category>
    {
        Task<Category> GetCategoryWithProductsAsync(int categoryId);
    }

Developing Infrastructure Layer

When we finished to Core layer operations its good to continue with Infrastructure Layer in order to implement interfaces. So the above section we were defined custom repository interfaces, so in this layer we should implement them.

Its not mandatory to create and implement repository classes, IAsyncRepository and its implementation class AspnetRunRepository.cs is cover all crud operations. So if you have custom database requirements than you should choose this way.

public class ProductRepository : AspnetRunRepository<Product>, IProductRepository
    {
        public ProductRepository(AspnetRunContext dbContext) : base(dbContext)
        {
        }

        public async Task<IEnumerable<Product>> GetProductListAsync()
        {
            // return await GetAllAsync();

            var spec = new ProductWithCategorySpecification();
            return await GetAsync(spec);
        }

        public async Task<IEnumerable<Product>> GetProductByNameAsync(string productName)
        {
            var spec = new ProductWithCategorySpecification(productName);
            return await GetAsync(spec);
        }
        
        public async Task<IEnumerable<Product>> GetProductByCategoryAsync(int categoryId)
        {
            return await _dbContext.Products
                .Where(x => x.CategoryId==categoryId)
                .ToListAsync();
        }
    }

public class CategoryRepository : AspnetRunRepository<Category>, ICategoryRepository
    {
        public CategoryRepository(AspnetRunContext dbContext) : base(dbContext)
        {            
        }

        public async Task<Category> GetCategoryWithProductsAsync(int categoryId)
        {            
            var spec = new CategoryWithProductsSpecification(categoryId);
            var category = (await GetAsync(spec)).FirstOrDefault();
            return category;
        }
    }

As you can see that these implementation classes inherit from AspnetRunRepository.cs in order to use Entity Framework Core dbContext object and use benefits from db abstractions.

Product-Category Add to Context

We were choosing code-first approach of Entity Framework Core so we should add entities into Entity Framework Core Context object in order to reflect database.

public class AspnetRunContext : DbContext
{     
        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }
}

At above, we added database sets of entity objects into our Entity Framework Core context. By this way we can navigate this entities with InMemory and Read Sql Server databases.

Developing Application Layer

When we finished to Infrastructure layer operations its good to continue with Application Layer in order to implement our business logics, use case operations. The first point of implementation is definition of Dtos.

Its not mandatory to create and implement Dto classes, You can use direct Core entities but its good to seperate entity and application required objects.

 public class ProductDto : BaseDto
    {
        public string ProductName { get; set; }
        public string QuantityPerUnit { get; set; }
        public decimal? UnitPrice { get; set; }
        public short? UnitsInStock { get; set; }
        public short? UnitsOnOrder { get; set; }
        public short? ReorderLevel { get; set; }
        public bool Discontinued { get; set; }
        public int? CategoryId { get; set; }
        public CategoryDto Category { get; set; }
    }
public class CategoryDto : BaseDto
    {
        public string CategoryName { get; set; }
        public string Description { get; set; }        
        public ICollection<ProductDto> Products { get; set; }
    }

As you can see that these Dto classes inherit BaseDto.cs.

Application Interfaces and Implementations

The use case of projects should be handled by Application layer. So we are creating Interface and Implementation classes as below way.

Interfaces ;

public interface IProductAppService
    {
        Task<IEnumerable<ProductDto>> GetProductList();
        Task<ProductDto> GetProductById(int productId);
        Task<IEnumerable<ProductDto>> GetProductByName(string productName);
        Task<IEnumerable<ProductDto>> GetProductByCategory(int categoryId);
        Task<ProductDto> Create(ProductDto entityDto);
        Task Update(ProductDto entityDto);
        Task Delete(ProductDto entityDto);
    }
public interface ICategoryAppService
    {
        Task<IEnumerable<CategoryDto>> GetCategoryList();
    }

Implementations ;

public class ProductAppService : IProductAppService
    {
        private readonly IProductRepository _productRepository;
        private readonly IAppLogger<ProductAppService> _logger;

        public ProductAppService(IProductRepository productRepository, IAppLogger<ProductAppService> logger)
        {
            _productRepository = productRepository ?? throw new ArgumentNullException(nameof(productRepository));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public async Task<IEnumerable<ProductDto>> GetProductList()
        {
            var productList = await _productRepository.GetProductListAsync();
            var mapped = ObjectMapper.Mapper.Map<IEnumerable<ProductDto>>(productList);
            return mapped;
        }

        public async Task<ProductDto> GetProductById(int productId)
        {
            var product = await _productRepository.GetByIdAsync(productId);
            var mapped = ObjectMapper.Mapper.Map<ProductDto>(product);
            return mapped;
        }

        public async Task<IEnumerable<ProductDto>> GetProductByName(string productName)
        {
            var productList = await _productRepository.GetProductByNameAsync(productName);
            var mapped = ObjectMapper.Mapper.Map<IEnumerable<ProductDto>>(productList);
            return mapped;
        }

        public async Task<IEnumerable<ProductDto>> GetProductByCategory(int categoryId)
        {
            var productList = await _productRepository.GetProductByCategoryAsync(categoryId);
            var mapped = ObjectMapper.Mapper.Map<IEnumerable<ProductDto>>(productList);
            return mapped;
        }

        public async Task<ProductDto> Create(ProductDto entityDto)
        {
            await ValidateProductIfExist(entityDto);

            var mappedEntity = ObjectMapper.Mapper.Map<Product>(entityDto);
            if (mappedEntity == null)
                throw new ApplicationException($"Entity could not be mapped.");

            var newEntity = await _productRepository.AddAsync(mappedEntity);
            _logger.LogInformation($"Entity successfully added - AspnetRunAppService");

            var newMappedEntity = ObjectMapper.Mapper.Map<ProductDto>(newEntity);
            return newMappedEntity;
        }

        public async Task Update(ProductDto entityDto)
        {
            ValidateProductIfNotExist(entityDto);

            var mappedEntity = ObjectMapper.Mapper.Map<Product>(entityDto);
            if (mappedEntity == null)
                throw new ApplicationException($"Entity could not be mapped.");

            await _productRepository.UpdateAsync(mappedEntity);
            _logger.LogInformation($"Entity successfully updated - AspnetRunAppService");
        }

        public async Task Delete(ProductDto entityDto)
        {
            ValidateProductIfNotExist(entityDto);

            var mappedEntity = ObjectMapper.Mapper.Map<Product>(entityDto);
            if (mappedEntity == null)
                throw new ApplicationException($"Entity could not be mapped.");

            await _productRepository.DeleteAsync(mappedEntity);
            _logger.LogInformation($"Entity successfully deleted - AspnetRunAppService");
        }

        private async Task ValidateProductIfExist(ProductDto entityDto)
        {
            var existingEntity = await _productRepository.GetByIdAsync(entityDto.Id);
            if (existingEntity != null)
                throw new ApplicationException($"{entityDto.ToString()} with this id already exists");
        }

        private void ValidateProductIfNotExist(ProductDto entityDto)
        {
            var existingEntity = _productRepository.GetByIdAsync(entityDto.Id);
            if (existingEntity == null)
                throw new ApplicationException($"{entityDto.ToString()} with this id is not exists");
        }
    }
public class CategoryAppService : ICategoryAppService
    {
        private readonly ICategoryRepository _categoryRepository;
        private readonly IAppLogger<CategoryAppService> _logger;

        public CategoryAppService(ICategoryRepository categoryRepository, IAppLogger<CategoryAppService> logger)
        {
            _categoryRepository = categoryRepository ?? throw new ArgumentNullException(nameof(categoryRepository));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public async Task<IEnumerable<CategoryDto>> GetCategoryList()
        {
            var category = await _categoryRepository.GetAllAsync();
            var mapped = ObjectMapper.Mapper.Map<IEnumerable<CategoryDto>>(category);
            return mapped;
        }        
        
    }

All validation , authorization, logging, exception handling etc. -- this kind of cross cutting activities should be handled by these classes.

Developing Web Layer

The last layer is UI layer, so in this layer we consumed all other layers. Lets start with the ViewModel classes. This classes you can think that imagine of Page components and what is the required data of page.

public class ProductViewModel : BaseViewModel
    {
        public string ProductName { get; set; }
        public string QuantityPerUnit { get; set; }
        public decimal? UnitPrice { get; set; }
        public short? UnitsInStock { get; set; }
        public short? UnitsOnOrder { get; set; }
        public short? ReorderLevel { get; set; }
        public bool Discontinued { get; set; }
        public int? CategoryId { get; set; }
        public CategoryViewModel Category { get; set; }
    }
 public class CategoryViewModel : BaseViewModel
    {
        public string CategoryName { get; set; }
        public string Description { get; set; }
    }

Developing Page Services

Page Services provide to support Razor pages in order to implement screen logics. Its the same way we create interface and also implementation classes.

Interfaces ;

 public interface IProductPageService
    {
        Task<IEnumerable<ProductViewModel>> GetProducts(string productName);
        Task<ProductViewModel> GetProductById(int productId);
        Task<IEnumerable<ProductViewModel>> GetProductByCategory(int categoryId);
        Task<IEnumerable<CategoryViewModel>> GetCategories();
        Task<ProductViewModel> CreateProduct(ProductViewModel productViewModel);
        Task UpdateProduct(ProductViewModel productViewModel);
        Task DeleteProduct(ProductViewModel productViewModel);
    }
public interface ICategoryPageService
    {
        Task<IEnumerable<CategoryViewModel>> GetCategories();
    }

Implementations;

 public class ProductPageService : IProductPageService
    {
        private readonly IProductAppService _productAppService;
        private readonly ICategoryAppService _categoryAppService;
        private readonly IMapper _mapper;
        private readonly ILogger<ProductPageService> _logger;

        public ProductPageService(IProductAppService productAppService, ICategoryAppService categoryAppService, IMapper mapper, ILogger<ProductPageService> logger)
        {
            _productAppService = productAppService ?? throw new ArgumentNullException(nameof(productAppService));
            _categoryAppService = categoryAppService ?? throw new ArgumentNullException(nameof(categoryAppService));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public async Task<IEnumerable<ProductViewModel>> GetProducts(string productName)
        {
            if (string.IsNullOrWhiteSpace(productName))
            {
                var list = await _productAppService.GetProductList();
                var mapped = _mapper.Map<IEnumerable<ProductViewModel>>(list);
                return mapped;
            }

            var listByName = await _productAppService.GetProductByName(productName);
            var mappedByName = _mapper.Map<IEnumerable<ProductViewModel>>(listByName);
            return mappedByName;
        }

        public async Task<ProductViewModel> GetProductById(int productId)
        {
            var product = await _productAppService.GetProductById(productId);
            var mapped = _mapper.Map<ProductViewModel>(product);
            return mapped;
        }

        public async Task<IEnumerable<ProductViewModel>> GetProductByCategory(int categoryId)
        {
            var list = await _productAppService.GetProductByCategory(categoryId);
            var mapped = _mapper.Map<IEnumerable<ProductViewModel>>(list);
            return mapped;
        }

        public async Task<IEnumerable<CategoryViewModel>> GetCategories()
        {
            var list = await _categoryAppService.GetCategoryList();
            var mapped = _mapper.Map<IEnumerable<CategoryViewModel>>(list);
            return mapped;
        }

        public async Task<ProductViewModel> CreateProduct(ProductViewModel productViewModel)
        {
            var mapped = _mapper.Map<ProductDto>(productViewModel);
            if (mapped == null)
                throw new Exception($"Entity could not be mapped.");

            var entityDto = await _productAppService.Create(mapped);
            _logger.LogInformation($"Entity successfully added - IndexPageService");

            var mappedViewModel = _mapper.Map<ProductViewModel>(entityDto);
            return mappedViewModel;
        }

        public async Task UpdateProduct(ProductViewModel productViewModel)
        {
            var mapped = _mapper.Map<ProductDto>(productViewModel);
            if (mapped == null)
                throw new Exception($"Entity could not be mapped.");

            await _productAppService.Update(mapped);
            _logger.LogInformation($"Entity successfully added - IndexPageService");
        }

        public async Task DeleteProduct(ProductViewModel productViewModel)
        {
            var mapped = _mapper.Map<ProductDto>(productViewModel);
            if (mapped == null)
                throw new Exception($"Entity could not be mapped.");

            await _productAppService.Delete(mapped);
            _logger.LogInformation($"Entity successfully added - IndexPageService");
        }
    }
public class CategoryPageService : ICategoryPageService
    {        
        private readonly ICategoryAppService _categoryAppService;
        private readonly IMapper _mapper;

        public CategoryPageService(ICategoryAppService categoryAppService, IMapper mapper)
        {
            _categoryAppService = categoryAppService ?? throw new ArgumentNullException(nameof(categoryAppService));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
        }

        public async Task<IEnumerable<CategoryViewModel>> GetCategories()
        {
            var list = await _categoryAppService.GetCategoryList();
            var mapped = _mapper.Map<IEnumerable<CategoryViewModel>>(list);
            return mapped;
        }
    }

Developing Razor Pages

The final part of UI implementation is Razor Pages. So we should create a Product and Category folder into Pages folder. And in these folder, create cshtml razor pages with Index-Create-Edit-Delete pages in order to building web pages.

Lets create with Index page;

Html Part;

@page
@model AspnetRun.Web.Pages.Product.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Product List</h1>

<form method="get">
    <div class="form-group">
        <div class="input-group">
            <input type="search" class="form-control" asp-for="SearchTerm" />
            <span class="input-group-btn">
                <button class="btn btn-default">
                    Search
                </button>
            </span>
        </div>
    </div>
</form>

<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table table-hover">
    <thead>
        <tr>
            <th scope="col">Id</th>
            <th scope="col">Name</th>
            <th scope="col">UnitPrice</th>
            <th scope="col">Category</th>
            <th scope="col">Action</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var product in Model.ProductList)
        {
            <tr>
                <th scope="row">@product.Id</th>
                <td>@product.ProductName</td>
                <td>@product.UnitPrice</td>
                <td>@product.Category.CategoryName</td>
                <td>
                    <a class="btn"
                       asp-page="./Details"
                       asp-route-productId="@product.Id">
                        Details
                    </a>
                    <a class="btn"
                       asp-page="./Edit"
                       asp-route-productId="@product.Id">
                        Edit
                    </a>
                    <a class="btn"
                       asp-page="./Delete"
                       asp-route-productId="@product.Id">
                        Delete
                    </a>
                </td>
            </tr>
        }
    </tbody>
</table>

IndexModel.cs

 public class IndexModel : PageModel
    {
        private readonly IProductPageService _productPageService;

        public IndexModel(IProductPageService productPageService)
        {
            _productPageService = productPageService ?? throw new ArgumentNullException(nameof(productPageService));
        }

        public IEnumerable<ProductViewModel> ProductList { get; set; } = new List<ProductViewModel>();

        [BindProperty(SupportsGet = true)]
        public string SearchTerm { get; set; }

        public async Task<IActionResult> OnGetAsync()
        {
            ProductList = await _productPageService.GetProducts(SearchTerm);
            return Page();
        }
    }

Lets continue with Create Page;

Html Part ;

@page
@model AspnetRun.Web.Pages.Product.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Product</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Product.ProductName" class="control-label"></label>
                <input asp-for="Product.ProductName" class="form-control" />
                <span asp-validation-for="Product.ProductName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Product.QuantityPerUnit" class="control-label"></label>
                <input asp-for="Product.QuantityPerUnit" class="form-control" />
                <span asp-validation-for="Product.QuantityPerUnit" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Product.UnitPrice" class="control-label"></label>
                <input asp-for="Product.UnitPrice" class="form-control" />
                <span asp-validation-for="Product.UnitPrice" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Product.UnitsInStock" class="control-label"></label>
                <input asp-for="Product.UnitsInStock" class="form-control" />
                <span asp-validation-for="Product.UnitsInStock" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Product.UnitsOnOrder" class="control-label"></label>
                <input asp-for="Product.UnitsOnOrder" class="form-control" />
                <span asp-validation-for="Product.UnitsOnOrder" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Product.ReorderLevel" class="control-label"></label>
                <input asp-for="Product.ReorderLevel" class="form-control" />
                <span asp-validation-for="Product.ReorderLevel" class="text-danger"></span>
            </div>
            <div class="form-group form-check">
                <label class="form-check-label">
                    <input class="form-check-input" asp-for="Product.Discontinued" /> @Html.DisplayNameFor(model => model.Product.Discontinued))
                </label>
            </div>
            <div class="form-group">
                <label asp-for="Product.CategoryId" class="control-label"></label>
                <select asp-for="Product.CategoryId" class ="form-control" asp-items="ViewBag.CategoryId"></select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

CreateModel.cs;

public class CreateModel : PageModel
    {
        private readonly IProductPageService _productPageService;

        public CreateModel(IProductPageService productPageService)
        {
            _productPageService = productPageService ?? throw new ArgumentNullException(nameof(productPageService));
        }

        public async Task<IActionResult> OnGetAsync()
        {
            var categories = await _productPageService.GetCategories();
            ViewData["CategoryId"] = new SelectList(categories, "Id", "CategoryName");
            return Page();
        }

        [BindProperty]
        public ProductViewModel Product { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            Product = await _productPageService.CreateProduct(Product);
            return RedirectToPage("./Index");
        }
    }

Add Depency Injections into Startup.cs

In order to use all implementations properly, we should add dependecies with using ASP.NET Core Default DI classes. So for this implementation we should write all dependencies in Startup.cs ConfigureAspnetRunServices method;

private void ConfigureAspnetRunServices(IServiceCollection services)
        {
            // Add Core Layer
            services.Configure<AspnetRunSettings>(Configuration);

            // Add Infrastructure Layer
            ConfigureDatabases(services);
            services.AddScoped(typeof(IAsyncRepository<>), typeof(AspnetRunRepository<>));
            services.AddScoped<IProductRepository, ProductRepository>();
            services.AddScoped<ICategoryRepository, CategoryRepository>();
            services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));

            // Add Application Layer
            services.AddScoped<IProductAppService, ProductAppService>();
            services.AddScoped<ICategoryAppService, CategoryAppService>();

            // Add Web Layer
            services.AddAutoMapper(); // Add AutoMapper
            services.AddScoped<IIndexPageService, IndexPageService>();
            services.AddScoped<IProductPageService, ProductPageService>();
            services.AddScoped<ICategoryPageService, CategoryPageService>();

            // Add Miscellaneous
            services.AddHttpContextAccessor();
            services.AddHealthChecks()
                .AddCheck<IndexPageHealthCheck>("home_page_health_check");
        }

This step is very important in order to run application. Without giving implementations Asp.Net Core can not find implemented class and will raised the error.

Clone this wiki locally