Gdzie powinno się umieszczać logikę walidacji danych logowania użytkownika: w walidatorach (korzystam z FluentValidation) czy w serwisach, z których korzystają kontrolery? Jeśli nie da się zalogować użytkownika, to powinienem zwrócić 401, więc pierwsza opcja chyba odpada. Z drugiej strony, mam wrażenie, że nie po to piszę własne walidatory, żeby potem walidację przeprowadzać w serwisach.
Coś takiego jak wymagane pola czy minimalna długość loginu możesz sprawdzić walidatorami w rodzaju FluentValidation. Natomiast do wywołanie serwisu logującego, providera SSO czy bazy danych, to już musisz jakiś swój "serwis" napisać.
Moje podejście jest takie, że używam fluent validation do sprawdzenia czy nie jest puste hasło bądź user. (korzystam z ModelState.IsValid). Jeśli to przechodzi i trafia do serwisu to z serwisu zwracam obiekt, który zawiera pola {bool Success, string Message}. I jeśli logowanie się nie powiedzie to mam info w message.
@szydlak: Właściwie, to mam coś podobnego u siebie.
@somekind: A co jeśli miałbym formularz rejestracji i chciałbym sprawdzić, czy użytkownik nie wybrał jakiegoś zajętego nicka? Tego typu walidacja powinna być już w walidatorze czy gdzieś "dalej"? Przy użyciu Fluent Validation sprawdza się to dość przyjemnie. Zakładam, że logikę sprawdzającą dostępność nazwy użytkownika mam wydzieloną.
Sprawdzenie zajętości nicka wymaga sprawdzenia w jakimś źródle tych nicków (bazie, serwisie, itp.) To nie jest zadanie dla walidatora inputu.
W sumie to racja, walidatory nie powinny wiedzieć o bazie danych. Dotychczas miałem taki walidator:
public class RegisterModelValidator : AbstractValidator<RegisterModel>
{
private readonly UserService userService;
public RegisterModelValidator(UserService userService)
{
this.userService = userService;
RuleFor(m => m.UserName)
.NotEmpty()
.WithMessage("Nazwa użytkownika nie może być pusta.")
.MinimumLength(2)
.WithMessage("Nazwa użytkownika musi mieć co najmniej 2 znaki.")
.MustAsync(async (userName, cancellationToken) => await userService.IsUserNameAvailableAsync(userName, cancellationToken))
.WithMessage("Użytkownik o podanej nazwie już istnieje.");
RuleFor(m => m.Email)
.NotEmpty()
.WithMessage("Adres email nie może być pusty.")
.EmailAddress()
.WithMessage("Adres email musi być poprawny.")
.MustAsync(async (email, cancellationToken) => await userService.IsEmailAvailableAsync(email, cancellationToken))
.WithMessage("Użytkownik o podanym adresie email już istnieje.");
RuleFor(m => m.Password)
.NotEmpty()
.WithMessage("Hasło nie może być puste.")
.Matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,}")
.WithMessage("Hasło nie spełnia wymagań.");
RuleFor(m => m.PasswordConfirmation)
.NotEmpty()
.WithMessage("Hasło musi zostać potwierdzone.")
.Equal(m => m.Password)
.WithMessage("Hasła nie zgadzają się.");
}
}
Ale tworzenie takich zależności raczej nie pachnie za dobrze...
Czyli tak to ma wyglądać?
public async Task<Result> RegisterAsync(RegisterModel model)
{
var validationResult = await ValidateRegisterModel(model);
if (!validationResult.ActionSucceeded)
{
return validationResult;
}
var user = new User
{
UserName = model.UserName,
Email = model.Email,
};
var creationResult = await userManager.CreateAsync(user, model.Password);
if (!creationResult.Succeeded)
{
return new Result(new Error(ErrorName.NotSpecified, "Rejestracja nie powiodła się z nieznanych przyczyn."));
}
// TODO: Send email
return new Result(null);
}
private async Task<Result> ValidateRegisterModel(RegisterModel model)
{
var isUserNameAvailableTask = userService.IsUserNameAvailableAsync(model.UserName);
var isEmailAvailableTask = userService.IsEmailAvailableAsync(model.Email);
var isUserNameAvailable = await isUserNameAvailableTask;
var isEmailAvailable = await isEmailAvailableTask;
var errors = new Dictionary<string, string>();
if (!isEmailAvailable)
{
errors[nameof(model.UserName)] = "Użytkownik o podanej nazwie już istnieje.";
}
if (!isEmailAvailable)
{
errors[nameof(model.Email)] = "Użytkownik o podanym adresie email już istnieje.";
}
if (errors.Count > 0)
{
return new Result(new Error(ErrorName.NotValid, errors));
}
return new Result(null);
}
EDIT:
Dlaczego, gdy kopiuję kod z VS, robią się te dziwne wcięcia?