Nano ASP.NET SaaS Boilerplate
Admin credentials (all tenants): admin@email.com / Password123!
Sample data resets every hour

In this walkthrough, we’re going to create a new entity and build a service for it with an API controller to handle CRUD operations.

We’ll use the Nano boilerplate, a starter template for building web applications with .NET. You can follow along even if you don’t have the boilerplate, the code here could be applied to any .NET project.

We will start by creating a new entity called Supplier. This entity will be part of a one-to-many relationship with the Products entity, a sample entity provided by the boilerplate.

The code is divided into layers, with the entities being part of the Domain layer.

Open the Domain layer and in the /Catalog folder, create a new entity class called Supplier. Supplier will have a Name field and an ICollection navigation property for Products. Inherit from AuditableEntity.

    public class Supplier : AuditableEntity
    {
        public string Name { get; set; }

        public ICollection<Product> Products { get; set; }
    }

In the Product entity, add a GUID SupplierId and navigation property to complete the one-to-many relationship. Once these changes are applied, a SupplierId will be required when creating Products.

    public class Product : AuditableEntity 
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public Supplier Supplier { get; set; }
        public Guid SupplierId { get; set; }
    }

All entities should derive from BaseEntity, found in /Entities/Common. TenantBaseEntity ensures that the ID will be of type GUID. It implements the IMustHaveTenant interface and tables will isolate tenant data.

AuditableEntity is a base class that derives from TenantBaseEntity and adds CreatedBy, CreatedOn, LastModifiedBy, and LastModifiedOn fields. The values of these fields are handled automatically whenever save changes occurs.

Open up ApplicationDbContext in Infrastructure/Persistence/Contents/ and copy the add-migration command. Provide a name for the migration like AddSupplierEntity and remember to set the default project in PMC to infrastructure. Any pending migrations will be applied on app startup so you don’t need to explicitly run update-database.

Create a new folder called /SupplierService in Application/Services and a new interface ISupplierService.

Make sure ISupplierService inherits from ITransientService. This will ensure the interface is registered automatically to the application’s service container with a transient lifecycle.

A quick and easy way to build new services is to simply copy code from the Product example. Copy all the code from IProductService, and paste it in to ISupplierService. Use find and replace in current document, with preserve case, to replace Product with Supplier.

  public interface ISupplierService : ITransientService
  {
      Task<Response<IEnumerable<SupplierDTO>>> GetSuppliersAsync(string keyword = "");
      Task<PaginatedResponseTanstack<SupplierDTO>> GetSuppliersTanstackPaginatedAsync(SupplierTableFilter filter); // used by Tanstack Table v8 (React, Vue)
      Task<Response<SupplierDTO>> GetSupplierAsync(Guid id);
      Task<Response<Guid>> CreateSupplierAsync(CreateSupplierRequest request);
      Task<Response<Guid>> UpdateSupplierAsync(UpdateSupplierRequest request, Guid id);
      Task<Response<Guid>> DeleteSupplierAsync(Guid id);
  }

The interface contains two paginated methods but one of them can be removed. Remove the JQuery Datatables method unless you are planning to use Razor pages.

Create a new /DTOs folder within Application/Services/SupplierService and create a SupplierDTO. Create the DTO with an Id and Name field and an IList of ProductDTO. Implement the IDto interface on all DTOs.

    public class SupplierDTO : IDto
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public IList<ProductDTO> Products { get; set; }
    }

Create two new DTOs for the create and update requests. Ensure the name field is not empty using fluent validation.

    public class CreateSupplierRequest : IDto
    {
        public string Name { get; set; }
    }
    public class CreateSupplierValidator : AbstractValidator<CreateSupplierRequest>
    {
        public CreateSupplierValidator()
        {
            RuleFor(x => x.Name).NotEmpty();
        }
    }

Create a new folder called /Filters with a SupplierTableFilter class and inherit from the common PaginationFilterTanstack class. PaginationFilterTanstack adds fields for pagination. Add an optional string field for keyword to allow searching paginated requests.

    public class SupplierTableFilter : PaginationFilterTanstack
    {
        public string? Keyword { get; set; }
    }

Copy over the code from the Product service and do a search and replace while preserving the case on Product; replace with Supplier. At this point, the only missing elements should be the Ardalis specification classes, which we will now create.

Create a new folder called /Specifications and create a new class called SupplierSearchList. Inherit from Specification and pass Supplier as the type.

public class SupplierSearchList : Specification<Supplier>
{
    public SupplierSearchList(string? keyword)
    {
        // filters
        if (!string.IsNullOrWhiteSpace(keyword))
        {
            _ = Query.Where(x => x.Name.Contains(keyword));
        }

        _ = Query.OrderByDescending(x => x.CreatedOn); // default sort order
    }
}

In this specification, we will check an optional keyword string to search the Supplier name field and provide a default sort order. This specification will be used in the GetSuppliersAsync method to search a full list of suppliers.

public async Task<Response<IEnumerable<SupplierDTO>>> GetSuppliersAsync(string keyword = "")
{
    SupplierSearchList specification = new(keyword); // ardalis specification
    IEnumerable<SupplierDTO> list = await _repository.GetListAsync<Supplier, SupplierDTO, Guid>(specification); // full list, entity mapped to dto
    return Response<IEnumerable<SupplierDTO>>.Success(list);
}

The nice thing about using specification classes to query data is that they can be descriptively named and easily reused.

Next create a class called SupplierSearchTable and inherit from Specification. Like the list specification, check the optional keyword string to search the Supplier name field.

public class SupplierSearchTable : Specification<Supplier>
{
    public SupplierSearchTable(string? keyword, string? dynamicOrder = "")
    {
        if (!string.IsNullOrWhiteSpace(keyword))
        {
            _ = Query.Where(h => h.Name.Contains(keyword));
        }

        // sort order
        if (string.IsNullOrEmpty(dynamicOrder))
        {
            _ = Query.OrderBy(h => h.Name);
        }
        else
        {
            _ = Query.OrderBy(dynamicOrder); // dynamic sort order
        }
    }
}

Data table components like Tanstack Table or JQuery Datatables allow the user to dynamically sort table data by columns. To handle this, in the service method GetSuppliersTanstackPaginatedAsync, check the sorting field on SupplierTableFilter. If the sorting field contains data, generate a standardized sorting string using the NanoHelpers function. The string can then be evaluated by Ardalis specification.

        public async Task<PaginatedResponseTanstack<SupplierDTO>> GetSuppliersTanstackPaginatedAsync(SupplierTableFilter filter)
        {
            if (!string.IsNullOrEmpty(filter.Keyword)) // set to first page if any search filters are applied
            {
                filter.PageNumber = 1;
            }

            string dynamicOrder = (filter.Sorting != null) ? NanoHelpers.GenerateOrderByStringFromTanstack(filter) : ""; // possible dynamic ordering from datatable
            SupplierSearchTable specification = new(filter?.Keyword, dynamicOrder); // ardalis specification
            PaginatedResponseTanstack<SupplierDTO> pagedResponse = await _repository.GetTanstackPaginatedResultsAsync<Supplier, SupplierDTO, Guid>(filter.PageNumber, filter.PageSize, specification); // paginated response, entity mapped to dto
            return pagedResponse;
        }

In the SupplierSearchTable specification, accept an optional string field called dynamicOrder. If the dynamicOrder string is null, return a default order. Otherwise, pass the dynamicOrder to the OrderBy method. Add a reference to the ArdalisSpecificationExtensions class, an extension method provided by the boilerplate, so that it knows how to evaluate the dynamic string.

Create one more specification called SupplierMatchName with a required string field name. This will be used in the CreateSupplierAsync method to check if any suppliers exist already with the given name.

 public class SupplierMatchName : Specification<Supplier>
 {
     public SupplierMatchName(string name)
     {
         Query.Where(h => h.Name == name);

     }
 }
 public async Task<Response<Guid>> CreateSupplierAsync(CreateSupplierRequest request)
 {
     SupplierMatchName specification = new(request.Name); // ardalis specification 
     bool supplierExists = await _repository.ExistsAsync<Supplier, Guid>(specification);
     if (supplierExists)
     {
         return Response<Guid>.Fail("Supplier already exists");
     }

     Supplier newSupplier = _mapper.Map(request, new Supplier()); // map dto to domain entity

     try
     {
         Supplier response = await _repository.CreateAsync<Supplier, Guid>(newSupplier); // create new entity 
         _ = await _repository.SaveChangesAsync(); // save changes to db
         return Response<Guid>.Success(response.Id); // return id
     }
     catch (Exception ex)
     {
         return Response<Guid>.Fail(ex.Message);
     }
 }

Products now require a supplier ID. Navigate to Services/ProductService/DTOs and add a GUID SupplierId property for the Update and Create product requests.

public class CreateProductRequest : IDto
{
    public string Name { get; set; }
    public string Description { get; set; }
    public Guid SupplierId { get; set; }

}

public class CreateProductValidator : AbstractValidator<CreateProductRequest>
{
    public CreateProductValidator()
    {
        _ = RuleFor(x => x.Name).NotEmpty();
        _ = RuleFor(x => x.Description).NotEmpty();
        _ = RuleFor(x => x.SupplierId).NotEmpty();

    }
}

On the ProductDTO, add a GUID SupplierId and a SupplierName string property.

    public class ProductDTO : IDto
    {
        public Guid Id { get; set; }
        public string? Name { get; set; }
        public string? Description { get; set; }
        public DateTime CreatedOn { get; set; }
        public string SupplierName { get; set; }
        public Guid SupplierId { get; set; }
    }

Now that we’ve added all the new DTOs for Supplier, we need to ensure that Automapper knows how to map them correctly.

Open up MappingProfiles in Infrastructure/Mapper/ and add the following configurations from Supplier to the SupplierDTO, and from CreateSupplierRequest and UpdateSupplierRequest to Supplier

            // product mappings
            _ = CreateMap<Product, ProductDTO>().ForMember(x => x.SupplierName, o => o.MapFrom(s => s.Supplier.Name));
            _ = CreateMap<CreateProductRequest, Product>();
            _ = CreateMap<UpdateProductRequest, Product>();


            // supplier mappings...
            _ = CreateMap<Supplier, SupplierDTO>();
            _ = CreateMap<CreateSupplierRequest, Supplier>();
            _ = CreateMap<UpdateSupplierRequest, Supplier>();

We can also configure Automapper to populate the SupplierName field with the Name field whenever mapping is performed on products.

Create a new API controller in WebApi/Controllers called SuppliersController. Copy all of the code from the Products controller. Perform a find and replace on Product for Supplier with preserve case.

Now we can test with Postman. Create a new folder for Suppliers with a Post request called create-supplier. Create a few suppliers; remember to first obtain a token.

Test API Post

Create a new Get request called get-suppliers-full-list and retrieve the list of suppliers. Copy one of the IDs so that we can create a product in the next step.

Test API Get

In the Products folder, modify the create-product request body to have a SupplierId field. Create a new product for one of the suppliers

Test API Create Product

Now when you retrieve a list of suppliers, an array of products will be returned.

Test API Get Listing

Returning a list of products works as well, with Automapper taking care of the supplier name field for us

Test API Get List of Products

That concludes the walkthrough, hopefully now you have a better understanding of how to build API endpoints with the Nano ASP.NET boilerplate!

Leave a Reply

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

Need an ASP.NET Boilerplate to build your next MVP?
Check out Nano ASP.NET multi-tenant SaaS boilerplate project and save weeks or months of development time.
Learn More