StronglyTypedId - używacie?

0

Co sądzicie o StronglyTypedId?

Używacie? Planujecie?

3

Zauważyłem tą paczkę u Nicka Chapsasa. W nowym C# nawet autor paczki wspomniał, że używanie jej trochę mija się z celem bo można to samo uzyskać za pomocą

public record struct SomeId(Guid Value);

ale dla starego c# można jak najbardziej. Zaczęliśmy używać w projekcie recordów i sprawdza się całkiem fajnie na razie.

0

Działa to w DAL? Mapuje się do bazy danych czy tylko DTO, ViewModele itp?

1

Największy zysk widzimy w warstwie związaną z logiką aplikacji. W DTO korzystamy z klasycznych Guidów bo to jednak kontrakty dla innych usług podobnie w obiektach bazodanowych.

1

nie widzę zalety, ktoś wykaże jakąś?

1
1a2b3c4d5e napisał(a):

nie widzę zalety, ktoś wykaże jakąś?

Taki sam sens jak opakowywania prostych typów typu string w obiekty

W większości bajer bardziej utrudniający życie niż ułatwiający, ale może zmniejszyć trochę ryzyko pomyłki jeśli się babramy dużo z obiektami z jakiegoś api które mają w sobie dużo innych idków. Jedyną dużą zaletę jaką widzę to na przykład w przypadku jak po latach okazuje się że na przykład int nie wystarcza do trzymania idków i możemy w jednym miejscu prosto zwiększyć do longa. Można odroczyć w czasie decyzję o typie idka i prosto zmieniać

1

O ile reprezentowanie czasu/daty jako TimeSpan zamiast jakiegoś longa, czy adresu zamiast stringa uważam za bardzo zasadne, to takiej nakładki na Guida już nie bardzo.

Autor na tym blogu piszę takie coś:

Part of the problem is that we're using a primitive object (System.Guid) to represent two different concepts: the unique identifier of a user, and the unique identifier of an order.

Według mnie koncept i cel jest ten sam - możność identyfikowania obiektów, a jedynie zmienia się kontekst.

Na filmiku autor twierdzi że nasz soft będzie way safer, a na blogu wrzucony jest taki kod

[HttpGet("{orderId}")]
public ActionResult<Order> Get(Guid orderId)
{
    var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
    var order = _service.GetOrderForUser(userId, orderId);

O ile na blogu nie było już pokazanego tego fragmentu po refaktorze, to na filmiku chociaż było to pokazane

że nikt nie broni abyś zrobił new UserId(ProductId)

i we wszystkich tych miejscach gdzie będą następować te mapowania nadal nie masz tego safety.

Jasne, w całej domenie za to dostaniesz to safety, ale szczerze mówiąc nie mam jakoś zamiaru dodawać warstwy nad idkiem

Co innego obiekty biznesowe typu phone number, a co innego wartości wręcz techniczne typu Id.

Może jestem skrzywiony przez relacyjne bazki gdzie każda tego typu rzeczy kojarzy mi się z yet another table

2
1a2b3c4d5e napisał(a):

Według mnie koncept i cel jest ten sam - możność identyfikowania obiektów, a jedynie zmienia się kontekst.

No tylko że guidy nie identyfikują obiektów. Masz guida i nie wiesz co to jest, jak dodasz opis albo typ to wtedy wiesz co identyfikuje. Tak samo można powiedzieć że koncept i cel każdego DTO jest ten sam - przechowanie jakichś danych i że można to samo zrobić za pomocą słownika string na object. No i w sumie można.
Jeszcze plus jaki dostrzegam to jeśli robisz jakąś na przykład bibliotekę to te typy mogą być pomocne dla użytkownika. Widzi że musi mieć ID konkretnego typu i może poszukać skąd taki dostać. Trochę jak w grze - wiesz że do niebieskiego zamka potrzebujesz znaleźć niebieski klucz. Oczywiście można przeczytać dokumentację ale prawdziwy mężczyzna zagląda do instrukcji tylko jak wszystko inne zawiodło.
BTW to nie jest filmik autora tej paczki.

Też tego za bardzo nie kupuję, trochę over-engineered. Gdyby już to bym zdecydowanie użył "record struct" zamiast osobnej paczki a w starszych wersjach po prostu guida.

6

Używam. To ma dużą wartość przy rozrastającej się domenie, tego nie widać na początku, gdzie rzeczywiście zwykły GUID/int załatwia sprawę i nie bardzo jest jak popełnić błąd. Ale po pewnym czasie może dojść coś takiego:

  • Zamiast GUID-a chcemy wprowadzić inny rodzaj identyfikatora (chociażby dla bezpieczeństwa) - wymieniamy tylko konstruktory identyfikatora, a cała reszta domeny działa poprawnie
  • Chcemy mieć identyfikator w wielu formatach i z mapowaniem między nimi - trzeba byłoby dokładać wszędzie serwis, a tak, to mamy tylko jedno miejsce do zmiany
  • Identyfikator może mieć dodatkowe informacje, na przykład jednostkę wydającą - z GUID-em trzeba będzie wszędzie dokładać jakiś serwis czy extension method, a z silnie typowanym identyfikatorem wystarczy zrobić to w jednym miejscu (to jest szczególnie przydatne, bo taki extension method lubi być kopiowany w gazylion miejsc albo trafiać do jakiegoś syfnego UtilsModule, co przy dużej domenie robi się problematyczne)
  • Identyfikator może chcieć serializować się do wielu formatów (tekstowy, skompresowany, zanonimizowany itp) - łatwo jest dodać do biblioteczki JSON-owej zwykły serializer dla typu X, niż serializer dla typu GUID, który musiałby rozkminiać, czy ten GUID jest zwykłym GUID-em, czy trzeba go specjalnie obsługiwać

@1a2b3c4d5e Moim zdaniem ten przykład jest słaby, bo woła konstruktor i przekazuje tam GUID-a. Lepiej jest zrobić UserId.FromStringIdentifier(User.FindFirstValue(ClaimTypes.NameIdentifier)) (czy co tam ma być), a ta metoda już sobie sama rozpozna, że to GUID. Tym sposobem możesz wspierać wiele formatów identyfikatora i jesteś odporny na zmiany.

0

Trochę nie rozumiem sensu istnienia tej paczki, skoro globalne konwersje silnie typowanych ID na potrzeby EF Core czy jakiegoś Cosmosa można osiągnąć w kilkadziesiąt linii. Tu przykład z Cosmosem:

public class EmployeeId : TypedId
{
    public EmployeeId(string id) : base(id)
    {
    }
}

[JsonConverter(typeof(IdConverter))]
public abstract class TypedId : ValueObject
{
    public string Value { get; }

    public TypedId(string value)
    {
        Value = value;
    }
}

public class IdConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var id = (TypedId)value;
        serializer.Serialize(writer, id.Value);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var guid = serializer.Deserialize<string>(reader);
        return Activator.CreateInstance(objectType, guid);
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(TypedId).IsAssignableFrom(objectType);
    }
}

Oczywiście Activator.CreateInstance jest wolniejsze niż wywołanie konstruktora, ale pewnie można to dalej optymalizować jakimś cachem.

1

No sens jest taki żeby tego nie pisać samemu. Ta paczka to source generator - nie dodaje żadnych zależności tylko pisze ten kod za Ciebie, nic nie kosztuje żeby ją dodać poza ułamkiem sekundy czasu kompilacji. Zobacz w 5:17 na filmiku powyżej co generuje.

0

No, tylko po co dodawać te paczkę i generować sporo powtarzalnego kodu, skoro można samemu napisać jeden globalny konwerter w 5 minut, który będzie działać dla wszystkich silnie typowanych ID?

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