Создание Serverless API с помощью Azure Cosmos DB
Azure Cosmos DB - глобально распределенная, многомодельная служба базы данных NoSQL, используемая для создания высокодоступных и масштабируемых приложений. Cosmos DB поддерживает приложения, использующие данные модели документов через SQL API and MongoDB API.
Azure Cosmos DB реализует проводной протокол для Mongo DB, разрешающий использовать привычные драйверы и инструменты, но с возможностью размещения данных в Azure Cosmos DB.
Стало возможным менять приложения для использования Azure Cosmos DB без внесения кардинальных изменений в базу исходного кода, пользоваться преимуществами Azure Cosmos DB, такими как распределение Turnkey и эластичная масштабируемость как в пропускной способности, так и в хранилище.
Настройка учетной записи с помощью MongoDB API
Начнем с создания учетной записи Cosmos DB. Войдите в Azure и нажмите " Create a resource". Найдите Azure Cosmos DB и нажмите "New".
На странице ‘Create Azure Cosmos DB Account’ укажите следующую информацию:
· Resource Group – группы ресурсов в Azure, представляющие собой логическую коллекцию ресурсов. Создайте новую или добавьте свою учетную запись в существующую.
· Account Name - уникальное в Azure имя учетной записи. API — Azure Cosmos DB - многомодельная база данных, но при создании учетной записи в Cosmos DB выбираем только один API на весь срок действия учетной записи.
· Location - то, где будет зарегистрирована учетная запись. В примере выбрана Восточная Австралия. Выбирайте удобный для себя.
· Capacity Mode - то, как будет обеспечиваться пропускная способность в учетной записи.
· Account Type - выберите производственную среду
· Version - версия протокола Mongo DB, который будет поддерживать учетная запись. Выбираем 3.6
· Availability Zones - отключите.
Нажмите Review+Create, затем Create для создания учетной записи Cosmos DB.
После настройки учетной записи появляется возможность создания базы данных и коллекции.
В учетной записи Cosmos DB зайдите в проводник данных и нажмите ‘New Collection’. Введите ‘BookstoreDB’ в качестве имени базы данных и Books - как название коллекции. Затем выберите ключ для разделения документов из коллекции по узлам.
Сегментирование в Mongo DB - метод распределения данных между несколькими машинами. Важно, этот сегмент поможет приложению масштабироваться как с точки зрения пропускной способности, так и с точки зрения хранения за счет горизонтального масштабирования.
Теперь созданы база данных, коллекция и учетная запись. Но для настройки нужна еще строка подключения. Нажмите Connection String и скопируйте значение PRIMARY CONNECTION STRING.
Создание Function Application
Выберите ‘Azure Functions’ в качестве шаблона для создания проекта (Убедитесь, что выбран язык C#).
Назовем проект ‘CosmosBooksApi’, сохраните проект в выбранном месте и нажмите создать.
Выберите Azure Functions v3 (.NET Core) в качестве среды выполнения кода и создайте пустой проект без триггеров.
Перед началом создания функций важно установить пакет MongoDB.Driver. Для это нажмите правой клавишей мыши на проект и выберите ‘Manage NuGet Packages’. В разделе Browse введите MongoDB.Driver и установите последнюю стабильную версию.
После установки создадим файл Startup.cs, который создаст экземпляр нашего MongoClient:
using CosmosBooksApi;using CosmosBooksApi.Services;using Microsoft.Azure.Functions.Extensions.DependencyInjection;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using MongoDB.Driver;using System.IO;using System.Security.Authentication; [assembly: FunctionsStartup(typeof(Startup))]namespace CosmosBooksApi{ public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.AddSingleton<IConfiguration>(config); MongoClientSettings settings = MongoClientSettings.FromUrl(new MongoUrl(config["ConnectionString"])); settings.SslSettings = new SslSettings() { EnabledSslProtocols = SslProtocols.Tls12 }; builder.Services.AddSingleton((s) => new MongoClient(settings)); builder.Services.AddTransient<IBookService, BookService>(); } }}Начиная со второй версии Azure Functions появилась поддержка для внедрения зависимостей, разрешающая создавать экземпляр MongoClient как Singleton. Таким образом можно совместно использовать MongoClient среди функций и избежать создания нового экземпляра для клиента каждый раз, когда хотим вызвать Функции.
Для регистрации наших сервисов добавим компоненты в экземпляр IFunctionsHostBuilder, который передается как метод настройки
Для использования этого метода добавим атрибут FunctionsStartup в сам класс загрузки.
Затем создадим новую конфигурацию типа IConfiguration. Все, что нужно - это выбрать конфигурацию для Function application из файла local.settings.json Затем добавить сервис IConfiguration как Singleton.
Теперь настроим MongoClient. Начинаем с настройки строки подключения путем передачи нашей PRIMARY CONNECTION STRING в качестве объекта MongoUrl() Сохраним это в нашем local.settings.json file.
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "ConnectionString": "<PRIMARY_CONNECTION_STRING>", "DatabaseName": "BookstoreDB", "CollectionName": "Books" }}Затем передаем ключ настройки в объект MongoUrl.Подключаем SSL, используя протокол Tls12 в SslSettings для MongoClientSettings. Это требование Azure Cosmos DB для подключения к учетной записи API MongoDB.После настройки MongoClientSettings, передаем их в объект MongoClient, который настроен как Singleton Service.Теперь создаем базовый класс для представления нашей модели Book. Напишем следующее:
using MongoDB.Bson;using MongoDB.Bson.Serialization.Attributes; namespace CosmosBooksApi.Models{ public class Book { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } [BsonElement("name")] public string BookName { get; set; } [BsonElement("price")] public decimal Price { get; set; } [BsonElement("category")] public string Category { get; set; } [BsonElement("author")] public string Author { get; set; } }}В этом классе есть свойства для Book id, имени, цены, категории, автора. ID отмечен BsonId как первичный ключ документа. Также аннотировали ID [BsonRepresentation(BsonType.ObjectId)] для передачи ID как строкового типа, а не ObjectId. Mongo будет обрабатывать преобразование строки в объект.
Это простой интерфейс CRUD, определяющий контракт, который должен быть реализован сервисами. Реализуем этот интерфейс:
using CosmosBooksApi.Models;using Microsoft.Extensions.Configuration;using MongoDB.Driver;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks; namespace CosmosBooksApi.Services{ public class BookService : IBookService { private readonly MongoClient _mongoClient; private readonly IMongoDatabase _database; private readonly IMongoCollection<Book> _books; public BookService( MongoClient mongoClient, IConfiguration configuration) { _mongoClient = mongoClient; _database = _mongoClient.GetDatabase(configuration["DatabaseName"]); _books = _database.GetCollection<Book>(configuration["CollectionName"]); } public async Task CreateBook(Book bookIn) { await _books.InsertOneAsync(bookIn); } public async Task<Book> GetBook(string id) { var book = await _books.FindAsync(book => book.Id == id); return book.FirstOrDefault(); } public async Task<List<Book>> GetBooks() { var books = await _books.FindAsync(book => true); return books.ToList(); } public async Task RemoveBook(Book bookIn) { await _books.DeleteOneAsync(book => book.Id == bookIn.Id); } public async Task RemoveBookById(string id) { await _books.DeleteOneAsync(book => book.Id == id); } public async Task UpdateBook(string id, Book bookIn) { await _books.ReplaceOneAsync(book => book.Id == id, bookIn); } }}Введем зависимости в MongoClient и IConfiguration, затем создадим базу данных и коллекцию для выполнения операций с ними. Рассмотрим различные методы.InsertOneAsync - асинхронно вставляет документ в IMongoCollection. Передается документ, который хотим сохранить. В данном случае это объект Book. Также можем передать некоторые пользовательские параметры (InsertOneOptions) и CancellationToken.FindAsync - асинхронно находит документ, который соответствует фильтру. Используется лямбда-выражение для поиска книги с тем же ID, который указан в методе. Linq используется для возврата соответствующей книги.DeleteOneAsync - асинхронно удаляет документ, соответствующий выражению. Опять же, используется лямба-выражение для поиска книги, которую необходимо удалить. В этом методе возвращается только результат операции.ReplaceOneAsync - приводит к асинхронной замене документа.Итак, MongoClient создан и есть базовая служба CRUD, которая используется для взаимодействия с учетной записью Cosmos DB. Перейдем к созданию функций.В качестве примера создадим следующие функции:
- CreateBook
- DeleteBook
- GetAllBooks
- GetBookById
- UpdateBook
Для создания новой функции щелкните правой клавишей мыши по файлу нашего решения и выберите ‘Add New Azure Function’. Появляется всплывающее окно. Выбираем Http Trigger, далее Anonymous в качестве уровня авторизации функции.Начнем с функции CreateBook:
using CosmosBooksApi.Models;using CosmosBooksApi.Services;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.Azure.WebJobs;using Microsoft.Azure.WebJobs.Extensions.Http;using Microsoft.Extensions.Logging;using MongoDB.Bson;using Newtonsoft.Json;using System;using System.IO;using System.Threading.Tasks; namespace CosmosBooksApi.Functions{ public class CreateBook { private readonly ILogger<CreateBook> _logger; private readonly IBookService _bookService; public CreateBook( ILogger<CreateBook> logger, IBookService bookService) { _logger = logger; _bookService = bookService; } [FunctionName(nameof(CreateBook))] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "Book")] HttpRequest req) { IActionResult result; try { var incomingRequest = await new StreamReader(req.Body).ReadToEndAsync(); var bookRequest = JsonConvert.DeserializeObject<Book>(incomingRequest); var book = new Book { Id = ObjectId.GenerateNewId().ToString(), BookName = bookRequest.BookName, Price = bookRequest.Price, Category = bookRequest.Category, Author = bookRequest.Author }; await _bookService.CreateBook(book); result = new StatusCodeResult(StatusCodes.Status201Created); } catch (Exception ex) { _logger.LogError($"Internal Server Error. Exception: {ex.Message}"); result = new StatusCodeResult(StatusCodes.Status500InternalServerError); } return result; } }}Здесь вводим IBookService и ILogger в функцию. Вызываем эту функцию, отправляя запрос POST на ‘/Book’. Берем входящий HttpRequest и выполняем десериализацию в объект Book. Затем вставляет книгу в коллекцию Books. Если все прошло успешно, то получим ответ 201 (Создан). Если нет, ответ 500.
Теперь рассмотрим функцию DeleteBook:
using CosmosBooksApi.Services;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.Azure.WebJobs;using Microsoft.Azure.WebJobs.Extensions.Http;using Microsoft.Extensions.Logging;using System;using System.Threading.Tasks; namespace CosmosBooksApi.Functions{ public class DeleteBook { private readonly ILogger<DeleteBook> _logger; private readonly IBookService _bookService; public DeleteBook( ILogger<DeleteBook> logger, IBookService bookService) { _logger = logger; _bookService = bookService; } [FunctionName(nameof(DeleteBook))] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "Book/{id}")] HttpRequest req, string id) { IActionResult result; try { var bookToDelete = await _bookService.GetBook(id); if (bookToDelete == null) { _logger.LogWarning($"Book with id: {id} doesn't exist."); result = new StatusCodeResult(StatusCodes.Status404NotFound); } await _bookService.RemoveBook(bookToDelete); result = new StatusCodeResult(StatusCodes.Status204NoContent); } catch (Exception ex) { _logger.LogError($"Internal Server Error. Exception thrown: {ex.Message}"); result = new StatusCodeResult(StatusCodes.Status500InternalServerError); } return result; } }}На этот раз передаем ID в нашу функцию (‘/Book/id’) для поиска книга, которую необходимо удалить из коллекции. Сначала ищем книгу, используя метод IBookService..GetBook(id). Если книга не существует, функция выдаст ответ 404 (не найдена).
Если книга найдена, передаем ее в метод RemoveBook(book) для удаления из коллекции в Cosmos DB. В случае успешного выполнения - ответ 204.
Код для функции GetAllBooks:
using CosmosBooksApi.Services;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.Azure.WebJobs;using Microsoft.Azure.WebJobs.Extensions.Http;using Microsoft.Extensions.Logging;using System;using System.Threading.Tasks; namespace CosmosBooksApi.Functions{ public class GetAllBooks { private readonly ILogger<GetAllBooks> _logger; private readonly IBookService _bookService; public GetAllBooks( ILogger<GetAllBooks> logger, IBookService bookService) { _logger = logger; _bookService = bookService; } [FunctionName(nameof(GetAllBooks))] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Books")] HttpRequest req) { IActionResult result; try { var books = await _bookService.GetBooks(); if (books == null) { _logger.LogWarning("No books found!"); result = new StatusCodeResult(StatusCodes.Status404NotFound); } result = new OkObjectResult(books); } catch (Exception ex) { _logger.LogError($"Internal Server Error. Exception thrown: {ex.Message}"); result = new StatusCodeResult(StatusCodes.Status500InternalServerError); } return result; } }}В этой функции передается запрос GET на ‘/Books’. Эта функция вызовет метод .GetBooks() в IBookService для извлечения всех книг из коллекции. Если книг нет - ответ 404. Если книги есть, функция вернет их пользователю в виде массива.
Функция GetBookById схожа с GetAllBooks , но на этот раз передается ID книги, которую нужно вернуть:
using CosmosBooksApi.Services;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.Azure.WebJobs;using Microsoft.Azure.WebJobs.Extensions.Http;using Microsoft.Extensions.Logging;using System;using System.Threading.Tasks; namespace CosmosBooksApi.Functions{ public class GetBookById { private readonly ILogger<GetBookById> _logger; private readonly IBookService _bookService; public GetBookById( ILogger<GetBookById> logger, IBookService bookService) { _logger = logger; _bookService = bookService; } [FunctionName(nameof(GetBookById))] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Book/{id}")] HttpRequest req, string id) { IActionResult result; try { var book = await _bookService.GetBook(id); if (book == null) { _logger.LogWarning($"Book with id: {id} doesn't exist."); result = new StatusCodeResult(StatusCodes.Status404NotFound); } result = new OkObjectResult(book); } catch (Exception ex) { _logger.LogError($"Internal Server Error. Exception thrown: {ex.Message}"); result = new StatusCodeResult(StatusCodes.Status500InternalServerError); } return result; } }}Также передается ID на функцию UpdateBook. Сначала вызываем метод. GetBook(id) для поиска книги, которую хотим обновить. После нахождения книги читаем входящий запрос и десериализируем его в объект Book. Затем используем десериализованный запрос для обновления объекта Book и передаем этот объект в метод .UpdateBook() вместе с ID, который использовали для вызова функции.
using CosmosBooksApi.Models;using CosmosBooksApi.Services;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.Azure.WebJobs;using Microsoft.Azure.WebJobs.Extensions.Http;using Microsoft.Extensions.Logging;using Newtonsoft.Json;using System;using System.IO;using System.Threading.Tasks; namespace CosmosBooksApi.Functions{ public class UpdateBook { private readonly ILogger<UpdateBook> _logger; private readonly IBookService _bookService; public UpdateBook( ILogger<UpdateBook> logger, IBookService bookService) { _logger = logger; _bookService = bookService; } [FunctionName(nameof(UpdateBook))] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "Book/{id}")] HttpRequest req, string id) { IActionResult result; try { var bookToUpdate = await _bookService.GetBook(id); if (bookToUpdate == null) { _logger.LogWarning($"Book with id: {id} doesn't exist."); result = new StatusCodeResult(StatusCodes.Status404NotFound); } var input = await new StreamReader(req.Body).ReadToEndAsync(); var updateBookRequest = JsonConvert.DeserializeObject<Book>(input); Book updatedBook = new Book { Id = id, BookName = updateBookRequest.BookName, Author = updateBookRequest.Author, Category = bookToUpdate.Category, Price = updateBookRequest.Price }; await _bookService.UpdateBook(id, updatedBook); result = new StatusCodeResult(StatusCodes.Status202Accepted); } catch (Exception ex) { _logger.LogError($"Internal Server Error: {ex.Message}"); result = new StatusCodeResult(StatusCodes.Status500InternalServerError); } return result; } }}Проверка функции
После завершения кодирования функций следующие этапы - развертывание и тестирование. Нажмите F5 для локального запуска функции. Через пару секунд функции будут развернуты и появятся endpoints для функций.
Функции выполняются в локальном хосте. Используем Postman для тестирования endpoints.
Начнем с функции CreateBook. Скопируйте и вставьте крайнюю точку функции в Postman. Установите метод запроса POST и щелкните вкладку. Необходимо отправить запрос в качестве JSON payload, поэтому установите для него JSON и добавьте следующее:
{ "BookName" : "Computer Science: Distilled", "Price": 11.99, "Category": "Technology", "Author": "Wladston Ferreira Filho"}Нажмите Send для отправки запроса. Получен ответ (201).
Чтобы убедиться, что документ добавлен в учетную запись, можно просмотреть документ в учетной записи Cosmos DB.
Вставьте еще пару книг прежде, чем двигаться дальше. Теперь попытаемся извлечь все книги из нашей коллекции, используя функцию GetAllBooks. Удалите JSON payload из тела и измените метод запроса на GET. Нажмите Send для создания запроса.
Должен быть получен подобный ответ:
Здесь книги коллекции, возвращенные в качестве массива JSON. Теперь протестируем функцию GetBookById. В ответ на функцию GetAllBooks, возьмите ID и добавьте его в качестве параметра в маршрут обработки запроса. Все, что здесь изменилось это то, что нужно найти конкретную книгу, используя ее ID.
Нажмите Send для создания запроса. У нас должен быть объект книга, возвращенный к нам:
Теперь удалим книгу из коллекции Cosmos DB. Измените метод запроса на DELETE в Postman и нажмите Send.
Должен быть получен следующий ответ:
Если проверить коллекцию в Azure Cosmos DB, увидим, что книги здесь больше нет.
Наконец, попробуем обновить книгу. Возьмем следующую книгу из коллекции:
{ "id": "603ae1b621786dd7fd92d5c0", "bookName": "The Dark Net", "price": 18.99, "category": "Technology", "author": "Jamie Bartlett"}Возьмем ID и используем его в качестве параметра в функции UpdateBook. Изменим метод на запрос PUT и добавим следующее в запрос:
{ "bookName": "The Dark Net v2", "price": 11.99, "author": "Jamie Bartlett"}Эта текстовая часть отправляется как JSON payload. Нажимаем Send для обновления документа книга.
Получаем следующий ответ.
Проверим успешность обновления документа в учетной записи Cosmos DB.
На этом примере удалось увидеть, что даже если приложения созданы с использованием MongoDb, Вы можете легко изменить его на Azure Cosmos DB, не внося серьезных изменений в базу исходного кода.