Jak obsługiwać resulty/eithery

0

Do osób, które używają resultów/eitherów do obsługi błędów: czy piszecie kod w stylu

var entity = _repository.GetEntity(id);

if (!entity.HasValue)
{
    return NotFound();
}

var result = _entity.Data.Something();

if (!result.Success)
{
    return NotValid(result.ErrorMessage);
}

var anotherResult = _someService.AnotherOperation(entity.Data.Id);

if (!anotherResult.Success)
{
    return NotValid(anotherResult.ErrorMessage);
}

var oneMoreResult = _anotherRepository.Add(new OtherEntity());

if (!oneMoreResult.Success)
{
   return NotValid(oneMoreResult.ErrorMessage);
}

// ...

czy raczej staracie się to robić na jakichś map czy bind? Macie jakieś przykłady takiego real world kodu w tym drugim podejściu? Wiem że w javie to drugie podejście jest modne, a jak to wygląda w .net? Kiedyś się bawiłem z language-ext, ale nie było łatwo to sensownie zrobić iirc. @fasadin Ty teraz chyba coś piszesz funkcyjnie.

1

Docelowo nigdzie nie powinno być konieczności sprawdzania Success/HasValue/IsRight, bo taki kod jest zbliżony do null-checków, tylko wydaje się być lepszy.

Result/Either/Option można zwracać wprost z kontrolerów i w filtrach mapować (najlepiej globalnie) na kody/action resulty, np.

    [HttpGet, Route("test/{arg}"), OptionFilter]
    public Option<int> Get(int arg)
    {
        return arg == 42 ? Some(42) : None;
    }

    public class OptionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Result is ObjectResult objectResult)
            {
                if (objectResult.Value is IOptional optional)
                {
                    context.Result = optional.MatchUntyped(
                        Some: x => (IActionResult)new OkObjectResult(x),
                        None: () => new NotFoundResult());
                }
            }

            base.OnActionExecuted(context);
        }
    }

Tutaj Option jest z LanguageExt, ale równie dobrze może być to customowy Either/Maybe.

1

W tym wątku przewija się podobna kwestia:
Zwracanie odpowiedzi serwisu

0
twoj_stary napisał(a):

Zwracanie odpowiedzi serwisu

Widziałem, ale to taki uproszczony przykład imo. Co jak trzeba sprawdzać typ błędu i różnie reagować w zależności od jego typu? Co jak typy błędów nie są kompatybilne? Czy każda metoda ma być takim łańcuszkiem wywołań? Jak to potem debugować? Do tego dochodzi obsługa nie tylko Either, ale też Option czy Try. Chodzi mi o taki real-world przykład.

0

@Salin:

Docelowo nigdzie nie powinno być konieczności sprawdzania Success/HasValue/IsRight, bo taki kod jest zbliżony do null-checków, tylko wydaje się być lepszy.

Zbliżony może i jest, ale zazwyczaj ten Result zwraca coś jeszcze, a mianowicie co poszło nie tak.

1
nobody01 napisał(a):

Widziałem, ale to taki uproszczony przykład imo. Co jak trzeba sprawdzać typ błędu i różnie reagować w zależności od jego typu?

Tzn. co na przykład zrobić w jakiej sytuacji?
Generalnie to raczej zadanie dla klasy zanim zwróci taki wynik.

Co jak typy błędów nie są kompatybilne?

No to wtedy nie mogą być zwrócone w takim Resulcie.

Czy każda metoda ma być takim łańcuszkiem wywołań?

Może. Jeśli łańcuszek wygląda źle i nieczytelnie, to trzeba znaleźć ładniejszy sposób zapisania.

Jak to potem debugować?

F5

Do tego dochodzi obsługa nie tylko Either, ale też Option czy Try. Chodzi mi o taki real-world przykład.

W moim prawdziwym świecie nie ma Option ani Try, jest tylko Either, a raczej Result. Po prostu nie udaję, że C# jest czymś, czym nie jest.

Jeśli ktoś używa tego wszystkiego, albo jeszcze lepiej language-ext na produkcji, to chętnie poczytam jak to wygląda.

0
somekind napisał(a):

Tzn. co na przykład zrobić w jakiej sytuacji?

Np. ponowić wywołanie dla innych parametrów. Wydaje mi się, że po to przekazujemy w odpowiedzi informację o błędzie, żeby klient mógł z tym coś zrobić.

No to wtedy nie mogą być zwrócone w takim Resulcie.

Tak się zastanawiam, że jeśli mamy aplikację, w której jest jedynie kilka typów błędów mapowanych przez kontroler na odpowiedzi http (żaden serwis nic z tymi błędami nie robi, tylko przekazuje dalej), to jaka jest zaleta zwracania resultów nad rzucaniem wyjątków i łapaniem ich w filtrze?

Może. Jeśli łańcuszek wygląda źle i nieczytelnie, to trzeba znaleźć ładniejszy sposób zapisania.

No właśnie ciekaw jestem, czy można mieć ładną obsługę błędów bez konieczności zmieniania paradygmatu całego systemu na funkcyjny.

F5

Hmm, ale naprawdę nie ma z takimi łańcuszkami żadnych problemów? Debuguje się to tak samo jak kod imperatywny?

W moim prawdziwym świecie nie ma Option ani Try, jest tylko Either, a raczej Result. Po prostu nie udaję, że C# jest czymś, czym nie jest.

Hmm, ale jeśli miałbym CustomerDao, to jednak wolałbym zwrócić Option<Customer> zamiast Result<Customer>, gdzie Result oprócz NotFound zawiera też NotValid czy Forbidden :) Chociaż pewnie Result będzie wystarczająco dobry.

Trochę gram adwokata diabła tymi pytaniami :P

1
nobody01 napisał(a):

Np. ponowić wywołanie dla innych parametrów. Wydaje mi się, że po to przekazujemy w odpowiedzi informację o błędzie, żeby klient mógł z tym coś zrobić.

No ok, to od tego jest jakiś Match, żeby można było także i błąd obsłużyć niby-funkcyjnie, albo IsError, żeby użyć imperatywnie.
Ja takich akurat sytuacji nigdy nie miewam, bo ponawianie najczęściej robię wewnątrz klasy komunikującej się z jakimś API przy użyciu Polly, zanim jeszcze zwracam Result.

Tak się zastanawiam, że jeśli mamy aplikację, w której jest jedynie kilka typów błędów mapowanych przez kontroler na odpowiedzi http (żaden serwis nic z tymi błędami nie robi, tylko przekazuje dalej), to jaka jest zaleta zwracania resultów nad rzucaniem wyjątków i łapaniem ich w filtrze?

Zwracany wynik to coś, co przewidziałeś, a wyjątek to coś, czego nie obsłużyłeś. Dla mnie osobiście jest bardzo istotne wiedzieć, z którą z tych sytuacji mam do czynienia, bo druga oznacza dodatkową, robotę, a pierwsza nie.

No właśnie ciekaw jestem, czy można mieć ładną obsługę błędów bez konieczności zmieniania paradygmatu całego systemu na funkcyjny.

Jak dla mnie to taki Result z metodami pozwalającymi na wywoływanie łańcuchowe to nie jest zmiana paradygmatu. Dlatego właśnie nazywam go Result, a nie Either, żeby nikt nie zarzucał, że to nie jest programowanie funkcyjne, bo nie ma HKT i innych takich. Nawet nie nazwałbym tego luźną inspiracją.
Dla mnie jest wystarczająco dobre do moich potrzeb, jeśli ktoś potrzebuje więcej, to może faktycznie potrzeba zmienić paradygmat.

Hmm, ale naprawdę nie ma z takimi łańcuszkami żadnych problemów? Debuguje się to tak samo jak kod imperatywny?

No lambdy debugować można od dawna. A jak kodu jest dużo, to i tak lepiej nie robić długiej lambdy tylko metodę prywatną/funkcję anonimową.

Hmm, ale jeśli miałbym CustomerDao, to jednak wolałbym zwrócić Option<Customer> zamiast Result<Customer>, gdzie Result oprócz NotFound zawiera też NotValid czy Forbidden :) Chociaż pewnie Result będzie wystarczająco dobry.

No tak ideologicznie to pewnie masz rację. Tylko z drugiej strony, to może lepiej tych nullable reference types użyć?

Ogólnie wydaje mi się, że jak mamy więcej typów błędów do obsłużenia niż kilka, to trudno jest to zrobić w OOP. Jak jest kilka błędów, które po prostu przepychamy przez kilka warstw, to OK, ale jak jest tego więcej, to słabo to zaczyna wyglądać.

Nawet z przepychaniem przez warstwy to słabo wygląda, bo inferencja typów jest za słaba, a async to w ogóle wszystko psuje. No moim zdaniem mimo wszystko jak się da uniknąć bezsensownych wyjątków i przydługawej ifologii, to jest lepiej. A trochę lepiej, to dużo lepiej niż trochę gorzej. :P

0
somekind napisał(a):

A trochę lepiej, to dużo lepiej niż trochę gorzej. :P

Fajna puenta :P

Btw, do language ext powstał tutorial, jak tego używać: https://github.com/stumathews/UnderstandingLanguageExt

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