.NET [PL] Backend Entity Framework Security

Soft Delete z Entity Framework Dynamic/Global Filters

Ostatnio w aplikacjach panuje moda na Soft Delete. Idea jest słuszna: zamiast usuwać trwale rekordy z bazy danych, oznaczamy je tylko jako usunięte, a przy pobieraniu wybieramy tylko te „nieusunięte”. Problem pojawia się wtedy gdy nasze zapytanie zawiera wiele joinów na tabelach, które są soft deletable. Dlaczego? Ano dlatego, że każdorazowo musimy pamiętać o dołączeniu warunku dla każdej tabeli. W przeciwnym przypadku, może się okazać, że nasze zapytanie będzie pobierało dane, które zostały oznaczone jako usunięte. Sam problem może nie jest jakiś wielki jeżeli tylko odpowiednio się pilnujemy, ale jest to dość męczące. Dziś zaprezentuje Wam bibliotekę, która ułatwi nam życie. Do roboty !

Mamy taki oto interfejs i jego implementację w encji użytkowników:

public interface ISoftDeletable
{
        bool IsActive { get; }
        void Delete();
}


public class UserEntity : IdentityUser,ISoftDeletable
{        
        public bool IsActive { get; private set; }
       
        public UserEntity()
        {            
            IsActive = true;           
        }
 
        void ISoftDeletable.Delete()
        {
            IsActive = false;
        } 
}

Za wiele magii tu nie ma. Każdy nowy użytkownik będzie oznaczony jako aktywny, a metodą Delete zmieniamy jego flagę IsActive na false. Przejdźmy teraz do tytułowej biblioteki. Możemy ją pobrać z NuGeta. Po instalacji przechodzimy do kontekstu naszej bazy danych, a następnie przesłaniamy metodę OnModelCreating w taki oto sposób:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
            modelBuilder.Filter("SoftDeleteFilter", (ISoftDeletable entity) => entity.IsActive, true);
            base.OnModelCreating(modelBuilder);
}

Nasz modelBuilder posiada teraz metodę rozszerzającą Filter, która za pierwszy parametr przyjmuje nazwę naszego filtra, dalej Funca, który określa po jakim polu dokonujemy naszego filtrowania, a trzeci parametr określa jaką wartość pola mają posiadać przefiltrowane obiekty. W naszym przypadku interesują nas jedynie te aktywne. Byliw or not, ale to wszystko ! Teraz nie musimy się już martwić o dołączanie wszędzie warunków, przykładowo zamiast:


var filteredUsers = UserRepository.Query.Any(u => u.IsActive)

teraz robimy:


var filteredUsers = UserRepository.Query.Any()

Oczywiście filtr nie jest „jednopoziomowy”, a warunek dołącza do każdej joinowanej tabeli, która implementuje interfejs ISoftDeletable. 

Zaraz jednak ktoś mógłby mi zarzucić, że no dobra fajnie, ale jak chce zliczyć sobie np. ilość usuniętych osób w lutym to tego nie zrobię bo nawet jeśli dodam warunek negujący flagę IsActive , to do tego automatycznie dołączy mi się warunek, który tej flagi nie zaneguje. W rezultacie otrzymam zbiór pusty, ponieważ boolean nie może być jednocześnie true i false. Prawda ! Dlatego, żeby uprzedzić falę hejtu zaimplementujemy taki magiczny wyłącznik filtra. Generalnie sama biblioteka posiada taką opcję i w naszym przypadku wygląda ona następująco:

context.DisableFilter("SoftDeleteFilter");
//potem włączamy go
context.EnableFilter("SoftDeleteFilter");

Mógłbym Wam powiedzieć: „nooo to jak chcecie wyłączyć to to pierwsze, a potem odpalacie to drugie, żeby włączyć”. No, ale tu pojawia się problem. Zawsze musimy pamiętać o ponownym jego włączeniu. Może się więc okazać, że jakiś programista napisze zapytanie, które ów filtr wyłączy, następnie pobierze dane, po czym wykona drugie zapytanie nadal nie posiadając włączonego filtra (wszystkie warunki przecież usunął). W takim razie zrobimy to ciut lepiej, a na pewno bezpieczniej. Implementujemy sobie taką oto klasę:

public class SoftDeleteFilterScope : IDisposable
{
        private readonly Context _context;
        public SoftDeleteFilterScope(Context context)
        {
            _context = context;
            context.DisableFilter("SoftDeleteFilter");
        }
 
        public void Dispose()
        {
            _context.EnableFilter("SoftDeleteFilter");
        }
}

Jak widać, w konstruktorze naszego scope-a, dezaktywujemy filtr. Ponadto nasza klasa implementuje interfejs IDisposable, a w metodzie Dispose nasz filtr ponownie zostaje włączony.  Następnie w naszym kontekście oraz repozytorium  (u mnie będzie to repozytorium generyczne) tworzymy metody, które dostarczą nam instancję scope-a:


// kontekst
public IDisposable GetSoftDeleteFilterScope()
{
       return new SoftDeleteFilterScope(this);
}

//repozutorium

public IDisposable GetSoftDeleteFilterScope()
{
       return _context.GetSoftDeleteFilterScope();
}

I teraz cała magia, kiedy chcemy aby nasze zapytanie nie korzystało z filtra robimy po prostu:

using (var scope = Repository.GetSoftDeleteFilterScope())
{
      // nasze zapytanie
}

Dzięki temu mamy pewność, że po wyjściu z usinga filtr zostanie włączony w metodzie Dispose naszego scope-a. Aha, jeszcze jedna sprawa! Ważne jest to, aby rezultat został zmaterializowany jeszcze w samym usingu, ponieważ dopiero wtedy wykonany zostanie round trip do bazy. W przeciwnym przypadku filtr zostanie wyłączony w momencie tworzenia samego zapytania, a nie przy jego wykonywaniu. Druga ważna sprawa to fakt, że na dzień dzisiejszy biblioteka nie obsługuje EF7 🙁 Postaram się znaleźć jakiś zamiennik, ale wszyscy z Was, którzy tworzą aplikacje w EF6 powinni rozważyć jej użycie (szczególnie, że nie musimy ograniczać się do samego soft delete-u) .

No to tyle, następny wpis będzie kontynuacją Aurory. W końcu muszę napisać o tym seed-owaniu bazy w EF7, ale jakoś nie mogę się do tego zabrać 😛  BTW, wpadajcie na githuba bo trochę kodu, już tam jest. Może komuś przyda się coś do swojej aplikacji w ASP.NET Core i nie będzie musiał przeżywać zbędnego piekła jak ja 😀

Do następnego !