SignUpUserCommandValidator.cs — MustBeValidMobilePhone() 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(); }}
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.
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.
public sealed record GetAdminBagisBasvuruPlanlarQuery(AdminBagisBasvuruPlanlarFilter Filter) : IRequest<IResponseWrapper<IPaged<AdminBagisBasvuruPlanDto>>>;
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.