Использование минимальных API-интерфейсов в ASP.NET Core страницы Razor
Если вы используете ASP.NET Core страницы Razor для разработки вашего веб-приложения вы уже решили, что большая часть вашего HTML-кода будет сгенерирована на сервере. Тем не менее, есть вероятность, что вы захотите внедрить некоторые операции на стороне клиента в приложение, чтобы местами улучшить его удобство для пользователя. Если эти операции связаны с данными, вы, вероятно, захотите работать с JSON. Начиная с .NET 6, вы можете использовать упрощенный API-интерфейс минимального обработчика запросов, который по умолчанию работает с JSON.
Вплоть до .NET 6 ваши возможности работы с JSON на страницах Razor были в значительной степени ограничены использованием методов обработки страниц для приема и возврата JSON или добавлением контроллеров Web API в приложение. Возврат результатов Json из методов обработки страниц работает, но в этом подходе есть что-то немного хакерское. Страницы Razor предназначены для создания пользовательского интерфейса, а не для предоставления услуг передачи данных по протоколу HTTP. Каждый раз, когда вы вызываете метод обработчика страницы из скрипта, модель страницы и все ее зависимости создаются независимо от того, нужны они или нет. Конечно, вы можете использовать контроллеры Web API, но для их работы требуется некоторая дополнительная настройка.
Работа с минимальными обработчиками запросов API предназначена для обеспечения низкой сложности работы. Вы регистрируете обработчики запросов с помощью метода Map[HttpMethod] в WebApplication - MapPost, MapGet, MapPut и т. Д., Используя соглашение об именовании на основе HTTP-методов, аналогичное тому, которое вы используете для регистрации методов обработки страниц в классе PageModel. Напомним, что экземпляр типа WebApplication возвращается из конструктора. Вызов метода сборки в Program.cs (обсуждался в моей предыдущей статье). Вы передаете шаблон маршрута и обработчик маршрута - стандартный делегат .NET, который выполняется при совпадении маршрута. Это может быть именованная функция или лямбда-выражение, которое может принимать параметры. Обработчик маршрута может быть настроен на возврат одного из многих встроенных типов ответов, включая JSON, текст и файлы. Очевидным упущением из встроенных возвращаемых типов является HTML. Вот для чего нужны страницы Razor.
Я собираюсь изучить эту новую функцию, внедрив простое одностраничное приложение CRUD, которое вращается вокруг той же модели автомобиля и сервиса, которые я использовал в предыдущих сообщениях.
У меня есть класс автомобиля, определенный в папке под названием Models:
namespace MinimalAPIs.Models;
public class Car
{
public int Id { get; set; }
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }
public int Doors { get; set; }
public string Color { get; set; }
public decimal Price { get; set; }
}
У меня есть простой сервис, который содержит некоторые базовые операции CRUD со списком автомобилей:
public class CarService : ICarService
{
public List<Car> GetAll() => cars;
public void Save(Car car)
{
if (car.Id != 0)
{
var carToUpdate = cars.FirstOrDefault(c => c.Id == car.Id);
if (carToUpdate != null)
{
cars.Remove(carToUpdate);
}
}
else
{
car.Id = cars.Max(c => c.Id) + 1;
}
cars.Add(car);
}
public Car Get(int id) => cars.FirstOrDefault(c => c.Id == id);
private List<Car> cars = new (){
new Car { Id = 1, Make = "Audi", Model = "R8", Year = 2018, Doors = 2, Color = "Red", Price = 79995 },
new Car { Id = 2, Make = "Aston Martin", Model = "Rapide", Year = 2014, Doors = 2, Color = "Black", Price = 54995 },
new Car { Id = 3, Make = "Porsche", Model = " 911 991", Year = 2020, Doors = 2, Color = "White", Price = 155000 },
new Car { Id = 4, Make = "Mercedes-Benz", Model = "GLE 63S", Year = 2021, Doors = 5, Color = "Blue", Price = 83995 },
new Car { Id = 5, Make = "BMW", Model = "X6 M", Year = 2020, Doors = 5, Color = "Silver", Price = 62995 },
};
}
Служба реализует этот интерфейс:
public interface ICarService
{
List<Car> GetAll();
Car Get(int id);
void Save(Car car);
}
И он зарегистрирован как одноэлементный, так что поддерживается любое состояние внутри службы:
builder.Services.AddSingleton<ICarService, CarService>();
В следующем объявлении регистрируется обработчик запросов, который отвечает на запросы GET. Он принимает шаблон маршрута, необязательно некоторые параметры и возвращает результат. Обработчик запроса объявляется в Program.cs непосредственно перед вызовом метода app.Run. Если вы используете более старый подход к настройке при запуске, регистрация будет выполнена в методе Configure после вызова UseEndpoints:
app.MapGet("/api/cars", (ICarService service) =>
{
return Results.Ok(service.GetAll());
});
Параметром в этом примере является служба ICar, которая разрешена из контейнера DI. Параметры привязаны к ряду других источников:
- значения маршрута
- строка
- запроса заголовки
- запроса тело
Мы можем явно указать источник привязки параметра, используя один из атрибутов From*: From Route, From Body, FromHeader, FromQuery или FromServices. Результаты.Метод Ok возвращает переданное в него значение, сериализованное в JSON с кодом состояния 200. Итак, у нас есть API, который будет повторно использоваться для GET запросов в / api /cars и вернет коллекцию автомобилей, сериализованных в JSON. Давайте добавим страницу для вызова этой конечной точки и отображения данных. Вот несколько простых разметок для кнопки и неупорядоченного списка:
<button class="btn btn-primary" id="get-cars">Get Cars</button>
<ul class="results mt-3"></ul>
Next, we need some script that wires up a click event handler to the button, fetches the data from the API and populates the unordered list:
@section scripts{
<script>
const list = document.querySelector('ul.results');
const getAll = document.getElementById('get-cars');
getAll.addEventListener('click', () => {
showCars();
});
const showCars = () =>
{
list.innerHTML = '';
fetch("/api/cars")
.then(response => response.json())
.then(cars =>
{
for(let i = 0;i < cars.length;i++) {
let item = document.createElement('li');
item.innerText = `${cars[i].id} ${cars[i].make }
${cars[i].model}, ${cars[i].year} £${cars[i].price
};
item.classList.add('edit-car');
item.dataset.id = cars[i].id;
item.dataset.bsToggle = 'modal';
item.dataset.bsTarget = '#car-modal';
item.addEventListener('click', (event) =>
{
getCar(event.target.dataset.id);
});
list.appendChild(item);
}
});
}
</script>
}
Этот код использует API выборки для выполнения вызова API, а затем выполняет итерацию возвращенных данных, присваивая каждому автомобилю элемент списка. При этом он добавляет некоторые атрибуты к элементу списка, которые будут использоваться для вызова загрузочного модала для последующего редактирования автомобиля. Он также добавляет обработчик события щелчка к каждому элементу, который будет вызывать метод get Car, который еще предстоит определить. Однако, если вы запустите страницу и нажмете на кнопку, появится список автомобилей:
Далее мы добавляем частичную страницу в папку Pages/Shared с именем _Car Modal.cshtml. Это будет содержать форму в режиме начальной загрузки, которую мы будем использовать для добавления новых автомобилей и редактирования существующих. Полная разметка для файла выглядит следующим образом:
@model Car
<div class="modal fade" tabindex="-1" role="dialog" id="car-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Save Car</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input asp-for="Id" type="hidden">
<div class="form-group">
<label asp-for="Make"></label>
<input asp-for="Make" class="form-control">
</div>
<div class="form-group">
<label asp-for="Model"></label>
<input asp-for="Model" class="form-control">
</div>
<div class="form-group">
<label asp-for="Year"></label>
<input asp-for="Year" class="form-control">
</div>
<div class="form-group">
<label asp-for="Doors"></label>
<input asp-for="Doors" class="form-control">
</div>
<div class="form-group">
<label asp-for="Color"></label>
<input asp-for="Color" class="form-control">
</div>
<div class="form-group">
<label asp-for="Price"></label>
<input asp-for="Price" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="save-car">Save changes</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
Моделью для этого файла является автомобиль. Чтобы избежать необходимости использовать полную ссылку на тип автомобиля, мы добавляем директиву using в файл _View Imports:
@using MinimalAPIs.Models
Мы используем помощник partial tag для включения partial на страницу, передавая новый экземпляр Car в модель:
<partial name="_CarModal" model="new Car()"/>
Теперь давайте реализуем метод JavaScript getchar, который вызывается обработчиками событий щелчка по элементам списка:
const getCar = (id) => {
fetch(`/api/car/${id}`)
.then(response => response.json())
.then(car => {
document.getElementById('Id').value = car.id;
document.getElementById('Model').value = car.model;
document.getElementById('Make').value = car.make;
document.getElementById('Year').value = car.year;
document.getElementById('Doors').value = car.doors;
document.getElementById('Color').value = car.color;
document.getElementById('Price').value = car.price;
});
}
Этот метод выполняет запрос на выборку к новой конечной точке API по адресу /api/car/, передавая идентификатор конкретного автомобиля, полученный из атрибута data-id, который был добавлен к элементу списка при его создании. Возвращенный автомобиль имеет свои свойства, присвоенные элементам управления формой в модели. Нам нужно определить эту конечную точку и заставить ее возвращать экземпляр Car, сериализованный в JSON. следующее добавляется в Program.cs сразу после предыдущего API:
app.MapGet("/api/car/{id:int}", (int id, ICarService service) =>
{
var car = service.Get(id);
return car;
});
Шаблон маршрута принимает параметр, представляющий значение маршрута, точно так же, как и остальные страницы Razor. Параметр может быть дополнительно ограничен. На этот раз возвращаемый тип API представляет собой простой объект, а не результат. По умолчанию это будет сериализовано в JSON. Как только все это будет добавлено, мы можем запустить приложение, нажать кнопку "Получить автомобили", а затем щелкнуть по автомобилю в списке и увидеть, как его данные отображаются в модальном:
На данный момент кнопка Save Changes не делает ничего, кроме закрытия модального. Поэтому мы добавим ссылку на него в блок скрипта:
const save = document.getElementById('save-car');
Затем мы подключаем прослушиватель событий, который вызывает метод save Car:
save.addEventListener('click', () => {
saveCar();
});
Затем мы добавляем метод save Car в блок скрипта:
const saveCar = () => {
const model = {
id: document.getElementById('Id').value,
model: document.getElementById('Model').value,
make: document.getElementById('Make').value,
year: document.getElementById('Year').value,
doors: document.getElementById('Doors').value,
color: document.getElementById('Color').value,
price: document.getElementById('Price').value,
};
fetch('/api/save', {
method: model.id > 0 ? 'put' : 'post',
headers: {
"Content-type": "application/json",
},
body: JSON.stringify(model)
});
}
This method obtains values from the modal form and creates a model. The model is converted to a JSON string before it is passed to the body of a Fetch
request. the content type is specified as JSON and the method used for the request depends on whether the model has an Id
value greater than zero. If it does, we are editing a car and use the PUT
method. Otherwise it's a new car so we use the POST
method. This differentiation isn't essential, but it conforms to best practice for RESTful services, and it helps to illustrate another feature of the minimal request handler - the ability to support multiple HTTP methods.
Here is the handler defined for the /api/save
route:
app.MapMethods("/api/save", new[] {"POST", "PUT"}, (Car car, ICarService service) =>
{
service.Save(car);
return Results.Ok();
});
На этот раз мы используем методы Map для регистрации обработчика запросов, что позволяет нам определять HTTP-методы, поддерживаемые этим обработчиком. Они указаны в массиве, который мы передаем во второй параметр, поэтому этот обработчик запроса поддерживает как методы POST, так и PUT.
Остается сделать последний шаг, а именно добавить кнопку, которая вызывает модальную форму с пустыми входными данными, чтобы мы могли добавлять новые автомобили:
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#car-modal">New Car</button>
Резюме
В этом введении я только коснулся поверхности, но минимальные обработчики запросов API отлично подходят для работы с JSON в приложении .NET 6 Razor Pages. Они легкие и быстрые и избавляют вас от необходимости настраивать контроллеры веб-API или взламывать возврат JsonResults из метода обработчика страниц.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.