Avanade

coreex-policy

"Create or modify a CoreEx Application-layer policy class. USE FOR: new policy class in Application/Policies/, EnsureExists guard (referenced entity must exist), business rule guards requiring I/O, multi-method policy, composing policies in Result<T> pipelines. DO NOT USE FOR: synchronous validation rules (use coreex-validator), Infrastructure repositories (use coreex-repository), application service scaffolding (use coreex-app-service)."

Avanade 28 8 Updated 3mo ago

Resources

1
GitHub

Install

npx skillscat add avanade/coreex/coreex-policy

Install via the SkillsCat registry.

SKILL.md

CoreEx: Policy

Guides you through creating or modifying a CoreEx Application-layer policy class in Application/Policies/. Policies encapsulate domain-level guard logic that requires async I/O — adapter or repository calls — providing a named, independently testable home for rules that cannot live in a synchronous validator or directly in the domain model.

When to Use

  • A guard check needs to call an adapter (external domain) or a repository — synchronous validators cannot do this
  • An "ensure referenced entity exists" check that must translate NotFoundError into a user-visible validation error
  • A business-rule check (e.g. entity is inactive, quota exceeded) that requires loading state
  • Multiple related guards that share the same adapter/repository dependency — group them in one policy class

When Not to Use

  • Synchronous field validation (no I/O) — use the coreex-validator skill
  • Guard logic that belongs entirely inside the domain aggregate — keep it in the aggregate method
  • Service scaffolding — use the coreex-app-service skill; it covers how to wire a policy into a service

Quick Reference

Clarifying questions before writing any code:
0. Resolve rop-enabled (Result<T> vs exception style — policies compose into Result<T> pipelines) from the solution-root AGENTS.md Feature Configuration before asking the rest; only prompt for what is unrecorded and re-state resolved values for confirmation.

  1. What entity / concept is being guarded? (names the policy class)
  2. What type of guard? EnsureExists / business rule / state check
  3. Which adapter or repository does the policy call?
  4. Should the policy return the loaded entity (Result<T>) or just a pass/fail (Result)?
  5. Multiple guard methods in one class, or one method only?

Key rules at a glance:

  • Policy lives in Application/Policies/{Name}Policy.cs
  • Not DI-registered — instantiated at the call site: new {Name}Policy(_adapter).EnsureExistsAsync(id)
  • Constructor accepts adapters and/or repositories already injected into the calling service
  • Always returns Result or Result<T> — never throws (unless adapter itself throws unexpectedly)
  • EnsureExists: translate r.IsNotFoundError → Result.ValidationError(...) — do not let NotFoundException propagate as-is
  • MessageItem.CreateErrorMessage(nameof(param), "{Entity} was not found.") produces a field-level validation error
  • LText static fields hold localizable entity names used inside message text — e.g. new LText("Product") as the {0} substitution in "{0} was not found.". The property name (nameof(param)) is always a plain string? — never LText
  • Guard methods can return the loaded entity as Result<Contracts.T> so callers can use it without a second fetch
  • Always .ConfigureAwait(false) on every await
  • Always generate a matching {Name}PolicyTests.cs in *.Test.Unit/Policies/ — cover the success path (expect IsSuccess) for each guard method, plus its failure path: IsValidationError for EnsureExists-style not-found guards, IsBusinessError for state/condition guards (e.g. EnsureActive)

For full workflow and code examples see `references/workflow.md`.

Key References

Categories