Создание Serverless API с помощью Azure Cosmos DB

Tags: Azure, CosmosDB, MongoDB, Microsoft

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 будет обрабатывать преобразование строки в объект. Остальные свойства аннотированы с помощью [BsonElement()]. Это определит как свойства будут выглядеть в коллекции. Создадим службу, которая обрабатывает логику, работающую с учетной записью Cosmos DB. Дадим интерфейсу название IBookService.cs.

using CosmosBooksApi.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
 
namespace CosmosBooksApi.Services
{
    public interface IBookService
    {
        /// <summary>
        /// Get all books from the Books collection
        /// </summary>
        /// <returns></returns>
        Task<List<Book>> GetBooks();
 
        /// <summary>
        /// Get a book by its id from the Books collection
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        Task<Book> GetBook(string id);
 
        /// <summary>
        /// Insert a book into the Books collection
        /// </summary>
        /// <param name="book"></param>
        /// <returns></returns>
        Task CreateBook(Book bookIn);
 
        /// <summary>
        /// Updates an existing book in the Books collection
        /// </summary>
        /// <param name="id"></param>
        /// <param name="book"></param>
        /// <returns></returns>
        Task UpdateBook(string id, Book bookIn);
 
        /// <summary>
        /// Removes a book from the Books collection
        /// </summary>
        /// <param name="book"></param>
        /// <returns></returns>
        Task RemoveBook(Book bookIn);
 
        /// <summary>
        /// Removes a book with the specified id from the Books collection
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        Task RemoveBookById(string id);
    }
}

Это простой интерфейс 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, не внося серьезных изменений в базу исходного кода.

No Comments

Add a Comment