> ## Documentation Index
> Fetch the complete documentation index at: https://docs.diyanet.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Command & Query Deseni

> CQRS handler deseni, klasör konvansiyonu ve gerçek SignUpUser / VerifyOtp / admin query örnekleri

## Desen

Her use-case bir **Command** (yazma) veya **Query** (okuma) ile temsil edilir. Üçlü her zaman birlikte yaşar:

| Parça               | Sorumluluk                                     | Arayüz                                 |
| ------------------- | ---------------------------------------------- | -------------------------------------- |
| **Command / Query** | İstek verisini taşır                           | `IRequest<IResponseWrapper<T>>`        |
| **Validator**       | FluentValidation kuralları                     | `AbstractValidator<TRequest>`          |
| **Handler**         | Orkestrasyon, repository + aggregate çağrıları | `IRequestHandler<TRequest, TResponse>` |

### Klasör konvansiyonu

Her use-case kendi klasöründe izole edilir:

```text theme={null}
Features/<Feature>/<Admin|Website>/<Commands|Queries>/<UseCase>/
├── <UseCase>Command.cs            # veya <UseCase>Query.cs
├── <UseCase>CommandValidator.cs   # (opsiyonel)
├── <UseCase>CommandHandler.cs
└── <Dto>.cs                       # response DTO'su
```

<Note>
  Command'ler genelde `class` + `init` property'leridir; Query'ler `sealed record` pozisyonel syntax kullanır. İkisi de MediatR `IRequest<...>` döner.
</Note>

## Örnek 1 — SignUpUser (Command)

Vatandaş telefon numarasıyla kayıt başlatır; handler bir OTP challenge üretir ve doğrulama için kullanılacak challenge token'ı döner.

### Command

`Features/Authentication/Website/Commands/SignUpUser/SignUpUserCommand.cs`

```csharp theme={null}
public class SignUpUserCommand : IRequest<IResponseWrapper<OtpChallengeDto>>
{
    public string Phone { get; init; }
    public string? FullName { get; init; }
    public ConsentDto? Consents { get; init; }   // Kullanıcının onayladığı açık rızalar
}
```

### Validator

`SignUpUserCommandValidator.cs` — `MustBeValidMobilePhone()` extension'ı Türkiye mobil numara doğrulaması yapar (bkz. [Validation & Mapping](/application/validation-mapping)).

```csharp theme={null}
public class SignUpUserCommandValidator : AbstractValidator<SignUpUserCommand>
{
    public SignUpUserCommandValidator()
    {
        RuleFor(x => x.FullName)
            .NotEmpty().WithMessage("Ad soyad boş olamaz")
            .MaximumLength(100).WithMessage("Ad soyad en fazla 100 karakter olabilir");

        RuleFor(x => x.Phone)
            .Cascade(CascadeMode.Stop)
            .NotEmpty().WithMessage("Telefon numarası gerekli")
            .MustBeValidMobilePhone();
    }
}
```

### Handler

`SignUpUserCommandHandler.cs` — domain servisini, OTP value object'ini, aggregate metodunu ve UnitOfWork'ü orkestre eder:

```csharp theme={null}
public async Task<IResponseWrapper<OtpChallengeDto>> Handle(
    SignUpUserCommand request, CancellationToken cancellationToken = default)
{
    Phone? phone = new Phone(request.Phone);
    FullName? fullname = !string.IsNullOrEmpty(request.FullName)
        ? new FullName(request.FullName) : null;

    // 1) Domain servisi yeni User aggregate'i kurar (benzersizlik kontrolü içinde)
    var user = await _registrationService.RegisterNewUserAsync(fullname, phone, cancellationToken: cancellationToken);

    // 2) OTP value object'i üretilir, aggregate'a challenge eklenir (domain event yayılır)
    var otpCode = OtpCode.Generate();
    var otpType = phone != null ? OtpType.Sms : OtpType.Email;
    user.AddOtpChallenge(otpCode, otpType);

    // 3) Persist + domain event dispatch
    await _repository.AddAsync(user);
    bool result = await _unitOfWork.SaveEntitiesAsync(cancellationToken);
    if (!result)
        throw new ApplicationException("Kayıt işlemi sırasında bir sorun oluştu.");

    // 4) Aktif challenge için challenge token üret (sonraki adım VerifyOtp bunu kullanır)
    var challenge = user?.GetActiveOtpChallenge(otpType);
    if (challenge is null)
        throw new DomainException("Aktif OTP doğrulama kodu bulunamadı");

    var challengeToken = _authService.CreateOtpChallengeToken(user, otpType, challenge.Id);

    return ResponseWrapper<OtpChallengeDto>.Success(new OtpChallengeDto
    {
        ChallengeToken = challengeToken
    });
}
```

<Info>
  SMS gönderimi handler içinde değildir. `user.AddOtpChallenge(...)` bir `UserOtpGeneratedDomainEvent` yayar; `SaveEntitiesAsync` sonrası `SendOtpSmsDomainEventHandler` bunu in-process dinler ve NetGSM üzerinden gönderir. Handler sadece use-case'i orkestre eder.
</Info>

## Örnek 2 — VerifyOtp (Command)

Kullanıcı challenge token'ı (header) + OTP kodu (body) gönderir. Handler kodu doğrular, access/refresh token üretir ve oturum başlatır.

`Features/Authentication/Website/Commands/VerifyOtp/VerifyOtpCommandHandler.cs`

```csharp theme={null}
public async Task<IResponseWrapper<SignInResult>> Handle(
    VerifyOtpCommand request, CancellationToken cancellationToken)
{
    // userId + challengeId challenge token'ından (IOtpChallengeContext) çözülür
    Guid userId      = _otpChallengeContext.UserId;
    Guid challengeId = _otpChallengeContext.ChallengeId;

    // Specification ile aggregate + ilgili challenge yüklenir
    var spec = new UserByOtpChallengeSpecification(userId, challengeId);
    var user = await _userRepository.SingleOrDefaultAsync(spec, cancellationToken);
    if (user is null)
        throw new ApplicationException("Kullanıcı bulunamadı");

    // Aggregate iş kuralları: challenge geçerli mi + kod doğru mu
    if (!user.VerifyChallenge(challengeId, request.Code))
        throw new ApplicationException("Geçersiz veya süresi dolmuş kod");
    if (!user.VerifyOtp(request.Code, OtpType.Sms))
        throw new ApplicationException("Doğrulama kodu hatalı");

    // Token üretimi
    var accessToken  = await _authService.CreateAccessTokenAsync(user, ct: cancellationToken);
    var refreshToken = _authService.CreateRefreshToken(request.IsTrustedDevice);

    // Cihaz / IP bağlamı (BuildingBlocks.DeviceDetector)
    var device   = _clientSecurityContext.GetDevice();
    var clientIp = _clientSecurityContext.GetIp();
    DeviceInfo deviceInfo = DeviceInfo.Create(device.ClientType, device.Browser, /* ... */ device.IsBot);
    ClientIpInfo clientInfo = new ClientIpInfo(clientIp.IpAddress, /* ... */ clientIp.Source);

    var refreshTokenInfo = RefreshTokenInfo.Create(refreshToken.RefreshToken, refreshToken.ExpiresAt);

    // Oturum aggregate üzerinden başlatılır
    user.StartSession(refreshTokenInfo, request.IsTrustedDevice, deviceInfo, clientInfo);
    bool result = await _unitOfWork.SaveEntitiesAsync(cancellationToken);

    var mappedUser = _mapper.Map<UserDto>(user);
    return new ResponseWrapper<SignInResult>(
        new SignInResult(accessToken, refreshToken, mappedUser), result);
}
```

<Warning>
  Specification (`UserByOtpChallengeSpecification`) tüm filtreyi DB tarafına iter ve sadece ilgili challenge'ı yükler — handler içinde manuel `Where`/`Include` yazılmaz. Domain davranışı (`VerifyChallenge`, `VerifyOtp`, `StartSession`) aggregate'ta kapsüllenir; handler bu metodları yalnızca çağırır.
</Warning>

## Örnek 3 — GetAdminBagisBasvuruPlanlar (Query)

Sayfalanmış admin liste sorgusu. `sealed record` syntax'ı, `IReadRepository<T>` (cache decorator), specification, `Paged<T>` ve AutoMapper kullanır.

### Query

`Features/BagisBasvuruPlanlari/Admin/Queries/GetAdminBagisBasvuruPlanlar/GetAdminBagisBasvuruPlanlarQuery.cs`

```csharp theme={null}
public sealed record GetAdminBagisBasvuruPlanlarQuery(AdminBagisBasvuruPlanlarFilter Filter)
    : IRequest<IResponseWrapper<IPaged<AdminBagisBasvuruPlanDto>>>;
```

### Handler

```csharp theme={null}
public sealed class GetAdminBagisBasvuruPlanlarQueryHandler
    : IRequestHandler<GetAdminBagisBasvuruPlanlarQuery, IResponseWrapper<IPaged<AdminBagisBasvuruPlanDto>>>
{
    private readonly IReadRepository<BagisBasvuruPlan> _repository;
    private readonly IMapper _mapper;

    public GetAdminBagisBasvuruPlanlarQueryHandler(IReadRepository<BagisBasvuruPlan> repository, IMapper mapper)
    {
        _repository = repository;
        _mapper     = mapper;
    }

    public async Task<IResponseWrapper<IPaged<AdminBagisBasvuruPlanDto>>> Handle(
        GetAdminBagisBasvuruPlanlarQuery request, CancellationToken cancellationToken)
    {
        var filter = request.Filter ?? new AdminBagisBasvuruPlanlarFilter();
        var spec   = new AdminBagisBasvuruPlanlarPagedSpecification(filter);

        var total  = await _repository.CountAsync(spec, cancellationToken);
        var items  = await _repository.ListAsync(spec, cancellationToken);

        var data  = _mapper.Map<List<AdminBagisBasvuruPlanDto>>(items);
        var paged = new Paged<AdminBagisBasvuruPlanDto>(filter.Page, filter.PageSize, total, data);

        return ResponseWrapper<IPaged<AdminBagisBasvuruPlanDto>>.Success((IPaged<AdminBagisBasvuruPlanDto>)paged);
    }
}
```

<Warning>
  `filter.Page` 0-tabanlıdır. Backend `PaginationHelper` ve specification 0-based sayfa indeksi bekler; frontend `useState(0)` ile başlar. `page=1` gönderilirse ilk sayfa sessizce atlanır.
</Warning>

## ResponseWrapper zarfı

Tüm handler'lar `IResponseWrapper<TResponse>` döner. Şekil:

```csharp theme={null}
public interface IResponseWrapper<TResponse>
{
    TResponse Response { get; }
    string RequestId { get; }
    bool IsSuccess { get; }
    Exception Exception { get; }
}
```

`ResponseWrapper<T>` static factory metodları sunar:

```csharp theme={null}
ResponseWrapper<OtpChallengeDto>.Success(dto);             // IsSuccess = true
ResponseWrapper<OtpChallengeDto>.Success(dto, requestId);
ResponseWrapper<TResponse>.Error(exception);               // IsSuccess = false
ResponseWrapper<TResponse>.Error("hata mesajı");
```

## Sonraki adımlar

<CardGroup cols={2}>
  <Card title="Pipeline Behaviors" icon="layer-group" href="/application/pipeline-behaviors">
    Handler'dan önce çalışan Exception / Authorization / Validation / Permission zinciri.
  </Card>

  <Card title="Validation & Mapping" icon="check-double" href="/application/validation-mapping">
    MustBeValidMobilePhone ve IMapFrom convention detayları.
  </Card>

  <Card title="Authorization" icon="shield-halved" href="/application/authorization">
    IRequirePermissions / IRequireTenantAccess marker'ları.
  </Card>

  <Card title="Domain Events" icon="bolt" href="/events/domain-events">
    AddOtpChallenge'ın yaydığı event'ler ve handler'ları.
  </Card>
</CardGroup>
