ASP.NET Core + MongoDB

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 23:55, 1 июня 2018.

Тема взаимодействия этих двух перспективных технологий рассмотрена на примере создания web-приложения заметок: строим приложение web api.net core, подключенное асинхронно к MongoDB, с полной поддержкой HTTP GET, PUT, POST, DELETE.

ASP.NET Core - перспективная платформа от Microsoft, претендующая на лидерство в области Web-разработки, а MongoDB - мейнстримовая технология БД (NoSQL).

Версии инструментов

ASP.NET Core 2.0, MongoDB .NET Driver 2.4 (устанавливается через NuGet), Visual Studio 2017 Community. MongoDB устанавливается в виде непосредственно сервера издания Community (v.3.6) и .NET драйвера, далее оба должны быть синхронно сконфигурированы для совместной работы.

Почему MongoDB?

  1. В отличие от реляционных баз данных MongoDB предлагает документо-ориентированную модель данных, благодаря чему MongoDB работает быстрее, обладает лучшей масштабируемостью, ее легче использовать [1].
  2. Кроссплатформенность.
  3. Простая масштабируемость: отсутствие жесткой схемы базы данных и в связи с этим потребности при малейшем изменении концепции хранения данных пересоздавать эту схему.
  4. Удобство хранения данных: Если в традиционном мире SQL есть таблицы, то в мире MongoDB есть коллекции. И если в реляционных БД таблицы хранят однотипные жестко структурированные объекты, то в коллекции могут содержать самые разные объекты, имеющие различную структуру и различный набор свойств. А ещё, если реляционные базы данных хранят строки, то MongoDB хранит документы. В отличие от строк документы могут хранить сложную по структуре информацию.[2]

Ход разработки

Создаём проект

File > New Project > .Net Core > ASP.NET Core Web Application. Далее выбрать Web API.

Настройка подключения к БД

Существует несколько форматов файлов, которые поддерживаются в настройках конфигурации (JSON, XML или INI). По умолчанию шаблон проекта WebApi поставляется с включенным форматом JSON. Внутри файла настроек порядок имеет значение и включает сложные структуры. Ниже приведен пример структуры настройки уровня 2 для подключения к базе данных - обновляем AppSettings.json:

{
  "MongoConnection": {
    "ConnectionString": "mongodb://admin:abc123!@localhost",
    "Database": "NotesDb"
  },

  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  }
}

Внедрение зависимостей

Внедрение конструктора является одним из наиболее распространенных подходов к внедрению зависимостей (Injection Dependency), хотя и не единственной. ASP.NET Core использует этот подход в своем решении, поэтому мы также будем использовать его. Проект ASP.NET Core имеет файл Startup.cs, который настраивает среду, в которой будет работать наше приложение. Файл Startup.cs также размещает службы в уровне служб ASP.NET Core, что и приводит к внедрению зависимостей.

Чтобы сопоставить настройки пользовательского подключения к базе данных, мы добавим новый класс настроек:

namespace NotebookAppApi.Model
{
    public class Settings
    {
        public string ConnectionString;
        public string Database;
    }
}

Теперь изменим Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();
    services.Configure<Settings>(options =>
    {
        options.ConnectionString 
            = Configuration.GetSection("MongoConnection:ConnectionString").Value;
        options.Database 
            = Configuration.GetSection("MongoConnection:Database").Value;
    });
}

Далее в проекте настройки будут доступны через интерфейс IOptions:

IOptions<Settings>

Конфигурирование БД

После того, как вы установили MongoDB, вам нужно будет настроить доступ, а также указать где находятся данные. Пути задаются какие удобно.

Для этого создайте файл локально с именем mongod.cfg по пути C:\Dev\Data.Config\mongod.cfg, например. Он будет включать в себя установочный путь к папке с данными для сервера MongoDB, а также к файлу журнала MongoDB, пока он не включает настройку проверки подлинности (логин и пароль от БД).

systemLog:
  destination: file
  path: "C:\\tools\\mongodb\\db\\log\\mongo.log"
  logAppend: true
storage:
  dbPath: "C:\\tools\\mongodb\\db\\data"

Запустить в командной строке (не PowerShell !) следующую строку. Это запустит сервер MongoDB с учётом конфиг-файла:

"C:\Program Files\MongoDB\Server\3.6\bin\mongod.exe" --config C:\Dev\Data.Config\mongod.cfg

Оценить работоспособность сервера можно по логу, произвольный путь которого был задан выше (C:\\tools\\mongodb\\db\\log\\mongo.log), если всё прошло успешно, последняя запись будет похожа на эту:

2018-05-16T16:31:28.567+0300 I NETWORK  [initandlisten] waiting for connections on port 27017

Далее (не закрывая открытую консоль!) нужно открыть новую консоль от имени администратора и и ввести там следующее:

"C:\Program Files\MongoDB\Server\3.6\bin\mongod.exe"

Так мы откроем консоль для ввода команд к БД, вводим в неё данный блок (копи-пастом - нажатием правой кнопки мыши в консоли, но это не точно... ):

use admin
db.createUser(
  {
    user: "admin",
    pwd: "abc123!",
    roles: [ { role: "root", db: "admin" } ]
  }
);
exit;

Останавливаем сервер БД и дополняем конфиг C:\Dev\Data.Config\mongod.cfg, вот как выглядит новый конфиг:

systemLog:
  destination: file
  path: "C:\\tools\\mongodb\\db\\log\\mongo.log"
  logAppend: true
storage:
  dbPath: "C:\\tools\\mongodb\\db\\data"
security:
  authorization: enabled

С этого момента мы подключимся к MongoDb, используя пользователя admin. Существует хорошая практика не использовать роль суперпользователя (в нашем случае администратора) для обычных операций, но для того, чтобы все было просто, у нас будет только один пользователь.

Класс модели

Здесь мы определяем из чего в сущности состоят заметки.

using System;
using MongoDB.Bson.Serialization.Attributes;

namespace NotebookAppApi.Model
{
    public class Note
    {
        [BsonId]
        // standard BSonId generated by MongoDb
        public ObjectId InternalId { get; set; }                

        // external ID or key, which may be easier to reference (ex: 1,2,3 etc.)
        public string Id { get; set; }                           

        public string Body { get; set; } = string.Empty;
        public DateTime UpdatedOn { get; set; } = DateTime.Now;
        public DateTime CreatedOn { get; set; } = DateTime.Now;
        public int UserId { get; set; } = 0;
    }
}

Определяем DB context

Чтобы сохранить функции доступа к базе данных в отдельном месте, мы добавим класс NoteContext. Он будет использовать настройки, определенные выше.

public class NoteContext
{
    private readonly IMongoDatabase _database = null;

    public NoteContext(IOptions<Settings> settings)
    {
        var client = new MongoClient(settings.Value.ConnectionString);
        if (client != null)
            _database = client.GetDatabase(settings.Value.Database);
    }

    public IMongoCollection<Note> Notes
    {
        get
        {
            return _database.GetCollection<Note>("Note");
        }
    }
}

Добавляем менеджер заметок

Назовём его репозиторием. В сущности это интерфейс и класс, который реализует его. Он непосредственно работает с БД, но в его задачу не входит обработка HTTP-запросов.

public interface INoteRepository
{
    Task<IEnumerable<Note>> GetAllNotes();
    Task<Note> GetNote(string id);

    // add new note document
    Task AddNote(Note item);

    // remove a single document / note
    Task<bool> RemoveNote(string id);

    // update just a single document / note
    Task<bool> UpdateNote(string id, string body);

    // demo interface - full document update
    Task<bool> UpdateNoteDocument(string id, string body);

    // should be used with high cautious, only in relation with demo setup
    Task<bool> RemoveAllNotes();
}

Доступ к БД асинхронный, вот класс, определяющий асинхронные запросы для управления (получить, добавить, изменить, удалить) заметками:

public class NoteRepository : INoteRepository
{
    private readonly NoteContext _context = null;

    public NoteRepository(IOptions<Settings> settings)
    {
        _context = new NoteContext(settings);
    }

    public async Task<IEnumerable<Note>> GetAllNotes()
    {
        try
        {
            return await _context.Notes
                    .Find(_ => true).ToListAsync();
        }
        catch (Exception ex)
        {
            // log or manage the exception
            throw ex;
        }
    }

    // query after Id or InternalId (BSonId value)
    //
    public async Task<Note> GetNote(string id)
    {
        try
        {
            ObjectId internalId = GetInternalId(id);
            return await _context.Notes
                            .Find(note => note.Id == id 
                                    || note.InternalId == internalId)
                            .FirstOrDefaultAsync();
        }
        catch (Exception ex)
        {
            // log or manage the exception
            throw ex;
        }
    }

    private ObjectId GetInternalId(string id)
    {
        ObjectId internalId;
        if (!ObjectId.TryParse(id, out internalId))
            internalId = ObjectId.Empty;

        return internalId;
    }
    
    public async Task AddNote(Note item)
    {
        try
        {
            await _context.Notes.InsertOneAsync(item);
        }
        catch (Exception ex)
        {
            // log or manage the exception
            throw ex;
        }
    }

    public async Task<bool> RemoveNote(string id)
    {
        try
        {
            DeleteResult actionResult 
                = await _context.Notes.DeleteOneAsync(
                    Builders<Note>.Filter.Eq("Id", id));

            return actionResult.IsAcknowledged 
                && actionResult.DeletedCount > 0;
        }
        catch (Exception ex)
        {
            // log or manage the exception
            throw ex;
        }
    }

    public async Task<bool> UpdateNote(string id, string body)
    {
        var filter = Builders<Note>.Filter.Eq(s => s.Id, id);
        var update = Builders<Note>.Update
                        .Set(s => s.Body, body)
                        .CurrentDate(s => s.UpdatedOn);

        try
        {
            UpdateResult actionResult 
                = await _context.Notes.UpdateOneAsync(filter, update);

            return actionResult.IsAcknowledged
                && actionResult.ModifiedCount > 0;
        }
        catch (Exception ex)
        {
            // log or manage the exception
            throw ex;
        }
    }

    public async Task<bool> UpdateNote(string id, Note item)
    {
        try
        {
            ReplaceOneResult actionResult 
                = await _context.Notes
                                .ReplaceOneAsync(n => n.Id.Equals(id)
                                        , item
                                        , new UpdateOptions { IsUpsert = true });
            return actionResult.IsAcknowledged
                && actionResult.ModifiedCount > 0;
        }
        catch (Exception ex)
        {
            // log or manage the exception
            throw ex;
        }
    }

    public async Task<bool> RemoveAllNotes()
    {
        try
        {
            DeleteResult actionResult 
                = await _context.Notes.DeleteManyAsync(new BsonDocument());

            return actionResult.IsAcknowledged
                && actionResult.DeletedCount > 0;
        }
        catch (Exception ex)
        {
            // log or manage the exception
            throw ex;
        }
    }
}

Чтобы получить доступ к описанному классу с использованием модели Внедрения зависимостей, добавим новую строку в ConfigureServices:

services.AddTransient<INoteRepository, NoteRepository>();

Transient: Создается каждый раз (используем этот вариант). Scoped: Создано только один раз за запрос. Singleton: Создано в первый раз, когда они запрашиваются. Каждый последующий запрос использует экземпляр, который был создан в первый раз.

Добавляем главный контроллер

Он предоставляет все интерфейсы обработки запросов, доступные для внешних приложений (есть ещё один запрос, который не доступен в API для приложений, им занимается Системный контроллер). В действиях Get есть директива NoCache, чтобы гарантировать, что веб-клиенты всегда будут запрашивать только сервер.

[Produces("application/json")]
[Route("api/[controller]")]
public class NotesController : Controller
{
    private readonly INoteRepository _noteRepository;

    public NotesController(INoteRepository noteRepository)
    {
        _noteRepository = noteRepository;
    }

    [NoCache]
    [HttpGet]
    public async Task<IEnumerable<Note>> Get()
    {
        return await _noteRepository.GetAllNotes();
    }

    // GET api/notes/5 - retrieves a specific note using either Id or InternalId (BSonId)
    [HttpGet("{id}")]
    public async Task<Note> Get(string id)
    {
        return await _noteRepository.GetNote(id) ?? new Note();
    }

    // POST api/notes - creates a new note
    [HttpPost]
    public void Post([FromBody] NoteParam newNote)
    {
        _noteRepository.AddNote(new Note
                                    {
                                        Id = newNote.Id,
                                        Body = newNote.Body,
                                        CreatedOn = DateTime.Now,
                                        UpdatedOn = DateTime.Now,
                                        UserId = newNote.UserId
                                    });
    }

    // PUT api/notes/5 - updates a specific note
    [HttpPut("{id}")]
    public void Put(string id, [FromBody]string value)
    {
        _noteRepository.UpdateNoteDocument(id, value);
    }

    // DELETE api/notes/5 - deletes a specific note
    [HttpDelete("{id}")]
    public void Delete(string id)
    {
        _noteRepository.RemoveNote(id);
    }
}

Добавляем системный контроллер

Это будет контроллер, предназначенный для административных задач (мы используем для инициализации базы данных с помощью некоторых фиктивных данных). В реальных проектах мы должны очень осторожно использовать такой интерфейс. Для разработки только и быстрого тестирования этот подход может быть удобным.

[Route("api/[controller]")]
public class SystemController : Controller
{
    private readonly INoteRepository _noteRepository;

    public SystemController(INoteRepository noteRepository)
    {
        _noteRepository = noteRepository;
    }

    // Call an initialization - api/system/init
    [HttpGet("{setting}")]
    public string Get(string setting)
    {
        if (setting == "init")
        {
            _noteRepository.RemoveAllNotes();
            _noteRepository.AddNote(new Note() 
                        { Id = "1", Body = "Test note 1", 
                          CreatedOn = DateTime.Now, 
                          UpdatedOn = DateTime.Now, UserId = 1 });
            _noteRepository.AddNote(new Note() { Id = "2", 
                          Body = "Test note 2", 
                          CreatedOn = DateTime.Now, 
                          UpdatedOn = DateTime.Now, UserId = 1 });
            _noteRepository.AddNote(new Note() { Id = "3", 
                          Body = "Test note 3", 
                          CreatedOn = DateTime.Now, 
                          UpdatedOn = DateTime.Now, UserId = 2 });
            _noteRepository.AddNote(new Note() { Id = "4", 
                          Body = "Test note 4", 
                          CreatedOn = DateTime.Now, 
                          UpdatedOn = DateTime.Now, UserId = 2 });

            return "Done";
        }

        return "Unknown";
    }
}

Чтобы использовать его, мы просто добавим URL-адрес в браузер. Запустив приведенный ниже код, будет автоматически создана полная настройка (например, новая база данных, новая коллекция, образцы записей):

http://localhost:5000/api/system/init (сервер Kestrel)
http://localhost:53617/api/system/init(сервер IIS - наш слушай).

Настраиваем стартовые параметры

Обновляем файл launchSettings.json.

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:53617/",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/notes",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "NotebookAppApi": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "http://localhost:5000/api/notes",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Запуск

Теперь проект можно запустить (убедитесь, что БД запущена). Далее переходим по следующим ссылкам:

  1. http://localhost:53617/api/system/init (заполнение БД).
  2. http://localhost:53617/api/notes (запуск API).

Демонстрация работы приложения

Вспомогательные программы: Postman - симулятор запросов (не обязательно иметь работоспособный клиент, чтобы проверить сервер), MongoDB Compass - проводник БД Mongo.

Вывод по работе

Таким образом, было создано приложение, демонстрирующее функционирование MongoDB на ASP.NET Core. Фактически это костяк любой программы, работающей с Mongo на ASP и статью можно использовать не только для быстрой установки и настройки Mongo, но и как шаблон ASP-проекта.

В статье реализовано:

  • указаны преимущества MongoDB;
  • даны инструкции по установке и настройке MongoDB в систему и ASP.NET;
  • приведён алгоритм написания приложения-демонстратора БД на Mongo.

Примечания

  1. Введение в MongoDB: Что такое MongoDB. // Metanit: сайт. URL: https://metanit.com/nosql/mongodb/1.1.php (дата обращения: 18.05.18)
  2. Введение в MongoDB: Что такое MongoDB. // Metanit: сайт. URL: https://metanit.com/nosql/mongodb/1.1.php (дата обращения: 18.05.18)


Ссылки

  1. Using MongoDB .NET Driver with .NET Core WebAPI // Quality App Design: блог. URL: http://www.qappdesign.com/using-mongodb-with-net-core-webapi/ (дата обращения: 18.05.18)
  2. Install MongoDB // Docs mongoDB: сайт. URL: https://docs.mongodb.com/manual/installation/ (дата обращения: 18.05.18)