Microsoft logo

Skill

accordant-operations

define operations with Apply and Execute methods

Covers Architecture Microsoft Engineering

Description

How to define operations with Apply and Execute methods - use this skill when creating spec operations or understanding the Apply/Execute pattern

SKILL.md

Operations in Accordant

An operation represents an atomic action your system can perform. Each operation has two parts:

  • Apply: Describes what should happen (pure spec logic)
  • Execute: Makes it actually happen (calls the real system)

Defining Operations

Inline Syntax

For simple specs, define operations inline:

var spec = new Spec<BankState>();

spec.Operation<string, ApiResult<decimal>>("CreateAccount", (accountId, state) =>
{
    // Apply logic here
    if (state.Accounts.ContainsKey(accountId))
        return Expect.That<ApiResult<decimal>>(r => r.IsConflict).SameState();

    return Expect.That<ApiResult<decimal>>(r => r.IsSuccess && r.Data == 0)
           .ThenState<BankState>(s => s.Accounts[accountId] = 0);
});

Binding Execution

Connect spec operations to real system calls:

spec.ExecuteWith<BankApiClient>()
    .Bind<string, ApiResult<decimal>>("CreateAccount",
        (client, accountId) => client.CreateAccount(accountId).Result)
    .Bind<(string, decimal), ApiResult<decimal>>("Withdraw",
        (client, req) => client.Withdraw(req.Item1, req.Item2).Result);

Class-Based Operations

For complex operations, use classes:

public class WithdrawOperation : Operation<WithdrawRequest, ApiResult<decimal>, BankState>
{
    public override ExpectedOutcomes Apply(WithdrawRequest request, BankState state)
    {
        if (!state.Accounts.TryGetValue(request.AccountId, out var balance))
            return Expect.That<ApiResult<decimal>>(r => r.IsNotFound).SameState();

        if (balance < request.Amount)
            return Expect.That<ApiResult<decimal>>(r => r.IsBadRequest).SameState();

        var newBalance = balance - request.Amount;
        return Expect.That<ApiResult<decimal>>(r => r.IsSuccess && r.Data == newBalance)
               .ThenState<BankState>(s => s.Accounts[request.AccountId] = newBalance);
    }

    public override async Task<ApiResult<decimal>> ExecuteAsync(
        WithdrawRequest request, 
        TestingContext context)
    {
        var client = context.Get<BankApiClient>();
        return await client.WithdrawAsync(request.AccountId, request.Amount);
    }
}

The Apply Method

Apply defines expected behavior. Given a request and state, return ExpectedOutcomes:

spec.Operation<(string AccountId, decimal Amount), ApiResult<decimal>>("Deposit", (request, state) =>
{
    // Check preconditions first (guard clauses)
    if (!state.Accounts.TryGetValue(request.AccountId, out var balance))
        return Expect.That<ApiResult<decimal>>(r => r.IsNotFound).SameState();

    // Happy path last
    var newBalance = balance + request.Amount;
    return Expect.That<ApiResult<decimal>>(r => r.IsSuccess && r.Data == newBalance)
           .ThenState<BankState>(s => s.Accounts[request.AccountId] = newBalance);
});

Key points:

  • Apply is pure — no side effects, no API calls
  • Check error conditions first with early returns
  • Return expected response + state transition

Expect.That — Validating Responses

Basic Predicate

Expect.That<int>(r => r == 42)
Expect.That<ApiResult<decimal>>(
    r => r.IsSuccess && r.Data == expectedBalance,
    $"Should return 200 OK with balance {expectedBalance}")

Explanations appear in test failure output — crucial for debugging.

With ValidationResult

For complex validations with detailed error messages:

Expect.That<User>(response =>
{
    if (response.Name != expectedName)
        return ValidationResult.Invalid($"Name was '{response.Name}', expected '{expectedName}'");
    if (response.Email == null)
        return ValidationResult.Invalid("Email was null");
    return ValidationResult.Valid();
})

State Transitions

No Change: .SameState()

// Error cases
return Expect.That<ApiResult<User>>(r => r.IsNotFound).SameState();

// Read-only operations
return Expect.That<ApiResult<User>>(r => r.IsSuccess).SameState();

State Change: .ThenState()

return Expect.That<ApiResult<decimal>>(r => r.IsSuccess && r.Data == newBalance)
       .ThenState<BankState>(nextState => nextState.Accounts[accountId] = newBalance);

Response-Dependent State

When state depends on server-generated values:

return Expect.That<ApiResult<Order>>(r => r.IsSuccess && !string.IsNullOrEmpty(r.Data.OrderId))
       .ThenState<AppState>(
           (ApiResult<Order> response, AppState nextState) =>
               nextState.Orders[response.Data.OrderId] = new OrderState { /* ... */ },
           mock: () => new ApiResult<Order> 
           { 
               Data = new Order { OrderId = Guid.NewGuid().ToString() },
               StatusCode = 201 
           });

Multiple Valid Outcomes: Expect.OneOf

When more than one outcome is valid (timeouts, non-determinism):

spec.Operation<string, ApiResult<decimal>>("CreateAccount", (accountId, state) =>
{
    if (state.Accounts.ContainsKey(accountId))
        return Expect.That<ApiResult<decimal>>(r => r.IsConflict).SameState();

    return Expect.OneOf(
        // Success
        Expect.That<ApiResult<decimal>>(r => r.IsSuccess && r.Data == 0)
              .ThenState<BankState>(s => s.Accounts[accountId] = 0),
              
        // Timeout - request lost
        Expect.That<ApiResult<decimal>>(r => r.IsTimeout)
              .SameState(),
              
        // Timeout - response lost (account was created)
        Expect.That<ApiResult<decimal>>(r => r.IsTimeout)
              .ThenState<BankState>(s => s.Accounts[accountId] = 0)
    );
});

Exception Expectations

When operations should throw:

spec.Operation<string, Unit>("DeleteUser", (userId, state) =>
{
    if (!state.Users.ContainsKey(userId))
        return Expect.Throws<NotFoundException>("User not found").SameState();

    if (state.Users[userId].HasPendingOrders)
        return Expect.Throws<BusinessRuleException>("Cannot delete user with pending orders").SameState();

    return Expect.That<Unit>(r => true)
           .ThenState<AppState>(s => s.Users.Remove(userId));
});

Use Unit as the response type for void operations.

Writing Strong Predicates

Weak (catches fewer bugs):

Expect.That<ApiResult<User>>(r => r.IsSuccess)  // Only checks status

Strong (catches more bugs):

Expect.That<ApiResult<User>>(
    r => r.IsSuccess && 
         r.Data.Id == expectedId && 
         r.Data.Name == expectedName &&
         r.Data.Email == expectedEmail)

The stronger your predicates, the more bugs you'll catch.

Common Patterns

The Guard-Clause Pattern

spec.Operation<TransferRequest, ApiResult<decimal>>("Transfer", (req, state) =>
{
    // Guards first
    if (!state.Accounts.ContainsKey(req.FromAccount))
        return Expect.That<ApiResult<decimal>>(r => r.IsNotFound, "Source not found").SameState();
    
    if (!state.Accounts.ContainsKey(req.ToAccount))
        return Expect.That<ApiResult<decimal>>(r => r.IsNotFound, "Target not found").SameState();
    
    var balance = state.Accounts[req.FromAccount];
    if (balance < req.Amount)
        return Expect.That<ApiResult<decimal>>(r => r.IsBadRequest, "Insufficient funds").SameState();

    // Happy path last
    var newFromBalance = balance - req.Amount;
    var newToBalance = state.Accounts[req.ToAccount] + req.Amount;
    
    return Expect.That<ApiResult<decimal>>(r => r.IsSuccess && r.Data == newFromBalance)
           .ThenState<BankState>(s => {
               s.Accounts[req.FromAccount] = newFromBalance;
               s.Accounts[req.ToAccount] = newToBalance;
           });
});

Next Steps

  • Test Generation: See how operations combine into test sequences
  • Async Operations: Model background work with step functions

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.