Skip to main content

Desen

Her use-case bir Command (yazma) veya Query (okuma) ile temsil edilir. Üçlü her zaman birlikte yaşar:
ParçaSorumlulukArayüz
Command / Queryİstek verisini taşırIRequest<IResponseWrapper<T>>
ValidatorFluentValidation kurallarıAbstractValidator<TRequest>
HandlerOrkestrasyon, repository + aggregate çağrılarıIRequestHandler<TRequest, TResponse>

Klasör konvansiyonu

Her use-case kendi klasöründe izole edilir:
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
Command’ler genelde class + init property’leridir; Query’ler sealed record pozisyonel syntax kullanır. İkisi de MediatR IRequest<...> döner.

Ö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
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.csMustBeValidMobilePhone() extension’ı Türkiye mobil numara doğrulaması yapar (bkz. Validation & Mapping).
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:
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
    });
}
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.

Ö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
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);
}
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.

Ö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
public sealed record GetAdminBagisBasvuruPlanlarQuery(AdminBagisBasvuruPlanlarFilter Filter)
    : IRequest<IResponseWrapper<IPaged<AdminBagisBasvuruPlanDto>>>;

Handler

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);
    }
}
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.

ResponseWrapper zarfı

Tüm handler’lar IResponseWrapper<TResponse> döner. Şekil:
public interface IResponseWrapper<TResponse>
{
    TResponse Response { get; }
    string RequestId { get; }
    bool IsSuccess { get; }
    Exception Exception { get; }
}
ResponseWrapper<T> static factory metodları sunar:
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

Pipeline Behaviors

Handler’dan önce çalışan Exception / Authorization / Validation / Permission zinciri.

Validation & Mapping

MustBeValidMobilePhone ve IMapFrom convention detayları.

Authorization

IRequirePermissions / IRequireTenantAccess marker’ları.

Domain Events

AddOtpChallenge’ın yaydığı event’ler ve handler’ları.