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 Stripe, PayPal, and so on), file management, and even a GDPR module to manage personal data.
Prerequisites
- .NET 9 SDK installed.
- PostgreSQL installed and running locally.
- EF Core CLI
- Node.js v20.11+
- A code editor like Visual Studio or VS Code.
- Basic knowledge of C# and REST APIs.
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: Provides auditing (e.g., creation time) properties and uses
Guidas 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
-
Add a Migration: Navigate to the
ProductApi.EntityFrameworkCoreproject:
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.
- 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: Includes auditing properties (e.g., CreationTime).
- EntityDto: 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
- Build the solution:
dotnet build
- Run the web project:
cd ProductApi.Web
dotnet run
-
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.
-
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. -
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!
This article was originally posted on the berkansasmaz.com.
Top comments (0)