Coding Dojo CasanareDevs. Kata 3
Tercer Capítulo del Coding Dojo - Construcción de un API REST en .NET Core 3
El objetivo de esta kata es conocer el funcionamiento básico de .NET y desarrollar un servicio web (Web API REST) bajo ASPNET Core 3
Contenido
Requerimiento
Web API que muestre la frase del día,la frases de un dia específico y el listado de frases de un mes especificado.
Se busca el uso de TDD, Paradigma Orientado a Objetos y los Principios SOLID.
Prerequisitos
Comandos básicos para trabajar en .NET CLI
Para instalar los paquetes de nuget en el proyecto ejecute
dotnet restore
Para Compilar un proyecto, en la carpeta donde se encuentre el .csproj ejecute
dotnet build
Para ejecutar un proyecto, en la carpeta donde se encuentre el .csproj ejecute
dotnet run
Para ejecutar un proyecto de pruebas unitarias, en la carpeta donde se encuentre el proyecto de pruebas ejecute.
dotnet test
Paso a paso
El objetivo es ir avanzando en cada uno de los pasos. Se dejará un enlace al commit previo al inicio del paso y al final.
-1. Una breve explicación de .NET
Como introducción tomé una presentación del dotnet-foundation y las personalicé para dar los primeros pasos en .NET .
Descargar presentación ¿Que es .NET?
Estado del repositorio Link
0. Creación de la estructura de carpetas y arquitectura
La estructura básica del proyecto corresponde a la siguiente estructura:
-root
-src
-test
Usarémos una arquitectura sencilla, en la cual tendremos los siguientes componentes:
- Modelos. Clases POCO (Plain Old CLR Object).
-
DataAccess. Libreria para el acceso a los datos que no depende del Destino. Implementa los siguientes patrones de diseño:
- Patrón Repositorio Repository Pattern.
- Unidad de Trabajo Unit of Work.
-
Services. Con estos dos proyecto se busca facilitar el uso de los conceptos de Segregación de Interfaces e Inversión de Dependencias SOLID.
- Contratos. Definición de Interfaces.
- Implementación. Implementación de la Interfaz.
WebAPI. Proyecto implementado en ASPNET Core que entregará mediante REST las frases del dia y del mes.
Test. Proyecto de Pruebas Unitarias
1. Creación de Solución
Una solución es un elemento que nos permite agrupar proyectos.
dotnet new sln -n QuoteOfTheDay
Una solución es un elemento que nos permite agrupar proyectos.
.NET tiene un CLI muy poderoso que nos permite acceder a todas las funcionalidades y opciones de la plataforma.
La solución debe ser creada en la carpeta src
cd src
En esta carpeta ejecutamos el comando
dotnet new sln -n QuoteOfTheDay
2. Modelos (Entities)
Este proyecto contendrá las clases que representarán el modelo. Ejecute el siguiente comando en la carpeta src
dotnet new classlib -n QOTD.Models
Opcional. para agregar el proyecto a la solución puede escribir
dotnet sln QuoteOfTheDay.sln add QOTD.Models
El modelo consiste de las siguientes:
La implementación es como sigue:
Categoria
//Categoria.cs
using System.Collections.Generic;
namespace QOTD.Models
{
public class Categoria
{
public int Id { get; set; }
public string Nombre { get; set; }
public IList<Frase> Frases { get; set; }
}
}
Frase
//Frase.cs
using System;
namespace QOTD.Models
{
public class Frase
{
public int Id { get; set; }
public string Texto { get; set; }
public string Autor { get; set; }
public DateTime Fecha { get; set; }
public Categoria Categoria { get; set; }
}
}
3. Repositorio (DataAccess)
Este proyecto contendrá el Contexto (Clase que accede al origen de datos) y los Repositorios. Ejecute el siguiente comando en la carpeta
dotnet new classlib -n QOTD.DataAccess
Este proyecto referenciará unicamente al proyecto Models, para agregar esta referencia
dotnet add QOTD.DataAccess reference QOTD.Models
Opcional. para agregar el proyecto a la solución puede escribir
dotnet sln QuoteOfTheDay.sln add QOTD.DataAccess
El Proyecto DataAccess tendrá dos elementos fundamentales:
- Contexto de Datos. Es una clase que funciona como intermediario con el origen de datos
- Repositorio. Es una clase que realiza las operaciones CRUD
Es requerido el uso de los siguientes paquetes
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Relational
- Microsoft.EntityFrameworkCore.Sqlite (Usarémos SQLite para evitar pesadas instalaciones). EF tiene una lista de proveedores que pueden usarse Proveedores de acceso a datos .NET
Para instalarlos puede realizarlo a traves del dotnet CLI usando el siguiente comando
dotnet add package Microsoft.EntityFrameworkCore --version 3.0.0
O tambien copiando en el archivo .csproj la siguiente instrucción
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
Crear un archivo llamado QuoteDbContext.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using QOTD.Models;
namespace QOTD.DataAccess
{
public class QuoteDbContext : DbContext
{
public DbSet<Frase> Frases { get; set; }
public DbSet<Categoria> Categorias { get; set; }
public QuoteDbContext(DbContextOptions<QuoteDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
//EFCore por defecto pluraliza las tablas. Con esto deshabilitamos esta opción
foreach (IMutableEntityType entityType in builder.Model.GetEntityTypes())
{
entityType.SetTableName(entityType.DisplayName());
}
}
}
}
Cada objeto DbSet funciona como una "conexión" a la Base de Datos, cabe recordar que en este momento no es relevante el destino de la base de datos.
Otro punto importante es notar el contenido del método OnModelCreating, las lineas al interior del ciclo realizan una configuración en el Modelo, de tal forma que el nombre de la tabla a crearse será el mismo nombre de la Clase (Entity Framework por diseño pluraliza el nombre de la tabla)
Crearémos ahora los Repositorios
using System.Collections.Generic;
using System.Linq;
using QOTD.Models;
namespace QOTD.DataAccess
{
public class FraseRepository
{
private readonly QuoteDbContext _context;
public FraseRepository(QuoteDbContext context)
{
this._context = context;
}
public List<Frase> Get()
{
return this._context.Frases.ToList();
}
public bool Add(Frase frase)
{
this._context.Frases.Add(frase);
var count = this._context.SaveChanges();
return count > 0;
}
//Se omiten los otros CRUD
}
}
using System.Collections.Generic;
using System.Linq;
using QOTD.Models;
namespace QOTD.DataAccess
{
public class CategoriaRepository
{
private readonly QuoteDbContext _context;
public CategoriaRepository(QuoteDbContext context)
{
this._context = context;
}
public List<Categoria> Get()
{
return this._context.Categorias.ToList();
}
public bool Add(Categoria categoria)
{
this._context.Categorias.Add(categoria);
var count = this._context.SaveChanges();
return count > 0;
}
//Se omiten los otros CRUD
}
}
Implementación del Patron Repository y UnitOfWorf
El código anterior funciona perfectamente con pocos modelos, ¿pero que pasa si nuestra aplicación crece a 10, 20, 100 modelos?.
Para esto implementarémos los dos patrones de diseño mencionados.
Cree un archivo con el nombre IRepository.cs y el siguiente código
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace QOTD.DataAccess
{
public interface IRepository<T> where T : class
{
void Add(T entity);
T Get(int id);
IEnumerable<T> GetAll();
IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
int SaveChanges();
}
}
Cree un archivo con el nombre Repository.cs y el siguiente código
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
namespace QOTD.DataAccess
{
public class Repository<T> : IRepository<T> where T : class
{
private readonly QuoteDbContext _context;
protected DbSet<T> DbSet { get; }
public Repository(QuoteDbContext context)
{
this._context = context;
DbSet = this._context.Set<T>();
}
public void Add(T entity)
{
DbSet.Add(entity);
}
public IEnumerable<T> Find(Expression<Func<T, bool>> predicate)
{
return DbSet.Where(predicate);
}
public T Get(int id)
{
return DbSet.Find(id);
}
public IEnumerable<T> GetAll()
{
return DbSet.ToList();
}
public int SaveChanges()
{
return this._context.SaveChanges();
}
}
}
4. Servicios (Contratos e Implementación)
Se crearán dos proyectos, uno contendrá los contratos y otro la implementación de los contratos.
dotnet new classlib -n QOTD.Services.Contracts
dotnet new classlib -n QOTD.Services.Implementation
Ambos proyectos referenciarán a Models.
dotnet add QOTD.Services.Contracts reference QOTD.Models
dotnet add QOTD.Services.Implementation reference QOTD.Models
Adicionalmente, el proyecto de implementación referencia el proyecto de Contratos
dotnet add QOTD.Services.Implementation reference QOTD.Services.Contracts
Opcional. para agregar el proyecto a la solución puede escribir
dotnet sln QuoteOfTheDay.sln add QOTD.Services.Contracts
dotnet sln QuoteOfTheDay.sln add QOTD.Services.Implementation
Un correcto nos lleva a definir un Contrato y una Implementación
Para crear el contrato (Interface) creemos un archivo llamado IQuoteService (en el proyecto QOTD.Services.Contracts) con la siguiente implementación
namespace QOTD.Services.Contracts
{
using System;
using QOTD.Models;
using System.Collections.Generic;
public interface IQuoteService
{
Frase Get();
Frase GetByDate(DateTime day);
List<Frase> GetByWeek(DateTime firstDay, DateTime secondDay);
}
}
Para definir la implementación creemos un archivo llamado QuoteService (en el proyecto QOTD.Services.Implementation) con la siguiente implementación
using System;
using System.Collections.Generic;
using QOTD.Models;
using QOTD.DataAccess;
using QOTD.Services.Contracts;
using System.Linq;
namespace QOTD.Services.Implementation
{
public class QuoteService : IQuoteService
{
private readonly IRepository<Frase> _repository;
public QuoteService(IRepository<Frase> repository)
{
this._repository = repository;
}
public Frase Get()
{
return GetByDate(DateTime.Now);
}
public Frase GetByDate(DateTime day)
{
return _repository.Find(x => x.Fecha.Equals(day)).FirstOrDefault();
}
public List<Frase> GetByWeek(DateTime firstDay, DateTime secondDay)
{
return _repository.Find(x => x.Fecha >= firstDay && x.Fecha <= secondDay)
.ToList();
}
}
}
5. WebAPI
El proyecto WebApi es el que se encargará de escuchar y contestar las solicitudes HTTP
dotnet new webapi -n QOTD.WebApi
Este proyecto referenciará a todos los demas
dotnet add QOTD.WebApi reference QOTD.DataAccess QOTD.Models QOTD.Services.Contracts QOTD.Services.Implementation
Opcional. para agregar el proyecto a la solución puede escribir
dotnet sln QuoteOfTheDay.sln add QOTD.WebApi
Iniciarémos por configurar el sitio y generar las migraciones a la base de datos:
Configuración de la Base de datos
Para configurar una conexiòn a una base de datos debemos instalar el proveedor adecuado, en este caso, usarémos Sqlite
Para hacerlo debemos escribir el archivo .csproj la siguiente instrucción
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0" />
O escribir el comando (ubicados en la carpeta QOTD.WebApi)
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 3.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design
Posteriormente debemos configurar el archivo appsettings.Development.json, escribiendo la cadena de conexión
{
"ConnectionStrings": {
"QuoteDbContext": "Data Source=QuoteDb.db;"
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
Por ultimo debemos configurar el Startup.cs,
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<QuoteDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("QuoteDbContext"),
b => b.MigrationsAssembly("QOTD.WebApi")));
services.AddControllers();
}
Con esto la aplicación ya esta preparada para conectarse a la base de datos.
Generación de la migración y actualización de la base de datos
Entity Framework permite generar migraciónes, lo cual basicamente es tomar el Modelo definido y según el proveedor de base de datos seleccionado generar la estructura de base de datos adecuada.
Modifiquemos el modelo para incluir una propiedad de navegación
using System;
namespace QOTD.Models
{
public class Frase
{
public int Id { get; set; }
public string Texto { get; set; }
public string Autor { get; set; }
public DateTime Fecha { get; set; }
public int CategoriaId { get; set; }
public Categoria Categoria { get; set; }
}
}
Para llenar nuestra base de datos con la información de inicio, para eso vamos a agregar estos datos al DbContext (QuoteDbContext)
protected override void OnModelCreating(ModelBuilder builder)
{
//EFCore por defecto pluraliza las tablas. Con esto deshabilitamos esta opción
foreach (IMutableEntityType entityType in builder.Model.GetEntityTypes())
{
entityType.SetTableName(entityType.DisplayName());
}
builder.Entity<Categoria>().HasData(
new Categoria
{
Id = 1,
Nombre = "Tecnologia"
},
new Categoria
{
Id = 2,
Nombre = "Ciencia"
},
new Categoria
{
Id = 3,
Nombre = "Fisica"
}
);
//Copiar los datos de la las Frases (Archivo Frases.txt)
}
Para generar la migración debemos primero activar Entity Framework en el CLI de .NET, para hacerlo escriba
dotnet tool install --global dotnet-ef
Posteriormente generar la migración con el siguiente comando
dotnet ef migrations add InitialCreate
Esto agregará una carpeta Migrations al proyecto, y se almacenarán las migraciones (con cada cambio que se realice en el modelo, puede generar una nueva migracion cambiando el nombre InitialCreate por otro identitificador que considere)
Actualicemos la bd
dotnet ef database update
Creación del Controlador
En la carpeta Controllers agregue un nuevo archivo llamado QuoteController.cs, este será el código inicial
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace QOTD.WebApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class QuoteController : ControllerBase
{
}
}
Los controladores deben definir acciones (métodos) quienes serán los responsables de escuchar las peticiones y responder como corresponda. Para ello creemos una acción básica que conteste las peticiones GET
namespace QOTD.WebApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class QuoteController : ControllerBase
{
[HttpGet]
public IEnumerable<Frase> GetFrases()
{
//La magia viene aquí
}
}
}
En esta acciones debemos llamar el servicio creado, para esto debemos inyectarlo en el controlador usando el motor de Inyección de Dependencias de ASPNET, para ello terminemos de configurar la clase Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<QuoteDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("QuoteDbContext"),
b => b.MigrationsAssembly("QOTD.WebApi")));
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddTransient<IQuoteService, QuoteService>();
services.AddControllers();
}
Estas dos lineas le indican a ASPNET que cada que alguien requiera un objeto IRepository debe materializarlo en Repository y cada vez que requiera IQuoteService debe generar un QuoteService.
Con esta declaración nuestro controlador quedará de la siguiente forma
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using QOTD.Models;
using QOTD.Services.Contracts;
namespace QOTD.WebApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class QuoteController : ControllerBase
{
private readonly IQuoteService _quoteService;
public QuoteController(IQuoteService quoteService)
{
_quoteService=quoteService;
}
[HttpGet]
public IEnumerable<Frase> GetFrases()
{
return this._quoteService.GetAll();
}
}
}
Agreguemos ahora los métodos restantes
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using QOTD.Models;
using QOTD.Services.Contracts;
namespace QOTD.WebApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class QuoteController : ControllerBase
{
private readonly IQuoteService _quoteService;
public QuoteController(IQuoteService quoteService)
{
_quoteService = quoteService;
}
[HttpGet]
public IEnumerable<Frase> GetFrases()
{
return this._quoteService.GetAll();
}
[HttpGet]
[Route("hoy")]
public Frase GetFraseDeHoy()
{
DateTime hoy=DateTime.Now.Date;
return this._quoteService.GetByDate(hoy);
}
[HttpGet]
[Route("semana")]
public IEnumerable<Frase> GetFrasesDeLaSemana()
{
var culture = System.Threading.Thread.CurrentThread.CurrentCulture;
var diff = DateTime.Now.DayOfWeek - culture.DateTimeFormat.FirstDayOfWeek;
if (diff < 0)
{
diff += 7;
}
var firstDay = DateTime.Now.AddDays(-diff).Date;
var lastDay = firstDay.AddDays(6);
return this._quoteService.GetByWeek(firstDay, lastDay);
}
}
}
¿Probamos? Ejecute en una consola (en la carpeta del Proyecto)
dotnet run
Con el cliente HTTP que desee ejecute un llamado a http://localhost/quote/hoy
Punto Final - Swagger
Para probar API existe una herramiente muy interesante llamada Swagger, para configurarla instale en el proyecto WebApi el siguiente paquete Swashbuckle.AspNetCore
dotnet add package Swashbuckle.AspNetCore --version 5.0.0-rc4
Y agregue en el Startup las siguientes instrucciones
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<QuoteDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("QuoteDbContext"),
b => b.MigrationsAssembly("QOTD.WebApi")));
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddTransient<IQuoteService, QuoteService>();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Ahora vuelva a ejecutar la Aplicación (dotnet run) y abra la URL http://localhost:500/swagger/
6. Proyecto de pruebas unitarias
Este proyecto tendrá acceso a todas las pruebas unitarias, este comando debe ejecutarse en la carpeta test
dotnet ef migrations add UpdateData
dotnet ef database update
Agreguemos a este proyecto las referencias a los demas proyectos. Para hacerlo abra el archivo QOTD.Test.csproj y escriba antes de la etiqueta Project
<ItemGroup>
<ProjectReference Include="..\..\src\QOTD.DataAccess\QOTD.DataAccess.csproj" />
<ProjectReference Include="..\..\src\QOTD.Models\QOTD.Models.csproj" />
<ProjectReference Include="..\..\src\QOTD.Services.Contracts\QOTD.Services.Contracts.csproj" />
<ProjectReference Include="..\..\src\QOTD.Services.Implementation\QOTD.Services.Implementation.csproj" />
</ItemGroup>
Desde la carpeta src debemos agregar el proyecto a la solución
dotnet sln QuoteOfTheDay.sln add ..\test\QOTD.Test
Top comments (5)
No funciona la conexión
Startup.cs(32,21): error CS1061: "DbContextOptionsBuilder" no contiene una definición para "UseSqlite" ni un método de extensión accesible "UseSqlite" que acepte un primer argumento del tipo "DbContextOptionsBuilder" (¿falta alguna directiva using o una referencia de ensamblado?) [E:\jofrantoba_projects\netcorecolhum\apirest\apicolhum\src\ColHum.WebApi\ColHum.WebApi.csproj]
No se pudo llevar a cabo la compilación. Corrija los errores de compilación y vuelva a ejecutar el proyecto.
Hola, ¿Por qué? en IQuoteService el using de las libs va dentro del namespace y no fuera.
¿Hay alguna razón?
Este comando dotnet ef migrations add InitialCreate me lanza error build failed
Lo estoy ejecutando en la carpeta webapi.
Hola, otra vez. Por otro lado veo que en el paquete de servicios llamas al dataccess, pero en tu guía jamás haces la referencia en los contratos o los servicios. Solo referenciaste a los modelos.
Faltaba using Microsoft.EntityFrameworkCore, como no muestras los using de la clase Startup.cs y VisualCode no me avisa de los using