Entity Framework Core Audit Trail Nasıl Yapılır ?

Data bizler için oldukça önemlidir ve verinin güvenliğide dikkatlice ele alınması gereken bir konudur. Sürekli olarak crud işlemleri yapılan bir projede insert, update, delete gibi operasyonlar yapılırken bunların ne zaman kimler tarafından yapıldığıda önem arz-etmekte. Bunun yanı sıra geliştirdiğiniz projeler eğer bir denetime tabiyse örn. bir sertifikasyon denetimi (PCI DSS gibi) ve denetleyen kişi bu veri üzerinden kimler ne zaman ne tür değişiklikler yaptı görmek isteyebilir.

Audit log kısaca; belirli bir işlemi hangi zamanda kimler tarafından oluşturulduğu veya güncellendiği gibi bilgileri historical bir şekilde tutulmasıdır diyebiliriz.

Bu yazımızda Entity Framework Core kullandığımız bir uygulamamızda oldukça basit olan bir şekilde audit log bilgisini kolayca nasıl implement ederiz buna değineceğiz 

Customer adında bir api'ımızın olduğunu düşünelim ve db'de bulunan customer tablosunda CreatedDate, CreatedBy ve ModifiedDate, ModifiedBy bilgilerinide kaydetmek istiyoruz. Yani data ne zaman kim tarafından yaratıldı veya güncellendi bilgilerinin db'de yer almasını istiyoruz.  

CustomerDbContext 

Normal dbContext kullanarak bir proje geliştirdiğimizde standart .net dökümanlarında yer alan imp. uyguladığımızda projemizin db context sınıfı ve entity model'i üç aşağı beş yukarı aşağıdaki gibi olacaktır;

public class CustomerDbContext : DbContext
{
    public CustomerDbContext(DbContextOptions<CustomerDbContext> options)
        : base(options)
    {  }

    public DbSet<Domain.Customer> Customer { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}
public class Customer : Entity
{
    public string FullName { get; protected set; }
    public string CityCode { get; protected set; }
    public DateTime BirthDate { get; protected set; }

    public Customer(string fullName, string cityCode, DateTime birthDate)
    {
        if (
            string.IsNullOrEmpty(fullName) ||
            string.IsNullOrEmpty(cityCode) ||
            birthDate.Date == DateTime.Today)
        {
            throw new Exception("Fields are not valid to create a new customer.");
        }

        FullName = fullName;
        CityCode = cityCode;
        BirthDate = birthDate;
    }

    protected Customer()
    {  }
}

Bir repository sınıfı ile CRUD işlemlerini yapabiliyoruz ancak her bir customer kaydı için yazımızın başında dediğimiz gibi audit log bilgilerinide kaydetmek istiyoruz. Abstract Entity sınıfına bakacak olursak aşağıdaki gibi ihtiyacımız olan field'ları tanımlayalım.

public abstract class Entity
{
    public Guid Id { get; set; }
    public DateTime CreatedDate { get; set; }
    public string CreatedBy { get; set; }
    public DateTime? ModifiedDate { get; set; }
    public string ModifiedBy { get; set; }
}

Sonrasında dbContext'imize auditable özelliğini kazandırmak için aşağıdaki gibi SaveChanges() metodunu override edip içinde gerekli olan kod bloğunu yazalım. User bilgisi için httpRequest header'da auditUser bilgisinin pass edildiğini varsayarak bu bilgiyi oradan alalım.

   public override int SaveChanges()
   {
       var entries = ChangeTracker.Entries().Where(e => e.Entity is Entity && (e.State == EntityState.Added ||e.State == EntityState.Modified));

       foreach (var entityEntry in entries)
       {
           if (entityEntry.State == EntityState.Added)
           {
               ((Entity)entityEntry.Entity).CreatedDate = DateTime.Now;
               ((Entity)entityEntry.Entity).CreatedBy = _httpContextAccessor.HttpContext.Request.Headers["audityuser"].ToString();
           }
           else if (entityEntry.State == EntityState.Modified)
           {
               ((Entity)entityEntry.Entity).ModifiedDate = DateTime.Now;
               ((Entity)entityEntry.Entity).ModifiedBy = _httpContextAccessor.HttpContext.Request.Headers["audityuser"].ToString();
           }
       }
       return base.SaveChanges();
   }

Geliştirmemiz bu kadardı. Şimdi projede bulunan int. test metodunu çalıştırarak kodlarımızı test edelim. DbContext'i EFCore inmemory özelliğini kullanarak projemiz service'lerine ekledik yani ortada bir sql server aslında yok, bu şekilde çalıştırıp test metodu üzerinden doğruluğunu kontrol edeceğiz.

İlk olarak bir insert işlemi yapıp sonrasında get metodu ile bu işleme ait db kayıtlarını kontrol edelim.

    #region Insert
    var expectedResult = string.Empty;
    var expectedStatusCode = HttpStatusCode.OK;

    // Arrange-1
    var request = new CustomerDto
    {
        FullName = "Caner Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    _client.DefaultRequestHeaders.Add("audity-user", "admin-abraham lincoln");
    // Act-1
    var response = await _client.PostAsync(postUrl, content);

    var actualStatusCode = response.StatusCode;
    var actualResult = await response.Content.ReadAsStringAsync();

    // Assert-1
    Assert.Equal(expectedResult, actualResult);
    Assert.Equal(expectedStatusCode, actualStatusCode);
    #endregion

    #region GetAll
    // Act-2
    var responseGet = await _client.GetAsync(getUrl);
    responseGet.EnsureSuccessStatusCode();

    var actualGetResult = await responseGet.Content.ReadAsStringAsync();
    var getResultList = JsonConvert.DeserializeObject<List<CustomerDto>>(actualGetResult);

    var insertedCustomerExist = getResultList.Any(c => c.CityCode == request.CityCode);

    // Assert-2
    Assert.NotEmpty(getResultList);
    Assert.True(insertedCustomerExist);
    Assert.Equal("admin-abraham lincoln", getResultList.Single(c => c.CityCode == request.CityCode).CreatedBy);
    #endregion

Insert request header'da gönderdiğimiz "admin-abraham lincoln" bilgisi ilgili customer save işleminde createdBy olarak kaydedilmiş oldu ve bunu get yaptıktan sonraki en son assertion'da yukarıda olduğu gibi doğruladık.

Update işlemini tetikleyip modifiedBy bilgisi içinse aşağıdaki gibi test işlemini yapalım.

    #region Update
    // Arrange-3
    var insertedCustomer = getResultList.Single(c => c.CityCode == request.CityCode);
    var requestUpdate = new CustomerDto
    {
        FullName = "Ali Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1994, 1, 1),
        Id = insertedCustomer.Id
    };
    var contentUpdate = new StringContent(JsonConvert.SerializeObject(requestUpdate), Encoding.UTF8, "application/json");
    _client.DefaultRequestHeaders.Clear();
    _client.DefaultRequestHeaders.Add("audity-user", "super-admin nicola tesla");

    // Act-3
    var responseUpdate = await _client.PutAsync(postUrl, contentUpdate);
    responseUpdate.EnsureSuccessStatusCode();
    var updateActualResult = await responseUpdate.Content.ReadAsStringAsync();
    var obj = await JsonConvert.DeserializeObjectAsync<CustomerDto>(updateActualResult);
    // Assert-3
    Assert.Equal(obj.FullName, requestUpdate.FullName);
    Assert.Equal("super-admin nicola tesla", obj.ModifiedBy);
    #endregion

Burda da görüldüğü üzre header'da pass ettiğimiz "super-admin nicola tesla" bilgisi modifiedBy olarak kaydedildi ve yazdığımız assertion'la bunu doğrulamış olduk.

Bu örneğimiz aslında en basit haliyle nasıl audity bilgisi alınıp kaydedilir göstermiş oldu ancak, daha derinlere girmek gerekirse bu bilgileri ayrı bir tabloda daha farklı column'lar üzerinde oldData,newData yani değiştirilen kayıtların eski ve yeni halleriyle birlikte store edilebildiği bir yapı tasarlamak ihtiyaca göre daha verimli olabilir. Veya bu işlemi hard-coded yapmak yerine third party çözümler kullanabilmekte mümkün.

Source

Add comment