Cześć! Zainspirowany artykułami oraz działaniami na social mediach prowadzonych przez Arkency zdecydowałem się stworzyć bardzo krótki wpis. Zawierać on będzie wyłącznie techniczne rozwiązanie konkretnego problemu! Przejdźmy więc od razu do tematu. Potrzebowałem stworzyć rozwiązanie, które będzie zawierało listę dostępnych identyfikatorów oraz będzie pomagało w filtrowaniu różnych encji po tych identyfikatorach. Dodatkowym założeniem, była możliwość ominięcia tych filtrów w pewnych przypadkach. Załóżmy istnienie dowolnej implementacji poniższego interfejsu:

public interface IOmitIdentifiersValidationStrategy
{
    bool CanOmitValidation();
}

Głównym założeniem była prostota użycia oraz ewaluacja na bazie. Drugi punkt wymagał ode mnie użycia klasy Expression. Pierwszy natomiast wymusił poniższą deklarację metody (pod spodem przykład użycia)

Expression<Func<TEntity, bool>> CreateExpression<TEntity>(
    Expression<Func<TEntity, IEnumerable<int>, bool>> identifierValidator);

var identifiers = CreateIdentifiers(); //class I want to create
var phones = _context.Phones.Where(identifiers.CreateExpression<Phone>(
    (p, ids) => ids.Contains(p.OwnerId));

Potrzebowałem napisać kod, który pozwoli mi zmienić Expression z dwoma parametrami na taki, który pod jeden z nich podstawi posiadaną listę identyfikatorów. Wpierw musiałem zaimplementować klasę, która pozwoli mi podmienić dowolną cześć Expression.

        
private class Replacer : ExpressionVisitor     
{         
    private readonly Expression _toReplace;         
    private readonly Expression _replacement;   
         
    public Replacer(Expression toReplace, Expression replacement)
    {
        _toReplace = toReplace;
        _replacement = replacement;
    }
            
    public override Expression Visit(Expression node)                
        => node == _toReplace ? _replacement : base.Visit(node);
}

A następnie, musiałem odpowiednio przekształcić Expression, który otrzymałem na wejściu

public Expression<Func<TEntity, bool>> CreateExpression<TEntity>(
    Expression<Func<TEntity, IEnumerable<int>, bool>> identifierValidator)
{
    if (_omitIdentifiersValidationStrategy.CanOmnitValidation())
        return x => true;

    var replacer = new Replacer(identifierValidator.Parameters[1],
        Expression.Constant(_ids));

    var newExpression = replacer.Visit(identifierValidator.Body);

    return Expression.Lambda<Func<TEntity, bool>>(newExpression,
        identifierValidator.Parameters[0]);
}

Jak działa powyższy kod? Najpierw sprawdza, czy może pominąć filtrowania – w tym przypadku zwraca Expression, który zawsze zwraca true. Następnie tworzy Replacer, którego celem jest zmiana drugiego parametru (pamiętamy o indeksowaniu od zera 🙂 ) na stałą, która jest listą identyfikatorów. W kolejnym kroku korzystamy z opisanego „podmieniacza”, a następnie zwracamy Expression – reprezentuje on wywołanie oryginalnego Expression wraz z pierwszym parametrem (drugi jest już podmieniony na stałą) … to by było na tyle! Powyższy kod, po zamknięciu go jednej klasie, pozwala na dość proste używanie filtrowania po liście identyfikatorów dla różnych encji. Na sam koniec przedstawię dwa przykłady użycia:

var phones = _context.Phones.Where(identifiers.CreateExpression<Phone>(
    (p, ids) => ids.Contains(p.OwnerId));

var houses = _context.Houses.Where(identifiers.CreateExpression<House>(
    (h, ids) => h.Residents.Any(r => ids.Contains(r.Id)));

Jak wspomniałem na początku, dzisiaj wyjątkowo krótko. Mam nadzieję, że powyższy kod przyda się nie tylko mi, gdyż sam „straciłem” niemało czasu, aby powyższy problem rozwiązać. Niezależnie dziękuję Wam za przeczytanie tego wpisu i serdecznie zachęcam do komentowania oraz śledzenia kolejnych. Może macie jakiś inny sposób na rozwiązanie tego zagadnienia? Zachęcam również, do przejrzenia już istniejących wpisów – co powiesz na krótki opis projektu Tye?

Kategorie: Techniczne

3 Komentarze

Krzysztof · 2021-04-17 o 21:25

Hej. A jesteś w stanie napisać jaki „biznesowy” problem to rozwiązuje?
Kod, jak na moje zrozumienie, skomplikowany. I na poziomie pobierania danych z bazy, zaskakujący.

    Cesarz · 2021-04-17 o 21:39

    Cześć! Dzięki za komentarz.
    Osobiście zdecydowanie bardziej się odnajduje w pisaniu czystego kodu skupionego na domenie – w tym wypadku jednak nie wpadłem na lepszy sposób niż techniczne (a nie biznesowe) rozwiązanie problemu.
    Wysokopoziomowo mówiąc – dla każdego użytkownika istnieje pewna hierarchia kont do której ma dostęp. Użytkownik powinien widzieć wyłącznie obiekty związane z kontami, do których ma dostęp. Różny jest sposób wyznaczania tych powiązań (np jedna encja ma pole CreatorId, ale inna ma relacje many-to-many przez jakąś asocjację). Dodatkowo, w niektórych przypadkach (np. uprawnienie) ta walidacja powinna być ominięta. Powyższy kod powstał, w celu unifikacji procesu pobierania listy dostępnych ID dla użytkownika oraz walidacji tych ID wraz z możliwością jej pominięcia.
    Dość zawile to napisałem, ale mam nadzieję, że zrozumiale.
    Pozdrawiam!

dotnetomaniak.pl · 2021-04-15 o 22:52

Podmiana parametru w Expression na zmienną – Cesarstwo Dev

Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *