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)