.Net Domain-Driven Design(DDD)
Que es DDD?
Domain-Driven Design no es una arquitectura, es un conjunto de herramientas y practias para construir software correctamente, que resuelva las necesidades de los usuarios, es decir esta centrado en el dominio.
Para aplicar DDD tanto la parte que lleva el dominio en nuestro caso los veterinarios y la parte tecnica, los programadores, tienen que estar en constante comunicacion para poder definir de la mejor manera los contextos delimitados (bounded contexts) asegurando que el software refleje fielmente las necesidades y procesos del negocio.
Este proyecto práctico se ha desarrollado siguiendo, en la medida de lo posible, los lineamientos de DDD, con el objetivo de alinear el modelo de software con el conocimiento del dominio.
Enlace al repositorio de github: https://github.com/Dannyy3/.NET-DDD-Domain-Driven-Design-
Wisdom Pet Medicen
Este proyecto consiste en una veterinaria que necesita una aplicacion para poder realizar consultas a animales; recetar medicamentos, registras signos vitales, tener un catalogo de razas y sus pesos ideales entre otras.
Se ha definido previamente con los veterinarios(dominio) los requisitos.
Creacion proyecto Wisdom Pet Medicine
Wisdom Pet Medicine(Wpm) esta vez lo haremos con comandos
dotnet new classlib -n Wpm.Management.Domain dotnet new webapi -n Wpm.Management.Api –use-controllers dotnet new xunit -n Wpm.Management.Domain.Tests
A continuacion referenciamos
cd .\Wpm.Management.Api
dotnet add reference ..\Wpm.Management.Domain
cd ..
cd .\Wpm-Management.Domain.Tests
dotnet add reference ..\Wpm.Management.Domain
cd..
Creamos la solucion
dotnet new sln -n Wpm dotnet sln add .\Wpm.Management.Api
dotnet sln add \Wpm.Management.Domain
dotnet sln add \Wpm.Management.Domain.Tests\
Abrimos el proyecto
Wpm.sln
Encapsular y proteger el estado de las entidades
Para prohibir cambiar la propiedad, en este caso el Id, de una entidad vamos a usar Init:
A continuacion para nuestra clase Pet crearemos un constructor;
1
2
3
4
5
6
7
8
9
10
public class Pet : Entity
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public Pet(Guid id)
{
Id = id;
}
}
De esta manera solo podremos inicializar nuestra clase Pet con un Id pero no podremos modificarlo mas adelante
1
2
var guid = Guid.NewGuid();
var pet1 = new Pet(guid);
esto ya no se podria hacer, porque hemos encapsulado nuestra propiedad pet1.Id = Guid.NewGuid();
Implementacion de ValueObjects
Gracias a DDD podemos usar value objects que nos ayudan a evitar usar tipos de datos primitivos, que es considerado un antipatron. Y nos permite darle sentido a las propiedades por ejemplo Weight = -120
Creamos un record que funcionara como value object, y en Pet utilizamos el tipo Weight en vez de decimal.
1
2
3
4
5
6
7
8
9
10
11
12
13
public record Weight
{
public decimal Value { get; init; }
public Weight(decimal value)
{
if (value < 0)
{
throw new ArgumentException("Weight cannot be negative.", nameof(value));
}
Value = value;
}
}
Implementancion Servicio de Dominio en Value Object
Lo que hemos hecho en esta parte ha sido implementar en un value object, breedid, un servicio de dominio que hemos creado para validar un id.
Operadores implícitos en Value Objects
El operador implicit
nos permite crear un objeto (en este caso, Weight
) sin necesidad de instanciarlo explícitamente con new
.
De esta manera el código se simplifica y se hace más legible.
Definición del operador y ejemplo
1
2
3
4
public static implicit operator Weight(decimal value)
{
return new Weight(value);
}
Ejemplo; pet.SetWeight(new Weight(25), breedService);
Con implicit operator: pet.SetWeight(25, breedService)
¿Qué es un Aggregate Root?
En DDD, un Aggregate Root es la entidad principal de un agregado.
Un agregado es un conjunto de entidades y value objects que se tratan como una única unidad de consistencia.
Puntos clave:
- Solo se accede a un agregado a través de su Aggregate Root.
- El Aggregate Root se encarga de mantener las invariantes y la consistencia interna del agregado.
En nuestro caso el Aggregate Root es Consultation que estara en nuestro contexto de Clinic.
Contiene entidades y value objects relacionados con una consulta veterinaria:
DrugAdministration (entidad dentro de la consulta).
VitalSigns (lecturas asociadas a la consulta).
Diagnosis, Treatment, Weight (value objects).
Mapeo Shared Kernel
¿Qué es un Shared Kernel?
Un Shared Kernel es un módulo compartido entre varios contextos/dominios de nuestra aplicación.
Contiene conceptos generales y reutilizables que no son específicos de un solo dominio, si no que se usan en varios.
Gracias a esto:
- Evitamos duplicar código.
- Simplificamos la arquitectura.
Implementación en nuestro Shared Kernel
Para esta parte, crearemos una nueva librería de clases llamada Wpm.SharedKernel
.
Dentro de este módulo, crearemos la clase AggregateRoot
, que hereda de Entity
:
1
2
3
4
5
6
namespace Wpm.SharedKernel
{
public class AggregateRoot : Entity
{
}
}
A continuacion crearemos 3 nuevos contextos Clinic, dentro del domain de clinic una clase Consultation que extienda de AggregateRoot. Y por ultimo añadiremos nuestra clase Weight ya que sera usada tanto en el dominio de Clinic como en el de Management
Resumen
Hasta aquí hemos construido la base del proyecto aplicando los principios de Domain-Driven Design (DDD). Por no hacer mas largo este post procedere a hacer un resumen:
Nuestro proyecto final ha quedado tal que asi:
El foco principal ha sido aplicar DDD, por eso la estructura de carpetas en la solución Wpm.Clinic.Api está organizada en torno a los conceptos de infrastructure, application y commands, en lugar de una arquitectura tradicional por capas.
Para la persistencia de datos, hemos utilizado SQLite junto con Entity Framework. Cada bounded context tiene su propio contexto de base de datos. En el archivo Program
, configuramos el contexto para usar SQLite y, mediante una extensión, nos aseguramos de que la base de datos se cree automáticamente si no existe.
1
2
3
4
builder.Services.AddDbContext<ClinicDbContext>(options =>
{
options.UseSqlite("Data Source=Wpm.db");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class ClinicDbContextExtensions
{
public static void EnsureDbISCreated(this IApplicationBuilder applicationBuilder)
{
using var scope = applicationBuilder.ApplicationServices.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ClinicDbContext>();
if (context.Database.EnsureCreated())
{
Console.WriteLine("ClinicDbContext database created successfully.");
}
else
{
Console.WriteLine("ClinicDbContext database already exists.");
}
context.Database.CloseConnection();
}
}
A partir de aquí, el flujo es sencillo: cada servicio cuenta con su respectivo comando (un record que define los datos necesarios). El servicio ejecuta la lógica de negocio, interactúa con la base de datos y las entidades, y finalmente guarda los cambios o devuelve los resultados.
Tambien tenemos metodos dentro de Consultation
que encapsulan la logica de negocio y validaciones propias del dominio por ejemplo SetDiagnosis
que escribe el diagnostico.
1
2
3
4
5
6
7
8
9
public void SetDiagnosis(Text diagnosis)
{
ValidateConsultationStatus();
if (diagnosis == null)
{
throw new ArgumentNullException(nameof(diagnosis), "Diagnosis cannot be null.");
}
Diagnosis = diagnosis;
}
En resumen, el proyecto demuestra cómo estructurar una solución real aplicando DDD: separando responsabilidades, protegiendo la lógica de negocio y facilitando la evolución del software de forma alineada con el dominio.