.NET 6 WebAPI + Dapper - automatyczne dopisywanie danych trackingowych do obiektu

0

Witam.
Mam klasy, które zawierają właściwości o dacie i użytkowniku, który wykonał jakieś działania na nich

public DateTime CreatedAt { get; set; }
public int CreatedBy { get; set; }
public DateTime ModifiedAt { get; set; }
public int ModifiedBy { get; set; }

Jak to mądrze rozwiązać, aby się automatycznie przypisywało podczas zapisywania obiektu do bazy?

0

Pokaż kawałek kodu jak to tworzysz i wrzucasz do bazy.

0

Racja. Zmieniłem też tytuł, że to WebAPI.

CustomersController

[HttpPost("add")]
public async Task<IActionResult> PostCustomer([FromBody] Customer customer)
{
  try
  {
    return Ok(await _writer.AddCustomer(customer));
  }
  catch (Exception ex)
  {
    _logger.LogError(ex, "CustomersController");
    return BadRequest(ex);
  }
}

CustomersService

public async Task<int> AddCustomer(Customer customer)
{
  return await _sql.Insert(customer);
}

SqlService


public async Task<int> Insert<T>(T obj) where T : class
{
  using (IDbConnection db = new SqlConnection(_configuration.SqlConfiguration.SystemConnectionString))
  {
    return await db.InsertAsync(obj);
  }
}
3

Pierwszym naturalnym miejscem na takie coś jest interceptor ORMa, ale Dapper nie jest ORMem, więc raczej nie wspiera takich bajerów.
A więc może interceptor kontenera IoC?
A jeśli nie, to skoro i tak ten sam obiekt pchasz od API do bazy, to jakieś middleware

2

A może po prostu sam pisz zapytanie insertujące, w której uzupełniałbyś te wartości a nie korzystaj z InsertAsync()?

2

W Dapperze nie zrobisz tego automatycznie, musisz to robić ręcznie. Jedyna opcja żeby się nie narobić to zrobić sobie generyczne metody Insert i Update w których będziesz ustawiał te wartości. Czyli coś w stylu:

public interface IAuditableEntity
{
	DateTime CreatedAt { get; set; }
	int CreatedBy { get; set; }
	DateTime ModifiedAt { get; set; }
	int ModifiedBy { get; set; }
}

public class Entity : IAuditableEntity
{
}

public class SqlService<TEntity> where TEntity : Entity
{
	private readonly IAuthContext _authContext;
	
	public SqlService(IAuthContext authContext)
	{
		_authContext = authContext;
	}
	
	public void Insert(TEntity entity)
	{
		entity.CreatedAt = DateTime.UtcNow;
		entity.CreatedBy = _authContext.UserId;

        // db insert code...
	}
	
	public void Update(TEntity entity)
	{
		entity.ModifiedAt = DateTime.UtcNow;
		entity.ModifiedBy = _authContext.UserId;
        
        // db update code...
	}
}
3

Inną opcją jest jeszcze zapisywanie sobie takich informacji w eventach i zapisywanie streamu tych eventów jako audit log, co ja osobiście preferuję. O tak:

public interface IEvent
{
	Guid Id { get; }
	int EntityId { get; }
}

public class EntityCreated :  IEvent
{
	public EntityCreated(int entityId, int creatorId)
	{
		Id = Guid.NewGuid();
		EntityId = entityId;
		CreatedAt = DateTime.UtcNow;
		CreatedBy = creatorId;
	}
	public Guid Id { get; }
	public int EntityId { get; }
	public DateTime CreatedAt { get; }
	public int CreatedBy { get; }
}

public class EntityModified :  IEvent
{
	public EntityModified(int entityId, string fieldName, string oldValue, string newValue, int modifierId)
	{
		Id = Guid.NewGuid();
		EntityId = entityId;
        FieldName = fieldName;
        OldValue = oldValue;
        NewValue = newValue;
		ModifiedAt = DateTime.UtcNow;
		ModifiedBy = modifierId;
	}
	
	public Guid Id { get; }
	public int EntityId { get; }
    public string FieldName { get; }
    public string OldValue { get; }
    public string NewValue { get; }
	public DateTime ModifiedAt { get; }
	public int ModifiedBy { get; }
}

public abstract class EntityBase
{
	private readonly List<IEvent> _events = new List<IEvent>();
	
	public int Id { get; }
	public IReadOnlyCollection<IEvent> Events => _events;
	
	public void RegisterEvent(IEvent @event)
	{
		_events.Add(@event);
	}
	
	public void ClearEvents()
	{
		_events.Clear();
	}
}

public class Entity : EntityBase
{
	private Entity(int creatorId)
	{
		RegisterEvent(new EntityCreated(Id, creatorId));
	}
	
	public string Name { get; private set; }

    public static Entity Create(int creatorId) => new(creatorId);
 
	public void ChangeName(string newName, int modifierId)
	{
        RegisterEvent(new EntityModified(Id, nameof(Name), Name, newName, modifierId));
		Name = newName;
	}
}

public class Repository<TEntity> where TEntity : Entity
{
	public void Save(TEntity entity)
	{
		// insert entity and its events in one transaction
        entity.ClearEvents(); // after commit
	}
}

Oczywiście to tylko przykład, żeby pokazać kierunek. Konretna implementacja będzie się różnić w zależności od wymagań takiego audit loga i modelu dziedziny.

2

Można też zrobić coś w stylu dekoratora.

class MyDecoratedsqlConnection : IDbConnection 
{
        
    public void InsertAsync(object obj) // metoda z interface
    {
        // ty sprawdzasz czy obiekt ma pola auditable
        ///  uzupełniasz je
        using (IDbConnection db = new SqlConnection(_configuration.SqlConfiguration.SystemConnectionString))
        {
          return await db.InsertAsync(obj);
        }
        
    }
}

i w sql service używasz MyDecoratedsqlConnection

0

Dwie opcje by mnie ciekawiły, jedna z middleware, o której pisał @somekind , a druga @szydlak. Pytanie tylko - jak odwołać się do zalogowanego usera na podstawie tokena? Muszę jakoś te dane wpuścić do serwisu, w którym robię insert. Gdzie z DateTime.Now nie ma problemu, tak z ID usera, który wykonuje tą operację mogę mieć problem. Powinienem się odwołać do Claims? Da się to zrobić mądrzej?

1

Ja w projektach mam tylko i wyłącznie jakiś tam username jako pole auditable. Bez idka. I Biorę to z claim

0

Ok, a jeśli struktura solucji ma wydzieloną logikę do osobnego projektu, to jak mogę się odwołać do claimsów aktualnie "zalogowanego"? Musiałbym się odwołać z kontekstu do usera w kontrolerze i przepuszczać go przez wszystkie pośrednie serwisy, aż nie wpadnie tam gdzie jest insert?

2

Ja mam deklaracje interfacu gdzieś wyżej a implementacje i rejestracje np w warstwie API. A wstrzykuje interface

2

nie jestem pewien czy dobrze rozumiem o co Ci chodzi. Aby dostać się do Claimsów należy użyć IHttpContextAccessor np.

using System.Security.Claims;

public class UserContextService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public UserContextService(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public ClaimsPrincipal User => _httpContextAccessor.HttpContext?.User;

        // np id usera, którego potrzebujesz do swoich potrzeb modified ...
        public int? GetUserId => User is null ? null : int.Parse(User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier).Value);
   }

jak Ci spasi możesz to sobie wyrzucić do interfejsu.

0

Genialne @john_doe! To jest to czego mi było trzeba.

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