M4‐ Extra: Service Pattern - sjperalta/Web-Service-C-sharp GitHub Wiki

El patrón de diseño que estás utilizando es el Patrón de Servicio (Service Pattern). Este patrón se usa para encapsular la lógica de negocios y de acceso a datos en servicios separados que se pueden reutilizar en múltiples lugares de la aplicación. Al implementar este patrón, logras varias ventajas:

  1. Separación de responsabilidades: Mantienes la lógica de negocio y acceso a datos separada de la lógica de presentación o de la API, lo cual mejora la organización del código y facilita su mantenimiento.

  2. Reusabilidad: Los servicios pueden ser reutilizados por múltiples controladores o por otros servicios, lo cual reduce la duplicación de código.

  3. Facilita las pruebas unitarias: Al tener la lógica encapsulada en servicios, es más fácil escribir pruebas unitarias para esos servicios de manera aislada, sin necesidad de preocuparse por la API o la interfaz de usuario.

  4. Desacoplamiento: Al desacoplar la lógica de negocio de los controladores, puedes cambiar la implementación de los servicios sin afectar a los controladores, siempre y cuando la interfaz del servicio no cambie.

En resumen, el uso del Patrón de Servicio mejora la arquitectura y la mantenibilidad de tu aplicación, promoviendo una estructura de código más limpia y modular.

Claro, te mostraré un diagrama simplificado que representa la estructura de tu aplicación utilizando el Patrón de Servicio. El diagrama incluirá los componentes principales: el controlador, el servicio y el contexto de datos (DbContext).

Diagrama de Clases Simplificado

+-----------------------------------------+
|              TodoItemsController        |
|-----------------------------------------|
| - _todoItemService: ITodoItemService    |
|-----------------------------------------|
| + GetTodoItems(): Task<ActionResult>    |
| + GetTodoItem(id: long): Task<ActionResult> |
| + PutTodoItem(id: long, item: TodoItem): Task<IActionResult> |
| + PostTodoItem(item: TodoItem): Task<ActionResult> |
| + DeleteTodoItem(id: long): Task<IActionResult> |
+-----------------------------------------+
                 |
                 |
                 v
+-----------------------------------------+
|             ITodoService            |
|-----------------------------------------|
| + GetTodoItemsAsync(): Task<IEnumerable<TodoItem>> |
| + GetTodoItemAsync(id: long): Task<TodoItem>       |
| + UpdateTodoItemAsync(id: long, item: TodoItem): Task<bool> |
| + CreateTodoItemAsync(item: TodoItem): Task<TodoItem>       |
| + DeleteTodoItemAsync(id: long): Task<bool>                 |
| + TodoItemExists(id: long): bool                            |
+-----------------------------------------+
                 |
                 |
                 v
+-----------------------------------------+
|              TodoService            |
|-----------------------------------------|
| - _context: TodoContext                 |
|-----------------------------------------|
| + GetTodoItemsAsync(): Task<IEnumerable<TodoItem>> |
| + GetTodoItemAsync(id: long): Task<TodoItem>       |
| + UpdateTodoItemAsync(id: long, item: TodoItem): Task<bool> |
| + CreateTodoItemAsync(item: TodoItem): Task<TodoItem>       |
| + DeleteTodoItemAsync(id: long): Task<bool>                 |
| + TodoItemExists(id: long): bool                            |
+-----------------------------------------+
                 |
                 |
                 v
+-----------------------------------------+
|               TodoContext               |
|-----------------------------------------|
| + TodoItems: DbSet<TodoItem>            |
+-----------------------------------------+

Descripción del Diagrama

  1. TodoItemsController: Este es el controlador que maneja las solicitudes HTTP. Contiene métodos para obtener, crear, actualizar y eliminar elementos de la lista de tareas (TodoItems). Utiliza una instancia de ITodoItemService para delegar la lógica de negocio y acceso a datos.

  2. ITodoService: Esta es una interfaz que define los métodos que el servicio debe implementar. Proporciona una abstracción que permite al controlador interactuar con la lógica de negocio sin conocer los detalles de su implementación.

  3. TodoService: Esta es la implementación de ITodoItemService. Contiene la lógica para manejar las operaciones CRUD y utiliza TodoContext para acceder a la base de datos.

  4. TodoContext: Este es el contexto de la base de datos que maneja la conexión a la base de datos y proporciona acceso a las entidades (como TodoItem) a través de DbSet<TodoItem>.

Proceso de Refactor

Para crear un servicio a partir de los controladores proporcionados, podemos extraer la lógica de acceso a datos y operaciones CRUD en un servicio separado. Esto hará que el código sea más modular y facilite el mantenimiento y las pruebas unitarias.

Primero, definiremos una interfaz para el servicio. Luego, implementaremos esa interfaz en una clase de servicio. Finalmente, modificaremos el controlador para usar el servicio en lugar del contexto de la base de datos directamente.

Paso 1: Definir la Interfaz del Servicio

Crearemos una interfaz llamada ITodoService que definirá los métodos necesarios.

using System.Collections.Generic;
using System.Threading.Tasks;
using TodoApiRestfull.Models;

namespace TodoApiRestfull.Services
{
    public interface ITodoService
    {
        Task<IEnumerable<TodoItem>> GetTodoItemsAsync();
        Task<TodoItem> GetTodoItemAsync(long id);
        Task<bool> UpdateTodoItemAsync(long id, TodoItem todoItem);
        Task<TodoItem> CreateTodoItemAsync(TodoItem todoItem);
        Task<bool> DeleteTodoItemAsync(long id);
        bool TodoItemExists(long id);
    }
}

Paso 2: Implementar el Servicio

Implementaremos la interfaz en una clase llamada TodoItemService.

using Microsoft.EntityFrameworkCore;
using TodoApiRestfull.Data;
using TodoApiRestfull.Models;
using TodoApiRestfull.Services.Interfaces;

namespace TodoApiRestfull.Services
{
    public class TodoService : ITodoService
    {
        private readonly TodoContext _context;

        public TodoService(TodoContext context)
        {
            _context = context;
        }
        
        public async Task<IEnumerable<TodoItem>> GetTodoItemsAsync()
        {
            return await _context.TodoItems.ToListAsync();
        }

        public async Task<TodoItem> GetTodoItemAsync(long id)
        {
            var result = await _context.TodoItems.FindAsync(id);
            
            if(result == null)
            {
                throw new Exception("Esto es un error porque no existe el registro en la base de datos");
            }

            return result;
        }

        public async Task<bool> UpdateTodoItemAsync(long id, TodoItem todoItem)
        {
            if (id != todoItem.Id)
            {
                return false;
            }

            _context.Entry(todoItem).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
                return true;
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!TodoItemExists(id))
                {
                    return false;
                }
                else
                {
                    throw;
                }
            }
        }

        public async Task<TodoItem> CreateTodoItemAsync(TodoItem todoItem)
        {
            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();

            return todoItem;
        }

        public async Task<bool> DeleteTodoItemAsync(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
            {
                return false;
            }

            _context.TodoItems.Remove(todoItem);
            await _context.SaveChangesAsync();

            return true;
        }

        public bool TodoItemExists(long id)
        {
            return _context.TodoItems.Any(e => e.Id == id);
        }

        public async Task<List<TodoItem>> Search(string name)
        {
            //retornara uno o mas elementos
            //select * from dbo.TodoItems t where t.name like '%name%' 
            var result = await _context.TodoItems
                .Where(item => item.Name.Contains(name))
                .ToListAsync();

            return result;
        }
    }
}

Paso 3: Modificar el Controlador para Usar el Servicio

Modificaremos el controlador TodoItemsController para que use el servicio ITodoItemService.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApiRestfull.Data;
using TodoApiRestfull.Models;
using TodoApiRestfull.Services.Interfaces;

namespace TodoApiRestfull.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    {
        private readonly ITodoService _todoService;

        public TodoItemsController(ITodoService service)
        {
            _todoService = service;
        }

        // GET: api/TodoItems
        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
        {
            var items = await _todoService.GetTodoItemsAsync();
            return Ok(items);
        }

        // GET: api/TodoItems/5
        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
        {
            var todoItem = await _todoService.GetTodoItemAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return Ok(todoItem);
        }

        [HttpGet("search/{name}")]
        public async Task<ActionResult<List<TodoItem>>> Search(string name)
        {
            var result = await _todoService.Search(name);

            if(!result.Any()) {
                return NotFound();
            }

            return Ok(result);
        }

        // PUT: api/TodoItems/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("{id}")]
        public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
        {
            var result = await _todoService.UpdateTodoItemAsync(id, todoItem);

            if (!result)
            {
                return NotFound();
            }

            return NoContent();
        }

        // POST: api/TodoItems
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
        {
            var createdItem = await _todoService.CreateTodoItemAsync(todoItem);
            return CreatedAtAction("GetTodoItem", new { id = createdItem.Id }, createdItem);
        }

        // DELETE: api/TodoItems/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteTodoItem(long id)
        {
            var result = await _todoService.DeleteTodoItemAsync(id);

            if (!result)
            {
                return NotFound();
            }

            return NoContent();
        }
    }
}

Paso 4: Registrar el Servicio en el Contenedor de Dependencias

Finalmente, registraremos el servicio en el contenedor de dependencias en Program.cs.

    services.AddControllers();
    services.AddDbContext<TodoContext>(opt =>
       opt.UseInMemoryDatabase("TodoList"));
    services.AddScoped<ITodoItemService, TodoItemService>();
    // Otros servicios...

Con esto, hemos abstraído la lógica del controlador en un servicio, lo que mejora la organización del código y facilita su mantenimiento.

⚠️ **GitHub.com Fallback** ⚠️