En este post veremos cómo integrar Markdown en un proyecto con entrada de texto en este formato y mostrarlo correctamente desde SQL.

Motivación

Markdown es un lenguaje de marcado utilizado ampliamente en sitios web (normalmente relacionados con el desarrollo de software), por ejemplo: https://stackoverflow.com/ o https://github.com/.

Estos sitios permiten escribir en este lenguaje y transformarlo en código HTML, al transformarlo se tiene en cuenta la sintaxis escrita para representarlo en diferentes elementos, veamos un ejemplo:

imagen_ejemplo_motivacion

¡Comencemos!

Pasos que seguiremos:

  1. Creamos nuestra base de datos SQL
  2. Creación de la aplicación web .NET Core MVC
  3. Soporte Markdown
  4. Extra

Recursos que utilizaremos:

1. Creación de la base de datos SQL

En este ejemplo utilizaremos una única tabla de SQL para mostrar los posts con contenido Markdown que subiremos. Los pasos mostrados están realizados en la aplicación: Microsoft Management Studio utilizando un servidor SQL Express previamente instalado:

imagen_creacion_bbdd
imagen_nombre_bbdd

Llamamos a la base de datos: MARKDOWNPOST.

Una vez creada añadiremos una tabla con las columnas que necesitamos:

imagen_crear_table_bbdd
imagen_diagrama_tabla

Una vez se abra el diagrama, hacemos click derecho en el recuadro central en blanco y creamos la tabla como en la imagen de la derecha. Cuando terminemos, guardamos el diagrama y nos pedirá guardar los cambios, una vez guardados deberíamos de poder ver la tabla en el explorador de la derecha, bajo la pestaña tables (si no es así, hacemos click derecho en tables y pulsamos refresh).

2. Creación de la aplicación web .NET Core MVC

Una vez tenemos la base de datos deberemos de crear el proyecto y conectarnos a SQL. Para ello, haremos uso de las plantillas que nos ofrece Microsoft.

Para ello abrimos Visual Studio 2022 y creamos un proyecto del tipo ASP.NET Core WebApp (Model-View-Controller):

iamgen_creacion_mvc

Llamamos a la aplicación: MvcMarkdownPost.

Hacemos click en Next y finalizamos dejando todas las opciones por defecto.

Ahora vamos a realizar la conexión a nuestra base de datos SQL, para ello necesitaremos la cadena de conexión. Esto es fácil de conseguir si nos conectamos al servidor a través del IDE:

imagen_server_explorer

Al hacer click se nos abrirá una panel lateral, en el cual podremos indicar que queremos una nueva conexión:

image_server_explorer_conexion

Una vez añadido, podremos hacer click derecho con el ratón encima de la nueva conexión y ver las propiedades, de las cuales copiaremos la que nos interesa: connectionString.

image_propiedades_connectionString

Y la añadimos de la siguiente forma al archivo appsettings.json:

image_appsettings

IMPORTANTE: Debemos de cambiar Password por nuestra contraseña de SQL Express.

Conexión de .NET con SQL

Para conectarnos de forma cómoda utilizaremos Entity Framework gracias a los nugets de EF y EF SQL.

Una vez instalados en nuestro proyecto, podremos crear el model Post bajo la carpeta Models:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMarkdownPost.Models
{
    [Table("POST")]
    public class Post
    {
        [Key]
        [Column("ID")]
        public int Id { get; set; }
        [Column("POST_TEXT")]
        public string PostText { get; set; }
        [Column("DATE_POSTED")]
        public DateTime DatePosted { get; set; }
    }
}

Una vez tenemos el modelo, podemos mapear la base de datos con sus tablas en el context. Para ello creamos Una carpeta Data y bajo ella un class PostsContext, de la cual inyectaremos en el constructor lo necesario para crear nuestro contexto:

using Microsoft.EntityFrameworkCore;
using MvcMarkdownPost.Models;

namespace MvcMarkdownPost.Data
{
    public class PostsContext : DbContext
    {
        public PostsContext(DbContextOptions<PostsContext> options) : base(options) { }

        public DbSet<Post> Posts { get; set; }
    }
}

Una vez tenemos esto, podemos acceder a la tabla POST para leer, crear, borrar y actualizar elementos. Ahora, crearemos una carpeta Repositories, en la que crearemos una clase llamada RepositoryPosts, en la cual inyectamos en el constructor el contexto previamente creado:

using Microsoft.EntityFrameworkCore;
using MvcMarkdownPost.Data;
using MvcMarkdownPost.Models;

namespace MvcMarkdownPost.Repositories
{
    public class RepositoryPosts
    {
        private readonly PostsContext context;

        public RepositoryPosts(PostsContext context)
        {
            this.context = context;
        }

        public Task<List<Post>> GetPostsAsync()
        {
            return this.context.Posts.OrderByDescending(x => x.DatePosted).ToListAsync();
        }

        public async Task<Post?> FindPostByIdAsync(int postId)
        {
            return await this.context.Posts.FindAsync(postId);
        }

        private async Task<int> GetMaxPostAsync()
        {
            if (!await context.Posts.AnyAsync())
            {
                return 1;
            }

            return await this.context.Posts.MaxAsync(x => x.Id) + 1;
        }

        public async Task CreatePostAsync(string text)
        {
            await this.context.Posts.AddAsync(new Post()
            {
                Id = await this.GetMaxPostAsync(),
                DatePosted = DateTime.Now,
                PostText = text
            });
            await this.context.SaveChangesAsync();
        }

        public async Task DeletePostAsync(int postId)
        {
            Post? post = await this.FindPostByIdAsync(postId);
            if (post != null)
            {
                this.context.Posts.Remove(post);
                await this.context.SaveChangesAsync();
            }
        }
    }
}

Este código nos permitirá realizar operaciones sobre la tabla POST desde los controllers que manejan nuestras vistas.

Ahora nos pasamos a la carpeta Controllers y creamos un nuevo controller llamado PostsController:

using Microsoft.AspNetCore.Mvc;
using MvcMarkdownPost.Models;
using MvcMarkdownPost.Repositories;

namespace MvcMarkdownPost.Controllers
{
    public class PostsController : Controller
    {
        private readonly RepositoryPosts repositoryPosts;

        public PostsController(RepositoryPosts repositoryPosts)
        {
            this.repositoryPosts = repositoryPosts;
        }

        public async Task<IActionResult> Index()
        {
            List<Post> posts = await this.repositoryPosts.GetPostsAsync();
            // Enviamos texto opcional directamente a la vista
            ViewData["SUCCESS"] = TempData["SUCCESS"];
            ViewData["ERROR"] = TempData["ERROR"];
            return View(posts);
        }

        public async Task<IActionResult> Details(int postId)
        {
            Post? post = await this.repositoryPosts.FindPostByIdAsync(postId);
            if (post == null)
            {
            // Redirigimos al controller Index
                return RedirectToAction("Index");
            }

            return View(post);
        }

        // Decoracion que indica que este metodo solo recibe peticiones POST
        [HttpPost]
        public async Task<IActionResult> NewPost(string text)
        {
            // Si no se ha especificado ningun texto redirigimos sin realizar cambios
            if (text == null || !text.Any())
            {
                return RedirectToAction("Index");
            }

            try
            {
                // Esta operacion devolvera una excepcion si algo va mal
                await this.repositoryPosts.CreatePostAsync(text);
                // Enviamos un texto al siguiente controller
                TempData["SUCCESS"] = "Post creado correctamente";
            }
            catch (Exception ex)
            {
                // Enviamos un texto al siguiente controller
                TempData["ERROR"] = "Error al subir el post: " + ex.Message;
            }
            // Redirigimos al controller Index
            return RedirectToAction("Index");
        }

        public async Task<IActionResult> DeletePost(int postId)
        {
            await this.repositoryPosts.DeletePostAsync(postId);
            // Redirigimos al controller Index
            return RedirectToAction("Index");
        }
    }
}

Vista Index (lista de posts)

Una vez tenemos el controller, podemos crear las vistas utilizando scaffolding. Para ello hacemos click derecho con el mouse sobre cada método y configuramos la vista Index de la siguiente forma:

image_add_view_index
image_scaffolding_option
image_scaffolding_configuracion_index

Ahora podremos ver una nuevo carpeta llamada Posts dentro de la carpeta Views. En ella, veremos un archivo Index.cs que es la vista creada. Hacemos click y la editamos, añadiendo justo debajo de la línea:

@{
    ViewData["Title"] = "Index";
}

Lo siguiente:

<form method="post" asp-action="NewPost" asp-controller="Posts">
    <label class="form-label" for="postText">Introduzca un texto en Markdown</label>
    <div class="d-flex gap-2">
        <textarea type="text" id="postText" name="text" class="form-control w-50" cols="5" rows="5" required></textarea>
        <div id="markdownPreview" class="w-50 border rounded">Markdown preview</div>
    </div>

    <button class="mt-1 btn btn-outline-success">
        Nuevo post
    </button>
</form>

@{
    if (ViewData["ERROR"] != null)
    {
        <div class="mt-4 alert alert-danger" role="alert">
            @ViewData["ERROR"]
        </div>
    }

    if (ViewData["SUCCESS"] != null)
    {
        <div class="mt-4 alert alert-success" role="alert">
            @ViewData["SUCCESS"]
        </div>
    }
}

Vista Details (detalles de un post)

Hacemos lo mismo con la vista Details:

image_scaffolding_configuracion_details

Para poder acceder a estas vistas añadiremos un link en el archivo /Views/Shared/_Layout.chstml:

link_layout

Código del link (etiqueta <li></li> dentro de la etiqueta existente <ul></ul>):

<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-controller="Posts" asp-action="Index">Home</a>
</li>

Configuración del proyecto (Program.cs)

Como podéis ver, en las clases creadas hemos ido inyectando dependencias en los constructores. Si ejecutáis la aplicación ahora os dará erorres al querer activar las clases, ya que no se podrán resolver las dependencias.

Todo esto lo configuraremos en el archivo Program.cs. En el cual añadiremos código entre las líneas:

var builder = WebApplication.CreateBuilder(args);

y

builder.Services.AddControllersWithViews();

var app = builder.Build();

Líneas a añadir:

// Obtenemos la connectionString de appsettings.json
string connectionString = builder.Configuration.GetConnectionString("SqlPost");

// Activamos el contexto para la inyeccion
builder.Services.AddDbContext<PostsContext>(options => options.UseSqlServer(connectionString));
// Activamos el repositorio que hemos creado para la inyeccion
builder.Services.AddTransient<RepositoryPosts>();

También, modificamos las siguientes líneas para definir cuál será la página por defecto al cargar nuestra app:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

por:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Posts}/{action=Index}/{id?}");

Una vez añadidas, tendremos todo lo necesario para ejecutar la aplicación y probar la funcionalidad antes de implementar el soporte Markdown.

3. Añadimos soporte para Markdown

Pasos:

  1. Incluimos el nuget Markdig.
  2. Incluimos la librería JavaScript Showdown.

1. Librería Markdig

Esta librería para .NET nos permite parsear código Markdown a HTML. Para utilizarla la instalaremos en nuestro proyecto desde el nuget manager:

markdig_nuget

Una vez añadida, sólo tenemos que modificar dos líneas de nuestro PostsController dentro del método Details(int postId):

public async Task<IActionResult> Details(int postId)
{
    Post? post = await this.repositoryPosts.FindPostByIdAsync(postId);
    if (post == null)
    {
    // Redirigimos al controller Index
        return RedirectToAction("Index");
    }
    // Utilizamos la libreria MarkDig para parsear el markdown de la BBDD a HTML para la vista
    // En este caso utilizamos UseAdvancedExtensions() para añadir soporte para tablas y algunos elementos mas
    var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();

    post.PostText = Markdown.ToHtml(post.PostText, pipeline);
    return View(post);
}

Sólo con eso, ya podremos visualizar el código Markdown en nuestra vista correctamente.

2. Librería Showdown

Sin embargo, como habréis visto en la vista Index.cshtml, tenemos un <div id=»markdownPreview»></div> que utilizaremos para previsualizar el texto Markdown según el usuario escribe.

Para ello necesitaremos la librería Showdown. Como no estamos en un proyecto Node.js, vamos a descargar manualmente el archivo .js de la librería e incluirlo dentro de la carpeta del proyecto: wwwroot/lib/showdown/.

Link de descarga oficial (.zip): https://github.com/showdownjs/showdown/releases.

Descomprimos la carpeta, vamos a /dist/ y copiamos el archivo showdown.min.js en la ruta de nuestro proyecto MVC: wwroot/lib/showdown/:

image_showdown_path

El siguiente paso sería modificar la vista Index.cshtml para añadir dicha funcionalidad a través de un script en JavaScript en el que utilizamos, ya incluído en el template, jQuery:

@section scripts {
    <script>
        const converter = new showdown.Converter({ tables: true });

        function parseMarkdown() {
            const text = $("#postText").val();
            const html = converter.makeHtml(text);
            $("#markdownPreview").html(html);
        }

        $("#postText").keyup(function (e) {
            parseMarkdown();
        });
    </script>
}

Incluiremos estas líneas al final del archivo (aunque esto no tiene relevancia).

4. Extra

En este post hemos cubierto por encima cómo integrar Markdown en un proyecto, enfocado a la interacción con los usuarios. Sin embargo, quedan varios temas a tratar que dejo abiertos al lector.

Uno de ellos sería la falta de seguridad, el cual es un tema actual y desde luego que muy importante, no sabemos quiénes se conectarán a nuestra web ni sus intenciones. Es relativamente sencillo inyectar código malicioso en cualquier tipo de inputs, en este caso es tan fácil de implementar como introducir el siguente código en la entrada de texto:

<script>
    alert("te estoy atacando");
</script>

O dejo por aquí una solución que yo mismo he implementado en alguno de mis proyectos: https://www.npmjs.com/package/showdown-xss-filter.

Por otro lado, sería muy interesante poder escribir en archivos Markdown (.md) directamente, y que el framework los parsee a HTML, por ejemplo, como hace el framework Astro.

Me gustaría agradecer a Tajarmar y TajamarTech por la oportunidad de contribuir a esta comunidad.

Autor/a: Félix Martínez

Curso: Desarrollo Web Full Stack + MultiCloud con Azure y AWS

Centro: Tajamar

Año académico: 2022-2023

Código completo del post: https://github.com/host4ideas/netcore-markdown-post.

Os invito a conectar conmigo por LinkedIn: https://www.linkedin.com/in/felix-martinez-bendicho/.

Leave a Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.