OperationBase
La clase OperationBase<TEntity>
es una clase base abstracta para la implementación de operaciones de negocio sobre entidades en MongoDB. Hereda de RepositoryBase
y añade funcionalidades como el registro de auditoría y la actualización parcial de entidades. Está diseñada para ser la base de los servicios de negocio que gestionan las entidades.
¿Cómo Funciona?
OperationBase<TEntity>
extiende la funcionalidad de RepositoryBase
añadiendo lógica de negocio común. Al crear una entidad, establece automáticamente las propiedades de auditoría (CreatedBy
y CreatedAt
). Al actualizar una entidad, actualiza dinámicamente solo las propiedades no nulas y no protegidas en una lista negra, dejando intactas las propiedades de auditoría y la propiedad Id
. Utiliza el IUserContext
para obtener información del usuario autenticado y realiza las operaciones en MongoDB a través de los métodos heredados de RepositoryBase
.
Métodos
Esta sección describe los métodos principales proporcionados por la clase OperationBase<TEntity>
.
CreateAsync
Type: public virtual Task CreateAsync(TEntity entity, CancellationToken cancellationToken = default)
Crea una nueva entidad en la base de datos de forma asíncrona. Antes de insertar, establece las propiedades de auditoría (CreatedBy
y CreatedAt
) utilizando la información del IUserContext
. TEntity
debe ser una clase que implemente la interfaz IEntityBase
y la interfaz IEntity
cuando sea necesario.
// Create a new userawait repository.CreateAsync(user, CancellationToken.None);
DeleteAsync
Type: public virtual Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
Elimina una entidad de la base de datos por su Guid
de forma asíncrona. TEntity
debe ser una clase que implemente la interfaz IEntityBase
.
// Delete a userawait repository.DeleteAsync(user.Id, CancellationToken.None);
UpdateAsync
Type: public virtual async Task UpdateAsync(Guid id, TEntity entity, CancellationToken cancellationToken = default)
Actualiza una entidad en la base de datos por su Guid
de forma asíncrona. Actualiza solo las propiedades no nulas de la entidad, excluyendo propiedades de auditoría y la propiedad Id
. TEntity
debe ser una clase que implemente la interfaz IEntityBase
.
// Find a user to updateuser.Name = "John Doe Updated";
await repository.UpdateAsync(user.Id, user, CancellationToken.None);
Implementación
La clase OperationBase<TEntity>
se puede utilizar para crear servicios de negocio que gestionen entidades en MongoDB. Al heredar de OperationBase<TEntity>
, los servicios obtienen funcionalidades de auditoría y actualización parcial sin tener que implementarlas manualmente. A continuación, se muestra un ejemplo de cómo usar OperationBase<TEntity>
en una aplicación .NET Core:
using CodeDesignPlus.Net.Mongo.Extensions;
namespace CodeDesignPlus.Net.Mongo.Operations;
/// <summary>/// Base class for MongoDB operations./// </summary>/// <typeparam name="TEntity">The type of the entity.</typeparam>public abstract class OperationBase<TEntity> : RepositoryBase, IOperationBase<TEntity> where TEntity : class, IEntityBase{ private readonly List<string> blacklist = new List<string> { nameof(IEntityBase.Id), nameof(IEntity.CreatedAt), nameof(IEntity.CreatedBy), nameof(IEntity.UpdatedAt), nameof(IEntity.UpdatedBy) };
/// <summary> /// The authenticated user context. /// </summary> protected readonly IUserContext AuthenticateUser;
/// <summary> /// Initializes a new instance of the <see cref="OperationBase{TEntity}"/> class. /// </summary> /// <param name="authenticatetUser">The authenticated user context.</param> /// <param name="serviceProvider">The service provider.</param> /// <param name="options">The MongoDB options.</param> /// <param name="logger">The logger instance.</param> /// <exception cref="ArgumentNullException">Thrown when any of the parameters are null.</exception> protected OperationBase(IUserContext authenticatetUser, IServiceProvider serviceProvider, IOptions<MongoOptions> options, ILogger logger) : base(serviceProvider, options, logger) { ArgumentNullException.ThrowIfNull(authenticatetUser);
AuthenticateUser = authenticatetUser; }
/// <summary> /// Creates a new entity asynchronously. /// </summary> /// <param name="entity">The entity to create.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that represents the asynchronous create operation.</returns> /// <exception cref="ArgumentNullException">Thrown when the entity is null.</exception> public virtual Task CreateAsync(TEntity entity, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(entity);
if (entity is IEntity auditTrailEntity) { auditTrailEntity.CreatedBy = AuthenticateUser.IdUser; auditTrailEntity.CreatedAt = SystemClock.Instance.GetCurrentInstant(); }
return base.CreateAsync(entity, cancellationToken); }
/// <summary> /// Deletes an entity by its identifier asynchronously. /// </summary> /// <param name="id">The identifier of the entity to delete.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that represents the asynchronous delete operation.</returns> public virtual Task DeleteAsync(Guid id, CancellationToken cancellationToken) { return DeleteAsync(id, Guid.Empty, cancellationToken); }
/// <summary> /// Deletes an entity by its identifier asynchronously. /// </summary> /// <param name="id">The identifier of the entity to delete.</param> /// <param name="tenant">The tenant identifier only for entities that inherit from <see cref="AggregateRoot"/>.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that represents the asynchronous delete operation.</returns> public virtual Task DeleteAsync(Guid id, Guid tenant, CancellationToken cancellationToken) { var filter = Builders<TEntity>.Filter.Eq(x => x.Id, id);
return DeleteAsync(filter, tenant, cancellationToken); }
/// <summary> /// Updates an entity by its identifier asynchronously. /// </summary> /// <param name="id">The identifier of the entity to update.</param> /// <param name="entity">The entity with updated values.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that represents the asynchronous update operation.</returns> /// <exception cref="ArgumentNullException">Thrown when the entity is null.</exception> public virtual Task UpdateAsync(Guid id, TEntity entity, CancellationToken cancellationToken) { return UpdateAsync(id, entity, Guid.Empty, cancellationToken); }
/// <summary> /// Updates an entity by its identifier asynchronously. /// </summary> /// <param name="id">The identifier of the entity to update.</param> /// <param name="entity">The entity with updated values.</param> /// <param name="tenant">The tenant identifier only for entities that inherit from <see cref="AggregateRoot"/>.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that represents the asynchronous update operation.</returns> /// <exception cref="ArgumentNullException">Thrown when the entity is null.</exception> public virtual async Task UpdateAsync(Guid id, TEntity entity, Guid tenant, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(entity);
var filter = Builders<TEntity>.Filter.Eq(x => x.Id, id).BuildFilter(tenant);
var updates = new List<UpdateDefinition<TEntity>>();
var properties = typeof(TEntity).GetProperties().Where(x => !blacklist.Contains(x.Name)).ToList();
foreach (var property in properties.Select(property => property.Name)) { var value = entity.GetType().GetProperty(property).GetValue(entity);
if (value != null) { updates.Add(Builders<TEntity>.Update.Set(property, value)); } }
var update = Builders<TEntity>.Update.Combine(updates); await GetCollection<TEntity>().UpdateOneAsync(filter, update, cancellationToken: cancellationToken); }}
Ejemplo de Uso
Este código usa CodeDesignPlus.Net.Mongo
para realizar operaciones CRUD en MongoDB. Configura la base de datos, crea un repositorio (UserRepository
que herada de OperationBase
), y luego crea, actualiza y elimina un usuario (UserEntity
).
using CodeDesignPlus.Net.Mongo.Extensions;using CodeDesignPlus.Net.Mongo.Sample.OperationBase;using CodeDesignPlus.Net.Mongo.Sample.OperationBase.Entities;using CodeDesignPlus.Net.Mongo.Sample.OperationBase.Respositories;using CodeDesignPlus.Net.Mongo.Sample.OperationBase.Services;using CodeDesignPlus.Net.Security.Abstractions;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;
var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build();
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging();serviceCollection.AddMongo<Startup>(configuration);serviceCollection.AddSingleton<IUserContext, FakeUserContext>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var repository = serviceProvider.GetRequiredService<IUserRepository>();
var tenant = Guid.NewGuid();var createdBy = Guid.NewGuid();
var user = UserAggregate.Create(Guid.NewGuid(), "John Doe", "john.doe@codedesignplus.com", tenant, createdBy);
// Create a new userawait repository.CreateAsync(user, CancellationToken.None);
// Find a user to updateuser.UpdateName("John Doe Updated");
await repository.UpdateAsync(user.Id, user, tenant, CancellationToken.None);
// Delete a userawait repository.DeleteAsync(user.Id, tenant, CancellationToken.None);
-
Configuración de Mongo en
appsettings.json
:{"Core": {"Business": "CodeDesignPlus","AppName": "sample-mongo-operationbase","Version": "v1","Description": "Sample of CodeDesignPlus.Net.Core","Contact": {"Name": "CodeDesignPlus","Email": "custom@outlook.com"}},"Mongo": {"Enable": true,"ConnectionString": "mongodb://localhost:27017","Database": "db_sample","RegisterAutomaticRepositories": true}} -
Creamos la entidad para el ejemplo:
Procedemos a crear la entidad que representará los documentos en la colección de MongoDB, esta entidad debe implementar la interfaz
IEntity
deCodeDesignPlus.Net.Core.Abstractions
.using System;using CodeDesignPlus.Net.Core.Abstractions;namespace CodeDesignPlus.Net.Mongo.Sample.OperationBase.Entities;public class ProductEntity : IEntity{public Guid Id { get; set; }public bool IsActive { get; set; }public Instant CreatedAt { get; set; }public Guid CreatedBy { get; set; }public Instant? UpdatedAt { get; set; }public Guid? UpdatedBy { get; set; }public required string Name { get; set; }public string? Description { get; set; }}using CodeDesignPlus.Net.Core.Abstractions;namespace CodeDesignPlus.Net.Mongo.Sample.OperationBase.Entities;public class UserAggregate(Guid id) : AggregateRoot(id){public string Name { get; private set; } = string.Empty;public string Email { get; private set; } = string.Empty;public List<ProductEntity> Products { get; private set; } = [];private UserAggregate(Guid id, string name, string email, Guid tenant, Guid createdBy): this(id){Name = name;Email = email;Tenant = tenant;IsActive = true;CreatedBy = createdBy;CreatedAt = SystemClock.Instance.GetCurrentInstant();}public static UserAggregate Create(Guid id, string name, string email, Guid tenant, Guid createdBy){return new UserAggregate(id, name, email, tenant, createdBy);}public void AddProduct(ProductEntity product){Products.Add(product);}public void UpdateName(string name){Name = name;}} -
Creamos el repositorio:
Para efectos de este ejemplo, procederemos a crear el repositorio
UserRepository
que hereda deOperationBase
.using CodeDesignPlus.Net.Mongo.Abstractions.Options;using CodeDesignPlus.Net.Mongo.Repository;using CodeDesignPlus.Net.Mongo.Sample.OperationBase.Entities;using CodeDesignPlus.Net.Security.Abstractions;using Microsoft.Extensions.Logging;using Microsoft.Extensions.Options;namespace CodeDesignPlus.Net.Mongo.Sample.OperationBase.Respositories;public class UserRepository(IUserContext authenticatetUser, IServiceProvider serviceProvider, IOptions<MongoOptions> mongoOptions, ILogger<UserRepository> logger): CodeDesignPlus.Net.Mongo.Operations.OperationBase<UserAggregate>(authenticatetUser, serviceProvider, mongoOptions, logger), IUserRepository{} -
Creamos la clase Startup:
Al utilizar el SDK de CodeDesignPlus, generalmente es necesario crear una clase
Startup
que implemente la interfazIStartup
. Esto facilita una mejor organización de los servicios que se desean registrar en el contenedor de dependencias de la aplicación, para cada una de las capas del servicio.using System;using CodeDesignPlus.Net.Core.Abstractions;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;namespace CodeDesignPlus.Net.Mongo.Sample.OperationBase;public class Startup : IStartup{public void Initialize(IServiceCollection services, IConfiguration configuration){}} -
Creamos el servicio de usuario:
Para efectos de este ejemplo, procederemos a crear el servicio
FakeUserContext
que implementaIUserContext
. Este servicio se utiliza para obtener información del usuario autenticado. Para un uso en producción se debe usar la implementación que provee la libreriaCodeDesignPlus.Net.Security
.using System;using System.Security.Claims;using CodeDesignPlus.Net.Security.Abstractions;namespace CodeDesignPlus.Net.Mongo.Sample.OperationBase.Services;public class FakeUserContext : IUserContext{public bool IsApplication => false;public Guid IdUser => Guid.Parse("1f4eac7b-4b3b-4b3b-8b3b-4b3b4b3b4b3b");public bool IsAuthenticated => true;public string Name => "John Doe";public string FirstName => "John";public string LastName => "Doe";public string City => "Bogotá";public string Country => "Colombia";public string PostalCode => "110111";public string StreetAddress => "Calle 123 # 123 - 123";public string State => "Cundinamarca";public string JobTitle => "Software Developer";public string[] Emails => ["john.doe@codedesignplus.com"];public Guid Tenant => Guid.Parse("2f4eac7b-4b3b-4b3b-8b3b-4b3b4b3b4b3b");public ClaimsPrincipal User => new();public TValue GetClaim<TValue>(string claimType){throw new NotImplementedException();}public TValue GetHeader<TValue>(string header){throw new NotImplementedException();}} -
Registramos los servicios en el contenedor de dependencias:
serviceCollection.AddMongo<Startup>(configuration);serviceCollection.AddSingleton<IUserContext, FakeUserContext>(); -
Obtenemos la instancia de nuestro repositorio:
var repository = serviceProvider.GetRequiredService<IUserRepository>(); -
Creamos un nuevo usuario:
var user = new UserEntity{Id = Guid.NewGuid(),Name = "John Doe",Email = "john.doe@codedesignplus.com",//CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), //Valor asignado por la clase OperationBase//CreatedBy = Guid.NewGuid(), //Valor asignado por la clase OperationBaseIsActive = true};await repository.CreateAsync(user, CancellationToken.None);
Conclusiones
La clase OperationBase<TEntity>
simplifica la creación de servicios que interactúan con MongoDB al proporcionar una base sólida para las operaciones de negocio. Al extender RepositoryBase
con funcionalidades de auditoría y actualización parcial, permite a los desarrolladores concentrarse en la lógica específica de cada servicio sin tener que repetir código común.