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
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
- 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.
- 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)