In part I, I shared the background story and practical walkthrough for my first custom NuGet package called BabaFunke.DataAccess – a simple generic repository framework for CRUD (Create, Read, Update, Delete) operations which is typical of client/server Asp.Net web applications. Its sole purpose is to eliminate the need for some related repetitive tasks using the repository pattern.
In this concluding piece, I create a simple product management project that utilises my NuGet package. This project is included in the GitHub repo for the NuGet package as BabaFunke.DataAccessDemo and includes a UnitTest project.
Say, like me, you are an app developer who’d like to create a dynamic website like https://geniigames.app/ for managing your apps, you could easily develop a product management system using Asp.Net Core + any of the popular Database systems. This way, you have a way of storing, updating, deleting your products.
To keep things simple for this demo project, I use a pre-defined list of products instead of a database. Below, I share a simple walkthrough of the demo project showing snippets for some of the classes. Access the demo project in the GitHub repo for a complete implementation.
Create a Web Api Project
In Visual Studio, create a Asp.Net Core Web Api project.
Add the NuGet Package
Open the NuGet Package Manager and search for BabaFunke.DataAccess in the browse tab. Alternatively, simply enter the command line below into the Package Manager Console
Install-Package BabaFunke.DataAccess -Version 1.0.4
Add a Model
Create a class called Product; this is your model. Make sure it inherits the IPrimaryKey interface which ensures a property is implemented for the primary key. Note the addition of the namespace BabaFunke.DataAccess to make the interface accessible. I have also added other useful properties for a typical product. If your product was an App, the Title is the name of the app, Count could be the number of versions (Android, iOS), IsDisabled is used to determine the active or inactive status of the app and DateAdded is the date the product was added or created.
using BabaFunke.DataAccess; | |
using System; | |
using System.ComponentModel.DataAnnotations; | |
namespace Babafunke.DataAccessDemo.Models | |
{ | |
public class Product : IPrimaryKey | |
{ | |
[Required] | |
[Range(1, int.MaxValue)] | |
public int Id { get; set; } | |
[Required] | |
[MaxLength(50)] | |
public string Title { get; set; } | |
[Required] | |
[Range(1, int.MaxValue)] | |
public int Count { get; set; } | |
public bool IsDisabled { get; set; } | |
public DateTime DateAdded { get; } | |
public Product() | |
{ | |
DateAdded = DateTime.Now; | |
} | |
} | |
} |
Add the Service
Create a class and inherit the class SqlServerEfRepository<T> from the BabaFunke.DataAccess NuGet Package. Remember to include the Product model in the angular brackets as it’s a generic. Then override the methods already implemented in the parent class.
If using Visual Studio, right-click the inherited class and select ‘Go to definition’ to see the class and methods to override. You can simply copy it from there.
To override the implementation, add the async override keywords to each.
using Babafunke.DataAccessDemo.Data; | |
using Babafunke.DataAccessDemo.Models; | |
using BabaFunke.DataAccess; | |
using System.Collections.Generic; | |
using System.Threading.Tasks; | |
namespace Babafunke.DataAccessDemo.Services | |
{ | |
public class ProductService : SqlServerEfRepository<Product> | |
{ | |
public override async Task<IEnumerable<Product>> GetAllItems() | |
{ | |
var products = DataManager.GetAllProducts(); | |
return await Task.Run(() => products); | |
} | |
public override async Task<Product> GetItemById(int id) | |
{ | |
var product = DataManager.GetProduct(id); | |
return await Task.Run(() => product); | |
} | |
public override async Task<Product> CreateItem(Product item) | |
{ | |
var product = DataManager.AddProduct(item); | |
return await Task.Run(() => product); | |
} | |
public override async Task<Product> EditItem(Product item) | |
{ | |
var product = DataManager.UpdateProduct(item); | |
return await Task.Run(() => product); | |
} | |
public override async Task<bool> DeleteItem(int id) | |
{ | |
var response = DataManager.DeleteProduct(id); | |
return await Task.Run(() => response); | |
} | |
public override async Task<bool> ArchiveItem(int id) | |
{ | |
var response = DataManager.ArchiveProduct(id); | |
return await Task.Run(() => response); | |
} | |
} | |
} |
My overridden implementations use a static DataManager class to mock the CRUD operations.
In this case, I’m going to mock a database operation using a list. I add a static class MockDatabase and a static method to replicate some of the CRUD operations.
using Babafunke.DataAccessDemo.Models; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
namespace Babafunke.DataAccessDemo.Data | |
{ | |
public static class DataManager | |
{ | |
public static List<Product> products = new List<Product> | |
{ | |
new Product{Id = 1, Title = "Conversational Igbo for kids", Count = 5, IsDisabled = false}, | |
new Product{Id = 2, Title = "Conversational Yoruba for kids", Count = 8, IsDisabled = false} | |
}; | |
public static List<Product> GetAllProducts() | |
{ | |
return products.OrderBy(p => p.Id).ToList(); | |
} | |
public static Product GetProduct(int id) | |
{ | |
var product = products.SingleOrDefault(p => p.Id == id); | |
return product; | |
} | |
public static Product AddProduct(Product product) | |
{ | |
products.Add(product); | |
return product; | |
} | |
public static Product UpdateProduct(Product product) | |
{ | |
var existingProduct = GetProduct(product.Id); | |
if (existingProduct == null) | |
{ | |
return null; | |
} | |
var indexOfExistingProduct = products.IndexOf(existingProduct); | |
products.RemoveAt(indexOfExistingProduct); | |
products.Insert(indexOfExistingProduct,product); | |
return product; | |
} | |
public static bool DeleteProduct(int id) | |
{ | |
try | |
{ | |
var existingProduct = GetProduct(id); | |
if (existingProduct == null) | |
{ | |
return false; | |
} | |
products.Remove(existingProduct); | |
return true; | |
} | |
catch(Exception e) | |
{ | |
throw new Exception(e.Message); | |
} | |
} | |
public static bool ArchiveProduct(int id) | |
{ | |
try | |
{ | |
var existingProduct = GetProduct(id); | |
if (existingProduct == null) | |
{ | |
return false; | |
} | |
var indexOfExistingProduct = products.IndexOf(existingProduct); | |
var archivedProduct = new Product | |
{ | |
Id = existingProduct.Id, | |
Title = existingProduct.Title, | |
Count = existingProduct.Count, | |
IsDisabled = true | |
}; | |
products.RemoveAt(indexOfExistingProduct); | |
products.Insert(indexOfExistingProduct, archivedProduct); | |
return true; | |
} | |
catch (Exception e) | |
{ | |
throw new Exception(e.Message); | |
} | |
} | |
} | |
} |
I then register the interface and service to the Dependency Injection container in Startup.cs so it can be injected into the controller via its constructor
using Babafunke.DataAccessDemo.Models; | |
using Babafunke.DataAccessDemo.Services; | |
using BabaFunke.DataAccess; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
namespace Babafunke.DataAccessDemo | |
{ | |
public class Startup | |
{ | |
public Startup(IConfiguration configuration) | |
{ | |
Configuration = configuration; | |
} | |
public IConfiguration Configuration { get; } | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddControllers(); | |
services.AddScoped<IRepository<Product>, ProductService>(); | |
} | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseHttpsRedirection(); | |
app.UseRouting(); | |
app.UseAuthorization(); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapControllers(); | |
}); | |
} | |
} | |
} |
Note that if you have multiple services to register, you would still use the same IRepository<T> and simply replace the T with the model.
Lastly, add the Api controller and implement the calls for the different endpoints.
using Babafunke.DataAccessDemo.Models; | |
using BabaFunke.DataAccess; | |
using Microsoft.AspNetCore.Mvc; | |
using System.Threading.Tasks; | |
namespace Babafunke.DataAccessDemo.Controllers | |
{ | |
[ApiController] | |
public class ProductController : ControllerBase | |
{ | |
private readonly IRepository<Product> _productService; | |
public ProductController(IRepository<Product> productService) | |
{ | |
_productService = productService; | |
} | |
[HttpGet("product")] | |
public async Task<IActionResult> GetAllProducts() | |
{ | |
var products = await _productService.GetAllItems(); | |
return Ok(products); | |
} | |
[HttpGet("product/{id}")] | |
public async Task<IActionResult> GetProduct(int id) | |
{ | |
var product = await _productService.GetItemById(id); | |
if (product == null) | |
{ | |
return NotFound($"Product with Id {id} not found!"); | |
} | |
return Ok(product); | |
} | |
[HttpPost("product")] | |
public async Task<IActionResult> PostProduct([FromBody] Product product) | |
{ | |
if (await _productService.GetItemById(product.Id) != null) | |
{ | |
return BadRequest($"A product with Id {product.Id} already consists!"); | |
} | |
var response = await _productService.CreateItem(product); | |
return Ok(new {Message = "Successfully added!", Product = response}); | |
} | |
[HttpPut("product/{id}")] | |
public async Task<IActionResult> PutProduct(int id, [FromBody] Product product) | |
{ | |
if (id != product.Id) | |
{ | |
return BadRequest("Ensure the Url Id and Json Body Id are the same!"); | |
} | |
var response = await _productService.EditItem(product); | |
if (response == null) | |
{ | |
return NotFound($"The product with Id {id} does not exist!"); | |
} | |
return Ok(response); | |
} | |
[HttpPatch("product/{id}")] | |
public async Task<IActionResult> PatchProduct(int id) | |
{ | |
var response = await _productService.ArchiveItem(id); | |
if (!response) | |
{ | |
return NotFound("The product to archive does not exist!"); | |
} | |
return NoContent(); | |
} | |
[HttpDelete("product/{id}")] | |
public async Task<IActionResult> DeleteProduct(int id) | |
{ | |
var response = await _productService.DeleteItem(id); | |
if (!response) | |
{ | |
return NotFound(false); | |
} | |
return NoContent(); | |
} | |
} | |
} |
That’s it! Feel free to modify the code to suit your purpose. And if you need any help, feel free to contact me.