crud-generator

SKILL.md

ABP CRUD Service Generator (Project-Agnostic)

Generates complete, production-ready CRUD application services following ABP Framework best practices and DDD patterns. This skill is framework-agnostic and works with any ABP-based project.

When to Use

  • Creating a new entity service from scratch in any ABP project
  • Scaffolding standard CRUD operations quickly
  • Ensuring consistency with existing service patterns
  • Implementing reference-based service architecture
  • Accelerating feature development with proven templates
  • Interface-first development (generate implementation after interface exists)

What Gets Generated

The skill generates files in two separate layers following standard ABP architecture:

Contracts Layer ({Project}.Application.Contracts/)

The interface and DTOs are generated in the Contracts project. This layer is shared between server and client.

1. Interface (I{EntityName}AppService.cs)

using System;
using System.Threading.Tasks;
using {Project}.Dtos; // Adjust based on your project's DTO namespace
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;

namespace {Project}.Application.Contracts.{PluralEntityName};

public interface I{EntityName}AppService : IApplicationService
{
    Task<ResponseDataDto<object>> CreateAsync(CreateUpdate{EntityName}Dto input);
    Task<ResponseDataDto<object>> UpdateAsync(Guid id, CreateUpdate{EntityName}Dto input);
    Task<ResponseDataDto<object>> DeleteAsync(Guid id);
    Task<ResponseDataDto<{EntityName}Dto>> GetAsync(Guid id);
    Task<ResponseDataDto<PagedResultDto<{EntityName}Dto>>> GetListAsync(PagedAndSortedResultRequestDto input, {EntityName}Filter filter);
    Task<ResponseDataDto<object>> ActivateAsync(Guid id);          // If hasIsActive=true
    Task<ResponseDataDto<object>> DeactivateAsync(Guid id);       // If hasIsActive=true
    Task<ResponseDataDto<object>> ManageConfigAsync(ManageConfigDto input); // If hasConfig=true
    Task<ResponseDataDto<object>> BulkImportAsync([FromForm] BulkImportFileDto input); // If needsBulkImport=true
    Task<ResponseDataDto<ExportFileBlobDto>> ExportAsync({EntityName}Filter filter); // If needsBulkImport=true
    Task<ResponseDataDto<DropDownDto[]>> Get{EntityName}sAsync();
}

Note: Adjust the using statements and namespace to match your project conventions.

2. DTOs ({EntityName}Dtos.cs)

All required DTO classes:

  • {EntityName}Dto - Response DTO (extends EntityDto<Guid> or plain class)
  • CreateUpdate{EntityName}Dto - Input for create/update with validation attributes
  • {EntityName}Filter - Filter and pagination (extends PagedAndSortedResultRequestDto)
  • {EntityName}ExportDto - For export operations
  • ManageConfigDto - If hasConfig=true
  • {EntityName}ImportDto - If needsBulkImport=true

Application Layer ({Project}.Application/AppServices/)

3. Implementation ({EntityName}AppService.cs)

Complete implementation with:

  • Constructor injection of all dependencies
  • Full CRUD methods with try-catch and structured logging
  • Validation methods (ValidateInputAsync, etc.)
  • Optional activation/deactivation methods with business rules
  • Optional config management
  • Optional bulk import/export with validation
  • Helper methods (Get{EntityName}Async, etc.)

ABP CRUD Service Generator

Generates complete, production-ready CRUD application services following ABP Framework best practices and DDD patterns.

When to Use

  • Creating a new entity service from scratch
  • Scaffolding standard CRUD operations quickly
  • Ensuring consistency with existing service patterns
  • Implementing reference-based service architecture
  • Accelerating feature development with proven templates
  • Interface-first development (generate implementation after interface exists)

What Gets Generated

The skill generates files in two separate layers following standard ABP architecture:

Contracts Layer ({Project}.Application.Contracts/)

The interface and DTOs are generated in the Contracts project. This layer is shared between server and client.

1. Interface (I{EntityName}AppService.cs)

public interface I{EntityName}AppService : IApplicationService
{
    Task<ResponseDataDto<object>> CreateAsync(CreateUpdate{EntityName}Dto input);
    Task<ResponseDataDto<object>> UpdateAsync(Guid id, CreateUpdate{EntityName}Dto input);
    Task<ResponseDataDto<object>> DeleteAsync(Guid id);
    Task<ResponseDataDto<{EntityName}Dto>> GetAsync(Guid id);
    Task<ResponseDataDto<PagedResultDto<{EntityName}Dto>>> GetListAsync(PagedAndSortedResultRequestDto input, {EntityName}Filter filter);
    Task<ResponseDataDto<object>> ActivateAsync(Guid id);          // If hasIsActive=true
    Task<ResponseDataDto<object>> DeactivateAsync(Guid id);       // If hasIsActive=true
    Task<ResponseDataDto<object>> ManageConfigAsync(ManageConfigDto input); // If hasConfig=true
    Task<ResponseDataDto<object>> BulkImportAsync([FromForm] BulkImportFileDto input); // If needsBulkImport=true
    Task<ResponseDataDto<ExportFileBlobDto>> ExportAsync({EntityName}Filter filter); // If needsBulkImport=true
    Task<ResponseDataDto<DropDownDto[]>> Get{EntityName}sAsync();
}

2. DTOs ({EntityName}Dtos.cs)

All required DTO classes:

  • {EntityName}Dto - Response DTO (extends EntityDto)
  • CreateUpdate{EntityName}Dto - Input for create/update
  • {EntityName}Filter - Filter and pagination
  • {EntityName}ExportDto - For export operations
  • ManageConfigDto - If hasConfig=true
  • {EntityName}ImportDto - If needsBulkImport=true

Application Layer ({Project}.Application/AppServices/)

3. Implementation ({EntityName}AppService.cs)

Complete implementation with:

  • Constructor injection of all dependencies
  • Full CRUD methods with try-catch and logging
  • Validation methods (ValidateInput, ValidateDeletion, etc.)
  • Optional activation/deactivation methods
  • Optional config management
  • Optional bulk import/export
  • Helper methods (Get{EntityName}Async, BulkImportAsync)

Usage

/crud-generator EntityName [options]

Required Argument

  • EntityName: PascalCase singular entity name (e.g., User, Product, Department)

Options (key=value pairs, comma-separated)

Option Values Default Description
hasIsActive true | false true Include Activate/Deactivate methods
hasConfig true | false false Generate config management methods
needsBulkImport true | false false Include Export and BulkImport methods
permission string EntityName Base permission name (e.g., ApiClients, Users)
contractsNamespace string {RootNamespace}.Application.Contracts.{PluralEntityName} Namespace for interface & DTOs
appServiceNamespace string {RootNamespace}.Application.AppServices.{PluralEntityName} Namespace for implementation
dtoSuffix Dto | Input | custom Dto Suffix for DTO classes

Note: {RootNamespace} should be replaced with your project's root namespace (e.g., Acme.Crm, MyCompany.Ecommerce).

Examples

# Standard CRUD with activation (like User) - auto-detects layered architecture
/crud-generator User

# Entity without IsActive (like AuditLog)
/crud-generator AuditLog hasIsActive:false,permission:AuditLogs

# Full-featured with config + bulk import (like ApiClient)
/crud-generator ApiClient hasConfig:true,needsBulkImport:true,permission:ApiClients

# Specify custom namespaces (generic example)
/crud-generator Order \
  contractsNamespace:{RootNamespace}.Application.Contracts.Orders \
  appServiceNamespace:{RootNamespace}.Application.AppServices.Orders

# Entity with custom DTO naming convention
/crud-generator Customer dtoSuffix:Input permission:Customers

# Full customization
/crud-generator Product \
  hasIsActive:false \
  hasConfig:false \
  needsBulkImport:false \
  permission:Products \
  contractsNamespace:{RootNamespace}.Application.Contracts.Products \
  appServiceNamespace:{RootNamespace}.Application.AppServices.Products

Output Location

The skill generates files in two separate layers using namespace-based paths. You can run the command from any directory, but must provide namespace paths.

Standard ABP Layered Architecture (Recommended)

Files are distributed across two projects:

{ProjectRoot}/
├── src/
│   ├── {Project}.Application.Contracts/
│   │   └── {PluralEntityName}/
│   │       ├── I{EntityName}AppService.cs
│   │       └── {EntityName}Dtos.cs
│   └── {Project}.Application/
│       └── AppServices/
│           └── {PluralEntityName}/
│               └── {EntityName}AppService.cs

Generated files:

  1. Contracts ({contractsNamespace}):

    • I{EntityName}AppService.cs (interface)
    • {EntityName}Dtos.cs (all DTOs: {EntityName}Dto, CreateUpdate{EntityName}Dto, {EntityName}Filter, {EntityName}ExportDto, etc.)
  2. Application ({appServiceNamespace}):

    • {EntityName}AppService.cs (implementation)

Namespace-to-Path Conversion

The skill converts namespace paths to directory paths automatically:

# Namespace: {RootNamespace}.Application.Contracts.Customers
# Creates: src/{RootNamespace}.Application.Contracts/Customers/

# Namespace: {RootNamespace}.Application.AppServices.Customers
# Creates: src/{RootNamespace}.Application/AppServices/Customers/

Single-Layer Mode (Legacy)

If your project uses a single AppServices folder for everything:

# Navigate to the AppServices directory
cd src/MyProject.Application/AppServices/

# Generate all files here (no contractsNamespace needed)
/crud-generator Customer

This creates:

  • ICustomerAppService.cs
  • CustomerAppService.cs
  • CustomerDtos.cs

Auto-Detection Logic

The skill attempts to intelligently detect your project structure:

  1. Look for Contracts layer:

    • Searches current and parent directories for folder named {RootNamespace}.Application.Contracts
    • If found, sets default contractsNamespace to {DetectedNamespace}.{PluralEntityName}
  2. Look for Application layer:

    • Searches for folder named {RootNamespace}.Application
    • If found, sets default appServiceNamespace to {DetectedNamespace}.AppServices.{PluralEntityName}
  3. Decision:

    • If both contracts and app service patterns detected → Layered mode
    • If only application folder found → Single-layer mode
    • If neither found → You must explicitly specify namespaces

Explicit Override

Always use explicit namespaces for reproducibility:

/crud-generator EntityName \
  contractsNamespace:YourCompany.YourApp.Application.Contracts.EntityNamePlural \
  appServiceNamespace:YourCompany.YourApp.Application.AppServices.EntityNamePlural

Tip: Use placeholders like {RootNamespace} throughout your codebase for consistency. Replace {RootNamespace} with your actual root namespace (e.g., Acme, Contoso, MyCompany).

Reference Pattern

The generated code follows ABP Framework best practices and standard DDD patterns.

Key Characteristics

Logging Pattern:

_logger.LogInformation("{EntityName}AppService - {MethodName}: Requested by {UserId}", nameof(CreateAsync), currentUser.Id);
_logger.LogDebug("{EntityName}AppService - {MethodName}: Input = {@Input}", nameof(CreateAsync), input);
_logger.LogWarning("{EntityName}AppService - Validation: {Message}", validationMessage);
_logger.LogError(ex, "{EntityName}AppService - {MethodName}: Error occurred", nameof(CreateAsync));

Error Handling:

try
{
    // Business logic with validation first
    await ValidateInputAsync(input);
    // Perform operation
}
catch (UserFriendlyException)
{
    // Re-throw user-friendly validation errors
    throw;
}
catch (Exception ex)
{
    _logger.LogError(ex, "{EntityName}AppService - {MethodName}: Unexpected error", methodName);
    throw new UserFriendlyException("An unexpected error occurred. Please try again.");
}

Response Wrapper:

return new ResponseDataDto<T>
{
    Success = true,
    Code = 200,
    Message = "Operation completed successfully",
    Data = result
};

Unit of Work:

using var uow = _unitOfWorkManager.Begin();
// Perform operations
await uow.CompleteAsync();

Repository Access:

private readonly IRepository<{EntityName}, Guid> _{entityName}Repository;
// Use AsyncExecuter for async queries
var queryable = await _{entityName}Repository.GetQueryableAsync();
var result = await AsyncExecuter.FirstOrDefaultAsync(queryable.Where(...));

Customization Points

After generation, customize these sections:

1. DTO Properties (Contracts Layer)

Update {EntityName}Dto to include all entity fields you want to expose:

// In {EntityName}Dtos.cs
public class {EntityName}Dto
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public Guid CategoryId { get; set; }
    public string CategoryName { get; set; } // Navigation property
    public bool IsActive { get; set; }
    public DateTime CreationTime { get; set; }
    // Add other fields as needed
}

// In CreateUpdate{EntityName}Dto
public class CreateUpdate{EntityName}Dto
{
    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    [Range(0, double.MaxValue)]
    public decimal Price { get; set; }

    [Range(0, int.MaxValue)]
    public int StockQuantity { get; set; }

    public Guid CategoryId { get; set; }

    // Add other fields with appropriate validation attributes
}

2. Entity Mapping (Implementation)

In CreateAsync and UpdateAsync, map DTO properties to Entity:

// CreateAsync
var entity = new {EntityName}
{
    Name = input.Name,
    Price = input.Price,
    StockQuantity = input.StockQuantity,
    CategoryId = input.CategoryId,
    IsActive = input.IsActive,
    // Map all other properties
};
await _{entityName}Repository.InsertAsync(entity);

// UpdateAsync
var entity = await Get{EntityName}Async(id);
entity.Name = input.Name;
entity.Price = input.Price;
// Update all other properties
await _{entityName}Repository.UpdateAsync(entity);

3. Validation Rules

Extend ValidateInputAsync with business rules:

private async Task ValidateInputAsync(CreateUpdate{EntityName}Dto input, Guid? id = null)
{
    // Required field validation
    if (string.IsNullOrWhiteSpace(input.Name))
    {
        throw new UserFriendlyException("{EntityName} name is required.");
    }

    // Numeric validation
    if (input.Price <= 0)
    {
        throw new UserFriendlyException("Price must be greater than zero.");
    }

    // Foreign key validation
    var categoryExists = await _categoryRepository.AnyAsync(c => c.Id == input.CategoryId);
    if (!categoryExists)
    {
        throw new UserFriendlyException("Selected category does not exist.");
    }

    // Uniqueness validation (optional)
    var queryable = await _{entityName}Repository.GetQueryableAsync();
    var duplicate = await AsyncExecuter.FirstOrDefaultAsync(
        queryable.Where(e => e.SKU == input.SKU && (!id.HasValue || e.Id != id.Value))
    );
    if (duplicate != null)
    {
        throw new UserFriendlyException("An item with the same SKU already exists.");
    }
}

4. Search & Filter Logic

Enhance GetListAsync to support filtering on additional fields:

var query = _{entityName}Repository.GetQueryableAsync()
    .WhereIf(!string.IsNullOrWhiteSpace(filter.SearchKeyword),
        e => e.Name.ToLower().Contains(filter.SearchKeyword) ||
             e.SKU.ToLower().Contains(filter.SearchKeyword) ||
             e.Description.ToLower().Contains(filter.SearchKeyword))
    .WhereIf(filter.MinPrice.HasValue,
        e => e.Price >= filter.MinPrice.Value)
    .WhereIf(filter.MaxPrice.HasValue,
        e => e.Price <= filter.MaxPrice.Value)
    .WhereIf(filter.CategoryId.HasValue,
        e => e.CategoryId == filter.CategoryId.Value);

5. Foreign Key Lookups

Implement Get{EntityName}sAsync for dropdown lists (if referenced by other entities):

public async Task<ResponseDataDto<DropDownDto[]>> Get{EntityName}sAsync()
{
    var items = await _{entityName}Repository.GetQueryableAsync()
        .Select(e => new DropDownDto
        {
            Value = e.Id.ToString(),
            Name = e.Name,
            // Optional: Description = e.Description
        })
        .ToArrayAsync();

    return new ResponseDataDto<DropDownDto[]>
    {
        Success = true,
        Data = items,
        Code = 200
    };
}

6. Deletion Validation

Check for dependent records before deletion:

private async Task<ResponseDataDto<object>> Validate{EntityName}DeletionAsync(Guid id)
{
    var errors = new List<string>();

    // Check for orders
    var hasOrders = await _orderRepository.AnyAsync(o => o.{EntityName}Id == id);
    if (hasOrders)
    {
        errors.Add("This {entityName} has associated orders.");
    }

    // Check for active subscriptions
    var hasSubscriptions = await _subscriptionRepository.AnyAsync(s => s.{EntityName}Id == id && s.IsActive);
    if (hasSubscriptions)
    {
        errors.Add("This {entityName} has active subscriptions.");
    }

    if (errors.Any())
    {
        return new ResponseDataDto<object>
        {
            Success = false,
            Code = 400,
            Message = string.Join(" ", errors)
        };
    }

    return null; // No errors
}

Call this in DeleteAsync:

var validationResult = await Validate{EntityName}DeletionAsync(id);
if (validationResult != null)
{
    return validationResult;
}

7. Deactivation Rules (Optional)

Override ValidateDeactivationAsync to prevent deactivation under certain conditions:

private async Task ValidateDeactivationAsync(Guid id)
{
    var entity = await Get{EntityName}Async(id);

    // Prevent deactivation if entity is in use
    var isInUse = await _usageRepository.AnyAsync(u => u.{EntityName}Id == id && u.IsActive);
    if (isInUse)
    {
        throw new UserFriendlyException("Cannot deactivate {entityName} because it is currently in use.");
    }
}

8. Permissions (Authorization)

Define permission constants in your PermissionDefinitionProvider:

public const string {EntityName}Default = PermissionGroupName + ".{EntityName}s.Default";
public const string {EntityName}Create = {EntityName}Default + ".Create";
public const string {EntityName}Edit = {EntityName}Default + ".Edit";
public const string {EntityName}Delete = {EntityName}Default + ".Delete";
public const string {EntityName}Activate = {EntityName}Default + ".Activate";
public const string {EntityName}Deactivate = {EntityName}Default + ".Deactivate";

Apply to methods:

[Authorize(Permissions.{EntityName}.Create)]
public async Task<ResponseDataDto<object>> CreateAsync(CreateUpdate{EntityName}Dto input) { ... }

[Authorize(Permissions.{EntityName}.Activate)]
public async Task<ResponseDataDto<object>> ActivateAsync(Guid id) { ... }

9. Localization

Add localized error messages to your resource files:

// en.json
{
  "texts": {
    "{EntityName}:NameRequired": "{EntityName} name is required.",
    "{EntityName}:DuplicateName": "A {entityName} with this name already exists.",
    "{EntityName}:DeletionBlocked": "Cannot delete {entityName} because it has associated records."
  }
}

And reference them:

throw new UserFriendlyException(L["{EntityName}:NameRequired"]);

Required Dependencies

Ensure these NuGet packages are referenced in your Application.Contracts and Application projects:

<PackageReference Include="Volo.Abp.Application" Version="8.0.0" />
<PackageReference Include="Volo.Abp.Domain" Version="8.0.0" />
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="8.0.0" />

Project References (for layered architecture):

<!-- In Application project .csproj -->
<ProjectReference Include="..\..\Application.Contracts\{Project}.Application.Contracts.csproj" />

Note: Adjust package versions to match your ABP Framework version (7.x, 8.x, or later).

Optional Dependencies (for specific features)

If using needsBulkImport:true or hasConfig:true, you may need:

<!-- Project-specific Blob DTO library (create your own or use existing) -->
<PackageReference Include="{Project}.BlobDto" Version="1.0.0" />

<!-- Generic Bulk Import service (create your own or use existing) -->
<PackageReference Include="{Project}.GenericBulkImport" Version="1.0.0" />

<!-- JSON Export/Import services -->
<PackageReference Include="{Project}.JsonServices" Version="1.0.0" />

Note: Bulk import and export dependencies are project-specific. You may need to:

  • Create your own IExportService implementation (using CsvHelper, ClosedXML, Newtonsoft.Json, etc.)
  • Implement IBulkImportService for file parsing
  • Or reuse existing services from your project's infrastructure

Integration Checklist

After generating files:

Contracts Layer ({Project}.Application.Contracts/)

  • Review DTO properties: Ensure they match your Entity class properties
  • Add validation attributes: Update CreateUpdate{EntityName}Dto with [Required], [StringLength], etc.
  • Update interface: Verify method signatures match between contracts and implementation
  • Add permission constants: Define in PermissionDefinitionProvider if using authorization

Application Layer ({Project}.Application/AppServices/)

  • Update constructor dependencies: Add required repositories (IRepository<Entity, Guid>, etc.)
  • Implement entity mapping: Replace property assignments in CreateAsync/UpdateAsync
  • Add custom validations: Extend ValidateInput method with business rules
  • Add reference lookups: Implement Get{EntityName}Async helper methods for foreign keys
  • Check activation rules: Verify ActivateAsync/DeactivateAsync have correct logic
  • Configure export (if applicable): Ensure ExportAsync uses correct DTO and filter
  • Update filtering: Add entity-specific fields to GetListAsync WhereIf clauses
  • Register service: Usually auto-discovered via ABP's conventional registration
  • Verify DI container: All dependencies should be registered in your DbContext and module

Database & Testing

  • Create migration (if entity is new): dotnet ef migrations add Add{EntityName}
  • Apply migration: dotnet ef database update
  • Test endpoints: Verify all CRUD operations work correctly
    • POST /api/app/{entityName}/create
    • PUT /api/app/{entityName}/{id}
    • DELETE /api/app/{entityName}/{id}
    • GET /api/app/{entityName}/{id}
    • GET /api/app/{entityName}?page=1&pageSize=10
    • POST /api/app/{entityName}/activate/{id} (if hasIsActive)
    • POST /api/app/{entityName}/deactivate/{id} (if hasIsActive)
    • GET /api/app/{entityName}/export (if needsBulkImport)
  • Update Swagger: Add XML comments for API documentation
  • Add unit tests: Follow xunit-testing-patterns skill

Common Scenarios

Scenario 1: New Entity - Standard ABP Layered (Auto-detection)

# Navigate to project root (where .sln is)
cd /path/to/{RootNamespace}

# Skill auto-detects layered architecture
/crud-generator Product hasIsActive:false,permission:Products

# Result ( Creates in standard locations ):
# - src/{RootNamespace}.Application.Contracts/Products/IProductAppService.cs
# - src/{RootNamespace}.Application.Contracts/Products/ProductDtos.cs
# - src/{RootNamespace}.Application/AppServices/Products/ProductAppService.cs

Scenario 2: New Entity - Explicit Namespaces (Guaranteed)

# From any directory, specify exact namespaces
/crud-generator Customer \
  contractsNamespace:Acme.Crm.Application.Contracts.Customers \
  appServiceNamespace:Acme.Crm.Application.AppServices.Customers \
  permission:Customers

# Result:
# - src/Acme.Crm.Application.Contracts/Customers/ICustomerAppService.cs
# - src/Acme.Crm.Application.Contracts/Customers/CustomerDtos.cs
# - src/Acme.Crm.Application/AppServices/Customers/CustomerAppService.cs

Scenario 3: Entity with Configuration & Bulk Import

/crud-generator Subscription \
  hasConfig:true \
  needsBulkImport:true \
  permission:Subscriptions \
  contractsNamespace:Contoso.Billing.Application.Contracts.Subscriptions \
  appServiceNamespace:Contoso.Billing.Application.AppServices.Subscriptions

# Includes:
# - ISubscriptionAppService.cs with ManageConfigAsync
# - SubscriptionConfig repository (you'll need to implement)
# - ExportAsync and BulkImportAsync
# - SubscriptionImportDto

Scenario 4: Simple Entity (No Activation, No Bulk Import)

/crud-generator Tag hasIsActive:false,permission:Tags
# Generates: Create, Update, Delete, Get, GetList only

Scenario 5: Single-Layer Project (Everything in AppServices)

cd src/LegacyProject.Application/AppServices/

/crud-generator Log hasIsActive:false,permission:Logs

# All 3 files in current directory:
# - ILogAppService.cs
# - LogAppService.cs
# - LogDtos.cs

Scenario 6: Different DTO Naming Convention

Some teams prefer Create{Entity}Input instead of CreateUpdate{Entity}Dto:

/crud-generator Invoice dtoSuffix:Input permission:Invoices

# Generates:
# - CreateInvoiceInput (instead of CreateUpdateInvoiceDto)
# - InvoiceDto
# - InvoiceFilter
# - InvoiceExportDto

Troubleshooting

Issue Solution
Files not generated Ensure you're in a writable directory
Compilation errors Check all required using statements are present
Repository not found Add IRepository<{EntityName}, Guid> to your DbContext
Permission errors Verify permissions exist in PermissionDefinitionProvider
Duplicate SystemName error Case-insensitive; check for existing data with different case
JSON validation fails Ensure RequestConfig is valid JSON Schema format
Import file format error Verify file matches {EntityName}ImportDto structure exactly
NSubstitute not found Install NSubstitute NuGet package in test project

Design Decisions Explained

Why This Pattern?

  1. Single Responsibility: Each method handles one operation
  2. Consistent Error Handling: All methods follow same try-catch pattern
  3. Structured Logging: Every operation logs start, success, failure
  4. Authorization Granularity: Separate permissions per operation
  5. User-Friendly Errors: UserFriendlyException for client errors
  6. Transaction Management: Unit of Work per operation
  7. Validation First: Validate before any database operation
  8. DTO Boundaries: No entity leakage beyond repository layer

Why Include Activation Pattern?

Enables soft delete / archival pattern while maintaining referential integrity and audit trails.

Why Bulk Import with Upsert?

Allows efficient data synchronization and initial data seeding without duplication.

References

Design Decisions Explained

Why This Pattern?

  1. Single Responsibility: Each method handles one operation
  2. Consistent Error Handling: All methods follow same try-catch pattern
  3. Structured Logging: Every operation logs start, success, failure
  4. Authorization Granularity: Separate permissions per operation
  5. User-Friendly Errors: UserFriendlyException for client errors
  6. Transaction Management: Unit of Work per operation
  7. Validation First: Validate before any database operation
  8. DTO Boundaries: No entity leakage beyond repository layer

Why Include Activation Pattern?

Enables soft delete / archival pattern while maintaining referential integrity and audit trails. Instead of physically deleting records, you mark them as inactive, preserving historical data and maintaining relationships.

Why Bulk Import with Upsert?

Allows efficient data synchronization and initial data seeding without duplication. The generated upsert logic:

  • Inserts new records (by unique key, e.g., Name/Code)
  • Updates existing records (by unique key)
  • Validates each row individually, collecting all errors

Why Separate Contracts and Application?

Standard ABP layered architecture provides:

  • Separation of concerns: Contracts define the API surface, Application implements it
  • Shared contracts: Clients can reference only Contracts project, not full Application
  • Versioning: Contracts can be versioned independently
  • Clean architecture: Dependencies flow inward (Application → Contracts)

Architecture Considerations

Multi-Tenancy

If your ABP project uses multi-tenancy, ensure your queries include tenant filtering:

var queryable = await _{entityName}Repository.GetQueryableAsync();
// ABP automatically filters by CurrentTenant.Id when using repositories
// No additional code needed if using standard ABP repositories

Soft Delete vs. IsActive

The generated code uses an IsActive boolean for activation/deactivation. This is separate from ABP's built-in ISoftDelete pattern. If you prefer soft delete:

  1. Make your entity implement ISoftDelete
  2. Use DeleteAsync for soft delete (sets IsDeleted = true)
  3. Use RestoreAsync for recovery
  4. Remove IsActive field and Activate/Deactivate methods (or repurpose them)

Choose based on your business requirements:

  • Soft Delete: Data is hidden but retained for audit/history
  • IsActive: Entity can be toggled on/off while still visible in queries

Authorization Strategies

Generated methods are permission-ready but not decorated with [Authorize] by default. Choose your approach:

Per-method authorization:

[Authorize(Permissions.{EntityName}.Create)]
public async Task<ResponseDataDto<object>> CreateAsync(...) { ... }

Controller-level authorization (API Controllers):

[Authorize(Permissions.{EntityName}.Default)]
[Route("api/app/{entityName-plural}")]
public class {EntityName}Controller : AbpController

No authorization (public APIs): Don't add [Authorize]


Performance Tips

  1. Index your database: Add indexes on frequently queried columns (Name, Code, foreign keys)
  2. Use pagination: GetListAsync already uses pagination via PagedAndSortedResultRequestDto
  3. Select only needed fields: DTO projection already optimizes this
  4. Cache dropdown data: Get{EntityName}sAsync can benefit from caching if data is static
  5. Batch operations: For bulk updates, consider implementing batch endpoints separately

Troubleshooting

Issue Solution
Files not generated Ensure you're in a writable directory and have permission to create files
Compilation errors Check using statements match your project's namespace structure
Repository not found Add DbSet<Entity> to your DbContext and ensure IRepository<Entity, Guid> is registered
Permission errors Define permission constants in your PermissionDefinitionProvider or remove [Authorize] attributes
Duplicate name error Uniqueness checks are case-insensitive; check for existing data with different casing
Auto-detection fails Provide explicit contractsNamespace and appServiceNamespace options
Namespace conflicts Use unique plural forms for entity name (e.g., PersonPeople, not Persons)
AsyncExecuter not found Add using Volo.Abp.Linq; to your implementation file

Contributing

This skill is designed to be generic and reusable across all ABP projects. When customizing:

  1. Preserve placeholders: Keep {EntityName}, {PluralEntityName}, {Project} for reusability
  2. Test with multiple projects: Verify generation works with different namespace patterns
  3. Document edge cases: Update this doc with lessons learned
  4. Share improvements: Contribute back to the team's skill library

Glossary

  • Entity: Your domain entity class (e.g., Product, Customer, Order)
  • PluralEntityName: Plural form of entity name (e.g., Products, Customers, Orders)
  • Contracts Layer: {Project}.Application.Contracts - Contains DTOs and interfaces
  • AppServices Layer: {Project}.Application.AppServices - Contains business logic implementations
  • ABP: ASP.NET Boilerplate (modern ABP Framework)
  • DDD: Domain-Driven Design
  • DTO: Data Transfer Object
  • CRUD: Create, Read, Update, Delete
  • UoW: Unit of Work pattern
  • EF Core: Entity Framework Core

Post-Generation Workflow

Immediate Post-Generation

  1. Verify file locations:

    • Interface and DTOs in {Project}.Application.Contracts/{PluralEntityName}/
    • Implementation in {Project}.Application/AppServices/{PluralEntityName}/
  2. Contracts layer updates ({Project}.Application.Contracts/):

    • Review and customize {EntityName}Dto properties to match your Entity
    • Add validation attributes to CreateUpdate{EntityName}Dto
    • Update {EntityName}Filter with additional filter fields if needed
    • Ensure interface matches implementation
  3. Application layer updates ({Project}.Application/AppServices/):

    • Update constructor to inject all required repositories
    • Implement property mapping in CreateAsync and UpdateAsync
    • Add business-specific validation in ValidateInputAsync
    • Update GetListAsync filtering logic for your entity's searchable fields
    • Implement Get{EntityName}Async helper if not already present
    • Customize ActivateAsync/DeactivateAsync logic as needed
    • Add foreign key dependency checks in deletion validation
  4. Permissions: Add to your PermissionDefinitionProvider:

    public const string {EntityName}Default = PermissionGroupName + ".{EntityName}s.Default";
    public const string {EntityName}Create = {EntityName}Default + ".Create";
    public const string {EntityName}Edit = {EntityName}Default + ".Edit";
    public const string {EntityName}Delete = {EntityName}Default + ".Delete";
    // Optional:
    public const string {EntityName}Activate = {EntityName}Default + ".Activate";
    public const string {EntityName}Deactivate = {EntityName}Default + ".Deactivate";
    
  5. Database: Create and apply migrations if entity table doesn't exist:

    dotnet ef migrations add Add{EntityName}Table --project src/{RootNamespace}.Domain
    dotnet ef database update --project src/{RootNamespace}.Domain
    
  6. Testing: Write unit tests and integration tests following your project's testing patterns.

  7. Localization: Add error message keys to resource files for internationalization.

  8. API Documentation: Add XML comments to interface methods for Swagger/OpenAPI.

Ongoing Maintenance

  • Keep Contracts and Implementation in sync when modifying interfaces
  • Update both layers when adding new methods or changing signatures
  • Follow the established patterns in existing services for consistency

Notes

ABP Layered Architecture

This skill supports both single-layer and two-layer architectures:

  • Two-layer (Standard ABP): Interface & DTOs in {Project}.Application.Contracts, implementation in {Project}.Application.AppServices
  • Single-layer (Legacy): All files in one AppServices folder (for reference compatibility)

The skill auto-detects based on project structure, or you can explicitly specify namespaces.

Code Readiness

  • Generated code is ready to compile after property customization
  • Follows ABP 8.0+ conventions and .NET 8.0
  • Assumes EF Core with Guid primary keys
  • Uses ApplicationService as base class (standard ABP)
  • Response wrapper is ResponseDataDto<T> (project-specific)
  • Adjust base class and response types for other ABP projects

Namespace Conventions

  • Contracts: {RootNamespace}.Application.Contracts.{PluralEntityName} (e.g., Acme.Crm.Application.Contracts.Customers)
  • AppServices: {RootNamespace}.Application.AppServices.{PluralEntityName} (e.g., Acme.Crm.Application.AppServices.Customers)

Auto-Detection

The skill looks for folders named {Project}.Application.Contracts and {Project}.Application in the current or parent directories. If both exist, layered mode is automatic.

Weekly Installs
1
GitHub Stars
14
First Seen
1 day ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1