Wstrzykiwanie zależności do ViewModel podczas bindowania modelu

0

Czy da się poprzez konstruktor wstrzyknąć obiekt MyEfContext do klasy AddStudentViewModel podczas bindowania modelu? Jeśli nie, to w jaki sposób najlepiej otrzymać kontekst bazy danych w metodzie IsValid()?

Controller

public class StudentController : Controller
{
    [HttpPost]
    public IActionResult Add(AddStudentViewModel vm)
    {
        if (!ModelState.IsValid || !vm.IsValid())
        {
            return View();
        }

        //add student
        return RedirectToAction("Index");
    }
}

ViewModel

public class AddStudentViewModel
{
    private MyEfContext _context;

    public AddStudentViewModel() 
    {
    }

    public AddStudentViewModel(MyEfContext context) 
    {
        _context = context;
    }

    public string UniqueName { get; set; }
    public int Age{ get; set; }

    public bool IsValid()
    {
        if (_context.Students.Any(x => x.UniqueName == UniqueName))
        {
              return false;
        }

        return true;
    }
}
	
1

Nie, dostaniesz InvalidOperationException, bo bindowany ViewModel musi mieć konstruktor bezparametrowy.

Żeby zrobić ręczną walidację, czy spełnione są customowe warunki wykorzystaj własny atrybut ValidationAttribute: https://docs.microsoft.com/pl-pl/aspnet/core/mvc/models/validation?view=aspnetcore-2.1#custom-validation

4

Podpięcie czegokolwiek powiązanego z bazą danych do viewmodelu to jedna z najgorszych rzeczy jaką możesz zrobić

0

Podpięcie czegokolwiek powiązanego z bazą danych do viewmodelu to jedna z najgorszych rzeczy jaką możesz zrobić

Dlaczego? A czy we własnych atrybutach można używać połączenia z bazą, czy też się nie powinno?

1

W sumie rzuciłem te customowe ValidationAttribute jako przykład jak to należy zrobić (bo można: ValidationAttribute.IsValid() daje ci ValidationContext, w którym możesz zrobić GetService<T>(), aby uzyskać obiekt jaki ci trzeba, np. DbContext), tymczasem:

Validation Attributes like StringLength, RegularExpression, Required and such validations are examples of good attributes and Validation Attributes that checks for uniqness or other database related rules are examples of inappropriate attributes.

Za https://stackoverflow.com/a/32172401, rozwinięcie w https://stackoverflow.com/questions/32172157/c-sharp-custom-validation-unique-property-generic-classes#comment52231722_32172401:

(...) In cases that the system must prevent duplicate data, it is better to do this in your Business Logic codes.

Jednocześnie wygląda na to, że społeczność jest podzielona, bo są wątki, że OK, ale użyj FluentValidation: https://stackoverflow.com/questions/16678625/asp-net-mvc-4-ef5-unique-property-in-model-best-practice, ale też ktoś zrobił to jako atrybut https://github.com/fatihBulbul/UniqueAttribute

0

Jaki jest sens używania ViewModeli do walidacji? Przecież zdaniem modelu domeny jest zapewnienie takiego kontraktu dla klientów a by przekazany typ był prawidłowy, a nie na odwrót. Najlepszym podejściem jest wystawienie metody Validate w encji.

0

Moim zdaniem taki jest sens, że wówczas walidacja przeprowadzona jest w jednym miejscu, a nie część właściwości walidowanych jest za pomocą atrybutów, potem dodatkowo jeszcze walidacja tych samych właściwości przeprowadzana jest w encji, a potem kolejne właściwości walidowane są w kolejnej encji (jeśli w ViewModelu są dane z dwóch tabel) itd.

1

@panDawid: dwa pytanka:

  1. Dlaczego Twoim zdaniem warstwa prezentacji powinna cokolwiek wiedzieć o bazie danych?
  2. Jak przetestujesz jednostkowo taką walidację?
0

Dlaczego Twoim zdaniem warstwa prezentacji powinna cokolwiek wiedzieć o bazie danych?

Z postów wyżej dowiedziałem się, że jednak ani w ViewModelach, ani w ValidationAttribute nie powinno być komunikacji z bazą danych. Wcześniej po prostu nie sądziłem, że to jest źle. Nie mniej jednak, myślę, że walidacja danych powinna odbywać się w warstwie logiki aplikacji, a nie trochę tu, trochę w encjach. Jeśli zajdzie potrzeba użycia bazy danych do walidacji to myślę że dobrym rozwiązaniem jest coś na wzór tego, a nie tworzenie metod służących jedynie stricte do walidacji. Jeśli mój tok rozumowania jest zły, to popraw mnie proszę :)

Jak przetestujesz jednostkowo taką walidację?

Dawno nie pisałem testów jednostkowych, więc zagrożeń z tej strony nie dostrzegam.

2
panDawid napisał(a):

Z postów wyżej dowiedziałem się, że jednak ani w ViewModelach, ani w ValidationAttribute nie powinno być komunikacji z bazą danych. Wcześniej po prostu nie sądziłem, że to jest źle. Nie mniej jednak, myślę, że walidacja danych powinna odbywać się w warstwie logiki aplikacji

Ale to nie jest warstwa logiki aplikacji, to jest warstwa prezentacji. I o wszem, tam powinna być walidacja, ale tylko danych wprowadzanych przez użytkownika. To jest miejsce na sprawdzenie, czy np. nazwa użytkownika ma więcej niż 6, a mniej niż 20 znaków, i czy nie zawiera spacji - czyli walidacje małe i szybkie, pozwalające dać szybką odpowiedź użytkownikowi, i łatwe do zweryfikowania (czyli przetestowania jednostkowo).

a nie trochę tu, trochę w encjach.

A jak wprowadzisz system płatności, to napiszesz sobie atrybut walidacji sprawdzający środki na koncie?
Nie wszystko da się sprawdzić, w każdej warstwie.

Jeśli zajdzie potrzeba użycia bazy danych do walidacji to myślę że dobrym rozwiązaniem jest coś na wzór tego

Ja tu widzę wpychanie walidacji bazującej na regułach biznesowych, które do działania wymagają źródła danych. Dla mnie to jest bardzo daleko od dobrego rozwiązania - to tworzenie jednowarstwowego spaghetti.

Dawno nie pisałem testów jednostkowych, więc zagrożeń z tej strony nie dostrzegam.

Nie wiem jak rozumieć tę odpowiedź. :) Nie dostrzegasz zagrożeń, bo nie pamiętasz nawet co może pójść źle, czy po prostu nie masz zamiaru pisać testów wcale? Tak czy siak, to raczej nie jest najlepsze podejście.

0

Ale to nie jest warstwa logiki aplikacji, to jest warstwa prezentacji. I o wszem, tam powinna być walidacja, ale tylko danych wprowadzanych przez użytkownika. To jest miejsce na sprawdzenie, czy np. nazwa użytkownika ma więcej niż 6, a mniej niż 20 znaków, i czy nie zawiera spacji - czyli walidacje małe i szybkie, pozwalające dać szybką odpowiedź użytkownikowi, i łatwe do zweryfikowania (czyli przetestowania jednostkowo).

W jaki sposób zapewniasz spójny stan Encji, cze Encja zwraca jakiś notification, albo rzuca wyjątek. Czy wystarcza ci sprawdzenie punktu wejścia.?
Jeśli serwis aplikacyjny używa komend to dlaczego nie mogę zwalidowąć tych komend w warstwie aplikacji.?

1

proponuję zastosować remote validation

[Remote("ValidateName", "Check", ErrorMessage = "Użytkownik o takiej nazwie już istniej")]

np. https://www.c-sharpcorner.com/article/remote-validation-in-asp-net-mvc/

1
._. napisał(a):

W jaki sposób zapewniasz spójny stan Encji, cze Encja zwraca jakiś notification, albo rzuca wyjątek. Czy wystarcza ci sprawdzenie punktu wejścia.?

Punkt wejścia sprawdza się po to, aby ewidentnie błędnych danych nie wpuszczać do środka systemu. Dalsze reguły biznesowe (tak jak np. unikalność maila z tego wątku) można sprawdzić na poziomie jakiegoś serwisu aplikacyjnego. Tak więc do stworzenia spójnej encji wystarczy konstruktor i ewentualnie wyjątki w nim.

Jeśli serwis aplikacyjny używa komend to dlaczego nie mogę zwalidowąć tych komend w warstwie aplikacji.?

Ale dlaczego nie możesz?
Można zrezygnować z walidacji w warstwie prezentacji i wszystko sprawdzać na poziomie aplikacji, wtedy będzie jeden walidator, tylko jak w przypadku MVC nie będzie się zbyt ładnie integrował z GUI.
Wszystko zależy, co chcemy osiągnąć, jakie mamy wymagania i ograniczenia narzucone odgórnie.

0

A jak wprowadzisz system płatności, to napiszesz sobie atrybut walidacji sprawdzający środki na koncie?
Nie wszystko da się sprawdzić, w każdej warstwie.

Takie rozwiązanie byłoby ok, czy złe? Klasa AccountInfo znajdowałaby się w warstwie logiki biznesowej.

public class ChargeValidationAttribute: ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        AccountInfo accountInfo = new AccountInfo("jakis numer konta");

        if (accountInfo.AvailableFunds() < value)
        {
            return new ValidationResult("Nie posiadasz wystarczających środków na koncie.");
        }
        return ValidationResult.Success;
    }
}

Nie wiem jak rozumieć tę odpowiedź. :) Nie dostrzegasz zagrożeń, bo nie pamiętasz nawet co może pójść źle, czy po prostu nie masz zamiaru pisać testów wcale? Tak czy siak, to raczej nie jest najlepsze podejście.

Nie pamiętam co może pójść źle, ale w bliżej nieokreślonej przyszłości na pewno będę zgłębiał temat testów.

0

Punkt wejścia sprawdza się po to, aby ewidentnie błędnych danych nie wpuszczać do środka systemu. Dalsze reguły biznesowe (tak jak np. unikalność maila z tego wątku) można sprawdzić na poziomie jakiegoś serwisu aplikacyjnego. Tak więc do stworzenia spójnej encji wystarczy konstruktor i ewentualnie wyjątki w nim.

Mnie właśnie chodzi o takie defensywne podejście gdzie encja - agregat krzyczy "podany argument nie jest emailem, ponieważ nie ma znaku @ - chyba zapomniałeś o walidacji".

Ja raczej preferuje SelfEncapsulation jako formę "walidacji w encji" wtedy niemusze rzucać wyjątkiem w konstruktorze a zamiast tego w prywatnym seterze.

Spotykam się z opinią, że to bez sensu, ponieważ jest to nadmiarowy kod, który nie jest potrzebny do działania aplikacji... Jak to skomentujesz?. :---|

Jeszcze jedno pytanie tak poza tematem czasami mam takie zagwozdki, jak np. kiedy komunikuje się z 3 usługami zewnętrznymi asynchronicznie i jeśli z jednej usługi nie pobiorę informacji, nie chcę przerywać flow, tylko powiadomić użytkownika, że z jedną usługą została zerwana komunikacja.

Jak to się ma do tego, co pisze Dave Thomas w swojej książce. Mniej więcej tak to brzmiało "Ask your self. Will this code still run if I remove all the exception handlers?" If the answer is "no", then probably the exceptions are being used in nonexceptional circumstances."

Ale dlaczego nie możesz?
Można zrezygnować z walidacji w warstwie prezentacji i wszystko sprawdzać na poziomie aplikacji, wtedy będzie jeden walidator, tylko jak w przypadku MVC nie będzie się zbyt ładnie integrował z GUI.
Wszystko zależy, co chcemy osiągnąć, jakie mamy wymagania i ograniczenia narzucone odgórnie.

Myślałem raczej o jakimś aspekcie albo filtrze, który zadziała jak dekorator wtedy będę mógł to bezproblemowo integrować z GUI "teoretycznie", bardziej chodzi mi o sam fakt trzymania walidacji argumentów wejściowych w warstwie niżej, aby odciążyć z tego prezentacje.

0
panDawid napisał(a):

A jak wprowadzisz system płatności, to napiszesz sobie atrybut walidacji sprawdzający środki na koncie?
Nie wszystko da się sprawdzić, w każdej warstwie.

Takie rozwiązanie byłoby ok, czy złe? Klasa AccountInfo znajdowałaby się w warstwie logiki biznesowej.

public class ChargeValidationAttribute: ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        AccountInfo accountInfo = new AccountInfo("jakis numer konta");

        if (accountInfo.AvailableFunds() < value)
        {
            return new ValidationResult("Nie posiadasz wystarczających środków na koncie.");
        }
        return ValidationResult.Success;
    }
}

Nie wiem jak rozumieć tę odpowiedź. :) Nie dostrzegasz zagrożeń, bo nie pamiętasz nawet co może pójść źle, czy po prostu nie masz zamiaru pisać testów wcale? Tak czy siak, to raczej nie jest najlepsze podejście.

Nie pamiętam co może pójść źle, ale w bliżej nieokreślonej przyszłości na pewno będę zgłębiał temat testów.

Dla mnie ok. Tylko że prawdopodobnie będziesz musiał użyć fabryki do AccountInfo.

1 użytkowników online, w tym zalogowanych: 0, gości: 1