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:

  • 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:

Modelo

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:

  1. Contexto de Datos. Es una clase que funciona como intermediario con el origen de datos
  2. 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

Avance

  1. Arquitectura creada commit.
  2. Modelos creados commit.
  3. Repositorio creado commit.

    • Patron Repositorio Aplicado commit.
  4. Servicios creados commit.
  5. WebApi Creada commit.