ASP.NET Core Magic: Crafting APIs the Easy Way

Zuda Pradana Putra
7 min readFeb 2, 2024

Utilizing REST API with NetCore8 and SQL Server is an efficient solution in building application interfaces. Its simple and stateless advantages make it easy to implement, while high performance ensures fast and efficient data exchange. By integrating these technologies, developers can build modern applications with responsive interfaces and optimal scalability. In this article I will try to create a simple API on the topic of restaurants using layered architecture.

ERD Schema

1. The first step is to install the necessary packages

to perform the migration and generate the database schema according to the Entity Relationship Diagram (ERD). Make sure you have installed the Entity Framework Core (efcore) tools, SQL Server, and Newtonsoft.Json via the NuGet package manager (right click project -> manage nu package).

Make sure SQL Server is active, and create a new connection by pressing Ctrl+Alt+S, then add connection. Enter the Server Name according to your SQL Server configuration. After successfully connecting, you can create a new database or use an existing database on the Server. Make sure to record the datasource name using microsoftsqlclient.

You can setup the db server address configuration in appsetting.json:

"ConnectionStrings": {
"Connection": "server=your_servername;Database=your_db;TrustServerCertificate=True;Trusted_Connection=True"
}

The last step applies the string connection address to program.cs:

builder.Services.AddDbContext<AppDbContext>(options => {
options.UseSqlServer(builder.Configuration.GetConnectionString("Connection"));
}

2. create a DbContext class and Entity Model

  • Create a Model:
    Create a class-model to represent the tables or entities in the database. Here’s a simple example:
    public class Customer
{
public int Id { get; set; }

public string Name { get; set; }

public string Email { get; set; }

public string PhoneNumber { get; set; }
}


public class Transaction
{
public int Id { get; set; }

public int CustomerId { get; set; }

public DateTime TransactionDate { get; set; }

public int Quantity { get; set; }

public float AmountTotal { get; set; }

public string PaymentMethod { get; set; }

public float TotalPrice { get; set; }

public float TotalChange { get; set; }

public List<TransactionItems> Items { get; set; }

[ForeignKey("CustomerId")]
public Customer Customer { get; set; }
}
  • Create a DbContext Class:
    Create a class that inherits DbContext from EF Core. Make sure you define the entities that represent the tables in the database.
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options)
{
}

public DbSet<Food> Foods { get; set; }

public DbSet<Customer> Customers { get; set; }

public DbSet<Transaction> Transactions { get; set; }

public DbSet<TransactionItems> TransactionItems { get; set; }

//Relation Data
protected override void OnModelCreating(ModelBuilder modelBuilder)
{

modelBuilder.Entity<Transaction>()
.HasOne(t => t.Customer)
.WithMany() //
.HasForeignKey(t => t.CustomerId);

modelBuilder.Entity<TransactionItems>()
.HasOne(ti => ti.Food)
.WithMany()
.HasForeignKey(ti => ti.FoodId);

modelBuilder.Entity<TransactionItems>()
.HasOne(ti => ti.Transaction)
.WithMany(t => t.Items)
.HasForeignKey(ti => ti.TransactionId);
}
}

Before I didn’t have a database yet, in this class I also defined the relations of each table. In the OnModelCreating method, you define the relationships between entities (relations) or other database schemas. In this example:

  1. The relationship between Transaction and Customer is defined using HasOne and WithMany. One transaction can only belong to one customer, but conversely, one customer can have many transactions.
  2. The relationship between TransactionItems and Food is similarly defined. One transaction item can only be associated with one food, but conversely, one food can be associated with many transaction items.
  3. The relationship between TransactionItems and Transaction is also defined. One transaction item can only belong to one transaction, but one transaction can have multiple items.

After the DbContext setup and model creation are complete, let’s migrate with the following command:

Add-Migration name_migration ##for migrate
Update Database ##for updating db after migrate

3. Creating Repository and Implementation to service

This third step involves creating a repository and implementing it into a service.

    public interface ICustomerRepo<T>
{

Task<List<T>> GetAllCustomerAsync();

Task<T> GetCustomerByIdAsync(int id);

Task<T> AddCustomerAsync(CustomerReqDTO reqDTO);

Task<bool> UpdateCustomerAsync(CustomerReqDTO reqDTO, int id);

Task<bool> DeleteCustomerAsync(int id);

}

Interface ICustomerRepo<T>: Creates an interface with methods that will be used in the repository. In this case, the methods include basic operations such as getting all customers, getting customers by ID, adding new customers, updating customers, and deleting customers. For other interfaces you can customize it further. Then let’s implement the results into a service that will process all the business logic.

public class CustomerService : ICustomerRepo<Customer>
{
private readonly AppDbContext _context;

//inject db context
public CustomerService(AppDbContext context)
{
this._context = context;
}
public async Task<List<Customer>> GetAllCustomerAsync()
{
return await _context.Customers.ToListAsync();
}

public async Task<Customer> GetCustomerByIdAsync(int id)
{
var findById = await _context.Customers.FindAsync(id);

if (findById == null)
{
throw new DirectoryNotFoundException($"Customer With ID {id} Not Found");
}

return findById;
}

public async Task<Customer> AddCustomerAsync(CustomerReqDTO reqDTO)
{
var customer = new Customer
{
Name = reqDTO.Name,
Email = reqDTO.Email,
PhoneNumber = reqDTO.PhoneNumber,
};
//add data
await _context.Customers.AddAsync(customer);
//save after change
await _context.SaveChangesAsync();

return customer;
}

public async Task<bool> DeleteCustomerAsync(int id)
{
var deleteById = await GetCustomerByIdAsync(id);

if(deleteById != null) {
//remove
_context.Customers.Remove(deleteById);
//save after change
await _context.SaveChangesAsync();
return true;
}
return false;
}


public async Task<bool> UpdateCustomerAsync(CustomerReqDTO reqDTO, int id)
{
var editById = await GetCustomerByIdAsync(id);

var customer = new Customer
{
Name = reqDTO.Name,
Email = reqDTO.Email,
PhoneNumber = reqDTO.PhoneNumber,
};

if(editById != null)
{
editById.Name = customer.Name;
editById.Email = customer.Email;
editById.PhoneNumber = customer.PhoneNumber;

await _context.SaveChangesAsync();
return true;
}

return false;
}

CustomerService (Repository Implementation): Creates an implementation of the ICustomerRepo<T> interface in the CustomerService class. The constructor accepts DbContext to interact with the database. Each method in CustomerService accesses the database using DbContext to perform the appropriate operations.

  • GetAllCustomerAsync(): Retrieves all customers from the database.
  • GetCustomerByIdAsync(int id): Retrieves customers by ID, providing handling if the customer is not found.
  • AddCustomerAsync(Customer customer): Adds a new customer to the database and saves the changes.
  • UpdateCustomerAsync(Customer customer, int id): Updates the customer information based on the ID, providing handling if the customer is not found.
  • DeleteCustomerAsync(int id): Deletes a customer based on the ID, providing handling if the customer is not found.

As such, these repositories and services provide abstraction for operations involving customer entities in the database, separating business logic from data access. For other services you can customize it again, maybe a little extra calculation is in the Transaction service. Don’t worry I will include a github link at the end for you to understand the workflow. After all services have been created, make sure you register the service in program.cs

//inject service
builder.Services.AddScoped<CustomerService>();

4. Controller

Controllers in ASP.NET Core are responsible for managing incoming HTTP requests and returning appropriate responses. This controller acts as a link between requests from clients and services that provide business functionality. In this case, the CustomerController handles CRUD (Create, Read, Update, Delete) operations for the Customer entity.

[ApiController]
[Route("api/customers")]
public class CustomerController : ControllerBase
{
private readonly CustomerService _customerService;

public CustomerController(CustomerService customerService)
{
_customerService = customerService;
}

[HttpGet]
//Task merepresentasikan operasi asinkron
//ActionResult mewakili hasil dari metode aksi dalam kontroler
public async Task<ActionResult<List<Customer>>> GetAllCustomers()
{
var customers = await _customerService.GetAllCustomerAsync();
return Ok(customers);
}

[HttpGet("{id}")]
public async Task<ActionResult<Customer>> GetCustomerById(int id)
{
try
{
var customer = await _customerService.GetCustomerByIdAsync(id);

if (customer == null)
{
return NotFound($"Customer with ID {id} not found");
}

return Ok(customer);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal Server Error: {ex.Message}");
}
}

[HttpPost]
public async Task<ActionResult> AddCustomer([FromBody] CustomerReqDTO reqDTO)
{
try
{
await _customerService.AddCustomerAsync(reqDTO);
return new ObjectResult(reqDTO) { StatusCode = 201 };
}
catch (Exception ex)
{
// Handle other exceptions if needed
return StatusCode(500, $"Internal Server Error: {ex.Message}");
}

}

[HttpPut("{id}")]
public async Task<ActionResult> UpdateCustomer(int id, [FromBody] CustomerReqDTO reqDTO)
{
try
{
await _customerService.UpdateCustomerAsync(reqDTO, id);
return new ObjectResult($"Edited ID {id} Successfully");
}
catch (Exception ex)
{
// Handle other exceptions if needed
return StatusCode(500, $"Internal Server Error: {ex.Message}");
}

}

[HttpDelete("{id}")]
public async Task<ActionResult> DeleteCustomer(int id)
{
try
{
await _customerService.DeleteCustomerAsync(id);
return Ok(new OkObjectResult($"Delete ID {id} Successfully"));
}
catch (DirectoryNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
// Handle other exceptions if needed
return StatusCode(500, $"Internal Server Error: {ex.Message}");
}
}
}

Task<ActionResult>: These methods return an ActionResult object, which provides the flexibility to generate different HTTP responses according to the circumstances. For example, Ok for a 200 OK HTTP response, NotFound for a 404 Not Found HTTP response, and so on. To run the app, simply click the play button in the menu bar or use the “dotnet run” command.

  • [HttpGet]: This attribute specifies that the GetAllCustomers method handles HTTP GET requests.
  • [HttpGet(“{id}”)]: This attribute specifies that the GetCustomerById method handles HTTP GET requests with an id parameter.
  • [HttpPost]: This attribute specifies that the AddCustomer method handles HTTP POST requests.
  • [HttpPut(“{id}”)]: This attribute specifies that the UpdateCustomer method handles HTTP PUT requests with the id parameter
  • [HttpDelete(“{id}”)]: This attribute specifies that the DeleteCustomer method handles HTTP DELETE requests with an id parameter.

In building a REST API with ASP.NET Core, we have explored the essential steps from database creation to controller development. The ease of use of the REST API provides speed and efficiency in application development, with features such as the [ApiController] and [Route] attributes simplifying the process. The adoption of the Repository and Service design patterns ensures good separation of responsibilities, while the use of the Task<ActionResult> type provides consistent HTTP responses. Bringing together technologies such as Entity Framework, SQL Server, and .NET Core provides a solid foundation for building reliable and efficient applications. Finally, we recommend practicing code organization by creating separate DTO folders for Request and Response to improve code readability and maintainability in the future. Thus, implementing REST APIs with ASP.NET Core can be executed seamlessly and effectively. If you have an InvariantGlobalization error, you can open the project name then change the value to False. You can get my github link here

--

--