ES | My Movies checklist - 5/24 | En Progreso ...

http://localhost:4000/2022/12/01/movies-checklist-1/basic-pages.png

La idea de este post es hacer un proyecto completo desde cero. Este es el primero de 24 apartados y espero que este proyecto te llame la atención, cualquier comentario no dudes en contactarme @whistler092

Descripcion general y frontend

Disclamer: No soy un experto en react ni mucho menos. La idea es encapsular mi proceso de creación de un proyecto para los amigos que leen este post :D

El proyecto se llama My Movies Checklist y consiste en un software donde se puedan ver todas las películas del mercado, poder llevar un seguimiento de las películas que ya te vistes y las que te quieres ver.

También poder compartir tus opiniones con el resto de mundo y tener tu propio ranking de películas personales.

La idea es tener 3 paginas básicas:

  1. Películas: En esta página estarán todas las películas del cine, cada tarjeta tendrá el poster, el nombre de la película, genero, fecha y una corta sinopsis.

en esa carta habrán 2 botones, el primero es para marcar que la película ya te la viste, te pedirá un puntaje y agregar unos comentarios. En la segunda es para agregar esa película en una lista de películas por ver.

  1. Comentarios: En esta página se verán todos los comentarios de todos los usuarios a las películas que se han visto.

  2. Tu perfil: En esta página se pueden ver todas las películas que te has visto y las películas que están pendientes por ver.

La integración se hará con el web service themoviedb.org.

Repositorio del projecto

Vamos a ir saltando del back-end al front-end dependiendo de lo que necesitemos, así que vamos a iniciar con el proyecto react:


~/workspace|mkdir movies
~/workspace|cd movies
~/workspace/movies|git init
Initialized empty Git repository in /Users/ramiroandres/workspace/movies/.git/
~/workspace/movies|master ⇒  npm create vite@latest movies-frontend -- --template react

Need to install the following packages:
  create-vite@3.2.1
Ok to proceed? (y) Y

Scaffolding project in /Users/ramiroandres/workspace/movies/movies-frontend...

Done. Now run:

  cd movies-frontend
  npm install
  npm run dev

~/workspace/movies|master⚡ ⇒  cd movies-frontend
~/workspace/movies/movies-frontend|master⚡ ⇒  npm install
added 86 packages, and audited 87 packages in 11s

8 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
~/workspace/movies/movies-frontend|master⚡ ⇒  npm run dev

Ya tenemos nuestro proyecto react creado, así que agreguemos el enrutador con react router. Aquí un tutorial de la página oficial para aprender unos buenos fundamentos https://reactrouter.com/en/main/start/tutorial


~/workspace/movies/movies-frontend|master⚡ ⇒  npm install react-router-dom localforage match-sorter sort-by

Yo aprendí react antes de React Hooks pero no he tenido la oportunidad de seguir trabajando en él; para mi react ha cambiado un montón, ando en proceso de reaprender y me disculparan si cometo algún error. El enrutador de react también ha cambiado mucho así que básicamente sentí que aprendí nuevamente react con esto. ¡Así que vamos a darle!

Las primeras rutas que necesitamos aquí son las de:

  • Peliculas: /movies
  • Comentarios: /comments
  • Mi perfil: /profile

Vamos a crearlas:

Creamos un nuevo folder llamado routes donde ubicaremos todas las páginas. Por lo pronto nuestro main.jsx va a lucir asi:

import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./App";
import "./index.css";
import Comments from "./routes/comments";
import Movies from "./routes/movies";
import Profile from "./routes/profile";
import Root from "./routes/root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "movies",
        element: <Movies />,
      },
      {
        path: "comments",
        element: <Comments />,
      },
      {
        path: "profile",
        element: <Profile />,
      },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

Hemos cambiado el objeto <App /> que se retorna en el createRoot y ahora vamos a retornar un objeto del react-router-dom. El <RouterProvider> nos permite que todos los objetos que se le pasen, van a estar bajo el dominio del enrutador. Por tal razón, vamos a pasarle el objeto router definido arriba.

En este objeto hay que crear 4 nuevas rutas, el root / que contendrá todo el menú. En este componente <Root /> que por el momento va a ser algo dummy tendrá el objeto <Outlet /> donde se hará render de los children definidos anteriormente.

import { Outlet } from "react-router";

export default function Root() {
  return (
    <>
      <div id="header">
        <h1>React Movies</h1>
      </div>
      <div id="detail">
        <Outlet />
      </div>
    </>
  );
}

Para los children, vamos a tener 3 rutas, el /movies, /comments y /profile.

Vamos a crear estos componentes también vacíos con tal de tener un acercamiento básico de las páginas que tendremos.

export default function Profile() {
  return (
    <>
      <h2>My Profile</h2>
    </>
  );
}

Hasta el momento, hemos creado el siguiente workflow

Aquí podemos chequear el código en mi repositorio

Commit de creacion de rutas

En el próximo post vamos a armar un menú básico para poder navegar por las diferentes rutas y vamos a cargar una información dummy para poder interactuar.

Enrutamiento, info dummy y css

En el apartado anterior, agregamos el enrutamiento de react con las paginas que necesitamos: /Movies, /Comments y /Profile. La meta de este post es armar un menú básico para poder navegar por las diferentes rutas y vamos a cargar una información dummy para poder interactuar.

Hay varias formas de armar un menú, una de ellas es con un framework frontend como React Bootstrap y tiene un menú bastante interesante, si les interesa, échenle un ojo. En este caso, vamos a intentar evitar agregar Bootstrap y hacerlo manual para practicar un poco.

Así que … primero que todo, quiero presentarles Sass: Syntactically Awesome Style Sheets, Sass es un preprocesador de CSS que nos ayudara a darle vitaminas al CSS. Es muy fácil de usar, porque la sintaxis es la misma que la de CSS, solo que permite anidación. Ya vamos a verlo!

Para instalarlo, vamos a ir a la consola y escribir npm install sass y ya. React automáticamente detectara los archivos con extensión .scss.

Uno de los cambios que vamos a iniciar es modificar el root.jsx para agregar nuestro nuevo menú.

import { Outlet } from "react-router";
import { NavLink } from "react-router-dom";
import "./root.scss";

export default function Root() {
  return (
    <>
      <div id="header" className="header">
        <ul>
          <li key="Movies">
            <NavLink to="Movies">Movies</NavLink>
          </li>
          <li key="Comments">
            <NavLink to="Comments">Comments</NavLink>
          </li>
          <li key="Profile">
            <NavLink to="Profile">Profile</NavLink>
          </li>
          <div className="dot"></div>
        </ul>
      </div>
      <div id="detail">
        <Outlet />
        {/* other elements */}

Aquí vamos a hacer uso del NavLink que nos permitirá hacer un redireccionamiento con el react-router-dom.

Creemos un nuevo archivo llamado /src/routes/root.scss. Importado en el archivo anterior, vamos a agregarle un poquito de estilo a este menú:

#header {
  margin-top: 2rem;
  margin-bottom: 2rem;

  ul {
    display: flex;

    li {
      flex: 1;
      overflow: auto;
      padding-top: 1rem;
    }

    li a {
      color: #f6f4e6;
      /* border: 1px solid white; */
      text-decoration: none;
      font-size: 1.2em;
      font-weight: 500;
      display: inline-block;

      -webkit-transition: all 0.2s ease-in-out;
      transition: all 0.2s ease-in-out;
    }

    li a.active {
      color: #fddb3a;
    }

    li a:hover {
      color: #fddb3a;
    }
  }
}

Una de las cosas mas interesantes de usar scss es los componentes anidados. Como pueden ver, dentro del id header, todos los componentes ul serán afectados con el CSS adentro.

Otra de las cosas que me gusta usar es flexbox. Les recomiendo un tutorial bastante interesante de los magos de css-tricks.com. El valor por defecto de flex es organizar los componentes de forma horizontal, lo cual es lo que necesitamos. Adicionalmente, ajuste unos colores del src/index.css, importé un font llamado Montserrat y ajuste el body con flexbox también.

La forma que uso para importar un nuevo font es usando la etiqueta import. Si quieres mirar más fonts, chequea esta página

@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap");

:root {
  font-family: "Montserrat", sans-serif;
  /* other elements  */
}
/* other elements  */
body {
  margin: 0;

  min-width: 320px;
  min-height: 100vh;

  display: flex;
  flex-direction: column;
  align-content: space-between;
  justify-content: flex-start;
  align-items: center;
  text-align: center;
}

Aquí un commit hasta este punto

Ahora lo que vamos a hacer es mostrar un par de películas de forma fake antes de crear el backend y registrarnos en themoviedb.org, todo esto para saber exactamente que propiedades necesitamos mostrar en la UI. Así que descarguemos una json previamente guardad de del API de películas populares de la siguiente URL y guardemos ese json en el path /public/db.json.

Hay varias formas de hacer esto, vamos a hacer una petición HTTP para obtener este archivo de nuestra carpeta pública, y así para poder reutilizar parte del código cuando llegue muestro backend, vamos a instalar axios usando npm install axios.

Abramos nustro archivo src/routes/movies.jsx:

import axios from "axios";
import { useEffect, useState } from "react";

export default function Movies() {
  const [movies, setMovies] = useState([]);

  useEffect(() => {
    axios
      .get("db.json")
      .then((res) => setMovies(res.data))
      .catch((err) => console.error(err));
  }, [movies]);

  console.log("movies", movies);

  return (
    <>
      <h2>Movies</h2>
    </>
  );
}

Vamos a usar react hooks, esto nos permite usar estados y otras características de react sin tener que escribir clases. useState nos ayuda para mantener estados dentro de la función. Aquí puedes leer documentación al respecto si no estas familiarizado.

Si tuviéramos que escribir el anterior código de useState con clases, sería algo como lo siguiente:

class Movies extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      movies: [],
    };
  }

  render() {
    return (
      <>
        <h2>Movies</h2>
      </>
    );
  }
}

También vamos a usar useEffect, esto nos permite realizar efectos secundarios en un componente. Podemos realizar cosas como obtener data de un API, subscribirse o manualmente cambiar el DOM. En nuestro caso, vamos a usarlo para invocar axios y traer el json del path público, finalmente en el .then, vamos a guardar la información en el state. Así es como podremos obtener el resultado siguiente resultado en consola:

movies []
movies {page: 1, results: Array(20), total_pages: 36103, total_results: 722056}

Ahora hagamos render de los títulos:

export default function Movies() {
  /* other elements  */

  return (
    <>
      <div className="films">
        {movies &&
          movies?.results?.map((film) => <Film key={film.id} film={film} />)}
      </div>
    </>
  );
}

function Film({ film }) {
  return (
    <div key={film.id} className="film">
      <img
        src={`https://image.tmdb.org/t/p/w185_and_h278_bestv2${film.poster_path}`}
      ></img>
      <span>{film.original_title}</span>
    </div>
  );
}

Vamos a agregar un poco de estilo a esta página, por tal razón, vamos a crear un nuevo archivo src/routes/movies.scss y organicemos un poco las películas:

.films {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-around;
  align-items: center;

  .film {
    color: white;
    min-width: 200px;
    max-width: 300px;

    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: flex-start;

    margin: 1rem 1rem 2rem 1rem;
    inline-size: 150px;
    overflow-wrap: break-word;

    min-height: 340px;
  }
}

Vamos a organizar todas las películas en la clase films y usaremos flexbox con flex-direction: row; para acomodar todas las películas de forma horizontal y complementamos con flex-wrap: wrap para que salten de línea cuando se llene cada row.

Dentro de cada película, vamos a ajustar un poco cada uno, dándole un tamaño fijo mínimo y máximo. Tambien usaremos flexbox para que todo sea de tipo columna.

Ahora las películas podrían lucir de la siguiente forma:

Aquí un commit con los últimos cambios

Diagrama de base de datos

En el apartado anterior, mejoramos un poco la ruta de movies mostrando las últimas películas desde una data moq, la idea de este post es crear nuestro backend usando .net Core, EF y PostgreSQL.

Antes de arrancar a hacer un diagrama de base de datos, necesitamos validar algunos requerimientos.

  • Como usuario, quiero poder buscar cualquier película dentro del catálogo, cada película debe tener un identificador único. Se debe persistir la película en la base de datos con información básica.
  • Como usuario, quiero poder seleccionar una película y poder marcar cualquier película para ver.
  • Como usuario, quiero poder seleccionar una película y poder marcarla como vista. También poder agregar un comentario y una puntuación a las películas que el usuario haya visto.
  • Quiero poder listar mis películas pendientes a verme y las películas vistas.

Ahora sí, con esto podríamos definir la estructura de la base de datos, así que vamos al confiable draw.io

La idea es crear un usuario users, que es un usuario registrado desde el proveedor de autenticación de Auth0. Un usuario podrá tener varias películas asociadas users_movies y hay dos tipos de asociaciones: Las películas por ver to_watch y las películas vistas saw_it. Cuando se asocia una película vista, este puede tener uno o más comentarios comments.

Vamos a crear las tablas en la base de datos.


CREATE TABLE users (
    user_id serial PRIMARY KEY,
    email VARCHAR ( 255 ) UNIQUE NOT NULL,
    auth0_id VARCHAR ( 30 ) UNIQUE NOT NULL,
    created_on TIMESTAMP NOT NULL,
    last_login TIMESTAMP
);

CREATE TABLE movies (
    movie_id BIGSERIAL PRIMARY KEY,
    original_title VARCHAR ( 255 ) UNIQUE NOT NULL,
    title VARCHAR ( 255 ) UNIQUE NOT NULL,
    overview VARCHAR ( 1024 ) UNIQUE NOT NULL,
    themoviedb_id INT UNIQUE NOT NULL,
    genre  VARCHAR ( 255 ) UNIQUE NOT NULL,
    created_on TIMESTAMP NOT NULL,
    last_update TIMESTAMP
);

CREATE TABLE users_movies (
    user_movie_id BIGSERIAL PRIMARY KEY,
    movie_id bigint not null,
    user_id bigint not null,

    to_watch boolean not null default false,
    saw_it boolean not null default false,

    created_on TIMESTAMP NOT NULL,
    last_update TIMESTAMP,

    FOREIGN KEY (movie_id) REFERENCES movies(movie_id),
    FOREIGN KEY (user_id) REFERENCES users(user_id)
);

CREATE TABLE comments (
    comment_id BIGSERIAL PRIMARY KEY,
    user_movie_id bigint not null,

    comment varchar(200) default null,
    score decimal(2,1) default null,
    liked smallint default 0::smallint,

    created_on TIMESTAMP NOT NULL,
    last_update TIMESTAMP,

    FOREIGN KEY (user_movie_id) REFERENCES users_movies(user_movie_id)
);

Haciendo la prueba de concepto, podemos tener los siguientes query’s. Arranquemos por llenar información básica


-- Add new user
INSERT INTO public.users(
    user_id, email, auth0_id, created_on, last_login)
    VALUES (1, 'iamramiroo@gmail.com', '1',  now(), now());

select * from public.users;

1   "iamramiroo@gmail.com"  "1" "2022-12-08 12:47:42.539151"    "2022-12-08 12:47:42.539151"

-- add new movie
INSERT INTO public.movies(
    movie_id, original_title, title, overview, themoviedb_id, genre, created_on, last_update)
    VALUES (1,
            'Black Panther: Wakanda Forever',
            'Black Panther: Wakanda Forever',
            'Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T’Challa’s death.',
            505642,
            'Action, Adventure, Science Fiction',
            now(),
            now());

select movie_id,
 original_title,
 title,
 overview,
 themoviedb_id,
 genre,
 created_on,
 last_update
from movies;

1   "Black Panther: Wakanda Forever"    "Black Panther: Wakanda Forever"    "Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T’Challa’s death." 505642  "Action, Adventure, Science Fiction"    "2022-12-08 12:55:48.957749"    "2022-12-08 12:55:48.957749"

Ahora asociemos el usuario con una película de modo para ver


INSERT INTO public.users_movies(
    user_movie_id, movie_id, user_id, to_watch, saw_it, created_on, last_update)
    VALUES (1, 1, 1, true, false, now(), now());

select * from users_movies

1   1   1   true    false   "2022-12-08 12:59:57.516897"    "2022-12-08 12:59:57.516897"

-- All Movies to watch
select u.email,
    m.title,
    um.saw_it,
    um.to_watch
from users_movies um
inner join movies m on m.movie_id = um.movie_id
inner join users u on u.user_id = um.user_id
where um.user_id = 1
and um.to_watch = true

"iamramiroo@gmail.com"  "Black Panther: Wakanda Forever"    false   true

Ahora asociemos un comentario con una asociación de una película

-- Add new comment into a movie
INSERT INTO public.comments(
    comment_id, user_movie_id, comment, score, liked, created_on, last_update)
    VALUES (1, 1, 'New comment test', 4.5, 0, now(), now());

select * from public.comments;

1   1   "New comment test"  4.5 0   "2022-12-08 13:05:38.794491"    "2022-12-08 13:05:38.794491"


-- All movies with comments  by user
select u.email,
    m.title,
    um.saw_it,
    um.to_watch,
    c.score,
    c.liked,
    c.comment
from users_movies um
inner join movies m on m.movie_id = um.movie_id
inner join users u on u.user_id = um.user_id
left  join comments c on um.user_movie_id = c.user_movie_id
where um.user_id = 1

"iamramiroo@gmail.com"  "Black Panther: Wakanda Forever"    false   true    4.5 0   "New comment test"

-- Average Score

select
    um.movie_id,
    m.original_title,
    trunc(avg(c.score), 1) as score
from users_movies um
inner join movies m on um.movie_id = m.movie_id
inner join comments c on um.user_movie_id = c.user_movie_id
group by um.movie_id, m.original_title

1   "Black Panther: Wakanda Forever"    4.5

En caso de que sea necesario, un script para borrar todo


-- Reset all

drop table comments;
drop table users_movies;
drop table movies;
drop table users;

DDD y creacion del proyecto backend

En el apartado anterior, estuvimos creando lo que seria una idea inicial de la base de datos, ahora vamos a iniciar creando nuestra API. Este post estoy tratando de plasmar mi opinión personal en este momento, la cual puede cambiar 😅😅.

Nota 2: Puede que algunos conceptos sean avanzados para el proyecto que intento hacer, pero intentare explicarlos lo mejor que pueda, así que si sientes que algo no sea suficientemente claro, no dudes en escribirme y podemos mejorar el post. Gracias :D

El acercamiento que vamos a estar usando en este proyecto es el de Clean Architecture y Domain Driven Design. Vamos a iniciar dando una breve explicación de que es Domain-Driven Design aka DDD y porque me parece importante integrarlo en mis proyectos. ¿Entonces, en que consiste DDD y Arquitectura Limpia? Bueno, esto es una serie de principios y prácticas que nos permitirán modelar nuestro software para ser mantenible y mucho más escalables. Ampliamente usado en Microservicios para separar responsabilidades entre sí. Veamos un diagrama de cómo podríamos estructurar nuestra aplicación:

Con este acercamiento, (Como lo describe Jason Taylor en este gran video) el Domain y el Application son las unidades centrales de la aplicación, esto es conocido como el Core del sistema. En el dominio podemos encontrar la lógica empresarial y tipos del sistema y en el nivel de la aplicación podemos encontrar la lógica de negocio y los tipos. La diferencia entre estos dos es que la lógica empresarial puede ser compartida a través de muchos sistemas mientras que la lógica de negocio es especifico del sistema.

En este punto, en vez de que Core tenga conceptos como acceso a data y elementos de infraestructura, se invierten estas dependencias. Entonces Presentación y infraestructura dependen de Core. Aplicación depende únicamente de Dominio pero este no tiene dependencias, todas las dependencias apuntan hacia adentro. Todo esto se logra agregando interfaces y abstracciones al nivel de la aplicación que son implementadas afuera de la capa de aplicación.

Digamos que si quisiéramos implementar el patrón repository, se agregaría la interfaz de IRepository en la capa de aplicación y la implementación en la capa de infraestructura.

Cabe notar adicionalmente que Presentación y Infraestructura dependen de Core, pero no dependen entre ellos. Queremos asegurarnos de que la lógica que se crea para este sistema se quede en Core. Un ejemplo seria si la lógica de Presentation toma como dependencia Infrasctucture, porque necesita enviar un email, eso es lógica y eso debe estar en Core, porque esa lógica puede ser reutilizada. Los frontend y las APIs cambian a través del tiempo y se quiere aislar esa lógica de negocio para que pueda ser cambiada cuando sea necesario.

Algunas características de Clean Architecture:

  • Esto es independiente del Framework o lenguaje de programación
  • Testeable
  • Independiente de la UI
  • Independiente de la base de datos
  • Independiente de factores externos

Con este acercamiento, vamos a tener todos los conceptos como la lógica de negocio y de la aplicación en el centro de todo, llamado Domain. En la capa de Infraestructura, tendremos detalles como la base de datos e interacción con servicios externos, los cuales van a estar afuera de la lógica de negocio, teniendo la posibilidad de ser más fácil de reemplazarlo sin alterar la lógica del negocio.

Yendo un poquito más al detalle de los tipos de cosas que vamos a ver en cada capa son:

Domain:

  • Entities
  • Value Objects
  • Enumerations
  • Logic
  • Exceptions

Application:

  • Interfaces
  • Models
  • Logic
  • Commands/Queries
  • Validators
  • Exceptions

Infrastructure:

  • Persistence
  • Identity
  • Envia Emails
  • Integrations

Presentation:

  • Web API
  • MVC or Razor Pages
  • SPA pages como Angular, React, etc

Listo, entonces vamos a iniciar creando el proyecto desde cero. Para ello tenemos que tener el SDK de .Net Core instalado con editor de código de preferencia (Yo recomiendoVisual Studio Code) o tener instalado el IDE Visual Studio

Para iniciar, abrimos la terminal y ejecutamos los siguientes comandos para crear la solución:

PS C:\...> cd .\movies-api\

PS C:\...\movies-api> dotnet new sln -o Movies
Welcome to .NET 7.0!
---------------------
SDK Version: 7.0.103
...
--------------------------------------------------------------------------------------
The template "Solution File" was created successfully.

PS C:\...\movies-api> dotnet dev-certs https --trust
Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.
Successfully trusted the existing HTTPS certificate.

PS C:\...\movies-api> cd .\Movies\

Vamos a crear la capa de presentación. En esta capa tendremos dos proyectos, el Movies.Api que es de tipo webapi y los Contracts que es de tipo Class Library:

PS C:\...\movies-api\Movies> dotnet new webapi -o Movies.Api
The template "ASP.NET Core Web API" was created successfully.
Processing post-creation actions...
Restoring C:\...\movies-api\Movies\Movies.Api\Movies.Api.csproj:
  Determining projects to restore...
  Restored C:\...\movies-api\Movies\Movies.Api\Movies.Api.csproj (in 5,74 sec).
Restore succeeded.

PS C:\...\movies-api\Movies> dotnet new classlib -o Movies.Contracts
The template "Class Library" was created successfully.
Processing post-creation actions...
Restoring C:\...\movies-api\Movies\Movies.Contracts\Movies.Contracts.csproj:
  Determining projects to restore...
  Restored C:\...\movies-api\Movies\Movies.Contracts\Movies.Contracts.csproj (in 62 ms).
Restore succeeded.

Después creamos las siguientes capas que son Infraestructura, Application y Domain:

PS C:\...\movies-api\Movies> dotnet new classlib -o Movies.Infrastructure
The template "Class Library" was created successfully.
Processing post-creation actions...
Restoring C:\...\movies-api\Movies\Movies.Infrastructure\Movies.Infrastructure.csproj:
  Determining projects to restore...
  Restored C:\...\movies-api\Movies\Movies.Infrastructure\Movies.Infrastructure.csproj (in 62 ms).
Restore succeeded.

PS C:\...\movies-api\Movies> dotnet new classlib -o Movies.Application
The template "Class Library" was created successfully.
Processing post-creation actions...
Restoring C:\...\movies-api\Movies\Movies.Application\Movies.Application.csproj:
  Determining projects to restore...
  Restored C:\...\movies-api\Movies\Movies.Application\Movies.Application.csproj (in 63 ms).
Restore succeeded.

PS C:\...\movies-api\Movies> dotnet new classlib -o Movies.Domain
The template "Class Library" was created successfully.
Processing post-creation actions...
Restoring C:\...\movies-api\Movies\Movies.Domain\Movies.Domain.csproj:
  Determining projects to restore...
  Restored C:\...\movies-api\Movies\Movies.Domain\Movies.Domain.csproj (in 60 ms).
Restore succeeded.

PS C:\...\movies-api\Movies> ls
    Directory: C:\...\movies-api\Movies
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----      21/02/2023  4:36 a. m.                Movies.Api
d-----      21/02/2023  4:38 a. m.                Movies.Application
d-----      21/02/2023  4:36 a. m.                Movies.Contracts
d-----      21/02/2023  4:52 a. m.                Movies.Domain
d-----      21/02/2023  4:38 a. m.                Movies.Infrastructure
-a----      21/02/2023  4:31 a. m.            441 Movies.sln

PS C:\...\movies-api\Movies> dotnet sln add (ls -r **\*.csproj)
Project `Movies.Api\Movies.Api.csproj` added to the solution.
Project `Movies.Application\Movies.Application.csproj` added to the solution.
Project `Movies.Domain\Movies.Domain.csproj` added to the solution.
Project `Movies.Contracts\Movies.Contracts.csproj` added to the solution.
Project `Movies.Infrastructure\Movies.Infrastructure.csproj` added to the solution.

PS C:\...\movies-api\Movies> dotnet build
MSBuild version 17.4.1+fedecea9d for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
  Movies.Application -> C:\...\movies-api\Movies\Movies.Application\bin\Debug\net7.0\Movies.Application.dll
  Movies.Domain -> C:\...\movies-api\Movies\Movies.Domain\bin\Debug\net7.0\Movies.Domain.dll
  Movies.Contracts -> C:\...\movies-api\Movies\Movies.Contracts\bin\Debug\net7.0\Movies.Contracts.dll
  Movies.Infrastructure -> C:\...\movies-api\Movies\Movies.Infrastructure\bin\Debug\net7.0\Movies.Infrastructure.dll
  Movies.Api -> C:\...\movies-api\Movies\Movies.Api\bin\Debug\net7.0\Movies.Api.dll
Build succeeded.
    0 Warning(s)
    0 Error(s)
Time Elapsed 00:00:01.13

PS C:\...\movies-api\Movies>

Ya que tenemos todos los proyectos creados, agreguemos las dependencias entre ellos de acuerdo al siguiente diagrama:

PS C:\...\movies-api\Movies> dotnet add .\Movies.Api\ reference .\Movies.Contracts\ .\Movies.Application\
Reference `..\Movies.Contracts\Movies.Contracts.csproj` added to the project.
Reference `..\Movies.Application\Movies.Application.csproj` added to the project.

PS C:\...\movies-api\Movies> dotnet add .\Movies.Infrastructure\ reference .\Movies.Application\
Reference `..\Movies.Application\Movies.Application.csproj` added to the project.

PS C:\...\movies-api\Movies> dotnet add .\Movies.Application\ reference .\Movies.Domain\
Reference `..\Movies.Domain\Movies.Domain.csproj` added to the project.

NOTA: Aunque anteriormente había dicho que no iba a haber dependencias entre el proyecto Movies.Api y el proyecto Movies.Infrastructure. Hay una pequeña excepción tecnica a la regla, se debe de agregar una referencia entre estos para que el proyecto de Infraestructura pueda registrar sus dependencias. Lo que si tenemos que tener en cuenta es que desde la capa de presentación no se hará uso de librerías de esta capa.

PS C:\...\movies-api\Movies> dotnet add .\Movies.Api\ reference .\Movies.Infrastructure\
Reference `..\Movies.Infrastructure\Movies.Infrastructure.csproj` added to the project.

PS C:\...\movies-api\Movies> dotnet build
MSBuild version 17.4.1+fedecea9d for .NET
  Determining projects to restore...
  Restored C:\...\movies-api\Movies\Movies.Infrastructure\Movies.Infrastructure.csproj (in 111 ms).
  Restored C:\...\movies-api\Movies\Movies.Application\Movies.Application.csproj (in 111 ms).
  Restored C:\...\movies-api\Movies\Movies.Api\Movies.Api.csproj (in 168 ms).
  2 of 5 projects are up-to-date for restore.
  Movies.Domain -> C:\...\movies-api\Movies\Movies.Domain\bin\Debug\net7.0\Movies.Domain.dll
  Movies.Contracts -> C:\...\movies-api\Movies\Movies.Contracts\bin\Debug\net7.0\Movies.Contracts.dll
  Movies.Application -> C:\...\movies-api\Movies\Movies.Application\bin\Debug\net7.0\Movies.Application.dll
  Movies.Infrastructure -> C:\...\movies-api\Movies\Movies.Infrastructure\bin\Debug\net7.0\Movies.Infrastructure.dll
  Movies.Api -> C:\...\movies-api\Movies\Movies.Api\bin\Debug\net7.0\Movies.Api.dll
Build succeeded.
    0 Warning(s)
    0 Error(s)
Time Elapsed 00:00:03.70

Vamos a ver si funciona, para esto voy a utilizar una extensión llamada REST Client del VS Code para hacer de pruebas de las peticiones REST.

Name: REST Client
Description: REST Client for Visual Studio Code
VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=humao.rest-client

Corremos el backend:

PS C:\...\movies-api\Movies> dotnet run --project .\Movies.Api\
Building...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5066
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\...\movies-api\Movies\Movies.Api

Entidades y Conf CQRS

En el apartado anterior, estuvimos creando lo que seria la solucion .net core 7 usando DDD como guia. Y ya contamos con la base de datos inicial para validar nuestra idea. Ahora lo que vamos a hacer es crear las entidades del dominio y configurar la aplicacion con CQRS y MediatR.

Primero que todo, vamos a hacer una limpieza de el código por defecto que tenia Movies.Api.

PS C:\workspace\my-movies-checklist\movies-api\Movies> rm .\Movies.Api\Controllers\WeatherForecastController.cs
PS C:\workspace\my-movies-checklist\movies-api\Movies> rm .\Movies.Api\WeatherForecast.cs

Sorry, work in progress