Cómo integrar Markdown en un proyecto ASP.NET Core MVC
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:
¡Comencemos!
Pasos que seguiremos:
- Creamos nuestra base de datos SQL
- Creación de la aplicación web .NET Core MVC
- Soporte Markdown
- 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:
Llamamos a la base de datos: MARKDOWNPOST.
Una vez creada añadiremos una tabla con las columnas que necesitamos:
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):
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:
Al hacer click se nos abrirá una panel lateral, en el cual podremos indicar que queremos una nueva conexión:
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.
Y la añadimos de la siguiente forma al archivo appsettings.json:
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:
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:
Para poder acceder a estas vistas añadiremos un link en el archivo /Views/Shared/_Layout.chstml:
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. 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:
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/:
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/.