Sprawdzanie unikalności loginu

0

Cześć,
napisałem metodę do sprawdzania, czy login podawany przy dodawaniu/edycji użytkownika jest unikalny:

// _ctx to DbContext :)
public void ValidateUser(User user, bool isEditing)
{
    int count = _ctx.Users
        .Where(x => x.Login == user.Login).Count();

    User? dbUser = null;

    if (isEditing)
    {
        dbUser = _ctx.Users
            .Select(x =>
                new User()
                {
                    Id = x.Id,
                    Login = x.Login
                })
            .Where(x => x.Id == user.Id)
            .AsNoTracking()
            .First();
    }

    if ((!isEditing && count != 0) || (isEditing && count != 0 && user.Login != dbUser!.Login))
    {
        throw new SauerkrautValidationException("Podany login jest już zajęty!");
    }
}

Moje rozwiązania działa, mam tylko pytanie dot. samego kodu - czy jest on dobrze napisany : ) ? Chodzi mi głównie o pobieranie samego loginu z bazy (.Select x=> ...), zamiast robienia SELECT *.
Czy może EfCore ma jakąś funkcję do sprawdzania unikalności właściwości, o której nie wiem?

Z góry dziękuję za wszystkie odpowiedzi.

3

Użyj Any() -> tłumaczy się to na bazodanowe exists

if (_ctx.Users.Any(x => x.Login == user.Login))
    // return Login zajety

W tym kodzie po co robisz projekcję na User?

.Select(x =>
  new User()
  {
      Id = x.Id,
      Login = x.Login
  })

_ctx.Users już samo zwraca encje User czy to jakiś inny obiekt typu DTO/ViewModel?

0
markone_dev napisał(a):

Użyj Any() -> tłumaczy się to na bazodanowe exists

if (_ctx.Users.Any(x => x.Login == user.Login))
    // return Login zajety

W tym kodzie po co robisz projekcję na User?

.Select(x =>
  new User()
  {
      Id = x.Id,
      Login = x.Login
  })

_ctx.Users już samo zwraca encje User czy to jakiś inny obiekt typu DTO/ViewModel?

Dzięki za odpowiedź : )
Projekcję robię po to, żeby nie pobierać wszystkich właściwości, tylko sam login - bo tylko on mi jest potrzebny:

public int Id { get => _id; set { _id = value; NotifyPropertyChanged(); } }
public string FirstName { get => _firstName; set { _firstName = value; NotifyPropertyChanged(); } }
public string LastName { get => _lastName; set { _lastName = value; NotifyPropertyChanged(); } }
public string Login { get => _login; set { _login = value; NotifyPropertyChanged(); } }
public string Password { get => _password; set { _password = value; NotifyPropertyChanged(); } }
public DateTime CreationDate { get => _creationDate; set { _creationDate = value; NotifyPropertyChanged(); } }
public DateTime? ModifyDate { get => _modifyDate; set { _modifyDate = value; NotifyPropertyChanged(); } }
public bool IsDeleted { get => _isDeleted; set { _isDeleted = value; NotifyPropertyChanged(); } }
public List<Privilege> Privileges { get => _privileges; set { _privileges = value; NotifyPropertyChanged(); } }
2

Projekcję robię po to, żeby nie pobierać wszystkich właściwości, tylko sam login - bo tylko on mi jest potrzebny:

Tak czułem. Ogólnie widzę że stosujesz podejście encja na twarz i pchasz, czyli masz te same obiekty jako encje i View Modele. Jak rozumiem dopiero się uczysz i to nie jest kod produkcyjny (mam nadzieję), więc dobrą praktyką jest rozdzielenie tego.

No i całe to twoje wyrażenie można uprościć do:

dbUser = _ctx.Users.Find(user.Id);

Aha i poczytaj o async/await, żeby nie blokować głównego wątku aplikacji przy operacjach IO.

0
markone_dev napisał(a):

Projekcję robię po to, żeby nie pobierać wszystkich właściwości, tylko sam login - bo tylko on mi jest potrzebny:

Tak czułem. Ogólnie widzę że stosujesz podejście encja na twarz i pchasz, czyli masz te same obiekty jako encje i View Modele. Jak rozumiem dopiero się uczysz i to nie jest kod produkcyjny (mam nadzieję), więc dobrą praktyką jest rozdzielenie tego.

W jaki sposób mógłbym to rozdzielić? Pod jakimi hasłami mogę szukać informacji na ten temat :)?

No i całe to twoje wyrażenie można uprościć do:

dbUser = _ctx.Users.Find(user.Id);

Wiem, że istnieje metoda Find(), ale z tego co wiem, to wysyła ona zapytanie SELECT *, a ja potrzebuję pobrać tylko login, bo może wystąpić sytuacja, gdy:

  1. Edytowany użytkownik ma login "kowalski"
  2. Zmieniamy np. jego imię i nazwisko, ale nie ruszamy jego loginu
  3. Login się nie zmienił, ale występuje w bazie, bo użytkownik już istnieje - Any() i tak wyrzuci błąd. Dlatego sprawdzam czy edytowany login różni się od loginu pobranego z bazy, sprzed edycji.
3

A tak nie będzie czytelniej?

    public async Task ValidateUser(User user, bool isEditing)
    {
        var doesExistByName = await _dbContext.Users.AnyAsync(u => u.Login == user.Login);

        if (!isEditing && doesExistByName)
            throw new LoginIsAlreadyUsedException();

        if (isEditing)
        {
            var dbUserLogin = await _dbContext.Users
                .Where(u => u.Id == user.Id)
                .Select(u => u.Login)
                .FirstOrDefaultAsync()
                ?? throw new NoSuchUserException(user.Id);

            if (dbUserName != user.Login)
                throw new CantUpdateLoginException();
        }
    }
  1. Dodaj dedykowane wyjątki.
  2. Nie zliczaj użytkowników po danym loginie, tylko sprawdź, czy jest chociaż jeden (porównaj koszt z planu zapytania sql select count(*) from ... where ... z select top 1 (1) from ... where ...).
  3. Nie komplikuj warunków, które można rozbić i zapisać w trywialny sposób.
  4. Unikaj wbudowanych wyjątków - zamiast First() wywołaj FirstOrDefault() i obsłuż swoim wyjątkiem null, zamiast później zastanawiać się przy lekturze logów, skąd te InvalidOperationException.
  5. Jak już przedmówca wspomniał, używaj wersji async metod materializujących dane, acz nie dlatego, że blokujesz główny wątek aplikacji, tylko dlatego, że blokujesz jeden z wątków aplikacji (który w pewnych przypadkach może być głównym wątkiem procesu), marnując zasoby. Kiedy wołasz await, wątek jest zwalniany i może zająć się inną robotą; kiedy wołasz metodę synchronicznie, wątek czeka śpiąc, aż operacja I/O się zakończy.

BTW cały kod wydaje się przekomplikowany i chyba wkradł się tam błąd, który nie pozwala istniejącym użytkownikom na zmianę loginu. Ja bym to widział tak (bazując na już przepisanym powyższym kodzie):

    public async Task ValidateUser(User user, bool isEditing)
    {
        var userId = isEditing ? (int?)user.Id : null;
        var doesExistByNameExcludingCurrentUser = await _dbContext.Users
            .AnyAsync(u => u.Login == user.Login && u.Id != userId);

        if (doesExistByNameExcludingCurrentUser)
            throw new LoginIsAlreadyUsedException();

        if (userId.HasValue)
        {
            var doesUserExist = await _dbContext.Users.AnyAsync(u => u.Id == userId);

            if (!doesUserExist)
                throw new NoSuchUserException(userId);
        }
    }

Przy czym sprawdzenie w isEditing jedynie weryfikuje, czy jest użytkownik z takim id, na upartego możesz to usunąć. Ten kod pozwala istniejącemu użytkownikowi na zmianę loginu.

0

Dzięki : ) Błędu ze zmianą loginu tam nie było, ale faktycznie Twoja wersja jest czytelniejsza : )

0

A ja się czepię - po co piszesz własne rzeczy, skoro to wszystko jest już dobrze zrobione i przetestowane we wbudowanych mechanizmach Identity?

2

Mam zwidy, czy wszyscy tutaj macie rozwiązanie zawierające race? :P

Pytanie czy tylko można to jakoś nadużyć

Thread1 				Thread2

AnyAsync() - false		ValidateUser()

Something				AnyAsync() - false

return					Something

						return

O ile nie lubię programowania po stronie bazy, to checki unikatowości pozostawiłbym bazie, bo łatwo sobie krzywdę zrobić

0
WeiXiao napisał(a):

Mam zwidy, czy wszyscy tutaj macie rozwiązanie zawierające race? :P

Pytanie czy tylko można to jakoś nadużyć

Thread1 Thread2

AnyAsync() - false ValidateUser()

Something AnyAsync() - false

return Something

  					return

O ile nie lubię programowania po stronie bazy, to checki unikatowości pozostawiłbym bazie, bo łatwo sobie krzywdę zrobić

od tego jest unique index

0
szydlak napisał(a):
WeiXiao napisał(a):

Mam zwidy, czy wszyscy tutaj macie rozwiązanie zawierające race? :P

Pytanie czy tylko można to jakoś nadużyć

Thread1 				Thread2

AnyAsync() - false		ValidateUser()

Something				AnyAsync() - false

return					Something

						return

O ile nie lubię programowania po stronie bazy, to checki unikatowości pozostawiłbym bazie, bo łatwo sobie krzywdę zrobić

od tego jest unique index

o tym mowie - czy jest sens robic takiego checka w kodzie?

3

o tym mowie - czy jest sens robic takiego checka w kodzie?

Ja myślę tak - jeśli to jest wymaganie biznesowe, to taki check powinien na 100% znaleźć się w kodzie. No i fajnie jakby był do tego test 😀
Natomiast unique index czy constraint bazodanowy też powinien być - ochroni on te 0.0001% przypadków, w których 2 wątki akurat pechowo się nałożą.

1
kelog napisał(a):

o tym mowie - czy jest sens robic takiego checka w kodzie?

Ja myślę tak - jeśli to jest wymaganie biznesowe, to taki check powinien na 100% znaleźć się w kodzie. No i fajnie jakby był do tego test 😀
Natomiast unique index czy constraint bazodanowy też powinien być - ochroni on te 0.0001% przypadków, w których 2 wątki akurat pechowo się nałożą.

Racja, na pewno zmniejszy to zależność i ułatwi migrowanie, no ale pytanie czemu by nie zrobic tego dobrze w kodzie?

Gdy zaczniesz coś zmieniać w tech stacku to może się okazać że "zapomni się" że bazka coś tam więcej robiła

0.0001% (per np. day) przy odpowiedniej skali to może być 10, 100, itd. na dzien :P

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.