Skip to content

Commit

Permalink
Merge pull request #26 from evgomes/net-7-update
Browse files Browse the repository at this point in the history
.NET version update and code refactoring
  • Loading branch information
evgomes authored Jul 5, 2023
2 parents 8d71835 + 85f0e01 commit bd1fa83
Show file tree
Hide file tree
Showing 43 changed files with 521 additions and 590 deletions.
47 changes: 36 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,47 @@
[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

Simple RESTful API built with ASP.NET 5 to show how to create RESTful services using a decoupled, maintainable architecture.
Simple RESTful API built with ASP.NET Core 7 to show how to create RESTful services using a decoupled, maintainable architecture.

## A message from the author (July 5, 2023)

Hello, guys. It's been a while since I wrote the first version of this API and published the article on Medium and freeCodeCamp. My intention when I first wrote the API was to share knowledge on how to build applications in ASP.NET Core using patterns that I applied in my daily job and that I learned from many teaching sources.

I am not a professional writer, and I didn't have prior experience writing technical stuff, so I was happy to see the acceptance that my article received after its publication. I received lots of feedback from developers thanking me for the article and the knowledge that I shared, people asking me for guidance on how to develop features, people requesting articles about other programming concepts and technologies, and critiques from developers more experienced than me on things that I should improve.

Since then, I have been working on other interesting projects and concepts, and I try to share new things that I learn here on GitHub as frequently as I can. However, I haven't written any other articles due to personal life reasons and my work schedule.

I'm planning to start a tech blog soon. The acceptance I received from this article, the interactions here on GitHub, and people contacting me requesting more articles and asking for help with coding, in general, have motivated me to do it. I can't give you an exact date for this to happen, but I can tell you that the blog is already in development. For now, if you have any suggestions on topics you would like to read, both on .NET stuff or other technologies, please send a message to me, and I will consider your request. You can find my email in my GitHub profile bio.

For now, I'm updating this API and other projects I shared on GitHub to match the most recent versions of C# and .NET. If you want a reference to build full-stack .NET applications using a better architectural approach, and that uses modern tools and frameworks such as Docker and Blazor, please [refer to this other repository that I created](https://github.com/evgomes/net-core-notes).

Please let me know if you have any questions or suggestions regarding this project or any others that I have published. I'm always glad to help and to learn from you.

## Changes list

Some changes were made to the code presented at the tutorial published on [Medium](https://medium.com/free-code-camp/an-awesome-guide-on-how-to-build-restful-apis-with-asp-net-core-87b818123e28) and [freeCodeCamp](https://www.freecodecamp.org/news/an-awesome-guide-on-how-to-build-restful-apis-with-asp-net-core-87b818123e28/), to make the API code cleaner and to add functionalities that developers may find useful.
Many changes were made to the code presented at the tutorial published on [Medium](https://medium.com/free-code-camp/an-awesome-guide-on-how-to-build-restful-apis-with-asp-net-core-87b818123e28) and [freeCodeCamp](https://www.freecodecamp.org/news/an-awesome-guide-on-how-to-build-restful-apis-with-asp-net-core-87b818123e28/), to make the API code cleaner and to add functionalities that developers may find useful.

If you want to download the original code showed on the tutorial, download the [1.0.0](https://github.com/evgomes/supermarket-api/releases/tag/1.0.0) tag.

- 2.0.0 *[July 5, 2023]*
- Updated .NET version to .NET 7.
- Updated AutoMapper, Entity Framework Core, and Swashbuckle dependencies to match .NET 7.
- Enabled implicit usings and nullable types.
- Added global usings and removed implicit namespaces from the source code.
- Renamed the `UnitOfMeasurement` enum type to make it follow the official naming convention.
- Removed `CategoryResponse` and `ProductReponse` types to use a generic `Response<T>` record type instead.
- Changed API resources to use record types instead of classes, and to initialize values in an immutable way using `init`.
- Added configuration to make all API routes lower-case.
- Refactored services to include logging using the standar .NET logging provider and to make code cleaner.

- 1.4.0 *[November 26, 2021]*
- Updated .NET version to .NET 5 (see [#11](https://github.com/evgomes/supermarket-api/pull/11))
- Updated AutoMapper, Entity Framework Core, and Swashbuckle dependencies to match .NET 5.
- Created `BaseApiController` class to standardize routes and to automatically apply data annotations validation by using the `ApiController` attribute.
- Refactored logic to seed database data and to apply entity type configuration for application models.

- 1.3.0 *[December 15, 2019]*
- Updated ASP.NET Core version to 3.1, fixed issues related to InMemoryProvider, updated Swagger (see [#5](https://github.com/evgomes/supermarket-api/pull/5));
- Updated ASP.NET Core version to 3.1, fixed issues related to InMemoryProvider, updated Swagger (see [#5](https://github.com/evgomes/supermarket-api/pull/5)).
- Fixed paging calculation mistake, updated descriptions, updated "launchSettings.json" to open Swagger on running the application.

- 1.2.1 *[August 11, 2019]*
Expand All @@ -29,25 +54,25 @@ If you want to download the original code showed on the tutorial, download the [

- 1.1.0 *[June 18, 2019]*

- Added Swagger documentation through [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle);
- Added cache through native [IMemoryCache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2);
- Changed products listing to allow filtering by category ID, to show how to perform specific queries with EF Core;
- Added Swagger documentation through [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle).
- Added cache through native [IMemoryCache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2).
- Changed products listing to allow filtering by category ID, to show how to perform specific queries with EF Core.
- Changed ModelState validation to use *ApiController* attribute and *InvalidResponseFactory* in *Startup*.

- 1.0.0 *[February 4, 2019]*

- First version of the example API, presented in the tutorial on [Medium](https://medium.com/free-code-camp/an-awesome-guide-on-how-to-build-restful-apis-with-asp-net-core-87b818123e28) and [freeCodeCamp](https://www.freecodecamp.org/news/an-awesome-guide-on-how-to-build-restful-apis-with-asp-net-core-87b818123e28/).

## Frameworks and Libraries
- [ASP.NET 5](https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-5.0);
- [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) (for data access);
- [Entity Framework In-Memory Provider](https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory) (for testing purposes);
- [AutoMapper](https://automapper.org/) (for mapping resources and models);
- [ASP.NET Core 7](https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-7.0).
- [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) (for data access).
- [Entity Framework In-Memory Provider](https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory) (for testing purposes).
- [AutoMapper](https://automapper.org/) (for mapping resources and models).
- [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle) (API documentation).

## How to Test

First, install [.NET 5](https://dotnet.microsoft.com/download/dotnet/5.0). Then, open the terminal or command prompt at the API root path (```/src/Supermarket.API/```) and run the following commands, in sequence:
First, download and install the [.NET Core SDK](https://dotnet.microsoft.com/en-us/download). Then, open the terminal or command prompt at the API root path (```/src/Supermarket.API/```) and run the following commands, in sequence:

```
dotnet restore
Expand Down
1 change: 0 additions & 1 deletion src/Supermarket.API/Controllers/BaseApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ namespace Supermarket.API.Controllers
[ApiController]
public class BaseApiController : ControllerBase
{

}
}
161 changes: 78 additions & 83 deletions src/Supermarket.API/Controllers/CategoriesController.cs
Original file line number Diff line number Diff line change
@@ -1,102 +1,97 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;
using Supermarket.API.Resources;

namespace Supermarket.API.Controllers
{
public class CategoriesController : BaseApiController
{
private readonly ICategoryService _categoryService;
private readonly IMapper _mapper;
public class CategoriesController : BaseApiController
{
private readonly ICategoryService _categoryService;
private readonly IMapper _mapper;

public CategoriesController(ICategoryService categoryService, IMapper mapper)
{
_categoryService = categoryService;
_mapper = mapper;
}
public CategoriesController(ICategoryService categoryService, IMapper mapper)
{
_categoryService = categoryService;
_mapper = mapper;
}

/// <summary>
/// Lists all categories.
/// </summary>
/// <returns>List os categories.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<CategoryResource>), 200)]
public async Task<IEnumerable<CategoryResource>> ListAsync()
{
var categories = await _categoryService.ListAsync();
var resources = _mapper.Map<IEnumerable<Category>, IEnumerable<CategoryResource>>(categories);
/// <summary>
/// Lists all categories.
/// </summary>
/// <returns>List os categories.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<CategoryResource>), 200)]
public async Task<IEnumerable<CategoryResource>> ListAsync()
{
var categories = await _categoryService.ListAsync();
return _mapper.Map<IEnumerable<CategoryResource>>(categories);
}

return resources;
}
/// <summary>
/// Saves a new category.
/// </summary>
/// <param name="resource">Category data.</param>
/// <returns>Response for the request.</returns>
[HttpPost]
[ProducesResponseType(typeof(CategoryResource), 201)]
[ProducesResponseType(typeof(ErrorResource), 400)]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
var category = _mapper.Map<Category>(resource);
var result = await _categoryService.SaveAsync(category);

/// <summary>
/// Saves a new category.
/// </summary>
/// <param name="resource">Category data.</param>
/// <returns>Response for the request.</returns>
[HttpPost]
[ProducesResponseType(typeof(CategoryResource), 201)]
[ProducesResponseType(typeof(ErrorResource), 400)]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
var category = _mapper.Map<SaveCategoryResource, Category>(resource);
var result = await _categoryService.SaveAsync(category);
if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message!));
}

if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message));
}
var categoryResource = _mapper.Map<CategoryResource>(result.Resource!);
return Ok(categoryResource);
}

var categoryResource = _mapper.Map<Category, CategoryResource>(result.Resource);
return Ok(categoryResource);
}
/// <summary>
/// Updates an existing category according to an identifier.
/// </summary>
/// <param name="id">Category identifier.</param>
/// <param name="resource">Updated category data.</param>
/// <returns>Response for the request.</returns>
[HttpPut("{id}")]
[ProducesResponseType(typeof(CategoryResource), 200)]
[ProducesResponseType(typeof(ErrorResource), 400)]
public async Task<IActionResult> PutAsync(int id, [FromBody] SaveCategoryResource resource)
{
var category = _mapper.Map<Category>(resource);
var result = await _categoryService.UpdateAsync(id, category);

/// <summary>
/// Updates an existing category according to an identifier.
/// </summary>
/// <param name="id">Category identifier.</param>
/// <param name="resource">Updated category data.</param>
/// <returns>Response for the request.</returns>
[HttpPut("{id}")]
[ProducesResponseType(typeof(CategoryResource), 200)]
[ProducesResponseType(typeof(ErrorResource), 400)]
public async Task<IActionResult> PutAsync(int id, [FromBody] SaveCategoryResource resource)
{
var category = _mapper.Map<SaveCategoryResource, Category>(resource);
var result = await _categoryService.UpdateAsync(id, category);
if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message!));
}

if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message));
}
var categoryResource = _mapper.Map<CategoryResource>(result.Resource!);
return Ok(categoryResource);
}

var categoryResource = _mapper.Map<Category, CategoryResource>(result.Resource);
return Ok(categoryResource);
}
/// <summary>
/// Deletes a given category according to an identifier.
/// </summary>
/// <param name="id">Category identifier.</param>
/// <returns>Response for the request.</returns>
[HttpDelete("{id}")]
[ProducesResponseType(typeof(CategoryResource), 200)]
[ProducesResponseType(typeof(ErrorResource), 400)]
public async Task<IActionResult> DeleteAsync(int id)
{
var result = await _categoryService.DeleteAsync(id);

/// <summary>
/// Deletes a given category according to an identifier.
/// </summary>
/// <param name="id">Category identifier.</param>
/// <returns>Response for the request.</returns>
[HttpDelete("{id}")]
[ProducesResponseType(typeof(CategoryResource), 200)]
[ProducesResponseType(typeof(ErrorResource), 400)]
public async Task<IActionResult> DeleteAsync(int id)
{
var result = await _categoryService.DeleteAsync(id);
if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message!));
}

if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message));
}

var categoryResource = _mapper.Map<Category, CategoryResource>(result.Resource);
return Ok(categoryResource);
}
}
var categoryResource = _mapper.Map<CategoryResource>(result.Resource!);
return Ok(categoryResource);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Extensions;
using Supermarket.API.Resources;
Expand Down
26 changes: 11 additions & 15 deletions src/Supermarket.API/Controllers/ProductsController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Models.Queries;
using Supermarket.API.Domain.Services;
using Supermarket.API.Resources;

Expand All @@ -20,18 +17,17 @@ public ProductsController(IProductService productService, IMapper mapper)
}

/// <summary>
/// Lists all existing products.
/// Lists all existing products according to query filters.
/// </summary>
/// <returns>List of products.</returns>
[HttpGet]
[ProducesResponseType(typeof(QueryResultResource<ProductResource>), 200)]
public async Task<QueryResultResource<ProductResource>> ListAsync([FromQuery] ProductsQueryResource query)
{
var productsQuery = _mapper.Map<ProductsQueryResource, ProductsQuery>(query);
var productsQuery = _mapper.Map<ProductsQuery>(query);
var queryResult = await _productService.ListAsync(productsQuery);

var resource = _mapper.Map<QueryResult<Product>, QueryResultResource<ProductResource>>(queryResult);
return resource;
return _mapper.Map<QueryResultResource<ProductResource>>(queryResult);
}

/// <summary>
Expand All @@ -44,15 +40,15 @@ public async Task<QueryResultResource<ProductResource>> ListAsync([FromQuery] Pr
[ProducesResponseType(typeof(ErrorResource), 400)]
public async Task<IActionResult> PostAsync([FromBody] SaveProductResource resource)
{
var product = _mapper.Map<SaveProductResource, Product>(resource);
var product = _mapper.Map<Product>(resource);
var result = await _productService.SaveAsync(product);

if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message));
return BadRequest(new ErrorResource(result.Message!));
}

var productResource = _mapper.Map<Product, ProductResource>(result.Resource);
var productResource = _mapper.Map<ProductResource>(result.Resource!);
return Ok(productResource);
}

Expand All @@ -67,15 +63,15 @@ public async Task<IActionResult> PostAsync([FromBody] SaveProductResource resour
[ProducesResponseType(typeof(ErrorResource), 400)]
public async Task<IActionResult> PutAsync(int id, [FromBody] SaveProductResource resource)
{
var product = _mapper.Map<SaveProductResource, Product>(resource);
var product = _mapper.Map<Product>(resource);
var result = await _productService.UpdateAsync(id, product);

if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message));
return BadRequest(new ErrorResource(result.Message!));
}

var productResource = _mapper.Map<Product, ProductResource>(result.Resource);
var productResource = _mapper.Map<ProductResource>(result.Resource!);
return Ok(productResource);
}

Expand All @@ -93,10 +89,10 @@ public async Task<IActionResult> DeleteAsync(int id)

if (!result.Success)
{
return BadRequest(new ErrorResource(result.Message));
return BadRequest(new ErrorResource(result.Message!));
}

var categoryResource = _mapper.Map<Product, ProductResource>(result.Resource);
var categoryResource = _mapper.Map<ProductResource>(result.Resource!);
return Ok(categoryResource);
}
}
Expand Down
14 changes: 6 additions & 8 deletions src/Supermarket.API/Domain/Models/Category.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using System.Collections.Generic;

namespace Supermarket.API.Domain.Models
{
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Product> Products { get; set; } = new List<Product>();
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public List<Product> Products { get; set; } = new();
}
}
Loading

0 comments on commit bd1fa83

Please sign in to comment.