Address Module in ECommerce Application

How to Design and Develop Address Module in ECommerce Application

In this article, I will discuss How to Design and Develop the Address Module in our ECommerce Application using ASP.NET Core Web API and Entity Framework Core. Please read our previous article discussing the Customer Module in our ECommerce Application.

The Address Module is designed to handle all operations related to managing customer addresses within the E-Commerce platform. It allows customers to add, view, update, and delete their billing and shipping addresses, ensuring their delivery and billing information is accurate and up-to-date. The module enhances the overall user experience and streamlines the checkout process by providing a structured and secure way to manage addresses.

Key Features of the Address Module

The following are the Key Features of the Address Module that we will implement in our Ecommerce Application:

  • Create Address: Customers can add new billing or shipping addresses to their profiles. Ensures that all necessary address details are provided and validated.
  • Retrieve Address by ID: Allows retrieval of specific address details using a unique identifier. Useful for viewing or editing a particular address.
  • Update Address: Permits customers to modify existing address information. Ensures that updates are accurately reflected in the database.
  • Delete Address: Facilitates the removal of an address from a customer’s profile. Ensures that only authorized deletions occur.
  • Retrieve All Addresses for a Customer: Provides a list of all addresses associated with a specific customer. Assists in selecting addresses during the checkout process or profile management.
Creating DTOs for Address Module

DTOs play a pivotal role in structuring and validating the data exchanged between the client and server. Now, we will create the Required Request and Response DTOs for managing the Customer Addresses. First, create a folder named AddressesDTOs within the DTOs folder, where we will create all our DTOs related to Customer Addresses.

AddressCreateDTO

Create a class file named AddressCreateDTO.cs within the DTOs\AddressesDTOs folder, and then copy and paste the following code. The AddressCreateDTO Encapsulates the data needed to create a customer’s address. It is used when a customer adds a new address to their profile.

using System.ComponentModel.DataAnnotations;

namespace ECommerceApp.DTOs.AddressesDTOs
{
    // DTO for customer address
    public class AddressCreateDTO
    {
        [Required(ErrorMessage = "CustomerId is required.")]
        public int CustomerId { get; set; }

        [Required(ErrorMessage = "Address Line 1 is required.")]
        [StringLength(100, ErrorMessage = "Address Line 1 cannot exceed 100 characters.")]
        public string AddressLine1 { get; set; }

        [Required(ErrorMessage = "Address Line 2 is required.")]
        [StringLength(100, ErrorMessage = "Address Line 2 cannot exceed 100 characters.")]
        public string AddressLine2 { get; set; }

        [Required(ErrorMessage = "City is required.")]
        [StringLength(50, ErrorMessage = "City cannot exceed 50 characters.")]
        public string City { get; set; }

        [Required(ErrorMessage = "State is required.")]
        [StringLength(50, ErrorMessage = "State cannot exceed 50 characters.")]
        public string State { get; set; }

        [Required(ErrorMessage = "Postal Code is required.")]
        [RegularExpression(@"^\d{4,6}$", ErrorMessage = "Invalid Postal Code.")]
        public string PostalCode { get; set; }

        [Required(ErrorMessage = "Country is required.")]
        [StringLength(50, ErrorMessage = "Country cannot exceed 50 characters.")]
        public string Country { get; set; }
    }
}
AddressResponseDTO

Create a class file named AddressResponseDTO.cs within the DTOs\AddressesDTOs folder, and then copy and paste the following code. The AddressResponseDTO Structures the address information to be sent back to the client, ensuring only necessary details are exposed. It is used in API responses when retrieving address details associated with a customer.

namespace ECommerceApp.DTOs.AddressesDTOs
{
    // DTO for returning address details.
    public class AddressResponseDTO
    {
        public int Id { get; set; }
        public int CustomerId { get; set; }
        public string AddressLine1 { get; set; }
        public string AddressLine2 { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
    }
}
AddressUpdateDTO

Create a class file named AddressUpdateDTO.cs within the DTOs\AddressesDTOs folder, then copy and paste the following code. The AddressUpdateDTO Encapsulates the data needed to update a customer’s address. It is used when a customer modifies an existing one in their profile.

using System.ComponentModel.DataAnnotations;

namespace ECommerceApp.DTOs.AddressesDTOs
{
    public class AddressUpdateDTO
    {
        [Required(ErrorMessage = "AddressId is required.")]
        public int AddressId { get; set; }

        [Required(ErrorMessage = "CustomerId is required.")]
        public int CustomerId { get; set; }

        [Required(ErrorMessage = "Address Line 1 is required.")]
        [StringLength(100, ErrorMessage = "Address Line 1 cannot exceed 100 characters.")]
        public string AddressLine1 { get; set; }

        [Required(ErrorMessage = "Address Line 2 is required.")]

        [StringLength(100, ErrorMessage = "Address Line 2 cannot exceed 100 characters.")]
        public string AddressLine2 { get; set; }

        [Required(ErrorMessage = "City is required.")]
        [StringLength(50, ErrorMessage = "City cannot exceed 50 characters.")]
        public string City { get; set; }

        [Required(ErrorMessage = "State is required.")]
        [StringLength(50, ErrorMessage = "State cannot exceed 50 characters.")]
        public string State { get; set; }

        [Required(ErrorMessage = "Postal Code is required.")]
        [RegularExpression(@"^\d{4,6}$", ErrorMessage = "Invalid Postal Code.")]
        public string PostalCode { get; set; }

        [Required(ErrorMessage = "Country is required.")]
        [StringLength(50, ErrorMessage = "Country cannot exceed 50 characters.")]
        public string Country { get; set; }
    }
}
AddressDeleteDTO

Create a class file named AddressDeleteDTO.cs within the DTOs\AddressesDTOs folder, then copy and paste the following code. The AddressDeleteDTO Specifies the necessary identifiers to remove a particular address from a customer’s profile. It is used when a customer requests to delete one of their saved addresses.

using System.ComponentModel.DataAnnotations;

namespace ECommerceApp.DTOs.AddressesDTOs
{
    public class AddressDeleteDTO
    {
        [Required(ErrorMessage = "CustomerId is Required")]
        public int CustomerId { get; set; }

        [Required(ErrorMessage = "AddressId is Required")]
        public int AddressId { get; set; }
    }
}
Address Service Class

Create a class file named AddressService.cs within the Services folder, then copy and paste the following code. The AddressService class is responsible for managing all business logic and data access operations related to customer addresses. By handling address-specific functionalities within the service class, it ensures that the controller remains focused on handling HTTP interactions, thereby maintaining a clear and manageable codebase.

using Microsoft.EntityFrameworkCore;
using ECommerceApp.Data;
using ECommerceApp.Models;
using ECommerceApp.DTOs;
using ECommerceApp.DTOs.AddressesDTOs;

namespace ECommerceApp.Services
{
    public class AddressService
    {
        private readonly ApplicationDbContext _context;

        public AddressService(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<ApiResponse<AddressResponseDTO>> CreateAddressAsync(AddressCreateDTO addressDto)
        {
            try
            {
                // Check if customer exists
                var customer = await _context.Customers.FindAsync(addressDto.CustomerId);
                if (customer == null)
                {
                    return new ApiResponse<AddressResponseDTO>(404, "Customer not found.");
                }

                // Manual mapping from DTO to Model
                var address = new Address
                {
                    CustomerId = addressDto.CustomerId,
                    AddressLine1 = addressDto.AddressLine1,
                    AddressLine2 = addressDto.AddressLine2,
                    City = addressDto.City,
                    State = addressDto.State,
                    PostalCode = addressDto.PostalCode,
                    Country = addressDto.Country
                };

                // Add address to the database
                _context.Addresses.Add(address);
                await _context.SaveChangesAsync();

                // Map to AddressResponseDTO
                var addressResponse = new AddressResponseDTO
                {
                    Id = address.Id,
                    CustomerId = address.CustomerId,
                    AddressLine1 = address.AddressLine1,
                    AddressLine2 = address.AddressLine2,
                    City = address.City,
                    State = address.State,
                    PostalCode = address.PostalCode,
                    Country = address.Country
                };

                return new ApiResponse<AddressResponseDTO>(200, addressResponse);
            }
            catch (Exception ex)
            {
                // Log the exception
                return new ApiResponse<AddressResponseDTO>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}");
            }
        }

        public async Task<ApiResponse<AddressResponseDTO>> GetAddressByIdAsync(int id)
        {
            try
            {
                var address = await _context.Addresses.AsNoTracking().FirstOrDefaultAsync(add => add.Id == id);

                if (address == null)
                {
                    return new ApiResponse<AddressResponseDTO>(404, "Address not found.");
                }

                // Map to AddressResponseDTO
                var addressResponse = new AddressResponseDTO
                {
                    Id = address.Id,
                    CustomerId = address.CustomerId,
                    AddressLine1 = address.AddressLine1,
                    AddressLine2 = address.AddressLine2,
                    City = address.City,
                    State = address.State,
                    PostalCode = address.PostalCode,
                    Country = address.Country
                };

                return new ApiResponse<AddressResponseDTO>(200, addressResponse);
            }
            catch (Exception ex)
            {
                // Log the exception
                return new ApiResponse<AddressResponseDTO>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}");
            }
        }

        public async Task<ApiResponse<ConfirmationResponseDTO>> UpdateAddressAsync(AddressUpdateDTO addressDto)
        {
            try
            {
                var address = await _context.Addresses
                    .FirstOrDefaultAsync(add => add.Id == addressDto.AddressId && add.CustomerId == addressDto.CustomerId);

                if (address == null)
                {
                    return new ApiResponse<ConfirmationResponseDTO>(404, "Address not found.");
                }

                // Update address properties
                address.AddressLine1 = addressDto.AddressLine1;
                address.AddressLine2 = addressDto.AddressLine2;
                address.City = addressDto.City;
                address.State = addressDto.State;
                address.PostalCode = addressDto.PostalCode;
                address.Country = addressDto.Country;

                await _context.SaveChangesAsync();

                // Prepare confirmation message
                var confirmationMessage = new ConfirmationResponseDTO
                {
                    Message = $"Address with Id {addressDto.AddressId} updated successfully."
                };

                return new ApiResponse<ConfirmationResponseDTO>(200, confirmationMessage);
            }
            catch (Exception ex)
            {
                // Log the exception
                return new ApiResponse<ConfirmationResponseDTO>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}");
            }
        }

        public async Task<ApiResponse<ConfirmationResponseDTO>> DeleteAddressAsync(AddressDeleteDTO addressDeleteDTO)
        {
            try
            {
                var address = await _context.Addresses
                    .FirstOrDefaultAsync(add => add.Id == addressDeleteDTO.AddressId && add.CustomerId == addressDeleteDTO.CustomerId);

                if (address == null)
                {
                    return new ApiResponse<ConfirmationResponseDTO>(404, "Address not found.");
                }

                _context.Addresses.Remove(address);
                await _context.SaveChangesAsync();

                // Prepare confirmation message
                var confirmationMessage = new ConfirmationResponseDTO
                {
                    Message = $"Address with Id {addressDeleteDTO.AddressId} deleted successfully."
                };

                return new ApiResponse<ConfirmationResponseDTO>(200, confirmationMessage);
            }
            catch (Exception ex)
            {
                // Log the exception
                return new ApiResponse<ConfirmationResponseDTO>(500, "An unexpected error occurred while processing your request.");
            }
        }

        public async Task<ApiResponse<List<AddressResponseDTO>>> GetAddressesByCustomerAsync(int customerId)
        {
            try
            {
                var customer = await _context.Customers
                    .AsNoTracking()
                    .Include(c => c.Addresses)
                    .FirstOrDefaultAsync(c => c.Id == customerId);

                if (customer == null)
                {
                    return new ApiResponse<List<AddressResponseDTO>>(404, "Customer not found.");
                }

                var addresses = customer.Addresses.Select(a => new AddressResponseDTO
                {
                    Id = a.Id,
                    CustomerId = a.CustomerId,
                    AddressLine1 = a.AddressLine1,
                    AddressLine2 = a.AddressLine2,
                    City = a.City,
                    State = a.State,
                    PostalCode = a.PostalCode,
                    Country = a.Country
                }).ToList();

                return new ApiResponse<List<AddressResponseDTO>>(200, addresses);
            }
            catch (Exception ex)
            {
                // Log the exception
                return new ApiResponse<List<AddressResponseDTO>>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}");
            }
        }
    }
}
Registering the AddressService:

Please modify the Program class as follows to register the AddressService.

using ECommerceApp.Data;
using ECommerceApp.Services;
using Microsoft.EntityFrameworkCore;

namespace ECommerceApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // This will use the property names as defined in the C# model
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // Configure EF Core with SQL Server
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection")));

            // Registering the CustomerService 
            builder.Services.AddScoped<CustomerService>();

            // Registering the AddressService
            builder.Services.AddScoped<AddressService>();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Creating Addresses Controller

The AddressesController will use the AddressService to handle customer address-related operations. This results in a cleaner controller that delegates business logic to the appropriate services. Create a new API Empty Controller named AddressesController within the Controllers folder and then copy and paste the following code.

using ECommerceApp.DTOs;
using ECommerceApp.Services;
using Microsoft.AspNetCore.Mvc;
using ECommerceApp.DTOs.AddressesDTOs;

namespace ECommerceApp.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AddressesController : ControllerBase
    {
        private readonly AddressService _addressService;

        // Injecting the services
        public AddressesController(AddressService addressService)
        {
            _addressService = addressService;
        }

        // Creates a new address for a customer.
        [HttpPost("CreateAddress")]
        public async Task<ActionResult<ApiResponse<AddressResponseDTO>>> CreateAddress([FromBody] AddressCreateDTO addressDto)
        {
            var response = await _addressService.CreateAddressAsync(addressDto);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // Retrieves an address by ID.
        [HttpGet("GetAddressById/{id}")]
        public async Task<ActionResult<ApiResponse<AddressResponseDTO>>> GetAddressById(int id)
        {
            var response = await _addressService.GetAddressByIdAsync(id);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // Updates an existing address.
        [HttpPut("UpdateAddress")]
        public async Task<ActionResult<ApiResponse<ConfirmationResponseDTO>>> UpdateAddress([FromBody] AddressUpdateDTO addressDto)
        {
            var response = await _addressService.UpdateAddressAsync(addressDto);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // Deletes an address by ID.
        [HttpDelete("DeleteAddress")]
        public async Task<ActionResult<ApiResponse<ConfirmationResponseDTO>>> DeleteAddress([FromBody] AddressDeleteDTO addressDeleteDTO)
        {
            var response = await _addressService.DeleteAddressAsync(addressDeleteDTO);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // Retrieves all addresses for a specific customer.
        [HttpGet("GetAddressesByCustomer/{customerId}")]
        public async Task<ActionResult<ApiResponse<List<AddressResponseDTO>>>> GetAddressesByCustomer(int customerId)
        {
            var response = await _addressService.GetAddressesByCustomerAsync(customerId);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }
    }
}
Testing the Address Module Endpoints of Our ECommerce Application:
Create Address:

Adds a new address to a customer’s profile after validating the customer’s existence. It is invoked when a customer saves a new shipping or billing address. This endpoint Adds a new address to a customer’s profile.

Endpoint: POST /api/Customers/CreateAddress
URL: http://localhost:5000/api/Customers/CreateAddress
Method: POST
Headers: Content-Type: application/json
Request Body:

{
    "CustomerId": 1,
    "AddressLine1": "123 Main St",
    "AddressLine2": "Apt 4B",
    "City": "Springfield",
    "State": "Illinois",
    "PostalCode": "62704",
    "Country": "USA"
}

Response in Swagger:

How to Design and Develop the Address Module in our ECommerce Application using ASP.NET Core Web API and Entity Framework Core

Get Address by ID:

Retrieves details of a specific address using its unique identifier. It is Used when displaying or editing a particular address associated with a customer. This endpoint Retrieves details of a specific address.

Endpoint: GET /api/Customers/GetAddressById/{id}
Method: GET
URL: http://localhost:5000/api/Customers/GetAddressById/1
Response in Swagger:

How to Design and Develop the Address Module in our ECommerce Application using ASP.NET Core Web API

Update Address:

Updates the details of an existing address, ensuring it belongs to the correct customer. It is called when a customer modifies one of their saved addresses. This endpoint Updates the details of an existing address.

Endpoint: PUT /api/Customers/UpdateAddress
URL: http://localhost:5000/api/Customers/UpdateAddress
Method: PUT
Headers: Content-Type: application/json
Request Body:

{
    "AddressId":1,
    "CustomerId": 1,
    "AddressLine1": "456 Elm St",
    "AddressLine2": "",
    "City": "Shelbyville",
    "State": "Illinois",
    "PostalCode": "62565",
    "Country": "USA"
}

Response in Swager:

Address Module in our ECommerce Application using ASP.NET Core Web API

Delete Address:

Deletes a specific address from a customer’s profile based on provided identifiers. It is used when a customer removes an address from their account. This endpoint deletes a specific address from a customer’s profile.

Endpoint: DELETE /api/Customers/DeleteAddress
Method: DELETE
URL: http://localhost:5000/api/Customers/DeleteAddress
Headers: Content-Type: application/json
Request Body:

{
    "CustomerId": 1,
    "AddressId": 1
}

Note: We are not going to delete the address.

Get Addresses by Customer:

Retrieves all addresses associated with a particular customer. It is used when a customer views all their saved addresses for selection during checkout or profile management.

Endpoint: GET /api/Customers/GetAddressesByCustomer/{customerId}
Method: GET
URL: http://localhost:5000/api/Customers/GetAddressesByCustomer/1
Response in Swagger:

Address Module in our ECommerce Application

So, we have completed the Address Module Implementation in our Ecommerce Application using ASP.NET Core Web API and Entity Framework Core. In the next article, I will discuss How to Implement the Category Module of our ECommerce Application for Product Inventory Management. In this article, we discussed How to Design and Develop the Address Module for our ECommerce Application using ASP.NET Core Web API and EF Core

Leave a Reply

Your email address will not be published. Required fields are marked *