Browse pages
LS2015: Command Handler Changes Records in a Loop
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 (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.
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#
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#
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.