Konfiguracja Audit z Entity Framework Plus

Wprowadzenie

W poprzednim wpisie pokazałem, w jaki sposób zapisać historię zmian obiektów w Entity Framework z wykorzystaniem mechanizmu Audit z Entity Framework Plus. W dzisiejszym wpisie pokażę, w jaki sposób skonfigurować ten mechanizm, aby zapisywał te informacje, których potrzebujemy, w odpowiedni sposób.

Modyfikacja przykładu

W tym wpisie będę bazował na przykładzie z wcześniejszego wpisu, ale go odrobinę zmieniłem.

Do klasy Product dodałem dwie nowe właściwości (Description oraz Price), które posłużą nam w dalszej części wpisu:

public class Product : BaseModel
{
public string Name { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
view raw Product.cs hosted with ❤ by GitHub

Do klasy Program dodałem kolejną metodę testową (SoftDeleteTest), aby zobaczyć, w jaki sposób zachowa się audyt w przypadku miękkiego usuwania obiektów, poprzez ustawienie flagi IsActive na false zamiast usuwania rekordu z bazy. Zmodyfikowany kod klasy Program wygląda tak:

class Program
{
static void Main(string[] args)
{
int id = AddTest();
EditTest();
SoftDeleteTest();
DeleteTest();
ShowHistory(id);
}
static int AddTest()
{
using (DataContext db = new DataContext())
{
var category = db.Categories.FirstOrDefault();
var product = new Product()
{
Name = "Product Name",
Category = category,
Description = "Product Description",
Price = 10.23M
};
db.Products.Add(product);
db.SaveChanges();
return product.Id;
}
}
static void EditTest()
{
using (DataContext db = new DataContext())
{
var category = db.Categories
.OrderByDescending(c => c.Id)
.FirstOrDefault();
var product = db.Products
.OrderByDescending(c => c.Id)
.FirstOrDefault();
product.Category = category;
product.Name = "New Product Name";
db.SaveChanges();
}
}
static void SoftDeleteTest()
{
using (DataContext db = new DataContext())
{
var product = db.Products
.OrderByDescending(c => c.Id)
.FirstOrDefault();
product.IsActive = false;
db.SaveChanges();
}
}
static void DeleteTest()
{
using (DataContext db = new DataContext())
{
var product = db.Products
.OrderByDescending(c => c.Id)
.FirstOrDefault();
db.Products.Remove(product);
db.SaveChanges();
}
}
static void ShowHistory(int id)
{
using (DataContext db = new DataContext())
{
var histories = db.AuditEntries.Where<Product>(id);
foreach (var history in histories)
{
Console.WriteLine($"State: {history.State}");
foreach (var property in history.Properties)
{
Console.WriteLine($"Propepty: {property.PropertyName}, OldValue: {property.OldValue}, NewValue: {property.NewValue}");
}
Console.WriteLine();
}
}
}
}
view raw Program.cs hosted with ❤ by GitHub

Ostatnią modyfikacją jest wydzielenie z metody SaveChanges do dedykowanej metody (ConfigureAudit) konfiguracji klasy Audit z Entity Framework Plus. W niej znajdować się będzie kod konfigurujący działanie mechanizmu. W dalszej części wpisu będę wrzucał tutaj tylko kod tej metody, aby listingi były czytelniejsze.

public class DataContext : DbContext
{
static DataContext()
{
AuditManager.DefaultConfiguration.AutoSavePreAction = (context, audit) =>
(context as DataContext).AuditEntries.AddRange(audit.Entries);
}
public DataContext()
: base("Name=DefaultConnection")
{
}
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<AuditEntry> AuditEntries { get; set; }
public DbSet<AuditEntryProperty> AuditEntryProperties { get; set; }
public string UserName { get; set; } = "System";
public override int SaveChanges()
{
var audit = ConfigureAudit();
audit.PreSaveChanges(this);
var rowAffecteds = base.SaveChanges();
audit.PostSaveChanges();
if (audit.Configuration.AutoSavePreAction != null)
{
audit.Configuration.AutoSavePreAction(this, audit);
base.SaveChanges();
}
return rowAffecteds;
}
private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
return audit;
}
}
view raw DataContext.cs hosted with ❤ by GitHub

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Wybór obiektów do zapisu w historii

Pierwszą z rzeczy, które możemy skonfigurować w Audit, jest wybór typów, dla których biblioteka będzie zapisywała historię. Domyślnie zapisywane są wszystkie typy powiązane z obiektem kontekstowym (poza klasami z Audit). Możemy z wszystkich typów wybrać te, które będą pomijane podczas zapisywania historii. Robimy to za pomocą metody Exclude, która ma kilka przeciążonych wersji.

Poniższy kod wyklucza zapis historii dla klasy Product:

private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
audit.Configuration.Exclude<Product>();
return audit;
}

Możemy również skorzystać z wersji metody Exclude, która przyjmuje jako parametr Func, a która na wejściu otrzymuje obiekt i musi zwrócić boola, który określi, czy zapisać historię, czy nie. Dzięki temu możemy filtrować przy zapisie historii nie tylko typy obiektów, ale również i poszczególne obiekty:

private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
audit.Configuration.Exclude(o => o is Product && ((Product)o).Name == "Product Name");
return audit;
}

Innym sposobem wyboru typów i obiektów do historii jest wykluczenie wszystkich typów, a następnie skorzystanie z metody Include, która wybiera typy i obiekty do zapisu w historii:

private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
audit.Configuration.Exclude(o => true);
//audit.Configuration.Include<Product>();
audit.Configuration.Include(o => o is Product && ((Product)o).Name == "Product Name");
return audit;
}

Wybór właściwości do zapisu w historii

Audit umożliwia również wybór poszczególnych właściwości, która mają być zapisane w historii. Właściwości możemy konfigurować w bardzo podobny sposób jak obiekty. Czyli wykluczyć jakieś właściwości (np. flagę IsActive z BaseModel):

private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
audit.Configuration.ExcludeProperty<BaseModel>(t => t.IsActive);
return audit;
}

lub wykluczyć wszystkie właściwości i wybierać tylko te, które chcemy zapisać w historii:

private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
audit.Configuration.ExcludeProperty<Product>();
audit.Configuration.IncludeProperty<Product>(t => t.Name);
return audit;
}

Tutaj warto zaznaczyć, że Audit w historii zawsze zapisuje kolumny, które są kluczami głównymi rekordu. Nawet gdy je ręcznie wykluczymy. Widać to na poniższym zrzucie ekranu, na którym znajduje się wynik działania ostatniej wersji metody ConfigureAudit (wykluczenie wszystkich właściwości i dodanie tylko Name z klasy Product):

audit include properties

Formatowanie danych

Podczas zapisywania historii możemy również określić format danych, jaki zostanie użyty do zapisania informacji w historii. W bazie poprzednia oraz aktualna wartość właściwości jest zapisywana w postaci napisu. Metoda Format określa, jak zapisać dane w bazie. Używamy jej dla poszczególnych właściwości, jak to widać poniżej na przykładzie ceny produktu:

private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
audit.Configuration.Format<Product>(x => x.Price, x => ((decimal)x).ToString("0.00 zl"));
return audit;
}

W wyniku otrzymujemy ładnie sformatowane dane:

audit property format

W przypadku formatowania danych na potrzeby wyświetlenia historii użytkownikowi wykorzystuję dwa podejścia. Dla prostych typów i sytuacji (tak jak wyżej w przypadku ceny) korzystam z tego, co udostępnia nam Audit. W niektórych sytuacjach dane formatuję dopiero podczas wyświetlania.

Robię tak głównie w przypadku relacji między obiektami. Audit zapisuje w bazie klucz główny powiązanego obiektu (np. CategoryId). Dzięki temu podczas wyświetlania użytkownikowi widoku z historią, mogę pobrać aktualną nazwę obiektu (np. nazwę kategorii) i ją wyświetlić. A w przypadku na przykład aplikacji ASP.NET MVC mogę również wygenerować link do strony danego obiektu.

Ignorowanie niezmienionych właściwości

Audit podczas zapisu historii pomija pola, które nie zostały zmienione (nie dotyczy to kluczy głównych obiektów). Możemy zmienić również i to, dzięki czemu w historii zostaną zapisane wszystkie właściwości obiektu z danej chwili. Przydaje się to w sytuacji, gdy chcemy łatwo wyświetlić stan całego obiektu z jakiegoś momentu w przeszłości, a nie tylko to, co zostało zmienione.

Aby zmienić to zachowanie, wystarczy tylko ustawić właściwość IgnorePropertyUnchanged na false:

private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
audit.Configuration.IgnorePropertyUnchanged = false;
return audit;
}

Miękkie usuwanie obiektów

Ostatnią rzeczą, którą chcę poruszyć w tym wpisie, jest wsparcie dla miękkiego usuwania obiektów. Domyślnie w sytuacji, gdy ustawimy flagę określającą, czy obiekt jest usunięty, czy nie (np. IsActive lub Deleted w zależności jak mamy to zorganizowanie w aplikacji), Audit potraktuje to jako modyfikację obiektu:

audit problem with soft delete

W konfiguracji Audit możemy określić warunek, który decyduje, czy usunęliśmy miękko obiekt, czy go tylko zmodyfikowaliśmy:

private Audit ConfigureAudit()
{
var audit = new Audit();
audit.CreatedBy = UserName;
audit.Configuration.SoftDeleted<BaseModel>(x => x.IsActive == false);
return audit;
}

Po takiej zmianie Audit rozpozna, że obiekt został usunięty, a nie zmodyfikowany:

audit support for soft delete

Przykład

Podczas pracy nad tym wpisem rozbudowałem przykład z wpisu wprowadzającego do zapisu historii zmian obiektów. Przykład możesz znaleźć na githubie (https://github.com/danielplawgo/EFAudit). Po jego pobraniu należy w app.config ustawić connection string do testowej bazy danych. Po tym można już uruchomić aplikację, która doda, zedytuje oraz usunie produkt i na końcu wyświetli jego historię.

W klasie DataContext w metodzie ConfigureAudit znajdują się zakomentowane wszystkie omawiane przypadki. Aby je przetestować, wystarczy tylko odkomentować określony fragment kodu.

Podsumowanie

Mechanizm Audit z Entity Framework Plus można bardzo łatwo dostosowywać do swoich potrzeb. Możemy zmienić jego zachowanie, aby wspierał to, czego potrzebuje nasz klient. Zachęcam do pobrania przykładu i testów mechanizmu.

W kolejnym wpisie zostaniemy dalej przy zapisie historii zmian, ale tym razem skorzystamy z innego mechanizmu, który różni się dość mocno od Audit z Entity Framework Plus.

Szkolenie Entity Framework Core

Szkolenie Entity Framework Core

Zainteresował Ciebie ten temat? A może chcesz więcej? Jak tak to zapraszam na moje autorskie szkolenie z Entity Framework Core.

1 thought on “Konfiguracja Audit z Entity Framework Plus

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.