From IBL ABP
Core ABP Framework knowledge for .NET projects: module system, dependency injection, base classes, audit entities, BusinessException with localized DomainErrorCodes, ConcurrencyStamp, localization, soft delete, multi-tenancy basics, and common anti-patterns. Use whenever the user works on ABP modules, ApplicationService, AggregateRoot, IRepository, IClock, CurrentUser, GuidGenerator, ITransientDependency, auto API controllers, or when another abp-* skill needs shared project detection through abp_context.py. Trigger on any ABP-specific work.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ibl-abp:abp-coreThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Official docs: <https://abp.io/docs/latest>
Official docs: https://abp.io/docs/latest API reference: https://abp.io/docs/api/
This skill is the entry point for any work on an ABP Framework project. It
covers the conventions that apply everywhere (modules, DI, base classes,
audit/persistence shape, exceptions, async, time, anti-patterns) and hosts the
shared scripts/abp_context.py module that other abp-* skills import for
project detection and placeholder resolution.
Before generating or modifying ABP code, resolve the project context so generated files use the right names, namespaces, and paths.
python <skills-root>/abp-core/scripts/abp_context.py
# Prints JSON like:
# {
# "project_name": "MyProject",
# "root_namespace": "MyProject",
# "template_type": "layered", # nolayers | layered | microservice
# "data_provider": "mongodb",
# "project_root": "src/MyProject.Domain",
# "solution_root": "C:/path/to/solution"
# }
# Add --show-layout to see where every artifact kind lands for the detected
# template (handy when porting a feature into a layered solution):
python <skills-root>/abp-core/scripts/abp_context.py --show-layout Books
Other abp-* skills import this module:
import sys, os
sys.path.insert(0, os.path.expanduser('<skills-root>/abp-core/scripts'))
from abp_context import load_or_prompt_config, resolve_placeholders, resolve_artifact
ctx = load_or_prompt_config()
code = resolve_placeholders(template_text, ctx)
# Where does a given artifact go? (directory relative to the solution root + namespace)
loc = resolve_artifact(ctx, "dto", "Books")
# layered → loc.dir = "src/MyProject.Application.Contracts/Books", ns "MyProject.Books"
# nolayers → loc.dir = "src/MyProject/Services/Dtos/Books", ns "MyProject.Services.Dtos.Books"
The skills support both the single-project template (nolayers — Simple
Monolith, the IBL360 layout) and the layered DDD template (separate
*.Domain, *.Domain.Shared, *.Application, *.Application.Contracts,
*.MongoDB projects). You never hand-pick paths: resolve_artifact(ctx, kind, plural) returns the correct directory + namespace for the detected template, so
the same skill works on both. Artifact kinds: entity, enum, consts,
error_codes, dto, appservice_interface, appservice_impl,
repo_interface, repo_impl, data_context, permissions, mapper,
data_seed, localization.
The key difference: nolayers bakes the layer into the namespace
(Root.Entities.Books, Root.Services.Books); layered uses a flat
Root.Books namespace across the aggregate and expresses the layer through the
physical project (with Root.MongoDB for persistence and Root.Permissions
for authorization as the two special cases).
Project-wide placeholders: {{PROJECT_NAME}}, {{ROOT_NAMESPACE}},
{{TEMPLATE_TYPE}}, {{DATA_PROVIDER}}, {{PROJECT_ROOT}}, {{SOLUTION_ROOT}}.
Per-layer project placeholders (each collapses onto {{PROJECT_ROOT}} in
nolayers, so one template string works on both): {{DOMAIN_PROJECT}},
{{DOMAIN_SHARED_PROJECT}}, {{APPLICATION_PROJECT}}, {{CONTRACTS_PROJECT}},
{{DATA_PROJECT}}, {{HTTPAPI_PROJECT}}, {{HOST_PROJECT}}.
Every ABP application or library is an AbpModule that declares its
dependencies and configures services.
[DependsOn(
typeof(AbpDddDomainModule),
typeof(AbpAutoMapperModule)
)]
public class {{PROJECT_NAME}}Module : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Register services, options, etc.
}
}
Middleware configuration (OnApplicationInitialization) belongs only in the
final host application, not in reusable modules.
To validate an existing module file:
python <skills-root>/abp-core/scripts/validate_module.py \
--module-path src/MyProject/MyProjectModule.cs
ITransientDependency,
IScopedDependency, or ISingletonDependency instead of calling
services.AddX<T>(...).ApplicationService,
DomainService, AbpController, Profile — don't add a marker interface.IXyzRepository : IRepository<Xyz, TKey> only when you need named query
methods or share a filter chain across endpoints. See the abp-mongodb
skill for the custom repo pattern.[ExposeServices(typeof(IMyService))]
public class MyService : IMyService, ITransientDependency { }
ABP base classes expose the services you'd otherwise inject 90% of the time.
Before adding a constructor parameter, check whether the property already
exists — injecting IClock into an ApplicationService is redundant
because Clock is already a property.
For the full table of base classes and the properties they provide, see references/base-classes.md.
The most commonly used properties:
Clock (all base classes) — use instead of DateTime.Now/UtcNowCurrentUser (all) — .Id, .UserName, .Email, .TenantId,
.IsAuthenticated, .RolesCurrentTenant (all) — multi-tenancy context, including
CurrentTenant.Change(tenantId) for using-scoped switchingGuidGenerator (all) — Create() returns a new GuidL (ApplicationService, AbpController) — L["MyKey"] localizationDataFilter (all) — toggle soft-delete / multi-tenant filters with usingUseful methods:
CheckPolicyAsync(permission) — throws if not grantedIsGrantedAsync(permission) — returns bool, never throwsABP exposes a ladder of base classes; pick the lowest rung that gives you what you need. The choice impacts auditing, soft-delete behavior, and the shape of every endpoint that touches the entity.
| Base | Adds | When to pick |
|---|---|---|
Entity<TKey> | nothing | Trivial value-object-like records, no audit needed |
AggregateRoot<TKey> | Domain events | DDD aggregates with no audit requirements |
AuditedAggregateRoot<TKey> | CreationTime, CreatorId, LastModificationTime, LastModifierId | Default for most CRUD entities. Cheap, useful, expected by admin tooling. |
FullAuditedAggregateRoot<TKey> | + DeleterId, DeletionTime, IsDeleted (soft delete) | Records that should never be hard-deleted (legal, financial, audit), or where accidental delete is a real risk and you want a recycle bin |
Soft delete is implicit with FullAuditedAggregateRoot — DeleteAsync
sets IsDeleted = true and queries filter it out automatically. To see
deleted rows for an admin "recycle bin":
using (DataFilter.Disable<ISoftDelete>())
{
var deleted = await _repository.GetListAsync(c => c.IsDeleted);
}
To restore: load via the bypassed filter, set IsDeleted = false,
UpdateAsync.
Don't pick FullAudited "just in case" — soft delete complicates many
queries (joins, aggregates, uniqueness constraints) and most teams
underestimate the cost.
If the entity is edited by many users with long edit sessions and the cost
of a lost update is high, mark it IHasConcurrencyStamp:
public class Customer : AuditedAggregateRoot<Guid>, IHasConcurrencyStamp
{
public string ConcurrencyStamp { get; set; } = default!;
// …
}
The output DTO carries the current stamp; the client passes it back on update; the AppService verifies and updates atomically:
public async Task<CustomerDto> UpdateAsync(Guid id, UpdateCustomerDto input)
{
var customer = await _repository.GetAsync(id);
customer.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp);
customer.Update(/* … */);
await _repository.UpdateAsync(customer);
return ObjectMapper.Map<Customer, CustomerDto>(customer);
}
ABP throws AbpDbConcurrencyException (HTTP 409 by default) when stamps
mismatch.
For most admin CRUD, skip it — the cost is a stamp the client must round-trip and a 409 to handle. Add it only when you have a real concurrency problem.
.Result or .Wait().Async.CancellationToken automatically. Add a CancellationToken
parameter only when you need to layer custom cancellation logic on top.DateTime.Now// ❌ Not testable, ignores ABP's timezone configuration
var ts = DateTime.UtcNow;
// ✅ Inside a base class
var ts = Clock.Now;
// ✅ Elsewhere — inject IClock
public class MyHelper : ITransientDependency
{
public MyHelper(IClock clock) => _clock = clock;
}
// ✅ Inside a non-base-class entity — accept IClock as method parameter
public void ChangeStatus(CustomerStatus newStatus, IClock clock)
{
// …
StatusChangedAt = clock.Now;
}
The pattern of "accept IClock as a method parameter" is what lets entities
remain testable without inheriting from an ABP base class.
Throw BusinessException (or a specialization) with a namespaced error
code. Codes are easier to grep, easier to localize, and easier to map to
HTTP status when grouped under a static class:
// One file, namespace {{ROOT_NAMESPACE}}: at the single project's root in
// nolayers, or in the Domain.Shared project ({{DOMAIN_SHARED_PROJECT}}) in layered.
namespace {{ROOT_NAMESPACE}};
public static class {{ROOT_NAMESPACE}}DomainErrorCodes
{
public static class Customers
{
public const string InvalidStatusTransition = "{{ROOT_NAMESPACE}}:Customers:InvalidStatusTransition";
public const string InvalidCountryCode = "{{ROOT_NAMESPACE}}:Customers:InvalidCountryCode";
public const string InvalidFiscalCode = "{{ROOT_NAMESPACE}}:Customers:InvalidFiscalCode";
}
}
// In the entity:
throw new BusinessException({{ROOT_NAMESPACE}}DomainErrorCodes.Customers.InvalidStatusTransition)
.WithData("From", Status)
.WithData("To", newStatus);
Map the namespace to a localization resource so the message is shown to the user in their language:
Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("{{ROOT_NAMESPACE}}", typeof({{PROJECT_NAME}}Resource));
});
In each language file:
"{{ROOT_NAMESPACE}}:Customers:InvalidStatusTransition":
"Transizione di stato non valida da {From} a {To}."
The {From} / {To} placeholders come from the .WithData(...) calls.
Specialized exceptions: EntityNotFoundException, UserFriendlyException,
AbpValidationException, AbpAuthorizationException,
AbpDbConcurrencyException. HTTP mapping is configurable — don't rely on
defaults in business logic.
L["MyKey"] (IStringLocalizer property).IStringLocalizer<TResource>.*.Domain.Shared/Localization/{Resource}/{lang}.json
(layered) or {{PROJECT_ROOT}}/Localization/{Resource}/{lang}.json
(single-layer).See references/anti-patterns.md for the full list with explanations. The short version:
DbContext in Application Services.DateTime.Now, no manual AddScoped for app code.Guid.NewGuid() in entity constructors — use GuidGenerator.Create()
from outside, pass Guid id into the constructor.| User intent | Skill |
|---|---|
| Add a new entity / DTO / AppService end-to-end | abp-feature-dev |
| Port a schema from Postgres/SQL/another system | abp-feature-dev |
| Decide module ownership, split a monolith, move features between modules | abp-module-architecture |
| Configure MongoDB context, custom repositories, indexes | abp-mongodb |
| Make an entity tenant-aware, switch tenant context, configure tenant resolution | abp-multitenancy |
| Write integration tests for an AppService / DomainService | abp-testing |
This skill (abp-core) covers everything that crosses those areas: modules,
DI, base classes, audit/persistence choices, exceptions, async, time,
localization. When the user pulls in any of those concepts, surface them
here; when the user moves into a specialized concern, delegate.
npx claudepluginhub inno-bit-lab/ibl-agent-plugins --plugin ibl-abpGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.