Introducción a Razor Pages en ASP.NET Core

Por Rick Anderson y Ryan Nowak

Razor Pages facilita la programación de escenarios centrados en páginas y hace que resulte más productiva que con controladores y vistas.

Si busca un tutorial que use el enfoque Model-View-Controller, consulte Introducción a ASP.NET Core MVC.

En este documento se proporciona una introducción a Razor Pages. No es un tutorial paso a paso. Si encuentra que alguna sección es demasiado avanzada, consulte Introducción a Razor Pages. Para obtener información general de ASP.NET Core, vea Introducción a ASP.NET Core.

Requisitos previos

Crear un proyecto de Razor Pages

Vea Introducción a Razor Pages para obtener instrucciones detalladas sobre cómo crear un proyecto Razor Pages.

Razor Pages

Razor Pages está habilitado en Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Considere la posibilidad de una página básica:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

El código anterior se parece mucho a un archivo de vista de Razor que se utiliza en una aplicación ASP.NET Core con controladores y vistas. La directiva @page lo hace diferente. @page transforma el archivo en una acción de MVC, lo que significa que administra las solicitudes directamente, sin tener que pasar a través de un controlador. @page debe ser la primera directiva de Razorde una página. @page afecta al comportamiento de otras construcciones de Razor. Los nombres de archivo de Razor Pages tienen el sufijo .cshtml.

Una página similar, con una clase PageModel, se muestra en los dos archivos siguientes. El archivo Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Modelo de página Pages/Index2.cshtml.cs:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

Por convención, el archivo de clase PageModel tiene el mismo nombre que el archivo de Razor Pages con .cs anexado. Por ejemplo, la instancia de Razor Pages anterior es Pages/Index2.cshtml. El archivo que contiene la clase PageModel se denomina Pages/Index2.cshtml.cs.

Las asociaciones de rutas de dirección URL a páginas se determinan según la ubicación de la página en el sistema de archivos. En la tabla siguiente, se muestra una ruta de acceso Razor Pages y la dirección URL correspondiente:

Ruta de acceso y nombre de archivo URL correspondiente
/Pages/Index.cshtml / o /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store o /Store/Index

Notas:

  • El entorno de ejecución busca archivos de páginas de Razor en la carpeta Pages de forma predeterminada.
  • Index es la página predeterminada cuando una URL no incluye una página.

Escritura de un formulario básico

Razor Pages está diseñado para facilitar la implementación de patrones comunes que se usan con exploradores web al compilar una aplicación. Los enlaces de modelos, los asistentes de etiquetas y los asistentes de HTML simplemente funcionan con las propiedades definidas en una clase de Razor Pages. Considere la posibilidad de una página que implementa un formulario básico del estilo "Póngase en contacto con nosotros" para el modelo Contact:

Para los ejemplos de este documento, DbContext se inicializa en el archivo Startup.cs.

La base de datos en memoria requiere el paquete NuGet Microsoft.EntityFrameworkCore.InMemory.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<CustomerDbContext>(options =>
                      options.UseInMemoryDatabase("name"));
    services.AddRazorPages();
}

El modelo de datos:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

El contexto de la base de datos:

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Models;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
    }
}

El archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Modelo de página Pages/Create.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using RazorPagesContacts.Models;
using System.Threading.Tasks;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateModel : PageModel
    {
        private readonly CustomerDbContext _context;

        public CreateModel(CustomerDbContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Customers.Add(Customer);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}

Por convención, la clase PageModel se denomina <PageName>Model y se encuentra en el mismo espacio de nombres que la página.

La clase PageModel permite la separación de la lógica de una página de su presentación. Define los controladores de página para solicitudes que se envían a la página y los datos que usan para representar la página. Esta separación permite lo siguiente:

La página tiene un método de controlador OnPostAsync, que se ejecuta en solicitudes POST (cuando un usuario envía el formulario). Se pueden agregar métodos de controlador para cualquier verbo HTTP. Los controladores más comunes son:

  • OnGet para inicializar el estado necesario para la página. En el código anterior, el método OnGet muestra la instancia CreateModel.cshtml de Razor Pages.
  • OnPost para controlar los envíos del formulario.

El sufijo de nombre Async es opcional, pero se usa a menudo por convención para funciones asincrónicas. El código anterior es típico de Razor Pages.

Si está familiarizado con las aplicaciones de ASP.NET con controladores y vistas:

  • El código OnPostAsync del ejemplo anterior es similar al típico código de controlador.
  • La mayoría de los primitivos de MVC, como el enlace de modelos, la validación y los resultados de acciones, funcionan del mismo modo con los controladores y Razor Pages.

El método OnPostAsync anterior:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Customers.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

El flujo básico de OnPostAsync:

Compruebe los errores de validación.

  • Si no hay ningún error, guarde los datos y redirija.
  • Si hay errores, muestre la página de nuevo con mensajes de validación. En muchos casos, los errores de validación se detectan en el cliente y nunca se envían al servidor.

El archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

El código HTML representado de Pages/Create.cshtml:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

En el código anterior, la publicación del formulario:

  • Con datos válidos:

    • El método del controlador OnPostAsync llama al método auxiliar RedirectToPage. RedirectToPage devuelve una instancia de RedirectToPageResult. RedirectToPage:

      • Es el resultado de una acción.
      • Es similar a RedirectToAction o RedirectToRoute (se usa en controladores y vistas).
      • Se ha personalizado para las páginas. En el ejemplo anterior, redirige a la página de índice raíz (/Index). RedirectToPage se detalla en la sección Generación de direcciones URL para las páginas.
  • Con errores de validación que se pasan al servidor:

    • El método del controlador OnPostAsync llama al método auxiliar Page. Page devuelve una instancia de PageResult. Devolver Page es similar a cómo las acciones en los controladores devuelven View. PageResult es el tipo de valor devuelto predeterminado para un método de controlador. Un método de controlador que devuelve void representa la página.
    • En el ejemplo anterior, la publicación del formulario sin valores hace que se devuelva ModelState.IsValid como false. En este ejemplo, no se muestra ningún error de validación en el cliente. La entrega de errores de validación se trata más adelante en este documento.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • Con errores de validación detectados mediante la validación del lado cliente:

    • Los datos no se publican en el servidor.
    • La validación del lado cliente se explica más adelante en este documento.

La propiedad Customer usa el atributo [BindProperty] para participar en el enlace de modelos:

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

[BindProperty]no debe usarse en modelos que contengan propiedades que el cliente no debe cambiar. Para más información, consulte Publicación excesiva.

De forma predeterminada, Razor Pages enlaza propiedades solo con verbos que no sean GET. El enlace a propiedades elimina la necesidad de escribir código para convertir los datos HTTP en el tipo de modelo. Enlazar reduce el código al usar la misma propiedad para representar los campos de formulario (<input asp-for="Customer.Name">) y aceptar la entrada.

Advertencia

Por motivos de seguridad, debe participar en el enlace de datos de solicitud GET con las propiedades del modelo de página. Compruebe las entradas de los usuarios antes de asignarlas a las propiedades. Si participa en el enlace de GET, le puede ser útil al trabajar con escenarios que dependan de cadenas de consultas o valores de rutas.

Para enlazar una propiedad en solicitudes GET, establezca la propiedad SupportsGet del atributo [BindProperty] en true:

[BindProperty(SupportsGet = true)]

Para obtener más información, vea ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

Revisión del archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>
  • En el código anterior, el asistente de etiquetas de entrada <input asp-for="Customer.Name" /> enlaza el elemento <input> HTML con la expresión del modelo Customer.Name.
  • @addTagHelper hace que los asistentes de etiquetas estén disponibles.

La página principal

Index.cshtml es la página principal:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customer)
            {
                <tr>
                    <td> @contact.Id  </td>
                    <td>@contact.Name</td>
                    <td>
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <button type="submit" asp-page-handler="delete"
                                asp-route-id="@contact.Id">delete
                        </button>
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

La clase PageModel asociada (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly CustomerDbContext _context;

    public IndexModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer> Customer { get; set; }

    public async Task OnGetAsync()
    {
        Customer = await _context.Customers.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customers.FindAsync(id);

        if (contact != null)
        {
            _context.Customers.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

El archivo Index.cshtml contiene el siguiente marcado:

<td>

El <a /a> asistente de etiquetas delimitadoras ha usado el atributo asp-route-{value} para generar un vínculo a la página de edición. El vínculo contiene datos de ruta con el identificador del contacto. Por ejemplo: https://localhost:5001/Edit/1. Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la representación de elementos HTML en archivos de Razor.

El archivo index.cshtml contiene marcado para crear un botón de eliminar para cada contacto de cliente:

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
<button type="submit" asp-page-handler="delete"

El código HTML representado:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

Al representar el botón de eliminar en HTML, el elemento formaction incluye parámetros para los siguientes elementos:

  • Id. de contacto de cliente especificado mediante el atributo asp-route-id.
  • handler especificado mediante el atributo asp-page-handler.

Al seleccionar el botón, se envía una solicitud de formulario POST al servidor. De forma predeterminada, el nombre del método de control se selecciona de acuerdo con el valor del parámetro handler y según el esquema OnPost[handler]Async.

Como en este ejemplo handler es delete, el método de control OnPostDeleteAsync se usa para procesar la solicitud POST. Si asp-page-handler se establece en otro valor, como remove, se seleccionará un método de controlador llamado OnPostRemoveAsync.

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customers.FindAsync(id);

    if (contact != null)
    {
        _context.Customers.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

El método OnPostDeleteAsync realiza las acciones siguientes:

  • Obtiene el elemento id de la cadena de consulta.
  • Realiza una consulta a la base de datos del contacto de cliente con FindAsync.
  • Si se encuentra el contacto del cliente, este se quita y se actualiza la base de datos.
  • Llama a RedirectToPage para redirigir la página Index raíz (/Index).

El archivo Edit.cshtml

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name"></span>
        </div>
    </div>

    <div>
        <button type="submit">Save</button>
    </div>
</form>

La primera línea contiene la directiva @page "{id:int}". La restricción de enrutamiento "{id:int}" indica a la página que acepte las solicitudes a la página que contienen datos de ruta int. Si una solicitud a la página no contiene datos de ruta que se puedan convertir en int, el tiempo de ejecución devuelve un error HTTP 404 (no encontrado). Para que el identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

El archivo Edit.cshtml.cs:

public class EditModel : PageModel
{
    private readonly CustomerDbContext _context;

    public EditModel(CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Customer = await _context.Customers.FindAsync(id);

        if (Customer == null)
        {
            return RedirectToPage("./Index");
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Customer).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new Exception($"Customer {Customer.Id} not found!");
        }

        return RedirectToPage("./Index");
    }

}

Validación

Reglas de validación:

  • Se especifican mediante declaración en la clase de modelo.
  • Se aplican en toda la aplicación.

El espacio de nombres System.ComponentModel.DataAnnotations proporciona un conjunto de atributos de validación integrados que se aplican mediante declaración a una clase o propiedad. DataAnnotations también contiene atributos de formato como [DataType], que ayudan a aplicar formato y no proporcionan ninguna validación.

Considere el modelo Customer:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

Con el siguiente archivo de vista Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

El código anterior:

  • Incluye scripts de validación de jQuery y jQuery.

  • Usa los asistentes de etiquetas <div /> y <span /> para habilitar lo siguiente:

    • Validación del lado cliente.
    • Representación del error de validación.
  • Se genera el siguiente código HTML:

    <p>Enter a customer name:</p>
    
    <form method="post">
        Name:
        <input type="text" data-val="true"
               data-val-length="The field Name must be a string with a maximum length of 10."
               data-val-length-max="10" data-val-required="The Name field is required."
               id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
        <input type="submit" />
        <input name="__RequestVerificationToken" type="hidden"
               value="<Antiforgery token here>" />
    </form>
    
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
    

Al publicar el formulario de creación sin un valor de nombre, se muestra el mensaje de error "El campo Nombre es obligatorio". en el formulario. Si JavaScript está habilitado en el cliente, el explorador muestra el error sin realizar la publicación en el servidor.

El atributo [StringLength(10)] genera data-val-length-max="10" en el código HTML representado. data-val-length-max impide que los exploradores superen la longitud máxima especificada al escribir. Si se usa una herramienta como Fiddler para editar y reproducir la publicación:

  • Con el nombre de más de 10 caracteres.
  • Se devolverá el mensaje de error "El nombre del campo debe ser una cadena con una longitud máxima de 10 caracteres". .

Considere el modelo Movie siguiente:

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

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}

Los atributos de validación especifican el comportamiento que se exigirá a las propiedades del modelo al que se aplican:

  • Los atributos Required y MinimumLength indican que una propiedad debe tener un valor, pero nada impide al usuario escribir espacios en blanco para satisfacer esta validación.

  • El atributo RegularExpression se usa para limitar los caracteres que se pueden escribir. En el código anterior, "Género":

    • Solo debe usar letras.
    • La primera letra debe estar en mayúsculas. No se permiten espacios en blanco, números ni caracteres especiales.
  • La "Clasificación" de RegularExpression:

    • Requiere que el primer carácter sea una letra mayúscula.
    • Permite caracteres especiales y números en los espacios posteriores. "PG-13" es válido para una "Clasificación", pero se produce un error en un "Género".
  • El atributo Range restringe un valor a un intervalo determinado.

  • El atributo StringLength establece la longitud máxima de una propiedad de cadena y, opcionalmente, su longitud mínima.

  • Los tipos de valor (como decimal, int, float, DateTime) son intrínsecamente necesarios y no necesitan el atributo [Required].

La página de creación del modelo Movie muestra errores de visualización con valores no válidos:

Formulario de vista de película con varios errores de validación de cliente de jQuery

Para obtener más información, consulte:

Control de solicitudes HEAD con un controlador OnGet de reserva

Las solicitudes HEAD permiten recuperar los encabezados de un recurso específico. A diferencia de las solicitudes GET, las solicitudes HEAD no devuelven un cuerpo de respuesta.

Normalmente, se crea un controlador OnHead al que se llama para las solicitudes HEAD:

public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

Razor Pages recurre a una llamada al controlador OnGet si no se define ningún controlador OnHead.

XSRF/CSRF y Razor Pages

Razor Pages está protegido mediante validación antifalsificación. El elemento FormTagHelper inserta tokens antifalsificación en los elementos de formulario HTML.

Usar diseños, parciales, plantillas y asistentes de etiquetas con Razor Pages

Las páginas funcionan con todas las características del motor de vista de Razor. Los diseños, parciales, plantillas, asistentes de etiquetas, _ViewStart.cshtml y _ViewImports.cshtml funcionan de la misma manera que lo hacen con las vistas de Razor convencionales.

Para simplificar esta página, aprovecharemos algunas de esas características.

Agregue una página de diseño a Pages/Shared/_Layout.cshtml:

<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

El diseño:

  • Controla el diseño de cada página (a no ser que la página no tenga diseño).
  • Importa las estructuras HTML como JavaScript y hojas de estilos.
  • El contenido de la página de Razor se representa donde se llama a @RenderBody().

Para más información, consulte la página de diseño.

La propiedad Layout se establece en Pages/_ViewStart.cshtml:

@{
    Layout = "_Layout";
}

El diseño está en la carpeta Pages/Shared. Las páginas buscan otras vistas (diseños, plantillas, parciales) de forma jerárquica, a partir de la misma carpeta que la página actual. Un diseño en la carpeta Pages/Shared se puede usar desde cualquier página de Razor en la carpeta Pages.

El archivo de diseño debería ir en la carpeta Pages/Shared.

Le recomendamos que no coloque el archivo de diseño en la carpeta Views/Shared. Views/Shared es un patrón de vistas de MVC. Razor Pages está diseñado para basarse en la jerarquía de carpetas, no en las convenciones de ruta de acceso.

La búsqueda de vistas de una instancia de Razor Pages incluye la carpeta Pages. Los diseños, plantillas y parciales que se usan con los controladores de MVC y las vistas de Razor convencionales simplemente funcionan.

Agregue un archivo Pages/_ViewImports.cshtml:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace se explica más adelante en el tutorial. La directiva @addTagHelper pone los asistentes de etiquetas integradas en todas las páginas de la carpeta Pages.

La directiva @namespace establecida en una página:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

La directiva @namespace establece el espacio de nombres de la página. La directiva @model no necesita incluir el espacio de nombres.

Cuando la directiva @namespace se encuentra en _ViewImports.cshtml, el espacio de nombres especificado proporciona el prefijo del espacio de nombres generado en la página que importa la directiva @namespace. El resto del espacio de nombres generado (la parte del sufijo) es la ruta de acceso relativa separada por puntos entre la carpeta que contiene _ViewImports.cshtml y la carpeta que contiene la página.

Por ejemplo, la clase PageModelPages/Customers/Edit.cshtml.cs establece explícitamente el espacio de nombres:

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

El archivo Pages/_ViewImports.cshtml establece el espacio de nombres siguiente:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El espacio de nombres generado para la página de Razor Pages/Customers/Edit.cshtml es el mismo que la clase PageModel.

@namespace también funciona con vistas de Razor convencionales.

Considere el archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

Archivo de vista actualizado Pages/Create.cshtml con _ViewImports.cshtml y el archivo de distribución anterior:

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

En el código anterior, el elemento _ViewImports. cshtml importó el espacio de nombres y los asistentes de etiquetas. El archivo de distribución importó los archivos JavaScript.

El proyecto de inicio de Razor Pages contiene Pages/_ValidationScriptsPartial.cshtml, que enlaza la validación del lado cliente.

Para más información sobre las vistas parciales, vea Vistas parciales en ASP.NET Core.

Generación de direcciones URL para las páginas

La página Create, mostrada anteriormente, usa RedirectToPage:

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

La aplicación tiene la siguiente estructura de archivos o carpetas:

  • /Pages

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

Las páginas Pages/Customers/Create.cshtml y Pages/Customers/Edit.cshtml redirigen a Pages/Customers/Index.cshtml si la operación se realiza correctamente. La cadena ./Index es un nombre de página relativo que se usa para acceder a la página anterior. Se usa para generar direcciones URL a la página Pages/Customers/Index.cshtml. Por ejemplo:

  • Url.Page("./Index", ...)
  • <a asp-page="./Index">Customers Index Page</a>
  • RedirectToPage("./Index")

El nombre de página absoluto /Index se usa para generar direcciones URL a la página Pages/Index.cshtml. Por ejemplo:

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">Home Index Page</a>
  • RedirectToPage("/Index")

El nombre de página es la ruta de acceso a la página de la carpeta raíz /Pages, incluido un / inicial, por ejemplo /Index. Los ejemplos anteriores de generación de URL ofrecen opciones mejoradas y capacidades funcionales en comparación con la escritura a mano de estas. La generación de direcciones URL usa el enrutamiento y puede generar y codificar parámetros según cómo se defina la ruta en la ruta de acceso de destino.

La generación de direcciones URL para las páginas admite nombres relativos. En la siguiente tabla, se muestra qué página de índice está seleccionada con diferentes parámetros RedirectToPage en Pages/Customers/Create.cshtml.

RedirectToPage(x) Página
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index"), RedirectToPage("./Index") y RedirectToPage("../Index") son nombres relativos. El parámetro RedirectToPage se combina con la ruta de acceso de la página actual para calcular el nombre de la página de destino.

Vincular el nombre relativo es útil al crear sitios con una estructura compleja. Cuando se usan nombres relativos para el vínculo entre las páginas de una carpeta:

  • Al cambiar el nombre de una carpeta, no se rompen los vínculos relativos.
  • Los vínculos no se rompen porque no incluyen el nombre de la carpeta.

Para redirigir a una página en otra área, especifique el área:

RedirectToPage("/Index", new { area = "Services" });

Para obtener más información, vea Áreas de ASP.NET Core y Convenciones de aplicación y de ruta de Razor Pages en ASP.NET Core.

Atributo ViewData

Se pueden pasar datos a una página con ViewDataAttribute. Las propiedades con el atributo [ViewData] tienen sus valores almacenados y cargados desde el elemento ViewDataDictionary.

En el ejemplo siguiente, el elemento AboutModel aplica el atributo [ViewData] a la propiedad Title:

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

En la página Acerca de, acceda a la propiedad Title como propiedad de modelo:

<h1>@Model.Title</h1>

En el diseño, el título se lee desde el diccionario ViewData:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core expone el elemento TempData. Esta propiedad almacena datos hasta que se leen. Los métodos Keep y Peek se pueden usar para examinar los datos sin que se eliminen. TempData es útil para el redireccionamiento cuando se necesitan los datos de más de una única solicitud.

El siguiente código establece el valor de Message mediante TempData:

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

El siguiente marcado en el archivo Pages/Customers/Index.cshtml muestra el valor de Message mediante TempData.

<h3>Msg: @Model.Message</h3>

El modelo de página Pages/Customers/Index.cshtml.cs aplica el atributo [TempData] a la propiedad Message.

[TempData]
public string Message { get; set; }

Para más información, consulte TempData.

Varios controladores por página

En la página siguiente se usa el asistente de etiquetas asp-page-handler para generar marcado para dos controladores:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

El formulario del ejemplo anterior tiene dos botones de envío, y cada uno de ellos usa FormActionTagHelper para enviar a una dirección URL diferente. El atributo asp-page-handler es un complemento de asp-page. asp-page-handler genera direcciones URL que envían a cada uno de los métodos de controlador definidos por una página. asp-page no se especifica porque el ejemplo se vincula a la página actual.

Modelo de página:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

El código anterior usa métodos de controlador con nombre. Los métodos de controlador con nombre se crean tomando el texto en el nombre después de On<HTTP Verb> y antes de Async (si existe). En el ejemplo anterior, los métodos de página son OnPost JoinList Async y OnPost JoinListUC Async. Quitando OnPost y Async, los nombres de controlador son JoinList y JoinListUC.

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es https://localhost:5001/Customers/CreateFATH?handler=JoinList. La ruta de dirección URL que envía a OnPostJoinListUCAsync es https://localhost:5001/Customers/CreateFATH?handler=JoinListUC.

Rutas personalizadas

Use la directiva @page para:

  • Especificar una ruta personalizada a una página. Por ejemplo, la ruta a la página Acerca de se puede establecer en /Some/Other/Path con @page "/Some/Other/Path".
  • Anexar segmentos a la ruta predeterminada de una página. Por ejemplo, se puede agregar un segmento "item" a la ruta predeterminada de una página con @page "item".
  • Anexar parámetros a la ruta predeterminada de una página. Por ejemplo, un parámetro de identificador, id, puede ser necesario para una página con @page "{id}".

Se admite una ruta de acceso relativa raíz designada por una tilde (~) al principio de la ruta de acceso. Por ejemplo, @page "~/Some/Other/Path" es lo mismo que @page "/Some/Other/Path".

Si no le gusta la cadena de consulta ?handler=JoinList en la dirección URL, puede cambiar la ruta para poner el nombre del controlador en la parte de la ruta de la dirección URL. Para personalizar la ruta, se puede agregar una plantilla de ruta entre comillas dobles después de la directiva @page.

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es https://localhost:5001/Customers/CreateFATH/JoinList. La ruta de dirección URL que envía a OnPostJoinListUCAsync es https://localhost:5001/Customers/CreateFATH/JoinListUC.

El signo ? que sigue a handler significa que el parámetro de ruta es opcional.

Valores de configuración avanzados

La mayoría de las aplicaciones no requieren la configuración de las siguientes secciones.

Para configurar las opciones avanzadas, use la sobrecarga AddRazorPages que configura RazorPagesOptions:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
    {
        options.RootDirectory = "/MyPages";
        options.Conventions.AuthorizeFolder("/MyPages/Admin");
    });
}

Use el elemento RazorPagesOptions para establecer el directorio raíz de páginas o agregar convenciones de modelo de aplicación para las páginas. Para obtener más información sobre las convenciones, vea Convenciones de autorización de Razor Pages.

Para precompilar vistas, consulte la sección sobre la compilación de vistas de Razor.

Especificación de Razor Pages en la raíz del contenido

De forma predeterminada, Razor Pages se encuentra en la raíz del directorio /Pages. Agregue WithRazorPagesAtContentRoot para especificar que sus instancias de Razor Pages se encuentran en la raíz de contenido (ContentRootPath) de la aplicación:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesAtContentRoot();
}

Especificación de Razor Pages en un directorio raíz personalizado

Agregue WithRazorPagesRoot para especificar que Razor Pages se encuentra en un directorio raíz personalizado en la aplicación (proporcione una ruta de acceso relativa):

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesRoot("/path/to/razor/pages");
}

Recursos adicionales

Advertencia

Si usa Visual Studio 2017, consulte dotnet/sdk problema #3124 para información sobre las versiones del SDK de .NET Core que no funcionan con Visual Studio.

Crear un proyecto de Razor Pages

Vea Introducción a Razor Pages para obtener instrucciones detalladas sobre cómo crear un proyecto Razor Pages.

Razor Pages

Razor Pages está habilitado en Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Includes support for Razor Pages and controllers.
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvc();
    }
}

Considere la posibilidad de una página básica:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

El código anterior se parece mucho a un archivo de vista de Razor que se utiliza en una aplicación ASP.NET Core con controladores y vistas. La directiva @page lo hace diferente. @page transforma el archivo en una acción de MVC, lo que significa que administra las solicitudes directamente, sin tener que pasar a través de un controlador. @page debe ser la primera directiva de Razor de una página. @page afecta al comportamiento de otras construcciones de Razor.

Una página similar, con una clase PageModel, se muestra en los dos archivos siguientes. El archivo Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model IndexModel2

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Modelo de página Pages/Index2.cshtml.cs:

using Microsoft.AspNetCore.Mvc.RazorPages;
using System;

namespace RazorPagesIntro.Pages
{
    public class IndexModel2 : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

Por convención, el archivo de clase PageModel tiene el mismo nombre que el archivo de Razor Pages con .cs anexado. Por ejemplo, la instancia de Razor Pages anterior es Pages/Index2.cshtml. El archivo que contiene la clase PageModel se denomina Pages/Index2.cshtml.cs.

Las asociaciones de rutas de dirección URL a páginas se determinan según la ubicación de la página en el sistema de archivos. En la tabla siguiente, se muestra una ruta de acceso Razor Pages y la dirección URL correspondiente:

Ruta de acceso y nombre de archivo URL correspondiente
/Pages/Index.cshtml / o /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store o /Store/Index

Notas:

  • El entorno de ejecución busca archivos de páginas de Razor en la carpeta Pages de forma predeterminada.
  • Index es la página predeterminada cuando una URL no incluye una página.

Escritura de un formulario básico

Razor Pages está diseñado para facilitar la implementación de patrones comunes que se usan con exploradores web al compilar una aplicación. Los enlaces de modelos, los asistentes de etiquetas y los asistentes de HTML simplemente funcionan con las propiedades definidas en una clase de Razor Pages. Considere la posibilidad de una página que implementa un formulario básico del estilo "Póngase en contacto con nosotros" para el modelo Contact:

Para los ejemplos de este documento, DbContext se inicializa en el archivo Startup.cs.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;

namespace RazorPagesContacts
{
    public class Startup
    {
        public IHostingEnvironment HostingEnvironment { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<AppDbContext>(options =>
                              options.UseInMemoryDatabase("name"));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
    }
}

El modelo de datos:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Data
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(100)]
        public string Name { get; set; }
    }
}

El contexto de la base de datos:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
    }
}

El archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

Modelo de página Pages/Create.cshtml.cs:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
    public class CreateModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }
    }
}

Por convención, la clase PageModel se denomina <PageName>Model y se encuentra en el mismo espacio de nombres que la página.

La clase PageModel permite la separación de la lógica de una página de su presentación. Define los controladores de página para solicitudes que se envían a la página y los datos que usan para representar la página. Esta separación permite lo siguiente:

La página tiene un método de controlador OnPostAsync, que se ejecuta en solicitudes POST (cuando un usuario envía el formulario). Puede agregar métodos de controlador para cualquier verbo HTTP. Los controladores más comunes son:

  • OnGet para inicializar el estado necesario para la página. Ejemplo OnGet.
  • OnPost para controlar los envíos del formulario.

El sufijo de nombre Async es opcional, pero se usa a menudo por convención para funciones asincrónicas. El código anterior es típico de Razor Pages.

Si está familiarizado con las aplicaciones de ASP.NET con controladores y vistas:

  • El código OnPostAsync del ejemplo anterior es similar al típico código de controlador.
  • La mayoría de los primitivos MVC como el enlace de modelos, la validación, la validación y los resultados de acciones se comparten.

El método OnPostAsync anterior:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _db.Customers.Add(Customer);
    await _db.SaveChangesAsync();
    return RedirectToPage("/Index");
}

El flujo básico de OnPostAsync:

Compruebe los errores de validación.

  • Si no hay ningún error, guarde los datos y redirija.
  • Si hay errores, muestre la página de nuevo con mensajes de validación. La validación del lado cliente es idéntica a las aplicaciones de ASP.NET Core MVC tradicionales. En muchos casos, los errores de validación se detectan en el cliente y nunca se envían al servidor.

Cuando los datos se escriben correctamente, el método del controlador OnPostAsync llama al método del asistente RedirectToPage para devolver una instancia de RedirectToPageResult. RedirectToPage es un resultado de acción nueva, similar a RedirectToAction o RedirectToRoute, pero personalizada para las páginas. En el ejemplo anterior, redirige a la página de índice raíz (/Index). RedirectToPage se detalla en la sección Generación de direcciones URL para las páginas.

Cuando el formulario enviado tiene errores de validación (que se pasan al servidor), el método del controlador OnPostAsync llama al método del asistente Page. Page devuelve una instancia de PageResult. Devolver Page es similar a cómo las acciones en los controladores devuelven View. PageResult es el tipo de valor devuelto predeterminado para un método de controlador. Un método de controlador que devuelve void representa la página.

La propiedad Customer usa el atributo [BindProperty] para participar en el enlace de modelos.

public class CreateModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateModel(AppDbContext db)
    {
        _db = db;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        return RedirectToPage("/Index");
    }
}

De forma predeterminada, Razor Pages enlaza propiedades solo con verbos que no sean GET. Enlazar a propiedades puede reducir la cantidad de código que se debe escribir. Enlazar reduce el código al usar la misma propiedad para representar los campos de formulario (<input asp-for="Customer.Name">) y aceptar la entrada.

Advertencia

Por motivos de seguridad, debe participar en el enlace de datos de solicitud GET con las propiedades del modelo de página. Compruebe las entradas de los usuarios antes de asignarlas a las propiedades. Si participa en el enlace de GET, le puede ser útil al trabajar con escenarios que dependan de cadenas de consultas o valores de rutas.

Para enlazar una propiedad en solicitudes GET, establezca la propiedad SupportsGet del atributo [BindProperty] en true:

[BindProperty(SupportsGet = true)]

Para obtener más información, vea ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

La página principal (Index.cshtml):

@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customers)
            {
                <tr>
                    <td>@contact.Id</td>
                    <td>@contact.Name</td>
                    <td>
                        <a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
                        <button type="submit" asp-page-handler="delete" 
                                asp-route-id="@contact.Id">delete</button>
                    </td>
                </tr>
            }
        </tbody>
    </table>

    <a asp-page="./Create">Create</a>
</form>

La clase PageModel asociada (Index.cshtml.cs):

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Pages
{
    public class IndexModel : PageModel
    {
        private readonly AppDbContext _db;

        public IndexModel(AppDbContext db)
        {
            _db = db;
        }

        public IList<Customer> Customers { get; private set; }

        public async Task OnGetAsync()
        {
            Customers = await _db.Customers.AsNoTracking().ToListAsync();
        }

        public async Task<IActionResult> OnPostDeleteAsync(int id)
        {
            var contact = await _db.Customers.FindAsync(id);

            if (contact != null)
            {
                _db.Customers.Remove(contact);
                await _db.SaveChangesAsync();
            }

            return RedirectToPage();
        }
    }
}

El archivo Index.cshtml contiene el siguiente marcado para crear un vínculo de edición para cada contacto:

<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>

El <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> asistente de etiquetas delimitadoras ha usado el atributo asp-route-{value} para generar un vínculo a la página de edición. El vínculo contiene datos de ruta con el identificador del contacto. Por ejemplo: https://localhost:5001/Edit/1. Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la representación de elementos HTML en archivos de Razor. Los asistentes de etiquetas se habilitan mediante @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El archivo Pages/Edit.cshtml:

@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@{
    ViewData["Title"] = "Edit Customer";
}

<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name" ></span>
        </div>
    </div>
 
    <div>
        <button type="submit">Save</button>
    </div>
</form>

La primera línea contiene la directiva @page "{id:int}". La restricción de enrutamiento "{id:int}" indica a la página que acepte las solicitudes a la página que contienen datos de ruta int. Si una solicitud a la página no contiene datos de ruta que se puedan convertir en int, el tiempo de ejecución devuelve un error HTTP 404 (no encontrado). Para que el identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

El archivo Pages/Edit.cshtml.cs:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Customer = await _db.Customers.FindAsync(id);

            if (Customer == null)
            {
                return RedirectToPage("/Index");
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Attach(Customer).State = EntityState.Modified;

            try
            {
                await _db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw new Exception($"Customer {Customer.Id} not found!");
            }

            return RedirectToPage("/Index");
        }
    }
}

El archivo index.cshtml también contiene una marca para crear un botón de eliminar para cada contacto de cliente:

<button type="submit" asp-page-handler="delete" 
        asp-route-id="@contact.Id">delete</button>

Al representar dicho botón de eliminar en HTML, formaction incluye parámetros para:

  • Id. de contacto de cliente especificado mediante el atributo asp-route-id.
  • handler especificado mediante el atributo asp-page-handler.

Aquí tiene un ejemplo de un botón de eliminar representado con un id. de contacto de cliente de 1:

<button type="submit" formaction="/?id=1&amp;handler=delete">delete</button>

Al seleccionar el botón, se envía una solicitud de formulario POST al servidor. De forma predeterminada, el nombre del método de control se selecciona de acuerdo con el valor del parámetro handler y según el esquema OnPost[handler]Async.

Como en este ejemplo handler es delete, el método de control OnPostDeleteAsync se usa para procesar la solicitud POST. Si asp-page-handler se establece en otro valor, como remove, se seleccionará un método de controlador llamado OnPostRemoveAsync. En el código siguiente se muestra el controlador OnPostDeleteAsync:

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _db.Customers.FindAsync(id);

    if (contact != null)
    {
        _db.Customers.Remove(contact);
        await _db.SaveChangesAsync();
    }

    return RedirectToPage();
}

El método OnPostDeleteAsync realiza las acciones siguientes:

  • Acepta el elemento id de la cadena de consulta. Si la directiva de página Index.cshtml contuviera la restricción de enrutamiento "{id:int?}", id provendría de los datos de la ruta. Los datos de la ruta de id se especifican en el URI. Por ejemplo, https://localhost:5001/Customers/2.
  • Realiza una consulta a la base de datos del contacto de cliente con FindAsync.
  • Si encuentra dicho contacto de cliente, se quita de la lista correspondiente. Luego, se actualiza la base de datos.
  • Llama a RedirectToPage para redirigir la página Index raíz (/Index).

Marcado de las propiedades de página según sea necesario

Las propiedades de un valor PageModel se pueden marcar con el atributo Required:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Pages.Movies
{
    public class CreateModel : PageModel
    {
        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        [Required(ErrorMessage = "Color is required")]
        public string Color { get; set; }

        public IActionResult OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            // Process color.

            return RedirectToPage("./Index");
        }
    }
}

Para obtener más información, vea Validación de modelos.

Control de solicitudes HEAD con un controlador OnGet de reserva

Las solicitudes HEAD permiten recuperar los encabezados de un recurso específico. A diferencia de las solicitudes GET, las solicitudes HEAD no devuelven un cuerpo de respuesta.

Normalmente, se crea un controlador OnHead al que se llama para las solicitudes HEAD:

public void OnHead()
{
    HttpContext.Response.Headers.Add("HandledBy", "Handled by OnHead!");
}

En ASP.NET Core 2.1 o versiones posteriores, Razor Pages recurre a una llamada al controlador OnGet si no se define ningún controlador OnHead. Este comportamiento se habilita mediante la llamada a SetCompatibilityVersion en Startup.ConfigureServices:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

Las plantillas predeterminadas generan la llamada SetCompatibilityVersion en ASP.NET Core 2.1 y 2.2. SetCompatibilityVersion define con eficacia la opción de Razor Pages true como AllowMappingHeadRequestsToGetHandler.

En lugar de aceptar todos los comportamientos con SetCompatibilityVersion, puede aceptar explícitamente comportamientos específicos. El código siguiente permite que las solicitudes HEAD se asignen al controlador OnGet:

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.AllowMappingHeadRequestsToGetHandler = true;
    });

XSRF/CSRF y Razor Pages

No tiene que escribir ningún código para la validación antifalsificación. La validación y generación de tokens antifalsificación se incluyen automáticamente en Razor Pages.

Usar diseños, parciales, plantillas y asistentes de etiquetas con Razor Pages

Las páginas funcionan con todas las características del motor de vista de Razor. Los diseños, parciales, plantillas, asistentes de etiquetas, _ViewStart.cshtml, _ViewImports.cshtml funcionan de la misma manera que lo hacen con las vistas de Razor convencionales.

Para simplificar esta página, aprovecharemos algunas de esas características.

Agregue una página de diseño a Pages/Shared/_Layout.cshtml:

<!DOCTYPE html>
<html>
<head> 
    <title>Razor Pages Sample</title>      
</head>
<body>    
   <a asp-page="/Index">Home</a>
    @RenderBody()  
    <a asp-page="/Customers/Create">Create</a> <br />
</body>
</html>

El diseño:

  • Controla el diseño de cada página (a no ser que la página no tenga diseño).
  • Importa las estructuras HTML como JavaScript y hojas de estilos.

Vea Layout page (Página de diseño) para obtener más información.

La propiedad Layout se establece en Pages/_ViewStart.cshtml:

@{
    Layout = "_Layout";
}

El diseño está en la carpeta Pages/Shared. Las páginas buscan otras vistas (diseños, plantillas, parciales) de forma jerárquica, a partir de la misma carpeta que la página actual. Un diseño en la carpeta Pages/Shared se puede usar desde cualquier página de Razor en la carpeta Pages.

El archivo de diseño debería ir en la carpeta Pages/Shared.

Le recomendamos que no coloque el archivo de diseño en la carpeta Views/Shared. Views/Shared es un patrón de vistas de MVC. Razor Pages está diseñado para basarse en la jerarquía de carpetas, no en las convenciones de ruta de acceso.

La búsqueda de vistas de instancia de Razor Pages incluye la carpeta Pages. Los diseños, plantillas y parciales que usa con los controladores de MVC y las vistas de Razor convencionales simplemente funcionan.

Agregue un archivo Pages/_ViewImports.cshtml:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace se explica más adelante en el tutorial. La directiva @addTagHelper pone los asistentes de etiquetas integradas en todas las páginas de la carpeta Pages.

Cuando la directiva @namespace se usa explícitamente en una página:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

La directiva establece el espacio de nombres de la página. La directiva @model no necesita incluir el espacio de nombres.

Cuando la directiva @namespace se encuentra en _ViewImports.cshtml, el espacio de nombres especificado proporciona el prefijo del espacio de nombres generado en la página que importa la directiva @namespace. El resto del espacio de nombres generado (la parte del sufijo) es la ruta de acceso relativa separada por puntos entre la carpeta que contiene _ViewImports.cshtml y la carpeta que contiene la página.

Por ejemplo, la clase PageModelPages/Customers/Edit.cshtml.cs establece explícitamente el espacio de nombres:

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

El archivo Pages/_ViewImports.cshtml establece el espacio de nombres siguiente:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El espacio de nombres generado para la página de Razor Pages/Customers/Edit.cshtml es el mismo que la clase PageModel.

@namespace también funciona con vistas de Razor convencionales.

El archivo de vista Pages/Create.cshtml original:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

El archivo de vista Pages/Create.cshtml actualizado:

@page
@model CreateModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

El proyecto de inicio de Razor Pages contiene Pages/_ValidationScriptsPartial.cshtml, que enlaza la validación del lado cliente.

Para más información sobre las vistas parciales, vea Vistas parciales en ASP.NET Core.

Generación de direcciones URL para las páginas

La página Create, mostrada anteriormente, usa RedirectToPage:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _db.Customers.Add(Customer);
    await _db.SaveChangesAsync();
    return RedirectToPage("/Index");
}

La aplicación tiene la siguiente estructura de archivos o carpetas:

  • /Pages

    • Index.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

Las páginas Pages/Customers/Create.cshtml y Pages/Customers/Edit.cshtml redirigen a Pages/Index.cshtml si se realiza correctamente. La cadena /Index forma parte del URI para tener acceso a la página anterior. La cadena /Index puede usarse para generar los URI para la página Pages/Index.cshtml. Por ejemplo:

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">My Index Page</a>
  • RedirectToPage("/Index")

El nombre de página es la ruta de acceso a la página de la carpeta raíz /Pages, incluido un / inicial, por ejemplo /Index. Los ejemplos anteriores de generación de URL ofrecen opciones mejoradas y capacidades funcionales en comparación con la escritura a mano de estas. La generación de direcciones URL usa el enrutamiento y puede generar y codificar parámetros según cómo se defina la ruta en la ruta de acceso de destino.

La generación de direcciones URL para las páginas admite nombres relativos. En la siguiente tabla, se muestra qué página de índice está seleccionada con diferentes parámetros RedirectToPage de Pages/Customers/Create.cshtml:

RedirectToPage(x) Página
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index"), RedirectToPage("./Index") y RedirectToPage("../Index") son nombres relativos. El parámetro RedirectToPage se combina con la ruta de acceso de la página actual para calcular el nombre de la página de destino.

Vincular el nombre relativo es útil al crear sitios con una estructura compleja. Si usa nombres relativos para vincular entre páginas en una carpeta, puede cambiar el nombre de esa carpeta. Todos los vínculos seguirán funcionando (porque no incluyen el nombre de carpeta).

Para redirigir a una página en otra área, especifique el área:

RedirectToPage("/Index", new { area = "Services" });

Para obtener más información, vea Áreas de ASP.NET Core.

Atributo ViewData

Se pueden pasar datos a una página con ViewDataAttribute. Los valores de las propiedades de los controladores o los modelos de Razor Pages con el atributo [ViewData] se almacenan y cargan desde ViewDataDictionary.

En el ejemplo siguiente, el valor AboutModel contiene una propiedad Title marcada con [ViewData]. La propiedad Title se establece en el título de la página Acerca de:

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

En la página Acerca de, acceda a la propiedad Title como propiedad de modelo:

<h1>@Model.Title</h1>

En el diseño, el título se lee desde el diccionario ViewData:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core expone la propiedad TempData en un controlador. Esta propiedad almacena datos hasta que se leen. Los métodos Keep y Peek se pueden usar para examinar los datos sin que se eliminen. TempData es útil para el redireccionamiento cuando se necesitan los datos de más de una única solicitud.

El siguiente código establece el valor de Message mediante TempData:

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

El siguiente marcado en el archivo Pages/Customers/Index.cshtml muestra el valor de Message mediante TempData.

<h3>Msg: @Model.Message</h3>

El modelo de página Pages/Customers/Index.cshtml.cs aplica el atributo [TempData] a la propiedad Message.

[TempData]
public string Message { get; set; }

Para obtener más información, vea TempData.

Varios controladores por página

En la página siguiente se usa el asistente de etiquetas asp-page-handler para generar marcado para dos controladores:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

El formulario del ejemplo anterior tiene dos botones de envío, y cada uno de ellos usa FormActionTagHelper para enviar a una dirección URL diferente. El atributo asp-page-handler es un complemento de asp-page. asp-page-handler genera direcciones URL que envían a cada uno de los métodos de controlador definidos por una página. asp-page no se especifica porque el ejemplo se vincula a la página actual.

Modelo de página:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

El código anterior usa métodos de controlador con nombre. Los métodos de controlador con nombre se crean tomando el texto en el nombre después de On<HTTP Verb> y antes de Async (si existe). En el ejemplo anterior, los métodos de página son OnPost JoinList Async y OnPost JoinListUC Async. Quitando OnPost y Async, los nombres de controlador son JoinList y JoinListUC.

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es https://localhost:5001/Customers/CreateFATH?handler=JoinList. La ruta de dirección URL que envía a OnPostJoinListUCAsync es https://localhost:5001/Customers/CreateFATH?handler=JoinListUC.

Rutas personalizadas

Use la directiva @page para:

  • Especificar una ruta personalizada a una página. Por ejemplo, la ruta a la página Acerca de se puede establecer en /Some/Other/Path con @page "/Some/Other/Path".
  • Anexar segmentos a la ruta predeterminada de una página. Por ejemplo, se puede agregar un segmento "item" a la ruta predeterminada de una página con @page "item".
  • Anexar parámetros a la ruta predeterminada de una página. Por ejemplo, un parámetro de identificador, id, puede ser necesario para una página con @page "{id}".

Se admite una ruta de acceso relativa raíz designada por una tilde (~) al principio de la ruta de acceso. Por ejemplo, @page "~/Some/Other/Path" es lo mismo que @page "/Some/Other/Path".

Si no le gusta la cadena de consulta ?handler=JoinList en la dirección URL, puede cambiar la ruta para poner el nombre del controlador en la parte de la ruta de la dirección URL. Para personalizar la ruta, se puede agregar una plantilla de ruta entre comillas dobles después de la directiva @page.

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es https://localhost:5001/Customers/CreateFATH/JoinList. La ruta de dirección URL que envía a OnPostJoinListUCAsync es https://localhost:5001/Customers/CreateFATH/JoinListUC.

El signo ? que sigue a handler significa que el parámetro de ruta es opcional.

Valores de configuración

Para configurar opciones avanzadas, use el método de extensión AddRazorPagesOptions en el generador de MVC:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddRazorPagesOptions(options =>
        {
            options.RootDirectory = "/MyPages";
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        });
}

Actualmente, puede usar RazorPagesOptions para establecer el directorio raíz de páginas, o agregar convenciones de modelo de aplicación a las páginas. En el futuro habilitaremos más extensibilidad de este modo.

Para precompilar vistas, consulte la sección sobre la compilación de vistas de Razor.

Descargue o vea el código de ejemplo.

Consulte Introducción a Razor Pages, que se basa en esta introducción.

Especificación de Razor Pages en la raíz del contenido

De forma predeterminada, Razor Pages se encuentra en la raíz del directorio /Pages. Agregue WithRazorPagesAtContentRoot a AddMvc para especificar que Razor Pages se encuentra en la raíz del contenido (ContentRootPath) de la aplicación:

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        ...
    })
    .WithRazorPagesAtContentRoot();

Especificación de Razor Pages en un directorio raíz personalizado

Agregue WithRazorPagesRoot a AddMvc para especificar que Razor Pages se encuentra en un directorio raíz personalizado de la aplicación (proporcione la ruta de acceso relativa):

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        ...
    })
    .WithRazorPagesRoot("/path/to/razor/pages");

Recursos adicionales