Building a CRUD API with ABP Framework, ASP.NET Core, and PostgreSQL

Building a CRUD API with ABP Framework, ASP.NET Core, and PostgreSQL
Topkapi Palace Museum

I recently read "Building a CRUD API with ASP.NET Core Web API and PostgreSQL" by M. Oly Mahmud on DEV Community. While it’s a solid introduction to building a basic CRUD API, it lacks features like permissions and validation—important for real-world scenarios. Inspired by this, I decided to write a guide that demonstrates how to build a production-ready CRUD API using the ABP Framework, ASP.NET Core, and PostgreSQL. This tutorial adds security, data validation, and leverages ABP’s conventions to create a robust API for managing products.

What is ABP Framework?

ABP Framework is an open-source web application framework for building modular, maintainable, and scalable applications using .NET and ASP.NET Core. It provides built-in functionalities for common application requirements like authentication, authorization, logging, monitoring, tenant management, feature management, payment gateway integration(supports StripePayPal, and so on), file management, and even a GDPR module to manage personal data.

Prerequisites

Let’s get started!

Step 1: Install ABP CLI

Install the ABP CLI globally:

dotnet tool install -g Volo.Abp.Studio.Cli

Verify:

abp --version

Alternatively, you can use ABP Studio to create projects.

Step 2: Create a New ABP Project

Generate an ABP solution with PostgreSQL:

abp new ProductApi -t app -u mvc -dbms PostgreSQL -cs="Host=localhost;Port=5432;Database=ProductApi;Username=root;Password=myPassword" -csf

Explanation:

  • ProductApi: Solution name.
  • -t app: specifies application(layered) template.
  • -u mvc: Includes MVC UI (API layer is included).
  • -dbms PostgreSQL: Uses PostgreSQL as a database management system
  • -cs: PostgreSQL connection string (adjust credentials as needed).
  • -csf:  Creates solution folder.

Navigate to the solution:

cd ProductApi

ABP attempts to create the ProductApi database during project generation if PostgreSQL is running and the connection string is valid. Check the database to confirm.

Step 3: Define Validation Constants

In ProductApi.Domain.Shared, create a Products folder and add ProductConsts.cs:

namespace ProductApi.Products;

public static class ProductConsts
{
    public const int NameMaxLength = 128;
    public const int DescriptionMaxLength = 256;
}

These constants will be reused for validation in DTOs and database configuration.

Step 4: Define the Product Entity

In the ProductApi.Domain project, create a Products folder and, add Product.cs:

using System;
using Volo.Abp.Domain.Entities.Auditing;

namespace ProductApi.Domain.Products;

public class Product : AuditedAggregateRoot<Guid>
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}
  • AuditedAggregateRoot<Guid>: Provides auditing (e.g., creation time) properties and uses Guid as the type of primary key.

Step 5: Configure the DbContext

In ProductApi.EntityFrameworkCore/EntityFrameworkCore/ProductApiDbContext.cs, add the Products DbSet:

using Microsoft.EntityFrameworkCore;
using ProductApi.Domain.Products;
using ProductApi.Products;
using Volo.Abp.AuditLogging.EntityFrameworkCore;
using Volo.Abp.BackgroundJobs.EntityFrameworkCore;
using Volo.Abp.BlobStoring.Database.EntityFrameworkCore;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.FeatureManagement.EntityFrameworkCore;
using Volo.Abp.Identity.EntityFrameworkCore;
using Volo.Abp.PermissionManagement.EntityFrameworkCore;
using Volo.Abp.SettingManagement.EntityFrameworkCore;
using Volo.Abp.OpenIddict.EntityFrameworkCore;
using Volo.Abp.TenantManagement.EntityFrameworkCore;

namespace ProductApi.EntityFrameworkCore;

[ConnectionStringName("Default")]
public class ProductApiDbContext : AbpDbContext<ProductApiDbContext>
{
    public DbSet<Product> Products { get; set; }

    public ProductApiDbContext(DbContextOptions<ProductApiDbContext> options)
        : base(options)
    {

    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.ConfigurePermissionManagement();
        builder.ConfigureSettingManagement();
        builder.ConfigureBackgroundJobs();
        builder.ConfigureAuditLogging();
        builder.ConfigureFeatureManagement();
        builder.ConfigureIdentity();
        builder.ConfigureOpenIddict();
        builder.ConfigureTenantManagement();
        builder.ConfigureBlobStoring();
        

        builder.Entity<Product>(b =>
        {
            b.ToTable(ProductApiConsts.DbTablePrefix + "Products", ProductApiConsts.DbSchema);
            b.ConfigureByConvention(); //auto configure for the base class props
            
            b.Property(x => x.Name).IsRequired().HasMaxLength(ProductConsts.NameMaxLength);
            b.Property(x => x.Description).HasMaxLength(ProductConsts.DescriptionMaxLength);
            b.Property(x => x.Price).IsRequired();
        });
    }
}

The constraints (e.g., NameMaxLength) align with ProductConsts for consistency across layers.

Add and Apply Migrations

  1. Add a Migration: Navigate to the ProductApi.EntityFrameworkCore project:
cd ProductApi.EntityFrameworkCore

Run the EF Core command to create a migration for the Product entity:

dotnet ef migrations add Added_Products
  • This generates a migration file (e.g., 2025XXXXXXXXXX_Added_Products.cs) in the Migrations folder.
  1. Apply the Migration: Update the database:
dotnet ef database update

Ensure PostgreSQL is running and the connection string is correct. This creates the AppProducts table in ProductApi.

Alternatively, ABP applies migrations automatically when you run the ProductApi.DbMigrator project.

Step 6: Define DTOs with Validation

In the ProductApi.Application.Contracts project, create a Products folder and add ProductDtos.cs:

using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Application.Dtos;

namespace ProductApi.Products;

public class ProductDto : AuditedEntityDto<Guid>
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}

public class CreateProductDto
{
    [Required]
    [StringLength(ProductConsts.NameMaxLength)]
    public string Name { get; set; }

    [StringLength(ProductConsts.DescriptionMaxLength)]
    public string Description { get; set; }

    [Required]
    public decimal Price { get; set; }
}

public class UpdateProductDto : EntityDto<Guid>
{
    [Required]
    [StringLength(ProductConsts.NameMaxLength)]
    public string Name { get; set; }

    [StringLength(ProductConsts.DescriptionMaxLength)]
    public string Description { get; set; }

    [Required]
    public decimal Price { get; set; }
}
  • AuditedEntityDto<Guid>: Includes auditing properties (e.g., CreationTime).
  • EntityDto<Guid>: Includes Id property.
  • Validation attributes ensure data integrity (e.g., required fields, length limits).

Step 7: Update Permissions

The ProductApi.Application.Contracts project already contains ProductApiPermissions and ProductApiPermissionDefinitionProvider in Permissions folder. Update ProductApiPermissions.cs:

namespace ProductApi.Permissions;

public static class ProductApiPermissions
{
    public const string GroupName = "ProductApi";

    public static class Products
    {
        public const string Default = GroupName + ".Products";
        public const string Create = Default + ".Create";
        public const string Update = Default + ".Update";
        public const string Delete = Default + ".Delete";
    }
}

Update ProductApiPermissionDefinitionProvider:

using ProductApi.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;

namespace ProductApi.Permissions;

public class ProductApiPermissionDefinitionProvider : PermissionDefinitionProvider
{
    public override void Define(IPermissionDefinitionContext context)
    {
        var productGroup = context.AddGroup(ProductApiPermissions.GroupName);

        var products = productGroup.AddPermission(ProductApiPermissions.Products.Default, L("Permission:Products"));
        products.AddChild(ProductApiPermissions.Products.Create, L("Permission:Products.Create"));
        products.AddChild(ProductApiPermissions.Products.Update, L("Permission:Products.Update"));
        products.AddChild(ProductApiPermissions.Products.Delete, L("Permission:Products.Delete"));
    }

    private static LocalizableString L(string name)
    {
        return LocalizableString.Create<ProductApiResource>(name);
    }
}

Update localization in ProductApi.Domain.Shared/Localization/ProductApi/en.json:

{
  "Culture": "en",
  "Texts": {
    "AppName": "ProductApi",
    "Menu:Home": "Home",
    "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information visit",
    "Welcome": "Welcome",
    "Permission:Products": "Manage Products",
    "Permission:Products.Create": "Create Products",
    "Permission:Products.Update": "Update Products",
    "Permission:Products.Delete": "Delete Products"
  }
}

Step 8: Implement the CRUD Service with CrudAppService

In ProductApi.Application.Contracts/Products, add IProductAppService.cs:

using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;

namespace ProductApi.Products;

public interface IProductAppService : ICrudAppService<
    ProductDto,              // DTO for responses
    Guid,                    // Primary key type
    PagedAndSortedResultRequestDto, // Request for GetList
    CreateProductDto,        // Create DTO
    UpdateProductDto>        // Update DTO
{
}

In ProductApi.Application/Products, add ProductAppService.cs to implement IProductAppService:

using System;
using Microsoft.AspNetCore.Authorization;
using ProductApi.Domain.Products;
using ProductApi.Permissions;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace ProductApi.Products;

[Authorize(ProductApiPermissions.Products.Default)]
public class ProductAppService : CrudAppService<
        Product,                  // Entity
        ProductDto,              // DTO for responses
        Guid,                    // Primary key type
        PagedAndSortedResultRequestDto, // Request for GetList
        CreateProductDto,        // Create DTO
        UpdateProductDto>,       // Update DTO
    IProductAppService
{
    public ProductAppService(IRepository<Domain.Products.Product, Guid> repository)
        : base(repository)
    {
        // Define permissions for CRUD operations
        GetPolicyName = ProductApiPermissions.Products.Default;
        GetListPolicyName = ProductApiPermissions.Products.Default;
        CreatePolicyName = ProductApiPermissions.Products.Create;
        UpdatePolicyName = ProductApiPermissions.Products.Update;
        DeletePolicyName = ProductApiPermissions.Products.Delete;
    }
}
  • [Authorize]: Enforces permission checks.
  • Permissions are assigned to each CRUD operation.

Step 9: Update AutoMapper

The ProductApi.Application project already has ProductApiApplicationAutoMapperProfile.cs. Update it:

using AutoMapper;
using ProductApi.Domain.Products;
using ProductApi.Products;

namespace ProductApi;

public class ProductApiApplicationAutoMapperProfile : Profile
{
    public ProductApiApplicationAutoMapperProfile()
    {
        CreateMap<Product, ProductDto>();
        CreateMap<CreateProductDto, Product>();
        CreateMap<UpdateProductDto, Product>();
    }
}

Step 10: Run and Test

  1. Build the solution:
dotnet build
  1. Run the web project:
cd ProductApi.Web
dotnet run
  1. Log In: Open your browser and navigate to https://localhost:xxxx (port varies). Use the default admin credentials:
    • Username: admin
    • Password: 1q2w3E* After logging in, you’ll be redirected to the home page.
  1. Access Swagger: Go to https://localhost:xxxx/swagger. ABP includes Swagger UI by default, and since you’re logged in, your session token is automatically included in API requests.
  2. Test Endpoints: ABP generates these endpoints from ICrudAppService:
  • GET /api/app/product (list)
  • GET /api/app/product/{id} (get)
  • POST /api/app/product (create)
  • PUT /api/app/product/{id} (update)
  • DELETE /api/app/product/{id} (delete)

In Swagger:

  • Click an endpoint (e.g., POST /api/app/product).
  • Enter a sample request body (e.g., {"name": "Laptop", "description": "High-end", "price": 999.99}).
  • Click "Execute" to test. The response will reflect your authenticated permissions.

If you encounter a 403 (Forbidden) error, ensure the admin role has the required permissions (ProductApi.Products.*) assigned via the UI (Administration > Identity Management > Roles > Actions > Permissions > ProductApi > Select all > Save).

Conclusion

This guide enhances the referenced article by adding real-world features like permissions and validation using ABP Framework. With ICrudAppService, automatic endpoint generation, and consistent validation via ProductConsts, we’ve built a secure, scalable CRUD API. ABP’s conventions make it ideal for production-ready applications—try extending it with custom logic or UI next!