# LS2015: Command Handler Changes Records in a Loop

## Intent

LS2015 is a compiler check that reports a command handler, the type that runs the state change, changing records in a loop, where one failing record rolls back the batch and the batch holds locks for its whole length. This page states what the rule reports, where the error sits, and how to move the work into a job.

## Reference

### What the Rule Enforces

LS2015 enforces [Issue per-record work from a job](https://litespec.dev/4.0.0/backend/application.md#issue-per-record-work-from-a-job) (`litespec/backend/application#issue-per-record-work-from-a-job`). It runs in Application, Infrastructure, and Worker. Its category is `LiteSpec.Layers`. Severity and enablement follow the [Analyzers overview](https://litespec.dev/4.0.0/analyzers/README.md).

### What the Rule Reports

The rule reports a command handler, which is a class whose name ends in `CommandHandler` or that implements `ICommandHandler`, with a `for`, `foreach`, or `while` body that calls a repository `Store`, `Add`, `Update`, or `Delete` method. The diagnostic sits on the invoked member name, and its message names the handler and the call.

### Failing Example

```csharp
internal sealed class ExpireLoansCommandHandler(ILoanRepository loanRepository)
{
    public void Handle(ExpireLoansCommand command, List<Loan> loans)
    {
        foreach (var loan in loans)
        {
            loan.Expire(command.Now);
            loanRepository.Store(loan);
        }
    }
}
```

### Passing Example

```csharp
internal sealed class ExpireLoanCommandHandler(ILoanRepository loanRepository)
{
    public void Handle(ExpireLoanCommand command, Loan loan)
    {
        loan.Expire(command.Now);
        loanRepository.Store(loan);
    }
}
```

### How to Fix

Move the work into a job that runs one command per record, each in its own unit of work, resuming from its cursor after a failure.

## Notes

### What the Rule Ignores

The rule does not report a loop that builds a result list, a write immediately followed by a return or throw in the same block, a loop that calls `Store`, `Add`, `Update`, or `Delete` on a type that is not a repository, a write outside a loop, or a loop in a query handler or an event handler. It does not run in Domain or WebApi.
