# LS2014: Command Handler Finds or Creates

## Intent

LS2014 is a compiler check that reports a command handler, the type that runs the state change, that both finds a record and creates it in one method, hiding two use cases behind one name. This page states what the rule reports, where the error sits, and how to split the flow across the boundary that owns it.

## Reference

### What the Rule Enforces

LS2014 enforces [Implement one outcome path per command](https://litespec.dev/4.0.0/backend/application.md#implement-one-outcome-path-per-command) (`litespec/backend/application#implement-one-outcome-path-per-command`). 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 one method that calls a repository method whose name starts in `Find` and creates the found type, through a static method or a constructor, only where the find came back empty, ignoring `Task` and nullability. A creation runs where the find came back empty when it sits on the null side of a test of the found value, after a branch that returns or throws when the value was found, or on the null side of `??` or `??=`. For a constructor the diagnostic sits on the created type; for a static factory it sits on the invoked member name. Its message names the handler and the created type.

### Failing Example

```csharp
internal sealed class RegisterReaderCommandHandler(IReaderRepository readerRepository)
{
    public void Handle(RegisterReaderCommand command)
    {
        var reader = readerRepository.FindByCard(command.CardId)
            ?? Reader.Register(command.CardId);
        readerRepository.Store(reader);
    }
}
```

### Passing Example

```csharp
internal sealed class RegisterReaderCommandHandler(IReaderRepository readerRepository)
{
    public void Handle(RegisterReaderCommand command)
    {
        var reader = Reader.Register(command.CardId);
        readerRepository.Store(reader);
    }
}
```

### How to Fix

Split the flow into two use cases: the boundary that owns the flow runs a query first, then sends the command that fits. When the create loses a race, it refuses with its conflict, and the owner sends the other command. Where the record is usually present, the boundary sends the command first and, on the record's not-found refusal, sends the create and then the command again. Keep that sequence in one helper beside the command mediator, such as `commands.SendToRecord(command).CreatingItOn<ReaderNotFoundException>(register, cancellationToken)`, rather than repeating it in each endpoint.

## Notes

### What the Rule Ignores

The rule does not report a handler that only finds, a handler that only creates, a creation of another type than the found one, a creation that runs whether or not the find came back empty, such as a fine statement that reads the reader's previous statement to carry its balance, a creation after the handler refuses a missing record, a creation inside a switch arm keyed on another value than the found one, or a find and a creation in different methods. It does not report query handlers. It does not run in Domain or WebApi.
