European ASP.NET 4.5 Hosting BLOG

BLOG about ASP.NET 4, ASP.NET 4.5 Hosting and Its Technology - Dedicated to European Windows Hosting Customer

European ASP.NET Core 9.0 Hosting - HostForLIFE :: Types, Illustrations, and Best Practices of Table Sharding in SQL

clock June 11, 2025 09:24 by author Peter

Table Sharding in SQL
European ASP.NET Core 9.0 Hosting - HostForLIFE :: Table sharding is a database design technique used to improve the scalability and performance of large-scale applications. It involves splitting a large table into smaller, more manageable pieces called "shards," which are distributed across multiple database instances or servers. Each shard contains a subset of the data, and together they form the complete dataset.

Why Use Table Sharding?

  • Scalability: Sharding allows horizontal scaling by distributing data across multiple servers.
  • Performance: Queries are faster because they operate on smaller datasets.
  • Fault Tolerance: If one shard fails, only a portion of the data is affected.
  • Cost Efficiency: Sharding enables the use of smaller, less expensive servers instead of a single, high-performance server.

Types of Table Sharding
Range-Based Sharding

  • Data is divided based on a range of values in a specific column.
  • Example: A table storing user data can be sharded by user ID ranges (e.g., Shard 1: User IDs 1–1000, Shard 2: User IDs 1001–2000).
  • Pros: Simple to implement and query.
  • Cons: Uneven data distribution if ranges are not carefully chosen.


Hash-Based Sharding

  • A hash function is applied to a column (e.g., user ID) to determine which shard the data belongs to.
  • Example: hash(user_id) % number_of_shards determines the shard.
  • Pros: Ensures even data distribution.
  • Cons: Harder to query across shards and to add/remove shards dynamically.


Geographic Sharding

  • Data is divided based on geographic location.
  • Example: Users in North America are stored in one shard, while users in Europe are stored in another.
  • Pros: Useful for applications with geographically distributed users.
  • Cons: Can lead to uneven distribution if one region has significantly more users.

Key-Based Sharding

  • Similar to hash-based sharding, but uses a specific key (e.g., customer ID or order ID) to determine the shard.
  • Pros: Flexible and allows for custom sharding logic.
  • Cons: Requires careful planning to avoid hotspots.


Directory-Based Sharding

  • A lookup table (directory) maps each record to its corresponding shard.
  • Pros: Highly flexible and allows for dynamic shard allocation.
  • Cons: Adds complexity and requires maintaining the directory.

Examples of Table Sharding
Example 1. Range-Based Sharding
-- Shard 1: User IDs 1–1000
CREATE TABLE users_shard1 (
user_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);

-- Shard 2: User IDs 1001–2000
CREATE TABLE users_shard2 (
user_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);


Example 2. Hash-Based Sharding
-- Shard 1: Hash(user_id) % 2 = 0
CREATE TABLE users_shard1 (
user_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);

-- Shard 2: Hash(user_id) % 2 = 1
CREATE TABLE users_shard2 (
user_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);


Example 3. Geographic Sharding
-- Shard 1: North America
CREATE TABLE users_na (
user_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
region VARCHAR(50)
);

-- Shard 2: Europe
CREATE TABLE users_eu (
user_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
region VARCHAR(50)
);

Best Practices for Table Sharding

Choose the Right Sharding Key
Select a column that ensures even data distribution and minimizes cross-shard queries.
Example: User ID or Order ID.

Plan for Growth

Design shards to accommodate future data growth.
Avoid hardcoding shard ranges to allow for dynamic scaling.

Minimize Cross-Shard Queries

  • Cross-shard queries can be slow and complex. Design your application to minimize them.
  • Example: Use denormalization or caching to reduce the need for joins across shards.

Monitor and Balance Shards

  • Regularly monitor shard sizes and redistribute data if necessary to avoid hotspots.

Use Middleware or Sharding Libraries

  • Middleware tools like ProxySQL or libraries like Hibernate Shards can simplify sharding logic.

Implement Backup and Recovery

  • Ensure each shard is backed up independently and has a recovery plan.

Test for Performance

  • Test your sharding strategy under realistic workloads to identify bottlenecks.

Document Sharding Logic

  • Clearly document how data is distributed across shards to help developers and DBAs.

Challenges of Table Sharding

  • Complexity: Sharding adds complexity to database design and application logic.
  • Cross-Shard Transactions: Managing transactions across shards can be difficult.
  • Rebalancing Data: Adding or removing shards requires redistributing data, which can be time-consuming.
  • Query Optimization: Queries need to be optimized to avoid unnecessary cross-shard operations.

Conclusion
Table sharding is a powerful technique for scaling large databases, but it requires careful planning and implementation. By understanding the different types of sharding, following best practices, and addressing potential challenges, you can design a sharding strategy that meets your application's scalability and performance needs.



European ASP.NET Core 9.0 Hosting - HostForLIFE :: ASP.NET Core Clean and Reliable Code Testing with Moq using C# 13 and xUnit

clock June 9, 2025 08:34 by author Peter

C# 13 and .NET 8 have greatly enhanced ASP.NET Core development capabilities. However, building scalable and maintainable systems requires robust testing in addition to feature implementation. Using xUnit, Moq, and the latest C# 13 features, you will learn how to write clean, reliable, and testable code.

This guide will walk you through testing a REST API or a service layer:

  • Creating a test project
  • Using xUnit to write clean unit tests
  • Using Moq to mock dependencies
  • Using best practices for test architecture and maintainability

Setting Up Your ASP.NET Core Project with C# 13
With ASP.NET Core Web API and C# 13, begin with .NET 8 and ASP.NET Core Web API.
dotnet new sln -n HflApi

dotnet new web -n Hfl.Api
dotnet new classlib -n Hfl.Domain
dotnet new classlib -n Hfl.Core
dotnet new classlib -n Hfl.Application
dotnet new classlib -n Hfl.Infrastructure


dotnet sln add Hfl.Api/Hfl.Api.csproj
dotnet sln add Hfl.Domain/Hfl.Domain.csproj
dotnet sln add Hfl.Core/Hfl.Core.csproj
dotnet sln add Hfl.Application/Hfl.Application.csproj
dotnet sln add Hfl.Infrastructure/Hfl.Infrastructure.csproj


Create a Test Project with xUnit and Moq
Add a new test project:
dotnet new xunit -n HflApi.Tests
dotnet add HflApi.Tests/HflApi.Tests.csproj reference HflApi/HflApi.csproj
dotnet add HflApi.Tests package Moq


Use Case: Testing a Service Layer
Domain, Service, Respository, Interface and API
namespace HflApi.Domain;
public record Order(Guid Id, string Status);


using HflApi.Domain;

namespace HflApi.Core.Interfaces;

public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id);
}


using HflApi.Core.Interfaces;

namespace HflApi.Application.Services;

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }

    public async Task<string> GetOrderStatusAsync(Guid orderId)
    {
        var order = await _repository.GetByIdAsync(orderId);
        return order?.Status ?? "Not Found";
    }
}


using HflApi.Core.Interfaces;
using HflApi.Domain;

namespace Hfl.Infrastructure.Repositories
{
    public class InMemoryOrderRepository : IOrderRepository
    {
        private readonly List<Order> _orders = new()
    {
        new Order(Guid.Parse("7c3308b4-637f-426b-aafc-471697dabeb4"), "Processed"),
        new Order(Guid.Parse("5aee5943-56d0-4634-9f6c-7772f6d9c161"), "Pending")
    };

        public Task<Order?> GetByIdAsync(Guid id)
        {
            var order = _orders.FirstOrDefault(o => o.Id == id);
            return Task.FromResult(order);
        }
    }
}

using Hfl.Infrastructure.Repositories;
using HflApi.Application.Services;
using HflApi.Core.Interfaces;


var builder = WebApplication.CreateBuilder(args);


builder.Services.AddScoped<IOrderRepository, InMemoryOrderRepository>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

app.MapGet("/orders/{id:guid}", async (Guid id, OrderService service) =>
{
    var status = await service.GetOrderStatusAsync(id);
    return Results.Ok(new { OrderId = id, Status = status });
});

app.Run();


Unit Testing with xUnit and Moq
Test Class
using Moq;
using HflApi.Application.Services;
using HflApi.Core.Interfaces;
using HflApi.Domain;


namespace OrderApi.Tests;

public class OrderServiceTests
{
    private readonly Mock<IOrderRepository> _mockRepo;
    private readonly OrderService _orderService;

    public OrderServiceTests()
    {
        _mockRepo = new Mock<IOrderRepository>();
        _orderService = new OrderService(_mockRepo.Object);
    }

    [Fact]
    public async Task GetOrderStatusAsync_ReturnsStatus_WhenOrderExists()
    {
        var orderId = Guid.NewGuid();
        _mockRepo.Setup(r => r.GetByIdAsync(orderId))
                 .ReturnsAsync(new Order(orderId, "Processed"));

        var result = await _orderService.GetOrderStatusAsync(orderId);

        Assert.Equal("Processed", result);
    }

    [Fact]
    public async Task GetOrderStatusAsync_ReturnsNotFound_WhenOrderDoesNotExist()
    {
        var orderId = Guid.NewGuid();
        _mockRepo.Setup(r => r.GetByIdAsync(orderId))
                 .ReturnsAsync((Order?)null);

        var result = await _orderService.GetOrderStatusAsync(orderId);

        Assert.Equal("Not Found", result);
    }

    [Theory]
    [InlineData("Processed")]
    [InlineData("Pending")]
    [InlineData("Shipped")]
    public async Task GetOrderStatus_ReturnsCorrectStatus(string status)
    {
        var orderId = Guid.NewGuid();
        _mockRepo.Setup(r => r.GetByIdAsync(orderId))
                 .ReturnsAsync(new Order(orderId, status));

        var result = await _orderService.GetOrderStatusAsync(orderId);

        Assert.Equal(status, result);
    }
}


Best Practices
1. Use Dependency Injection for Testability
All dependencies should be injected, so don't use static classes or service locator patterns.

2. Keep Tests Isolated
In order to isolate external behavior, Moq should be used to isolate database/network I/O from tests.

3. Use Theory for Parameterized Tests
[Theory]
[InlineData("Processed")]
[InlineData("Pending")]
[InlineData("Shipped")]
public async Task GetOrderStatus_ReturnsCorrectStatus(string status)
{
    var orderId = Guid.NewGuid();
    _mockRepo.Setup(r => r.GetByIdAsync(orderId))
             .ReturnsAsync(new Order(orderId, status));

    var result = await _orderService.GetOrderStatusAsync(orderId);

    Assert.Equal(status, result);
}

4. Group Tests by Behavior (Not CRUD)
Tests should be organized according to what systems do, not how they are performed. For example:

  • GetOrderStatus_ShouldReturnCorrectStatus
  • CreateOrder_ShouldSendNotification

5. Use Records for Test Data in C# 13
public record Order(Guid Id, string Status);

Immutable, concise, and readable test data objects can be created using records.

  • Test Coverage Tips
  • To measure test coverage, use Coverlet or JetBrains dotCover.
  • Business rules and logic at the service layer should be targeted.
  • Make sure you do not overtest third-party libraries or trivial getter/setter functions.

Recommended Tools

Tool

Purpose

xUnit

Unit Testing Framework

Moq

Mocking Dependencies

FluentAssertions

Readable Assertions

Coverlet

Code Coverage

Summary

Use xUnit, Moq, and C# 13 capabilities to test ASP.NET Core applications. To make sure your apps are dependable, provide a clean architecture, separated unit tests, and appropriate test names. Developers can find and address problems earlier in the development cycle by integrating DI, mocking, and xUnit assertions. This leads to quicker feedback, more confidence, and more maintainable systems. Unit tests' isolation guarantees that every part functions on its own, enhancing the overall dependability of the system. Over time, a codebase becomes easier to comprehend and maintain with a clear design and relevant test names. This method promotes a strong development process by lowering regressions and enhancing code quality. Furthermore, modular designs and well-defined test cases facilitate team member onboarding and debugging, encouraging cooperation. These procedures ultimately result in more scalable and resilient applications.



European ASP.NET Core 9.0 Hosting - HostForLIFE :: Using Blazor Server to Call REST APIs: An Introduction with Example

clock June 5, 2025 08:27 by author Peter

Prerequisites

  • Basic knowledge of Blazor Server
  • Visual Studio or VS Code
  • .NET 6 or .NET 8 SDK
  • A sample REST API (we'll use JSONPlaceholder

Step 1. Create a Blazor Server App

  • Open Visual Studio
  • Create a new Blazor Server App
  • Name it BlazorRestClientDemo

Step 2. Create the Model

public class Post
{
    public int UserId { get; set; }
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}

Step 3. Register HttpClient in Program.cs
builder.Services.AddHttpClient("API", client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
});


Step 4. Create a Service to Call the API

public class PostService
{
    private readonly HttpClient _http;

    public PostService(IHttpClientFactory factory)
    {
        _http = factory.CreateClient("API");
    }

    public async Task<List<Post>> GetPostsAsync()
    {
        var response = await _http.GetFromJsonAsync<List<Post>>("posts");
        return response!;
    }

    public async Task<Post?> GetPostAsync(int id)
    {
        return await _http.GetFromJsonAsync<Post>($"posts/{id}");
    }

    public async Task<Post?> CreatePostAsync(Post post)
    {
        var response = await _http.PostAsJsonAsync("posts", post);
        return await response.Content.ReadFromJsonAsync<Post>();
    }
}

C#

Step 5. Register the Service and Program.cs.

builder.Services.AddScoped<PostService>();

Step 6. Use in a Razor Component
@page "/posts"
@inject PostService PostService

<h3>All Posts</h3>

@if (posts == null)
{
    <p>Loading...</p>
}
else
{
    <ul>
        @foreach (var post in posts)
        {
            <li><b>@post.Title</b> - @post.Body</li>
        }
    </ul>
}

@code {
    private List<Post>? posts;

    protected override async Task OnInitializedAsync()
    {
        posts = await PostService.GetPostsAsync();
    }
}

Add a simple form and call CreatePostAsync().

Conclusion
Blazor Server apps can easily consume REST APIs using HttpClient and typed models. In this article, you learned how to,

  • Register and inject HttpClient
  • Call GET and POST endpoints
  • Display data in the UI

Blazor is a powerful front-end technology, and now you know how to connect it with real-world APIs.



European ASP.NET Core 9.0 Hosting - HostForLIFE :: Using C# 13, EF Core, and DDD to Create a Clean ASP.NET Core API

clock June 3, 2025 07:30 by author Peter

We'll show you how to create a scalable, modular, and tested RESTful API using the newest.NET technology and industry best practices. The solution is perfect for enterprise development in the real world because it is designed with maintainability and clean architecture principles in mind. HTTP requests and answers will be handled by ASP.NET Core 8, and by utilizing the most recent language improvements, C# 13 (with preview features) will allow us to develop code that is clearer and more expressive. To access and save data, we'll utilize SQL Server and Entity Framework Core 8.

By prioritizing key domain logic and dividing concerns across layers, the design complies with Domain-Driven Design (DDD). We'll use the Repository and Unit of Work patterns to abstract data access and guarantee transactional consistency. The project also includes structured logging, centralized exception management, and FluentValidation for input validation for stability and traceability.

  • ASP.NET Core 8
  • C# 13 (Preview features)
  • Entity Framework Core 8
  • MS SQL Server
  • Domain-Driven Design (DDD)
  • Repository + Unit of Work Patterns
  • Dependency Injection
  • Validation, Error Handling & Logging


Project Setup & Structure
Clean Folder Structure (Clean Architecture & DDD-Aligned)

In order to promote separation of concerns, maintainability, and testability, the project uses Clean Architecture principles and Domain-Driven Design principles. The src directory is divided into well-defined layers, each with a specific responsibility.

  • CompanyManagement.API: As the interface between the application layer and the outside world, CompanyManagement.API contains API controllers, dependency injection configurations, and middleware such as error handling and logging.
  • CompanyManagement.Application: In CompanyManagement.Application, the business logic is encapsulated in services (use cases), data transfer objects (DTOs), and command/query handlers. Without concern for persistence or infrastructure, this layer coordinates tasks and enforces application rules.
  • CompanyManagement.Domain: CompanyManagement.Domain defines the business model through entities, interfaces, enums, and value objects. This layer is completely independent of any other project and represents the domain logic.
  • CompanyManagement.Infrastructure: The CompanyManagement.Infrastructure class implements the technical details necessary to support the application and domain layers. This includes Entity Framework Core configurations, the DbContext, repository implementations, and database migrations.
  • CompanyManagement.Tests: CompanyManagement.Tests contains unit and integration tests to help maintain code quality and prevent regressions by testing each component of the system in isolation or as part of a broader workflow.

As a result of this layered structure, the application is able to evolve and scale while keeping the codebase clean, decoupled, and easy to test.

Step-by-Step Implementation
Define the Domain Model

Company.cs – Domain Entity
In Domain-Driven Design (DDD), the domain model captures the core business logic of your application. In the CompanyManagement.Domain.Entities namespace, the Company entity encapsulates key business rules and states of a real-world company.

There are three primary properties in the Company class:

  • Id: At the time of creation, a unique identifier (Guid) is generated.
  • Name: A company's name, which is immutable from outside the class, is its name.
  • EstablishedOn: The company's founding date is marked by the EstablishedOn property.

As Entity Framework Core (EF Core) requires a parameterless constructor for materialisation, the constructor is intentionally private. To ensure that Id is always generated and that required fields (Name, EstablishedOn) are always initialised during creation, a public constructor is provided.

This method contains a guard clause to ensure the new name is not null, empty, or whitespace, enforcing business rules directly within the domain model.

Rich domain modelling in DDD adheres to encapsulation, immutability (where appropriate), and self-validation, which are key principles.

Company.cs

namespace CompanyManagement.Domain.Entities;

public class Company
{
    public Guid Id { get; private set; }
    public string Name { get; private set; } = string.Empty;
    public DateTime EstablishedOn { get; private set; }

    private Company() { }

    public Company(string name, DateTime establishedOn)
    {
        Id = Guid.NewGuid();
        Name = name;
        EstablishedOn = establishedOn;
    }

    public void Rename(string newName)
    {
        if (string.IsNullOrWhiteSpace(newName))
            throw new ArgumentException("Name cannot be empty");

        Name = newname;
    }
}


Define the Repository Interface
In Domain-Driven Design (DDD), the repository pattern provides an abstraction over data persistence, enabling the domain layer to remain independent from infrastructure concerns like databases or external APIs. The ICompanyRepository interface, defined in the CompanyManagement.Domain.Interfaces namespace, outlines a contract for working with Company aggregates.

  • This interface declares the fundamental CRUD operations required to interact with the Company entity:
  • Task<Company?> GetByIdAsync(Guid id): Asynchronously retrieves a company by its unique identifier. Returns null if no match is found.
  • GetAllAsync(): Returns a list of all the companies in the system.
  • Task AddAsync(Company company): Asynchronously adds a new Company to the data store using the AddAsync(Company company) task.
  • void Update(Company company): Updates an existing company. This is typically used when domain methods like Rename modify the entity's status.
  • void Delete(Company company): Removes a company from the database.

The rest of the application depends only on abstractions, not implementations, when this interface is defined in the domain layer. A key component of Clean Architecture, the Dependency Inversion Principle (DIP) promotes loose coupling, testability (e.g., mocks in unit tests), and is based on loose coupling.

ICompanyRepository.cs

using CompanyManagement.Domain.Entities;

namespace CompanyManagement.Domain.Interfaces;

public interface ICompanyRepository
{
    Task<Company?> GetByIdAsync(Guid id);
    Task<List<Company>> GetAllAsync();
    Task AddAsync(Company company);
    void Update(Company company);
    void Delete(Company company);
}

EF Core Implementation
As the heart of the Entity Framework Core data access layer, AppDbContext represents a session with the SQL Server database, allowing us to query and save domain entity instances. Input into the constructor is handled through dependency injection, which allows the application to configure the context externally, such as setting the connection string or enabling SQL Server-specific functionality.

The Companies property is a strongly typed DbSet<Company>, which EF Core uses to track and manage Company entities in the database. It abstracts the Companies table and allows you to perform operations like querying, inserting, updating, and deleting data through LINQ and asynchronous methods.

The OnModelCreating method is overridden to configure entity mappings using the Fluent API. Here, we map the Company entity to the Companies table explicitly using modelBuilder.Entity<Company>().ToTable("Companies"). This approach provides flexibility for configuring additional constraints, relationships, and database-specific settings in the future — while keeping the domain model clean and free of persistence concerns.

We follow the Separation of Concerns principle by isolating all database configurations within AppDbContext, ensuring our domain remains pure and focused only on business logic.

AppDbContext.cs
using CompanyManagement.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace CompanyManagement.Infrastructure.Data;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Company> Companies => Set<Company>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Company>().ToTable("Companies");
    }
}
 

For managing Company entities in the database, the CompanyRepository class implements the ICompanyRepository interface concretely. The application leverages Entity Framework Core and is injected with an AppDbContext instance, so that it can interact directly with the database context. By using ToListAsync(), the GetAllAsync method retrieves all Company records from the database asynchronously. The GetByIdAsync method locates a specific company using its unique identifier (Guid id), returning null otherwise.

With AddAsync, a new Company entity is asynchronously added to the context's change tracker, preparing it for insertion into the database when SaveChangesAsync() is called. Update marks an existing company entity as modified, and Delete flags an entity for deletion.

Using the Repository Pattern, the data access logic is abstracted from the rest of the application, which keeps the underlying persistence mechanism hidden. As a result, we can swap or enhance persistence strategies without affecting the domain or application layers, promoting separation of concerns, testability, and maintainability.

CompanyRepository.cs

using CompanyManagement.Domain.Entities;
using CompanyManagement.Domain.Interfaces;
using CompanyManagement.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace CompanyManagement.Infrastructure.Repositories;

public class CompanyRepository(AppDbContext context) : ICompanyRepository
{
    public Task<List<Company>> GetAllAsync() => context.Companies.ToListAsync();
    public Task<Company?> GetByIdAsync(Guid id) => context.Companies.FindAsync(id).AsTask();
    public Task AddAsync(Company company) => context.Companies.AddAsync(company).AsTask();
    public void Update(Company company) => context.Companies.Update(company);
    public void Delete(Company company) => context.Companies.Remove(company);
}


Application Layer – Services & DTOs
As part of the Application Layer, Data Transfer Objects (DTOs) are crucial to separating external inputs from internal domain models. With C# 9+, the CreateCompanyDTO is an immutable and simple data structure that encapsulates only the necessary data for creating a new company, such as its name and its establishment date.

Using a DTO like CreateCompanyDto ensures that the API or service layer receives only the necessary information while maintaining a clear boundary from domain entities. In addition to improving maintainability, validation, and security, it simplifies testing and supports serialization/deserialization out of the box, which eliminates over-posting.

It is a clean, minimal contract aligned with Clean Architecture and Single Responsibility Principle by protecting the domain from direct external access.

CreateCompanyDto.cs

namespace CompanyManagement.Application.DTOs;

public record CreateCompanyDto(string Name, DateTime EstablishedOn);

In this class, business operations related to Company entities are handled by the core application service. When persisting changes, it relies on two key abstractions: ICompanyRepository for data access and IUnitOfWork for transactional consistency management.

In the CreateAsync method, a new company is created by instantiating a new Company domain entity based on the CreateCompanyDto provided, then delegating to the repository and adding it asynchronously. Then it commits the transaction to the database by calling the SaveChangesAsync method on the unit of work. Finally, the caller receives the unique identifier (Id) for the newly created company that can be referenced in the future.

Asynchronously retrieves all existing companies by invoking the corresponding repository method, returning a list of Company entities.

The service contains business logic and coordinates domain operations, while the repository abstracts persistence details. By using interfaces to support easy unit testing, it follows best practices for asynchronous programming, dependency injection, and dependency injection.

CompanyService.cs
using CompanyManagement.Application.DTOs;
using CompanyManagement.Domain.Entities;
using CompanyManagement.Domain.Interfaces;

namespace CompanyManagement.Application.Services;

public class CompanyService(ICompanyRepository repository, IUnitOfWork unitOfWork)
{
    public async Task<Guid> CreateAsync(CreateCompanyDto dto)
    {
        var company = new Company(dto.Name, dto.EstablishedOn);
        await repository.AddAsync(company);
        await unitOfWork.SaveChangesAsync();
        return company.Id;
    }

    public async Task<List<Company>> GetAllAsync() => await repository.GetAllAsync();
}

Unit of Work
IUnitOfWork represents a fundamental pattern for managing data persistence and transactional consistency in an application. It abstracts the concept of a "unit of work," which encapsulates a series of operations that should be treated as a single atomic transaction. The SaveChangesAsync() method commits all pending changes to the underlying data store asynchronously. This method returns an integer indicating the number of state entries committed.

By relying on this abstraction, the application ensures that all modifications across multiple repositories can be coordinated and saved together, preserving data integrity and consistency. Furthermore, implementations can be mocked or swapped without changing the consuming code, improving testability and separation of concerns.

IUnitOfWork.cs
namespace CompanyManagement.Domain.Interfaces;

public interface IUnitOfWork
{
    Task<int> SaveChangesAsync();
}

The UnitOfWork class implements the IUnitOfWork interface, encapsulating the transaction management logic for the application. It depends on the AppDbContext, which represents the Entity Framework Core database context.

It simply delegated the call to context.SaveChangesAsync(), asynchronously persisting all tracked changes. This ensures that any changes made through repositories within a unit of work are saved as one atomic operation.

Using the UnitOfWork class to centralize the commit logic makes it easier to coordinate multiple repository operations under one transaction, while also supporting dependency injection and testing.

UnitOfWork.cs
using CompanyManagement.Domain.Interfaces;

namespace CompanyManagement.Infrastructure.Data;

public class UnitOfWork(AppDbContext context) : IUnitOfWork
{
    public Task<int> SaveChangesAsync() => context.SaveChangesAsync();
}


API Layer
ASP.NET Core's Dependency Injection (DI) container is used to wire up essential services in Program.cs. In order to register the AppDbContext with the DI container, AddDbContext is first used. The connection string is retrieved from the application's configuration under the key "Default" and configures Entity Framework Core to use SQL Server as the database provider. As a result, the application can access the database seamlessly.

Through AddScoped, the repository and unit of work abstractions are then mapped to their concrete implementations. The result is that a new instance of CompanyRepository and UnitOfWork is created and shared for every HTTP request, providing scoped lifetime management for database operations.

Also registered as a scoped service is the CompanyService, which contains the business logic for managing companies. With this setup, controllers and other components receive these dependencies via constructor injection, thereby promoting loose coupling, testability, and separation of concerns in the API layer.

ServiceRegistration.cs
using CompanyManagement.Application.Services;
using CompanyManagement.Domain.Interfaces;
using CompanyManagement.Infrastructure.Data;
using CompanyManagement.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace CompanyManagement.Infrastructure.DependencyInjection;

public static class ServiceRegistration
{
    public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(configuration.GetConnectionString("Default")));

        services.AddScoped<ICompanyRepository, CompanyRepository>();
        services.AddScoped<IUnitOfWork, UnitOfWork>();
        services.AddScoped<CompanyService>();

        return services;
    }
}

SwaggerConfiguration.cs
using Microsoft.OpenApi.Models;

namespace CompanyManagement.API.Configurations;

public static class SwaggerConfiguration
{
    public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
    {
        services.AddEndpointsApiExplorer();

        services.AddSwaggerGen(options =>
        {
            options.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "Company API",
                Version = "v1",
                Description = "API for managing companies using Clean Architecture"
            });

            // Optional: Include XML comments
            var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename);
            if (File.Exists(xmlPath))
            {
                options.IncludeXmlComments(xmlPath);
            }
        });

        return services;
    }
}


SwaggerMiddleware.cs
namespace CompanyManagement.API.Middleware;

public static class SwaggerMiddleware
{
    public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app)
    {
        app.UseSwagger();

        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "Company API V1");
        });

        return app;
    }
}

Program.cs
using CompanyManagement.API.Configurations;
using CompanyManagement.API.Endpoints;
using CompanyManagement.API.Middleware;
using CompanyManagement.Infrastructure.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerDocumentation();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerDocumentation();
builder.Services.AddInfrastructure(builder.Configuration);

 .
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}
app.MapCompanyEndpoints();

app.UseHttpsRedirection();


appsettings.json
{
  "ConnectionStrings": {
    "Default": "Server=localhost;Database=CompanyDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

The CompanyEndpoints is the REST API entry point that handles HTTP requests related to company management. Decorated with the [ApiController] attribute, it features automatic model validation, binding, and API-specific conventions. The controller routes are prefixed with "api/[controller]", which means they will be based on the controller name (api/company). Using constructor injection, the controller receives an instance of CompanyService, which enables it to delegate business logic operations to the application layer.

In the Create action, a CreateCompanyDto object is accepted from the request body, the service's CreateAsync method is called, and a 201 Created response is returned with the new resource's location header.
When an HTTP GET request is made with a company ID in the route, GetById retrieves all companies via the service, searches for companies with the matching ID, and returns 200 OK if a match is found, or 404 Not Found otherwise.
A 200 OK response is returned by the GetAll action when HTTP GET requests are made to retrieve all companies.

By offloading business logic to the service layer, this controller adheres to the Single Responsibility Principle and ensures maintainability and testability.

CompanyEndpoints.cs
using CompanyManagement.Application.DTOs;
using CompanyManagement.Application.Services;

namespace CompanyManagement.API.Endpoints;

public static class CompanyEndpoints
{
    public static void MapCompanyEndpoints(this WebApplication app)
    {
        app.MapPost("/api/companies", async (CreateCompanyDto dto, CompanyService service) =>
        {
            var id = await service.CreateAsync(dto);
            return Results.Created($"/api/companies/{id}", new { id });
        });

        app.MapGet("/api/companies", async (CompanyService service) =>
        {
            var companies = await service.GetAllAsync();
            return Results.Ok(companies);
        });

        app.MapGet("/api/companies/{id:guid}", async (Guid id, CompanyService service) =>
        {
            var companies = await service.GetAllAsync();
            var match = companies.FirstOrDefault(c => c.Id == id);
            return match is not null ? Results.Ok(match) : Results.NotFound();
        });
    }
}

Best Practices
Several key best practices are followed in this project to ensure that the code is clean, maintainable, and scalable. For instance, Data Transfer Objects (DTOs) are used to avoid exposing domain entities directly to external clients, improving security and abstraction. The Single Responsibility Principle (SRP) is applied by ensuring each layer has a distinct, focused responsibility, resulting in improved code organisation and clarity.

The project uses Dependency Injection to promote flexibility, ease testing, and maintain separation of concerns. For I/O-bound operations such as database access, asynchronous programming with async/await improves application responsiveness and scalability.

As a result of exception handling middleware, error logging is consistent, and maintenance is simplified. Finally, the project includes comprehensive unit testing using xUnit and Moq frameworks for each layer, which ensures code quality and reliability in the future.



About HostForLIFE.eu

HostForLIFE.eu is European Windows Hosting Provider which focuses on Windows Platform only. We deliver on-demand hosting solutions including Shared hosting, Reseller Hosting, Cloud Hosting, Dedicated Servers, and IT as a Service for companies of all sizes.

We have offered the latest Windows 2016 Hosting, ASP.NET Core 2.2.1 Hosting, ASP.NET MVC 6 Hosting and SQL 2017 Hosting.


Tag cloud

Sign in