Микросервисная реализация объектно-ориентированных баз данных

от автора

В настоящее время объектно-ориентированные базы данных (ООБД) не имеют достаточно большого распространения в повседневном использовании, да и более того, не настолько популярны как реляционные базы данных, которые не один десяток лет уже активно поддерживаются различными сообществами и имеют долгую историю применения.

В данной статье рассматривается реализация ООБД в контексте разработки системы, состоящей из микросервисов, на примере Perst и Db4o. Также будет рассмотрена отдельная реализация с документно-ориентированной базой данных MongoDB, работа с которой имеет много общего с ООБД.

Целью данной статьи является рассмотрение практического применения ООБД и решения проблем совместимости с помощью микросервисной архитектуры.

Введение

При написании данной статьи я руководствовался личным опытом разработки микросервисов и работы с ООБД на языке C#, с помощью которого будут продемонстрированы все примеры. Некоторые темы связанные с микросервисной архитектурой включая декомпозицию бизнес логики системы, предметно-ориентированное проектирование и решение проблематики согласованности данных между сервисами рассмотрены не будут — рассмотрение данных тем читателю предлагается осуществить самостоятельно.

В разработанной системе будет представлено три сервиса, каждый из которых взаимодействует со своей базой данных и реализует основные CRUD-операции с коллекциями объектов. Также, каждый сервис по своему справляется с решением проблемы внутреннего и внешнего представления данных.

В данной работе представлен так называемый «распределённый монолит», поскольку все единицы модульности (сервисы) зависят друг от друга.

Под внутренним представлением данных я понимаю множество моделей, которые используются с конкретной ООБД. Моделью в рамках текущей статьи, будем считать класс, который содержит атрибуты, несущие смысловую нагрузку в рамках конкретной коллекции объектов.

Пример модели
namespace oodb_project.models {     /// <summary>     /// Модель, характеризующая источник данных для сервиса     /// </summary>     public class DataSourceModel : IdModel     {         public DataSourceModel() : base()         {         }          public DataSourceModel(string? id, string? name, string? url) : base(id)         {             Name = name;             Url = url;         }          /// <summary>         /// Имя ресурса         /// </summary>         public string? Name { get; set; }          /// <summary>         /// Ссылка на ресурс         /// </summary>         public string? Url { get; set; }     } }

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

Кто определяет общий формат выходных данных

Общий формат выходных данных для каждого сервиса определяет главный сервис — oodb-main-server. Выходные данные — это результат выполнения запроса конкретным сервисом (oodb-mongo-server или oodb-perst-server).

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

Данная статья не является полноценным руководством по использованию таких баз данных как MongoDB, Perst и db4o. По большей части в данной статье происходит описание уже реализованного функционала, в котором происходит работа со всеми перечисленными базами данных.

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

Предполагается, что при настройке MongoDB, подключение Perst и db4o к каждому конкретному сервису согласно рекомендациям, перечисленным в данной статье читатель сможет самостоятельно воспроизвести результаты данной работы (запустить систему).

Описание предметной области

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

При достижении определенного этапа разработки системы появляется задача развёртывания элементов (или модулей) на удалённые хосты, чтобы пользователи могли работать с каждым из них (как правило, этот момент скрывается за слоем реализации). 

Отмечу, что в данный момент существует большое множество инструментов, которые помогают решить большинство проблем связанных с непрерывной доставкой, интеграцией, автоматическим тестированием и мониторингом удалённых хостов. Например, CI/CD, Zabbix, Docker, Kubernetes, Shef, Ansible, NixOS и многие другие.

Однако, будем полагать, что стоит задача разработать новую систему решающие те же проблемы, что и известные аналоги. Для этой системы нужно спроектировать базу данных и разработать сервисы.

Проектирование модели базы данных

Спроектированная модель базы данных будет использоваться на каждом сервисе. В некотором смысле данная модель будет «идеальной» моделью, к которой каждая база данных на сервисах будет стремиться.

Начнём с описания сущностей, которые в данной системе будут активно использоваться.

Основными сущностями, не считая связей между ними, будут следующие:

  1. Хосты (hosts) — это конкретные сервера, на которых работают какие-либо приложения (сервисы).

  2. Сервисы (services) — это конкретное приложение, которое может быть запущено на любом хосту.

  3. Ресурсы (data_sources) — это ссылки на ресурсы, которые используются сервисом для осуществления своей работы (например, ссылка на GitHub-репозиторий).

  4. Админы (admins) — это администратор системы, которому доступен просмотр технических характеристик хоста (мониторинг хоста).

  5. Журнал мониторинга (monitor_apps) — это журнал, в котором фиксируется каждый конкретный администратор и закреплённый за ним хост, чтобы каждый админ мог мониторить только ему доступные хосты.

Каждая сущность определяется своим набором атрибутов, которые позволяют описать её как объект в контексте объектно-ориентированного подхода. Атрибуты каждой отдельной сущности и отношения между всеми сущностями представлены на рисунке 1.

Рисунок 1 - Модель базы данных

В модели базы данных обозначены атрибуты для каждой сущности, с которыми можно ознакомиться в таблицах.

Дополнительно схему базы данных сопровождают отношения между сущностями, количество которых достаточно для осуществления разнопланового тестирования системы: от реализации простых CRUD-операций, до реализации каскадного удаления.

Проектирование архитектуры системы

Что такое микросервисная архитектура? Микросервисная архитектура — это стиль проектирования, который разбивает систему на отдельные сервисы с разными функциями.

Единицей модульности в данном стиле проектирования выступает сервис.

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

В случае данной системы сервисы обладают разными базами данных, которые воспроизводят одну и ту же реляционную схему. Всего в системе три сервиса.

Схема спроектированной архитектуры представлена на рисунке 2.

Рисунок 2 - Архитектура системы

Из рисунка 2 можно сделать вывод, что в дополнении к трём сервисам в системе были добавлены desktop-клиентское приложение и два контейнера, один из которых для работы с базой данных, а другой — для работы с MongoDB через веб-клиент.

Кстати, на рисунке изображены также пояснения к технологиям, которые были использованы в текущей системе.

Вообще, микросервисная архитектура хороша тем, что каждая отдельная единица модульности может быть разработана с использованием разных технологий — от Node.js, Gin-Gonic, Nest.js до Spring Boot, Laravel и т.д. Я считаю это одним из достоинств данной архитектуры.

В рамках текущей статьи я рассчитываю раскрыть читателю практическую пользу данного достоинства избранной архитектуры при решении одной не совсем очевидной проблемы.

Описание проблемы, которая решается с помощью данной архитектуры

При разработке сервиса для работы с базой данных Perst хотелось бы использовать ASP Net Core и более новую версию C#, т.к. технологии развиваются и не хочется «отставать» от общего потока развития. Однако, всё не так просто. Если сервисы для работы с MongoDB и db4o были разработаны с помощью .NET 6 и ASP NET Core, то с этим сервисом пришлось сделать «откат» технологий до .NET Framework 4.8 и использовать несколько иные подходы к проектированию работы приложения, потому что Perst не работает с новыми версиями .NET (не поддерживает). В качестве доказательства существования этой проблемы предлагаю читателю самостоятельно попробовать подключить Perst к более новым версиям .NET.

Разработка сервиса для работы с MongoDB (oodb-mongo-server)

Настройка базы данных

Описание практической разработки системы будет начато с сервиса, который взаимодействует с документно-ориентированной базой данных. Будем двигаться «сверху вниз» согласно архитектуре, представленной на рисунке 2.

В начале необходимо развернуть базу данных MongoDB в контейнере, для взаимодействия с ней. Это необходимо, чтобы локально работать с данной базой без дополнительных инсталляций.

Я использовал docker-compose для развёртывания контейнера mongo, а также mongo-express для возможности использовать веб-интерфейс, взаимодействующей с MongoDB.

Следующие инструкции в docker-compose настраивают автоматическое локальное развёртывание описанных выше контейнеров:

version: '3.1'  services:    mongo:     image: mongo     restart: always     ports:       - 27017:27017     environment:       MONGO_INITDB_ROOT_USERNAME: root       MONGO_INITDB_ROOT_PASSWORD: example    mongo-express:     image: mongo-express     restart: always     ports:       - 8081:8081     environment:       ME_CONFIG_MONGODB_ADMINUSERNAME: root       ME_CONFIG_MONGODB_ADMINPASSWORD: example       ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/     depends_on:       - mongo

Здесь используется версия 3.1, описание состоит из двух сервисов: mongo и mongo-express.

В image указан образ, из которого будет собран контейнер. Если не указывать версию образа он будет загружен из Docker Hub согласно последней версии (она же является версией по-умолчанию).

Каждый из сервисов автоматически перезапускается, что обозначается как restart: always

В environment определяются значения конфигурации для подключения к базе данных (mongo-express) и инициализация паролем и логином базы данных для взаимодействия с ней (mongo).

Порты указаны в ports, а чтобы mongo-express запустился после mongo и был зависим от данного контейнера был указан depends_on в mongo-express.

Для запуска контейнеров, описанных в docker-compose, необходимо их сначала собрать используя команду docker-compose build , а затем запустить командой docker-compose up .

Рисунок 3 - Сборка контейнеров с помощью docker-compose

При запуске всех контейнеров они должны отображаться в списке контейнеров. Для этого удобно использовать команду docker container ps -a

Рисунок 4 - Активные контейнеры

После того, как контейнеры были развёрнуты следует проверить подключение к MongoDB с помощью веб-интерфейса. В случае текущих настроек, которые были определены в docker-compose, путь к веб-интерфейсу определён по адресу http://localhost:8081

Рисунок 5 - Веб-интерфейс для работы с MongoDB

При работе с веб-интерфейсом необходимо создать базу данных oodb (я это уже сделал), и в данной базе данных создать следующие коллекции: AdminList, DataSourceList, HostList, HostServiceList, MonitorAppList, ServiceList.

И при такой настройке MongoDB исходный код, который будет представлен в конце статьи, будет работать со средой тех читателей, которые решат попробовать запустить исходный код.

Рисунок 6 - Завершающий этап настройки MongoDB

Приступим непосредственно к разработке сервиса взаимодействия с MongoDB.

Разработка функционала

Файловая структура сервиса

Для начала стоит ознакомиться с получившейся структурой сервиса. Она представлена на рисунке 7.

Рисунок 7 - Структура проекта oodb-mongo-server

Точкой входа в сервис является файл Program.cs

В constants расположены константы API-маршрутов, по которым с этим сервисом можно взаимодействовать по HTTP.

В controllers расположены основные контроллеры, которые реализуют CRUD-операции для каждой отдельной коллекции объектов.

В database находятся папки config и context, которые предназначены для конфигурирования подключения к MongoDB и получению контекста этой базы данных.

В models находятся все модели. Отмечу, что с моделями будет одна интересная особенность — входные данные (в базу данных) и выходные (ответ от сервера) будут отличаться, в силу особенностей коммуникации между главным сервисом и текущим.

В utils располагаются утилиты. В данном сервисе это в основном утилита для работы с рефлексией.

Описание конфигурации MongoDB и её контекста

Файл определяющий конфигурацию MongoDB выглядит следующим образом:

namespace oodb_mongo_server.database.config {     /// <summary>     /// Класс, реализующий интерфейс конфигурации MongoDB     /// </summary>     public class MongoDbConfig : IMongoDbConfig     {         /// <summary>         /// Конструктор с параметром, принимающий в качестве значения интерфейс конфигурации         /// </summary>         /// <param name="config"></param>         public MongoDbConfig(IConfiguration? config)         {             if (config != null)             {                 Database = config["MongoDB:Database"];                 Port = int.Parse(config["MongoDB:Port"]);                 Host = config["MongoDB:Host"];                 User = config["MongoDB:User"];                 Password = config["MongoDB:Password"];             }         }          /// <summary>         /// Название базы данных         /// </summary>         public string? Database { get; set; }          /// <summary>         /// Хост         /// </summary>         public string? Host { get; set; }          /// <summary>         /// Порт, по которому происходит подключение к базе данных         /// </summary>         public int? Port { get; set; }          /// <summary>         /// Пользователь         /// </summary>         public string? User { get; set; }          /// <summary>         /// Пароль         /// </summary>         public string? Password { get; set; }          /// <summary>         /// Строка подключения (генерируется исхода из других атрибутов)         /// </summary>         public string? ConnectionString         {             get             {                 if (string.IsNullOrEmpty(User) || string.IsNullOrEmpty(Password))                 {                     return $@"mongodb://{Host}:{Port}";                 }                  return $@"mongodb://{User}:{Password}@{Host}:{Port}";             }         }     } }

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

Контекст, в рамках которого будут реализованы CRUD-операции, выглядит следующим образом:

namespace oodb_mongo_server.database.context {     /// <summary>     /// Класс, реализующий интерфейс контекста MongoDB     /// </summary>     public class DbContext : IDbContext     {         /// <summary>         /// База данных         /// </summary>         private readonly IMongoDatabase _db;          /// <summary>         /// Конструктор с параметром         /// </summary>         /// <param name="config">Конфигурация</param>         public DbContext(IMongoDbConfig config)         {             // Создание нового клиента с передачей строки подключения             var client = new MongoClient(config.ConnectionString);              // Получение доступа к конкретной базе данных             _db = client.GetDatabase(config.Database);         }          /// <summary>         /// Коллекция объектов AdminList         /// </summary>         public IMongoCollection<AdminModel>? AdminList => _db.GetCollection<AdminModel>("AdminList");          /// <summary>         /// Коллекция объектов DataSourceList         /// </summary>         public IMongoCollection<DataSourceModel>? DataSourceList => _db.GetCollection<DataSourceModel>("DataSourceList");          /// <summary>         /// Коллекция объектов HostList         /// </summary>         public IMongoCollection<HostModel>? HostList => _db.GetCollection<HostModel>("HostList");          /// <summary>         /// Коллекция объектов HostServiceList         /// </summary>         public IMongoCollection<HostServiceModel>? HostServiceList => _db.GetCollection<HostServiceModel>("HostServiceList");          /// <summary>         /// Коллекция объектов MonitorAppList         /// </summary>         public IMongoCollection<MonitorAppModel>? MonitorAppList => _db.GetCollection<MonitorAppModel>("MonitorAppList");          /// <summary>         /// Коллекция объектов ServiceList         /// </summary>         public IMongoCollection<ServiceModel>? ServiceList => _db.GetCollection<ServiceModel>("ServiceList");     } }

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

Описание точки входа в сервис

В точке входа в сервис определены все основные его настройки, подключение к MongoDB, инициализация контроллеров и запуск сервиса.

/*  * Точка входа в сервис ooodb-mongo-server  * **/  using MongoDB.Driver; using oodb_mongo_server.database.config; using oodb_mongo_server.database.context; using oodb_project.controllers;  // Создание приложения var builder = WebApplication.CreateBuilder(args); var app = builder.Build();  // Создание экземпляра MongoClient (подключение к БД) MongoClient client = new MongoClient("mongodb://root:example@localhost:27017");  // Выпод коллекций из базы данных (проверка работы подключения) using (var cursor = await client.ListDatabasesAsync()) {     var databases = cursor.ToList();     foreach (var database in databases)     {         Console.WriteLine(database);     } }  // Получение контекста базы данных на основе собранной конфигурации var context = new DbContext(new MongoDbConfig(null) {     Database = "oodb",     Host = "localhost",     Password = "example",     User = "root",     Port = 27017 });  // Инициализация маршрутов API сервиса var initMongoController = new InitMongoController(app, context); initMongoController.InitRoutes();  // Запуск сервиса app.Run(); 

Далее будет рассмотрено устройство контроллеров, которые позволяют реализовать бизнес-логику CRUD-операций.

Устройство контроллеров сервиса

Каждый контроллер является наследником абстрактного класса BaseController, который был создан с целью обобщить выполнение некоторых операций, в числе которых Create, Read, Read All, и Delete (Update не автоматизирована в силу сложности и оригинальности исполнения инструкций при данной операции).

Приведу пример обобщения функции для вывода всех объектов в коллекции:

        /// <summary>         /// Метод для получения всех объектов определённого типа         /// </summary>         /// <returns>Результат работы функции (массив документов)</returns>         public IResult GetAll()         {             // Проверка подключения к базе данных             if (_collection == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Поиск всех элементов в коллекции определённого типа (входной тип)                 var list = _collection.Find(Builders<IT>.Filter.Empty).ToList();                  // Результат представлен в отдельной структуре, т.к. данные нужно преобразовать перед отправкой пользователю                 var result = new List<OT>();                  foreach (var item in list)                 {                     // Получение всех полей элемента входного типа (IT)                     var fields = ReflectionUtil.getFields(item);                      // Процедура замены id входной модели (ObjectId) на id выходной модели (String)                     var id = fields[0].ToString();                     fields.RemoveAt(0);                     if(id != null)                     {                         fields.Insert(0, id);                     }                      // Создание нового объекта выходного типа OT с передачей в его конструктор определённых полей                     var value = Activator.CreateInstance(typeof(OT), fields.ToArray());                      // Если value не равен null, то добавляем его в список результатов                     if(value != null)                     {                         result.Add((OT)value);                     }                 }                  // Возвращаем преобразованный массив результатов                 return Results.Json(result.ToArray());             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }         }
Полный код абстрактного класса
using oodb_mongo_server.models; using MongoDB.Driver; using oodb_mongo_server.models.data; using oodb_project.models; using oodb_mongo_server.utils; using MongoDB.Bson; using System.Reflection;   /*  * В данном файле определён абстрактный класс BaseController, который  * использует механизм обобщений.  * Для данного класса необходимо указать тип для входных параметров и выходных параметров  * Абстрактный класс определяет методы Get, GetAll и Create  * **/  namespace oodb_mongo_server.controllers {     /// <summary>     /// Абстрактный класс для контроллеров     /// </summary>     public abstract class BaseController<IT, OT>          where IT : IdModel                          // Тип для входных параметров         where OT : IdDataModel, new()               // Тип для выходных параметров     {         // Коллекция входных параметров         protected IMongoCollection<IT>? _collection;          /// <summary>         /// Конструктор абстрактного класса         /// </summary>         /// <param name="collection">Ссылка на коллекцию</param>         public BaseController(IMongoCollection<IT>? collection)         {             _collection = collection;         }          /// <summary>         /// Метод для создания нового объекта коллекции         /// </summary>         /// <param name="data">Данные объекта коллекции</param>         /// <returns>Созданный объект коллекции</returns>         protected IResult Create(OT data)         {             // Проверка подключения к базе данных             if (_collection == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Получение всех полей выходного объекта                 var fields = ReflectionUtil.getDataFields(data);                  // Получение информации о всех полях                 var fieldsInfo = ReflectionUtil.getDataFieldsInfo(data);                  if(fields.Count > fieldsInfo.Count)                 {                     // Удаляем id в модели                     fields.RemoveAt(0);                 }                  // Создаём объект входного типа                 var value = Activator.CreateInstance(typeof(IT), fields.ToArray());                 if (value != null)                 {                      // Добавление объекта в коллекцию                     _collection.InsertOne((IT)value);                      var fieldsValue = ReflectionUtil.getFields((IT)value);                      // Процедура замены id входной модели (ObjectId) на id выходной модели (String)                     var id = fieldsValue[0].ToString();                     fieldsValue.RemoveAt(0);                     if (id != null)                     {                         fieldsValue.Insert(0, id);                     }                      var result = Activator.CreateInstance(typeof(OT), fieldsValue.ToArray());                      // Возвращаем результат поиска, если при создании объекта не получилось значение null                     return Results.Json(result);                 }                  // Возвращаем ошибку                 return Results.Json(new MessageModel("Ошибка: невозможно создать объект для коллекции"));             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }         }          /// <summary>         /// Метод для получения всех объектов определённого типа         /// </summary>         /// <returns>Результат работы функции (массив документов)</returns>         public IResult GetAll()         {             // Проверка подключения к базе данных             if (_collection == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Поиск всех элементов в коллекции определённого типа (входной тип)                 var list = _collection.Find(Builders<IT>.Filter.Empty).ToList();                  // Результат представлен в отдельной структуре, т.к. данные нужно преобразовать перед отправкой пользователю                 var result = new List<OT>();                  foreach (var item in list)                 {                     // Получение всех полей элемента входного типа (IT)                     var fields = ReflectionUtil.getFields(item);                      // Процедура замены id входной модели (ObjectId) на id выходной модели (String)                     var id = fields[0].ToString();                     fields.RemoveAt(0);                     if(id != null)                     {                         fields.Insert(0, id);                     }                      // Создание нового объекта выходного типа OT с передачей в его конструктор определённых полей                     var value = Activator.CreateInstance(typeof(OT), fields.ToArray());                      // Если value не равен null, то добавляем его в список результатов                     if(value != null)                     {                         result.Add((OT)value);                     }                 }                  // Возвращаем преобразованный массив результатов                 return Results.Json(result.ToArray());             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }         }          /// <summary>         /// Метод для получения конкретного объекта из коллекции         /// </summary>         /// <param name="id">Идентификатор искомого объекта</param>         /// <returns>Результат работы метода (конкретный объект)</returns>         public IResult Get(string id)         {             if (_collection == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Поиск конкретного документа по его идентификатору в коллекции                 var data = _collection.Find(document => document.Id == ObjectId.Parse(id)).FirstOrDefault();                  // Проверка обнаружения объекта в коллекции (при отсутствии возвращаем ошибку)                 if (data == null)                 {                     return Results.Json(new MessageModel($"Экземпляр объекта {typeof(IT).Name} с Id = {id} не обнаружен в БД"));                 }                  // Преобразование объекта входного типа в объект выходного типа                 var fields = ReflectionUtil.getFields(data);                 fields.RemoveAt(0);                 if (id != null)                 {                     fields.Insert(0, id);                 }                  // Создаём объект выходного типа                 var value = Activator.CreateInstance(typeof(OT), fields.ToArray());                 if (value != null)                 {                     // Возвращаем результат поиска, если при создании объекта не получилось значение null                     return Results.Json((OT)value);                 }                  return Results.Json("Internal Server Error");             }             catch (Exception)             {                 return Results.Json(new MessageModel($"Экземпляра объекта {typeof(IT).Name} с Id = {id} не обнаружен в БД"));             }         }          /// <summary>         /// Метод для удаления объекта из коллекции         /// </summary>         /// <param name="id">Идентификатор объекта</param>         /// <returns>Удалённый объект коллекции</returns>         public IResult Delete(string id)         {             if (_collection == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 var data = _collection.Find(document => document.Id == ObjectId.Parse(id)).FirstOrDefault();                 if (data == null)                 {                     return Results.Json(new MessageModel($"Экземпляра объекта {typeof(IT).Name} с Id = {id} не обнаружен в БД"));                 }                  // Удаление объекта из коллекции                 _collection.DeleteOne(x => x.Id == data.Id);                  var fields = ReflectionUtil.getFields(data);                 fields.RemoveAt(0);                 if (id != null)                 {                     fields.Insert(0, id);                 }                  var value = Activator.CreateInstance(typeof(OT), fields.ToArray());                 if (value != null)                 {                     return Results.Json((OT)value);                 }                  return Results.Json("Internal Server Error");             }             catch (Exception)             {                 return Results.Json(new MessageModel($"Экземпляра объекта {typeof(IT).Name} с Id = {id} не обнаружен в БД"));             }         }     } } 

Данный абстрактный класс позволяет обобщить выполнение некоторых операций в контроллерах, но не всех. Для каждого контроллера может быть использованы либо все методы из абстрактного класса, либо ни одного — всё индивидуально.

Можно заменить, что при получении всех значений атрибутов входного типа (IT) в методе осуществляется удаление Id. Дело в том, что для модели типа IT все модели имеют id с типом ObjectId (необходимый для MongoDB), и для конвертации его в выходные модели (тип OT) необходимо удалить значение первого поля, добавить в него id конвертированный в строку (ObjectId -> ToString()) и создать на основе значений этих атрибутов экземпляр объекта типа OT, через Activator.CreateInstance() (конструктор будет найден подходящий, т.к. id теперь не типа ObjectId, а типа string). Автоматизация процесса конвертации моделей из одного типа в другой является одной из целей применения механизма рефлексии в абстрактном классе.

Обобщающий класс использует утилиту для рефлексии. Далее я приведу пример функции для получения значения полей типа:

        /// <summary>         /// Получение значений всех полей объекта определённого типа (базовый тип - IdModel)         /// </summary>         /// <typeparam name="T">Тип целевого объекта</typeparam>         /// <param name="element">Целевой объект</param>         /// <returns>Список значений полей целевого объекта</returns>         public static List<object> getFields<T>(T element) where T : IdModel         {             List<object> fields = new List<object>();              // Определение флагов, по которым будет осуществляться поиск полей целевого объекта             BindingFlags bindingFlags = BindingFlags.Public |                     BindingFlags.NonPublic |                     BindingFlags.Instance |                     BindingFlags.Static;              // Если element.Id не равен null, то добавляем его значение список fields             if (element.Id != null)             {                 fields.Add(element.Id);             }              // Проходим по всем атрибутам экземпляра типа данных T и добавляем значение каждого атрибута в fields             foreach (FieldInfo field in element.GetType().GetFields(bindingFlags))             {                 var value = field.GetValue(element);                                  if(value != null)                 {                     fields.Add(value);                 }             }              return fields;         }

Данный код просто реализует функцию получения всех значений из полей экземпляра объекта типа T, который по своей сути ограничивается абстрактным классом с помощью where: where T : IdModel

Это сделано намерено. Здесь следует уточнить, с какой целью это было сделано.

Все входные и выходные модели — разные, поскольку модели связанные с объектами в коллекциях MongoDB должны иметь обязательный параметр ObjectId, который при конвертации в JSON-формат сгенерирует отдельную JSON-структуру, которая основным сервисом не определена. В данном случае основной сервис (oodb-main-server) определяет какого должны быть формата выходные данные.

namespace oodb_mongo_server.models {     /// <summary>     /// Абстрактный класс модели входных данных.     /// Используется для обобщения типов входных данных используемых внутри сервиса     /// </summary>     public abstract class IdModel     {         public IdModel()         {             Id = ObjectId.GenerateNewId();         }          public IdModel(ObjectId? id)         {             Id = id;         }          [BsonId]         public ObjectId? Id { get; set; }     } }

Модель IdModel определяет ObjectId, которое необходимо для каждого объекта в коллекции MongoDB.

Пример наследования от данной модели представлен ниже:

namespace oodb_project.models {     /// <summary>     /// Модель, характеризующая администратора приложения для мониторинга     /// </summary>     public class AdminModel : IdModel     {         public AdminModel() : base() {}          public AdminModel(ObjectId? id, string? email) : base(id)         {             Email = email;         }          public AdminModel(string? email) : base()         {             Email = email;         }          /// <summary>         /// Почтовый адрес пользователя         /// </summary>         public string? Email { get; set; }     } }

Модель AdminModel наследуется от IdModel, что позволяет использовать экземпляры данной модели при работе с механизмом рефлексии или абстрактным классом. Все модели используемые для работы с MongoDB наследуются таким образом.

Далее я опишу операцию обновления в контроллере AdminController

        /// <summary>         /// Обновление объекта в коллекции Admin         /// </summary>         /// <param name="data">Новые данные для объекта</param>         /// <returns>Данные обновлённого объекта</returns>         public IResult Update(AdminDataModel data)         {             if (_collection == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Поиск конкретного объекта                 var admin = _collection.Find(document => document.Id == ObjectId.Parse(data.Id)).FirstOrDefault();                 if (admin == null)                 {                     return Results.Json(new MessageModel($"Экземпляра объекта AdminModel с Id = {data.Id} не обнаружен в БД"));                 }                  // Создание фильтра для поиска объекта в коллекции                 var filter = Builders<AdminModel>.Filter.Eq(s => s.Id, ObjectId.Parse(data.Id));                  // Создаём определение для обновления объекта в коллекции                 var update = Builders<AdminModel>.Update.Set(s => s.Email, data.Email);                  // Обновляем объект в коллекции по определённому фильтру                 _collection.UpdateOne(filter, update);             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }              return Results.Json(data);         }
Полный код контроллера
using MongoDB.Bson; using MongoDB.Driver; using oodb_mongo_server.controllers; using oodb_mongo_server.database.context; using oodb_project.models;  /*  * В данном файле представлен класс AdminController,   * который является наследником абстрактного класса BaseController.  * Тип "входной" модели для данных, курсирующих внутри системы является AdminModel  * Тип "выходной" модели для данных, возвращаемых пользователю является AdminDataModel  * В данном классе переопределены методы Update и Delete, т.к. логика данных методов  * может различаться от класса к классу и не подлежит обобщению в рамках текущей системы.  * Также в данном классе переопределён метод Create, т.к. требуется явное указание параметров  * метода при отправки его контроллеру.  * Методы Get, GetAll не переопределены и используются из абстрактного класса.  * **/  namespace oodb_project.controllers {     /// <summary>     /// Класс контроллера для таблицы Admin     /// </summary>     public class AdminController : BaseController<AdminModel, AdminDataModel>     {         // Контекст базы данных         private DbContext _db;          /// <summary>         /// Конструктор класса         /// </summary>         /// <param name="db">Контекст базы данных</param>         public AdminController(DbContext db) : base(db.AdminList) // Вызываем конструктор абстрактного класса         {             _db = db;         }          /// <summary>         /// Обновление объекта в коллекции Admin         /// </summary>         /// <param name="data">Новые данные для объекта</param>         /// <returns>Данные обновлённого объекта</returns>         public IResult Update(AdminDataModel data)         {             if (_collection == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Поиск конкретного объекта                 var admin = _collection.Find(document => document.Id == ObjectId.Parse(data.Id)).FirstOrDefault();                 if (admin == null)                 {                     return Results.Json(new MessageModel($"Экземпляра объекта AdminModel с Id = {data.Id} не обнаружен в БД"));                 }                  // Создание фильтра для поиска объекта в коллекции                 var filter = Builders<AdminModel>.Filter.Eq(s => s.Id, ObjectId.Parse(data.Id));                  // Создаём определение для обновления объекта в коллекции                 var update = Builders<AdminModel>.Update.Set(s => s.Email, data.Email);                  // Обновляем объект в коллекции по определённому фильтру                 _collection.UpdateOne(filter, update);             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }              return Results.Json(data);         }          /// <summary>         /// Создание объекта для коллекции Admin         /// </summary>         /// <param name="data">Данные объекта</param>         /// <returns>Созданный объект</returns>         public new IResult Create(AdminDataModel data)         {             return base.Create(data);         }          /// <summary>         /// Удаление объекта из коллекции         /// </summary>         /// <param name="id">Идентификатор объекта в коллекции</param>         /// <returns>Удалённый объект</returns>         public new IResult Delete(string id)         {             if ((_collection == null) || (_db.MonitorAppList == null))             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 var data = _collection.Find(document => document.Id == ObjectId.Parse(id)).FirstOrDefault();                 if (data == null)                 {                     return Results.Json(new MessageModel($"Экземпляра объекта {typeof(AdminModel).Name} с Id = {id} не обнаружен в БД"));                 }                  // Удаление элемента в коллекции                 _collection.DeleteOne(x => x.Id == data.Id);                  // Каскадное удаление (удаляет все записи в коллекции MonitorApp, которые связаны с текущим объектом Admin)                 _db.MonitorAppList.DeleteMany(x => x.Admin!.Id == data.Id);                  // Возвращение удалённой модели                 return Results.Json(new AdminDataModel(data.Id.ToString()!, data.Email!));             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }         }     } } 

В данном контроллере используется абстрактный класс BaseController, который берёт на себя ответственность за работу функций Create, Get и GetAll.

Представленная выше функция Update реализована внутри каждого класса (AdminController в том числе) индивидуально.

Каким образом происходит связка контроллеров с конкретными маршрутами? Пример данного кода представлен ниже, на примере всё того же контроллера AdminController:

            /* ----------- */             /* CRUD операции для Host */             /* ----------- */             var hostController = new HostController(_db);             _app.MapPost(ApiUrl.API_SAVE_HOST, hostController.Create);             _app.MapPost(ApiUrl.API_UPDATE_HOST, hostController.Update);             _app.MapPost(ApiUrl.API_DELETE_HOST, hostController.Delete);             _app.MapGet(ApiUrl.API_GET_HOST, hostController.Get);             _app.MapGet(ApiUrl.API_GET_ALL_HOST, hostController.GetAll);

Инициализация находится в файле InitMongoController.cs

Весь представленный выше код находится в отдельном репозитории и при желании читатель может с ним ознакомится перейдя по ссылке на репозиторий данного сервиса.

А на этом рассмотрение сервиса работы с MongoDB заканчивается.

Разработка сервиса для работы с Perst (oodb-perst-server)

Проблема поддержки новых версий

К сожалению Perst не работает с новыми версиями C# и .NET (последняя версия Perst поддерживает .NET Framework 4.8 и меньше). По этой причине нет возможности использовать ASP NET Core.

Одним из достоинств микросервисной архитектуры является то, что можно использовать при разработке каждого отдельного сервиса любые технологии и не исключено, что эти технологии будут устаревшими, что в данном случае и является решением проблемы с совместимостью. И такое решение может быть использовано в других проектах.

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

Данный сервис был разработан с использованием .NET Framework 4.8 в виде консольного приложения. Для коммуникации с данным сервисом был использован WebSocketSharp, который позволяет создавать WebSocket-сервер, что и было сделано. Также WebSocketSharp позволяет создавать WebSocket-клиент, для взаимодействия с сервером. В общем, полнодуплексный канал связи получается.

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

В первую очередь необходимо самостоятельно загрузить необходимые файлы базы данных Perst на свой локальный компьютер (основной интерес представляет файл DLL, она подключается как зависимость).

Сделать это можно с помощью официального сайта. Там же есть руководство пользователя по настройке и установки данной БД.

Код инициализации базы данных расположен в точке входа в приложение и представлен в рамках следующего метода:

        /// <summary>         /// Статический метод инициализации объектно-ориентированной базы данных         /// </summary>         static void initPerstDb()         {             // Создание нового хранилища             _db = StorageFactory.Instance.CreateStorage();              // Открытие файла базы данных для записи             _db.Open("perst.dbs", 100);               // Получение корневого класса             if(_db.Root == null)             {                 _db.Root = new PerstRoot(_db);             }              // Связка корневого класса с атрибутом текущего класса             _root = (PerstRoot)_db.Root;         }

Чем является корневой элемент базы данных? Всё просто. Если привести аналогию, то это тот же объект, что выступает в роли контекста для базы данных MongoDB в сервисе oodb-mongo-server. Только в данном случае контекст будет для базы данных Perst.

namespace ConsoleApp1.root {     /// <summary>     /// Класс, представляющий корневой элемент базы данных     /// </summary>     public class PerstRoot : Persistent     {         /// <summary>         /// Индекс для доступа к коллекции объектов Admin         /// </summary>         public FieldIndex idxAdmin;          /// <summary>         /// Индекс для доступа к коллекции объектов Host         /// </summary>         public FieldIndex idxHost;          /// <summary>         /// Индекс для доступа к коллекции объектов MonitorApp         /// </summary>         public FieldIndex idxMonitorApp;          /// <summary>         /// Индекс для доступа к коллекции объектов DataSource         /// </summary>         public FieldIndex idxDataSource;          /// <summary>         /// Индекс для доступа к коллекции объектов HostService         /// </summary>         public FieldIndex idxHostService;          /// <summary>         /// Индекс для доступа к коллекции объектов Service         /// </summary>         public FieldIndex idxService;          /// <summary>         /// Конструктор с параметром         /// </summary>         /// <param name="db">Хранилище</param>         public PerstRoot(Storage db) : base(db)         {             idxAdmin = db.CreateFieldIndex(typeof(AdminModel), "Id", true);             idxHost = db.CreateFieldIndex(typeof(HostModel), "Id", true);             idxMonitorApp = db.CreateFieldIndex(typeof(MonitorAppModel), "Id", true);             idxDataSource = db.CreateFieldIndex(typeof(DataSourceModel), "Id", true);             idxHostService = db.CreateFieldIndex(typeof(HostServiceModel), "Id", true);             idxService = db.CreateFieldIndex(typeof(ServiceModel), "Id", true);         }          public PerstRoot()         {          }     } }

В общем-то, дополнительных настроек для подключения к базе данных не требуется. Специализированный файл базы данных создаётся автоматически (файл perst.dbs).

Разработка функционала

Файловая структура проекта

Рисунок 8 - Структура проекта сервиса oodb-perst-server

В constants определены константы для связывания путей и конкретных операций (аналогия с API).

В controllers располагаются контроллеры верхнего и низкого уровня.

В data расположен класс, используемый для генерации тестовых данных для ООБД.

В models определены модели, которые активно используются сервисом при работе как с ООБД, так и с выводом данных пользователю

В root расположен класс главного корневого элемента ООБД.

Описание точки входа в сервис

Точка входа в сервис выглядит следующим образом:

        /// <summary>         /// Точка входа в консольное приложение         /// </summary>         /// <param name="args"></param>         static void Main(string[] args)         {             // Инициализация БД Perst             initPerstDb();              // Наполнение БД тестовыми значениями             var mockData = new MockData(_db, _root);             mockData.generateData();              // Прослушивание WebSocket-соединений             var wssv = new WebSocketServer("ws://127.0.0.1");             wssv.AddWebSocketService("/admin", initAdminHighController);             wssv.AddWebSocketService("/host", initHostHighController);             wssv.AddWebSocketService("/host-service", initHostServiceHighController);             wssv.AddWebSocketService("/monitor-app", initMonitorAppHighController);             wssv.AddWebSocketService("/data-source", initDataSourceHighController);             wssv.AddWebSocketService("/service", initServiceHighController);              // Начало прослушивания соединений             wssv.Start();              // Остановка работы консольного приложения             Console.ReadKey(true);              // Остановка прослушивания соединений             wssv.Stop();              // Закрытие соединения с базой данных             _db.Close();         }
Полный код точки входа
namespace ConsoleApp1 {     internal class Program     {         /// <summary>         /// Хранилище         /// </summary>         static private Storage _db;          /// <summary>         /// Корневой класс         /// </summary>         static private PerstRoot _root;          /// <summary>         /// Статический метод инициализации объектно-ориентированной базы данных         /// </summary>         static void initPerstDb()         {             // Создание нового хранилища             _db = StorageFactory.Instance.CreateStorage();              // Открытие файла базы данных для записи             _db.Open("perst.dbs", 100);               // Получение корневого класса             if(_db.Root == null)             {                 _db.Root = new PerstRoot(_db);             }              // Связка корневого класса с атрибутом текущего класса             _root = (PerstRoot)_db.Root;         }          /// <summary>         /// Инициализация верхнеуровнего контроллера для коллекции объектов Admin         /// </summary>         /// <returns>Экземпляр верхнеуровневого контроллера Admin</returns>         static AdminHighController initAdminHighController()         {             return new AdminHighController(_db, _root);         }          /// <summary>         /// Инициализация верхнеуровнего контроллера для коллекции объектов Host         /// </summary>         /// <returns>Экземпляр верхнеуровневого контроллера Host</returns>         static HostHighController initHostHighController()         {             return new HostHighController(_db, _root);         }          /// <summary>         /// Инициализация верхнеуровнего контроллера для коллекции объектов HostService         /// </summary>         /// <returns>Экземпляр верхнеуровневого контроллера HostService</returns>         static HostServiceHighController initHostServiceHighController()         {             return new HostServiceHighController(_db, _root);         }          /// <summary>         /// Инициализация верхнеуровнего контроллера для коллекции объектов MonitorApp         /// </summary>         /// <returns>Экземпляр верхнеуровневого контроллера MonitorApp</returns>         static MonitorAppHighController initMonitorAppHighController()         {             return new MonitorAppHighController(_db, _root);         }          /// <summary>         /// Инициализация верхнеуровнего контроллера для коллекции объектов DataSource         /// </summary>         /// <returns>Экземпляр верхнеуровневого контроллера DataSource</returns>         static DataSourceHighController initDataSourceHighController()         {             return new DataSourceHighController(_db, _root);         }          /// <summary>         /// Инициализация верхнеуровнего контроллера для коллекции объектов Service         /// </summary>         /// <returns>Экземпляр верхнеуровневого контроллера Service</returns>         static ServiceHighController initServiceHighController()         {             return new ServiceHighController(_db, _root);         }          /// <summary>         /// Точка входа в консольное приложение         /// </summary>         /// <param name="args"></param>         static void Main(string[] args)         {             // Инициализация БД Perst             initPerstDb();              // Наполнение БД тестовыми значениями             var mockData = new MockData(_db, _root);             mockData.generateData();              // Прослушивание WebSocket-соединений             var wssv = new WebSocketServer("ws://127.0.0.1");             wssv.AddWebSocketService("/admin", initAdminHighController);             wssv.AddWebSocketService("/host", initHostHighController);             wssv.AddWebSocketService("/host-service", initHostServiceHighController);             wssv.AddWebSocketService("/monitor-app", initMonitorAppHighController);             wssv.AddWebSocketService("/data-source", initDataSourceHighController);             wssv.AddWebSocketService("/service", initServiceHighController);              // Начало прослушивания соединений             wssv.Start();              // Остановка работы консольного приложения             Console.ReadKey(true);              // Остановка прослушивания соединений             wssv.Stop();              // Закрытие соединения с базой данных             _db.Close();         }     } }

В точке входа в текущий сервис происходит инициализация подключения к базе данных Perst, наполнение базы данных тестовыми значениями, добавление WebSocket-сервисов к экземпляру WebSocketServer (каждый из которых отвечает за определённую коллекцию объектов), запуск прослушивания соединений и этапы завершения работы сервиса.

Так как взаимодействие с данным сервисом реализовано с помощью WebSocket-соединения, то основной сервис и текущий обмениваются данными в JSON-формате, который заранее определён. Модель, которая используется при конвертации входных данных в текущем сервисе представлена ниже:

namespace ConsoleApp1.models {     /// <summary>     /// Класс модели, которая используется для взаимодействия с другими сервисами     /// </summary>     public class HttpModel     {         public HttpModel()         {         }          public HttpModel(string path, string payload)         {             Path = path;             Payload = payload;         }          /// <summary>         /// Путь, по которому отправить данные         /// </summary>         public string Path { get; set; }          /// <summary>         /// Отправляемые данные         /// </summary>         public string Payload { get; set; }     } }

Данная модель определяет полезные данные (Payload) и путь, по которому отправить эти данные (Path, используется для отправки данных конкретной CRUD-операции).

Устройство контроллеров

Контроллеры я разделил на две части:

  1. Верхнеуровневые контроллеры — контроллеры, которые по пути (Path) определяют, какой конкретно CRUD-операции низкого уровня отправлять данные (Payload).

  2. Низкоуровневые контроллеры — контроллеры, которые реализуют конкретные операции с ООБД

Абстрактный класс для верхнеуровневых контроллеров выглядит следующим образом:

namespace ConsoleApp1.controllers.high_level {     /// <summary>     /// Абстрактный класс верхнеуровневых контроллеров     /// </summary>     public abstract class BaseHighController : WebSocketBehavior     {         // Ссылка на обработчик нижнего уровня         protected IBaseLowController _controller;          public BaseHighController(IBaseLowController controller)         {             _controller = controller;         }          /// <summary>         /// Метод, обрабатывающий приход сообщений         /// </summary>         /// <param name="e"></param>         protected override void OnMessage(MessageEventArgs e)         {             // Вызов функции InitSendRoute и передача ему десериализованных данных, полученных от вызывающей стороны             InitSendRoute(JsonConvert.DeserializeObject<HttpModel>(e.Data));         }          /// <summary>         /// Инициализация вызовов процедур, в зависимости от требуемого пути         /// </summary>         /// <param name="body">Данные, полученные от вызывающей стороны</param>         public void InitSendRoute(HttpModel body)         {             if (body.Path == ApiPerstServiceUrl.GET_ALL)             {                 Send(_controller.getAll());                 return;             }             else if (body.Path == ApiPerstServiceUrl.GET)             {                 Send(_controller.get(body.Payload));                 return;             }             else if (body.Path == ApiPerstServiceUrl.CREATE)             {                 Send(_controller.create(body.Payload));                 return;             }             else if (body.Path == ApiPerstServiceUrl.UPDATE)             {                 Send(_controller.update(body.Payload));                 return;             }             else if (body.Path == ApiPerstServiceUrl.DELETE)             {                 Send(_controller.delete(body.Payload));                 return;             }              Send(JsonConvert.SerializeObject(new MessageModel("Not found 404")));         }     } }

Цель данного класса аналогична той, которая была у предыдущего сервиса.

В данном случае абстрактный класс реализует функцию InitSendRoute(), в которой идёт распределение Payload операциям расположенным по разным путям.

Переопределённый метод OnMessage() выступает в роли обработчика события «данным WebSocket-сервисом получено сообщение от WebSocket-клиента». Он просто вызывает InitSendRoute() с передачей ему десериализованных данных по общей модели.

С данным абстрактным классом определение классов-контроллеров высокого уровня очень небольшое. Например, для контроллера AdminHighController будет следующее определение:

namespace ConsoleApp1 {     /// <summary>     /// Обработчик верхнего уровня для коллекции объектов Admin     /// </summary>     public class AdminHighController : BaseHighController     {         public AdminHighController(Storage db, PerstRoot root) : base(new AdminLowController(db, root)){}     } }

А что же с низкоуровневыми контроллерами?

Здесь чуть сложнее. Для низкоуровневых контроллеров я использовал другой подход: я определил интерфейс и наследуясь от данного интерфейса реализовал методы контроллеров.

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

Определение интерфейса низкоуровневых контроллеров
namespace ConsoleApp1.controllers.low_level {     /// <summary>     /// Интерфейс базового контроллера     /// </summary>     public interface IBaseLowController     {         /// <summary>         /// Обновление объекта         /// </summary>         /// <param name="obj">Новые данные объекта</param>         /// <returns>Данные обновлённого объекта</returns>         string update(string obj);          /// <summary>         /// Создание объкета         /// </summary>         /// <param name="obj">Данные объекта</param>         /// <returns>Данные созданного объекта</returns>         string create(string obj);          /// <summary>         /// Получение списка объектов         /// </summary>         /// <returns>Список объектов</returns>         string getAll();          /// <summary>         /// Получение конкретного объекта         /// </summary>         /// <param name="id">Идентификатор конкретного объекта</param>         /// <returns>Данные объекта</returns>         string get(string id);          /// <summary>         /// Удаление конкретного объекта         /// </summary>         /// <param name="obj">Данные объекта для удаления</param>         /// <returns>Удалённый объект</returns>         string delete(string obj);     } }

Приведу пример реализации метода удаления в низкоуровневом контроллере HostLowController:

        /// <summary>         /// Удаление объекта HostModel         /// </summary>         public string delete(string id)         {             if (_db == null)             {                 return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));             }              HostOutputModel hostOutput;              try             {                 // Поиск модели Host по идентификатору                 HostModel host = (HostModel)_root.idxHost[id];                 // Проверка на нахождение модели по id                 if (host == null)                 {                     return JsonConvert.SerializeObject(new MessageModel("Объекта по данному ID нет в БД"));                 }                  // Сборка выходной модели                 hostOutput = new HostOutputModel(                     host.Id,                     host.Name,                     host.Url,                     host.IPv4,                     host.System                 );                 // Каскадное удаление                 host.CascadeDelete(_root);             }             catch (Exception e)             {                 return JsonConvert.SerializeObject(new MessageModel(e.Message));             }              return JsonConvert.SerializeObject(hostOutput);         }
Полный код HostLowController.cs
namespace ConsoleApp1.controllers.low_level {     /// <summary>     /// Класс низкоуровневого контроллера для коллекции объектов Host     /// </summary>     internal class HostLowController : IBaseLowController     {         /// <summary>         /// Хранилище         /// </summary>         private static Storage _db;          /// <summary>         /// Корневой элемент ООБД         /// </summary>         private static PerstRoot _root;          public HostLowController(Storage db, PerstRoot root)         {             _db = db;             _root = root;         }          /// <summary>         /// Обновление объекта HostModel         /// </summary>         public string update(string obj)         {             // Проверка на подключение к ООБД             if (_db == null)             {                 return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));             }              // Десериализация входных данных             var data = JsonConvert.DeserializeObject<HostOutputModel>(obj);             try             {                 // Поиск объекта host в коллекции объектов по ID                 HostModel host = (HostModel)_root.idxHost[data.Id];                  // Проверка обнаружения объекта                 if (host == null)                 {                     return JsonConvert.SerializeObject(new MessageModel("Объекта по данному ID нет в БД"));                 }                  // Имзенение данных в найденном объекте host                 host.Url = data.Url;                 host.Name = data.Name;                 host.IPv4 = data.IPv4;                 host.System = data.System;                  // Фиксация изменений                 host.Modify();             }             catch (Exception e)             {                 return JsonConvert.SerializeObject(new MessageModel(e.Message));             }              return JsonConvert.SerializeObject(data);         }          /// <summary>         /// Создание объекта HostModel         /// </summary>         public string create(string obj)         {             if (_db == null)             {                 return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));             }              var data = JsonConvert.DeserializeObject<HostOutputModel>(obj);             try             {                 // Добавление уникального идентификатора объекту                 data.Id = Guid.NewGuid().ToString();                  // Добавление объекта в коллекцию Host                 _root.idxHost.Put(new HostModel(data.Id, data.Name, data.Url, data.IPv4, data.System, _db.CreateLink(), _db.CreateLink()));             }             catch (Exception e)             {                 return JsonConvert.SerializeObject(new MessageModel(e.Message));             }             return JsonConvert.SerializeObject(data);         }          /// <summary>         /// Получение всех объектов AdminModel         /// </summary>         public string getAll()         {             if (_db == null)             {                 return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Получение всех данных коллекции                 HostOutputModel[] items = new HostOutputModel[_root.idxHost.Count];                  // Процесс конвертации полученных данных в выходные данные (аналогично сервису oodb-mongo-server)                 for (var i = 0; i < _root.idxHost.Count; i++)                 {                     HostModel item = (HostModel)_root.idxHost.GetAt(i);                     items[i] = new HostOutputModel(                         item.Id,                         item.Name,                         item.Url,                         item.IPv4,                         item.System                     );                 }                  // Сериализация результата                 return JsonConvert.SerializeObject(items);             }             catch (Exception e)             {                 return JsonConvert.SerializeObject(new MessageModel(e.Message));             }         }          /// <summary>         /// Получение конкретного объекта HostModel         /// </summary>         public string get(string id)         {             if (_db == null)             {                 return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 HostOutputModel host = null;                  // Поиск объекта в коллекции объектов с конвертацией в выходную модель, при нахождении элемента                 for (var i = 0; i < _root.idxHost.Count; i++)                 {                     HostModel item = (HostModel)_root.idxHost.GetAt(i);                     if (item.Id == id)                     {                         host = new HostOutputModel(                             item.Id,                             item.Name,                             item.Url,                             item.IPv4,                             item.System                         );                          break;                     }                 }                  return JsonConvert.SerializeObject(host);             }             catch (Exception e)             {                 return JsonConvert.SerializeObject(new MessageModel(e.Message));             }         }          /// <summary>         /// Удаление объекта HostModel         /// </summary>         public string delete(string id)         {             if (_db == null)             {                 return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));             }              HostOutputModel hostOutput;              try             {                 HostModel host = (HostModel)_root.idxHost[id];                  if (host == null)                 {                     return JsonConvert.SerializeObject(new MessageModel("Объекта по данному ID нет в БД"));                 }                  hostOutput = new HostOutputModel(                     host.Id,                     host.Name,                     host.Url,                     host.IPv4,                     host.System                 );                  // Каскадное удаление объекта                 host.CascadeDelete(_root);             }             catch (Exception e)             {                 return JsonConvert.SerializeObject(new MessageModel(e.Message));             }              return JsonConvert.SerializeObject(hostOutput);         }     } } 

В HostLowController, при удалении объекта реализуется механизм каскадного удаления. На примере данного метода я расскажу о том, каким образом он реализован.

При выполнении host.CascadeDelete(_root) осуществляется каскадное удаление конкретного объекта host из коллекции Host.

Если перейти к определению модели HostModel, то можно узнать каким образом реализуется механика каскадного удаления.

Механика каскадного удаления выглядит следующим образом:

        /// <summary>         /// Каскадное удаление текущего экземпляра объекта из ООБД         /// </summary>         /// <param name="root">Корневой элемент ООБД</param>         /// <returns></returns>         public bool CascadeDelete(PerstRoot root)         {             // Проход по всем элементам коллекции HostServiceLink             foreach (HostServiceModel item in HostServiceLink)             {                 // Удаление связанных объектов                 item.Delete(root);             }              // Проход по всем элементам коллекции MonitorAppLink             foreach (MonitorAppModel item in MonitorAppLink)             {                 // Удаление связанных объектов                 item.Delete(root);             }              // Удаление текущего объекта по контексту             return root.idxHost.Remove(this);         }
Полное определение модели HostModel

namespace oodb_project.models {     /// <summary>     /// Модель, характеризующая конкретный удалённый хост     /// </summary>     public class HostModel : Persistent     {         public HostModel()         {         }          public HostModel(string id, string name, string url, string iPv4, string system, Link hostServiceLink, Link monitorAppLink)         {             Id = id;             Name = name;             Url = url;             IPv4 = iPv4;             System = system;             HostServiceLink = hostServiceLink;             MonitorAppLink = monitorAppLink;         }          /// <summary>         /// Идентификатор объекта         /// </summary>         public string Id { get; set; }          /// <summary>         /// Имя         /// </summary>         public string Name { get; set; }          /// <summary>         /// Адрес         /// </summary>         public string Url { get; set; }          /// <summary>         /// IP         /// </summary>         public string IPv4 { get; set; }          /// <summary>         /// Название системы         /// </summary>         public string System { get; set; }          /// <summary>         /// Ссылка на объект HostService (который ссылается на данный объект, в реляционном представлении)         /// </summary>         public Link HostServiceLink { get; set; }          /// <summary>         /// Ссылка на объект MonitorApp (который ссылается на данный объект, в реляционном представлении)         /// </summary>         public Link MonitorAppLink { get; set; }          /// <summary>         /// Каскадное удаление текущего экземпляра объекта из ООБД         /// </summary>         /// <param name="root">Корневой элемент ООБД</param>         /// <returns></returns>         public bool CascadeDelete(PerstRoot root)         {             // Проход по всем элементам коллекции HostServiceLink             foreach (HostServiceModel item in HostServiceLink)             {                 // Удаление связанных объектов                 item.Delete(root);             }              // Проход по всем элементам коллекции MonitorAppLink             foreach (MonitorAppModel item in MonitorAppLink)             {                 // Удаление связанных объектов                 item.Delete(root);             }              // Удаление текущего объекта по контексту             return root.idxHost.Remove(this);         }     } }

При каскадном удалении осуществляется проход по всем элементам связанных коллекций и удаление конкретных элементов в этих коллекциях. И только после удаления всех связанных объектов осуществляется удаление текущего.

Каждая модель, которая используется для работы с базой данных Perst наследуется от Persistent

Вот пример:

    /// <summary>     /// Модель, характеризующая источник данных для сервиса     /// </summary>     public class DataSourceModel : Persistent

И это наследование очень мешает при преобразовании объекта в строку (сериализации). Поэтому было принято решение также как и в сервисе oodb-mongo-server производить деление моделей которые используются внутри системы (для работы с ООБД) и моделей, которые возвращаются основному сервису.

Таким образом реализован сервис oodb-perst-server.

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

Разработка сервиса для работы с db4o (oodb-main-server)

Файловая структура сервиса

Рисунок 9 - Файловая структура сервиса

В constants расположены константы, связывающий все маршруты API с контроллерами.

В controllers определены три множества контроллеров, каждый из которых реализует взаимодействие с тем или иным сервисом. В рамках данной статьи будут рассмотрены только контроллеры для db4o, для ознакомления с работой других контроллеров я предлагаю читателю обратиться к репозиторию исходного кода.

В data расположен класс, реализующий логику наполнения db4o тестовыми данными.

В models располагаются модели, активно используемые в данном сервисе.

Настройка подключения к базе данных

Подключить db4o можно достаточно просто — для этого можно воспользоваться пакетным менеджером NuGet и просто загрузить все необходимые компоненты для работы с данной библиотекой.

Ссылка на пакет в NuGet

Рисунок 10 - Установленная библиотека db4o-devel

Код в точке входа, который соответствует настройке и конфигурированию db4o выглядит следующим образом:

// При запуске проверяем есть ли файл БД, и если есть - удаляем его if (File.Exists("db4o.yap")) {     File.Delete("db4o.yap"); }  // Конфигурирование ООБД (db4o) IObjectContainer dbDb4o = Db4oEmbedded.OpenFile(Db4oEmbedded.NewConfiguration(), "db4o.yap");  // Заполнение ООБД тестовыми данными var mockData = new MockData(dbDb4o); mockData.generateData();

Разработка функционала

Описание точки входа в сервис

Код точки входа выглядит следующим образом:

/*  * Точка входа в сервис oodb-main-server  * **/  using Db4objects.Db4o; using oodb_project.controllers.db4o; using oodb_project.controllers.mongo; using oodb_project.controllers.perst; using oodb_project.data;  // Создание экземпляра класса WebApplicationBuilder для конфигурирования веб-приложения var builder = WebApplication.CreateBuilder(args);  // Сборка веб-приложения var app = builder.Build();  // Конфигурирование статических путей к файлам app.UseDefaultFiles(); app.UseStaticFiles();  // При запуске проверяем есть ли файл БД, и если есть - удаляем его if (File.Exists("db4o.yap")) {     File.Delete("db4o.yap"); }  // Конфигурирование ООБД (db4o) IObjectContainer dbDb4o = Db4oEmbedded.OpenFile(Db4oEmbedded.NewConfiguration(), "db4o.yap");  // Заполнение ООБД тестовыми данными var mockData = new MockData(dbDb4o); mockData.generateData();  // Инициализация маршрутов для работы с ООБД db4o var initDb4oController = new InitDb4oController(app, dbDb4o); initDb4oController.InitRoutes();  // Инициализация маршрутов для работы с ООДБ perst var initPerstController = new InitPerstController(app); initPerstController.InitRoutes();  // Инициализация маршрутов для работы с MongoDB var initMongoController = new InitMongoController(app); initMongoController.InitRoutes();  // Запуск серверного приложения app.Run();  // Закрытие соединений с базой данных dbDb4o.Close(); 

В точке входа осуществляется сборка веб-приложения, настройка и конфигурирование ООБД db4o, инициализация маршрутов для работы с db4o, MongoDB и Perst и запуск серверного приложения. Все соответствующие этапы приведены к комментариях к коду.

Устройство контроллеров

В данном сервисе контроллеры устроены аналогично сервису oodb-mongo-server — также каждый контроллер наследуется от абстрактного базового класса, который покрывает определённое множество операций. Однако в данном случае обошлось без механизма рефлексии.

Например, так выглядит функция создания нового объекта:

        /// <summary>         /// Метод для создания нового объекта коллекции         /// </summary>         /// <param name="data">Данные объекта коллекции</param>         /// <returns>Созданный объект коллекции</returns>         protected IResult Create(T data)         {             // Проверка подключения к базе данных             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Автоматическая генерация UUID                 data.Id = Guid.NewGuid().ToString();                  // Сохранение модели в ООДБ                 _db.Store(data);             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }              return Results.Json(data);         }
Полный код абстрактного класса контроллеров db4o
namespace oodb_project.controllers.db4o {     /// <summary>     /// Абстрактный класс, реализующий обобщённые методы CRUD-операций для коллекций объектов     /// </summary>     /// <typeparam name="T">Тип данных моделей, используемых в рамках CRUD-операций</typeparam>     public abstract class BaseController<T>         where T : IdModel     {         // Коллекция входных параметров         protected IObjectContainer? _db;          /// <summary>         /// Конструктор абстрактного класса         /// </summary>         /// <param name="collection">Ссылка на коллекцию</param>         public BaseController(IObjectContainer? db)         {             _db = db;         }          /// <summary>         /// Метод для создания нового объекта коллекции         /// </summary>         /// <param name="data">Данные объекта коллекции</param>         /// <returns>Созданный объект коллекции</returns>         protected IResult Create(T data)         {             // Проверка подключения к базе данных             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Автоматическая генерация UUID                 data.Id = Guid.NewGuid().ToString();                  // Сохранение модели в ООДБ                 _db.Store(data);             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }              return Results.Json(data);         }          /// <summary>         /// Метод для получения всех объектов определённого типа         /// </summary>         /// <returns>Результат работы функции (массив документов)</returns>         public IResult GetAll()         {             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Поиск всех данных по текущей модели                 IObjectSet result = _db.QueryByExample(typeof(T));                  // Возвращение списка моделей                 return Results.Json(result);             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }         }          /// <summary>         /// Метод для получения конкретного объекта из коллекции         /// </summary>         /// <param name="id">Идентификатор искомого объекта</param>         /// <returns>Результат работы метода (конкретный объект)</returns>         public IResult Get(string id)         {             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Получение конкретной модели                 T data = _db.Query<T>(value => value.Id == id)[0];                  return Results.Json(data);             }             catch (Exception)             {                 return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));             }         }          /// <summary>         /// Метод для удаления объекта из коллекции         /// </summary>         /// <param name="id">Идентификатор объекта</param>         /// <returns>Удалённый объект коллекции</returns>         public IResult Delete(string id)         {             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Получение конкретного объекта                 var data = _db.Query<T>(value => value.Id == id);                 if(data.Count <= 0)                 {                     return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));                 }                  var cloneData = data.First();                  // Удаление объекта                 _db.Delete(data.First());                  return Results.Json(cloneData);             }             catch (Exception)             {                 return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));             }         }     } }

При создании нового объекта на вход поступает объект типа T, который определяется классом через where: where T : IdModel , что позволяет обобщить CRUD-операции, ведь все модели, которые в них участвуют, наследуются от абстрактного класса IdModel, представляющую идентификатор модели.

Приведу пример реализации операции обновления из контроллера DataSourceController:

        /// <summary>         /// Обновление объекта в коллекции         /// </summary>         /// <param name="data">Данные об объекте в коллекции</param>         /// <returns>Обновлённый объект</returns>         public IResult Update(DataSourceModel data)         {             // Проверка подключения к базе данных             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Поиск объекта по идентификатору                 DataSourceModel findObj = _db.Query<DataSourceModel>(value => value.Id == data.Id)[0];                  // Изменение данных в объекте                 findObj.Url = data.Url;                 findObj.Name = data.Name;                  // Фиксация изменений                 _db.Store(findObj);             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }              return Results.Json(data);         }
Полный код контроллера DataSourceController
namespace oodb_project.controllers.db4o {     /// <summary>     /// Класс определяющий контроллеры для коллекции объектов DataSource     /// </summary>     public class DataSourceController : BaseController<DataSourceModel>     {         public DataSourceController(IObjectContainer db) : base(db) { }          /// <summary>         /// Обновление объекта в коллекции         /// </summary>         /// <param name="data">Данные об объекте в коллекции</param>         /// <returns>Обновлённый объект</returns>         public IResult Update(DataSourceModel data)         {             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 DataSourceModel findObj = _db.Query<DataSourceModel>(value => value.Id == data.Id)[0];                 findObj.Url = data.Url;                 findObj.Name = data.Name;                  _db.Store(findObj);             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }              return Results.Json(data);         }          /// <summary>         /// Создание нового объекта коллекции         /// </summary>         /// <param name="data">Данные об объекте</param>         /// <returns>Созданный объект</returns>         public new IResult Create(DataSourceModel data)         {             return base.Create(data);         }          /// <summary>         /// Получение всех объектов коллекции с помощью SODA-запроса         /// </summary>         /// <returns>Список всех объектов коллекции</returns>         public new IResult GetAll()         {             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 // Получение всех записей из DataSource с помощью SODA-запроса                 IQuery query = _db.Query();                  // Установка ограничений для поиска                 query.Constrain(typeof(DataSourceModel));                  // Получение результата поиска                 IObjectSet result = query.Execute();                  return Results.Json(result);             }             catch (Exception e)             {                 return Results.Json(new MessageModel(e.Message));             }         }          /// <summary>         /// Каскадное удаление объекта DataSource         /// </summary>         /// <param name="id">Идентификатор объекта в коллекции</param>         /// <returns>Удалённый объект</returns>         public new IResult Delete(string id)         {             if (_db == null)             {                 return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));             }              try             {                 var data = _db.Query<DataSourceModel>(value => value.Id == id);                 if (data.Count <= 0)                 {                     return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));                 }                  var services = _db.Query<ServiceModel>(value => value.DataSourceId == id);                 foreach (var item in services)                 {                     var hostServices = _db.Query<HostServiceModel>(value => value.ServiceId == item.Id);                     foreach(var hostService in hostServices)                     {                         _db.Delete(hostService);                     }                      _db.Delete(item);                 }                  var cloneData = data.First();                  // Удаление модели                 _db.Delete(data.First());                  return Results.Json(cloneData);             }             catch (Exception)             {                 return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));             }         }     } }

Как видно из примера, сначала происходит поиск объекта в коллекции по идентификатору, затем происходит изменение его полей и фиксация изменений через объект базы данных.

Разработка desktop-клиента (oodb-desktop-client)

В заключительной части статьи будут описаны результаты разработки desktop-клиента, который отправляет запросы на сервис oodb-mongo-server, через основной сервис oodb-main-server. То есть, клиент работает с базой данных MongoDB.

Файловая структура проекта

Рисунок 11 - Файловая структура oodb-desktop-client

В constants расположены все важные константы, которые используются в клиентском приложении.

В generators располагаются классы, определяющие методы, в которых происходит генерация объектов для конкретных коллекций.

В models расположены модели.

В services определены сервисы, с помощью которых реализуется логика взаимодействия с основным сервером oodb-main-server.

В utils находятся утилиты

Общий вид клиентского приложения

Рисунок 12 - Визуальное представление клиентского приложения

Приложение снабжено различными элементами управления, которые позволяют производить операции с конкретными коллекциями. На рисунке 12 представлено взаимодействие с коллекцией Host.

Разработка функционала

Устройство сервисов

Разберём устройство сервисов на примере AdminService.

namespace oodb_desktop_client.services {     /// <summary>     /// Класс сервиса для коллекции объектов Admin     /// </summary>     public class AdminService : BaseService     {         public AdminService(DataGridView data) : base(data) {}          /// <summary>         /// Получение всех записей из таблицы Admins         /// </summary>         public void GetAll()         {             GetAll<AdminModel>(ApiUrl.ADMIN, "AdminId1");         }          /// <summary>         /// Сохранение новой записи в таблицу Admins         /// </summary>         public void Save(AdminModel body)         {             Save(body, ApiUrl.ADMIN);         }          /// <summary>         /// Обновление записи в таблице Admins         /// </summary>         /// <param name="body">Новые данные</param>         public void Update(AdminModel body)         {             Update(body, ApiUrl.ADMIN, "AdminId1");         }          /// <summary>         /// Удаление записи из таблицы Admins         /// </summary>         /// <param name="id">ID записи в ООБД</param>         public void Delete(string id)         {             Delete<AdminModel>(id, ApiUrl.ADMIN, "AdminId1");         }          /// <summary>         /// Формирование новой записи         /// </summary>         /// <param name="text"></param>         /// <returns></returns>         public void ShowDialog(             string text,                // Текст             string operation = "save",  // Операция (save / update)             AdminModel oldValue = null  // Старое значение (опционально)         )          {             // Создание экземпляра формы             Form prompt = new Form()             {                 Width = 300, // Ширина                 Height = 200, // Высота                 FormBorderStyle = FormBorderStyle.FixedDialog, // Стиль границы формы                 Text = text, // Текст формы                 StartPosition = FormStartPosition.CenterScreen // Стартовая позиция             };              // Текст перед строкой ввода             Label textLabel = new Label() { Left = 100, Top = 20, Width = 100, Text = "Введите Email" };             // Строка ввода данных             TextBox textBox = new TextBox() { Left = 100, Top = 50, Width = 100, Text = (oldValue != null)? oldValue.Email : "" };             // Кнопка отправки данных             Button confirmation = new Button() { Text = text, Left = 100, Width = 100, Top = 90, DialogResult = DialogResult.OK };                          // Обработчик нажания на кнопку отправки             confirmation.Click += (sender, e) => {                 prompt.Close();             };              // Добавление элементов управления на форму             prompt.Controls.Add(textBox);             prompt.Controls.Add(confirmation);             prompt.Controls.Add(textLabel);             prompt.AcceptButton = confirmation;              if(prompt.ShowDialog() != DialogResult.OK)             {                 return;             }              // Проверка валидности почтового адреса             if (!ValidateUtil.IsValidEmail(textBox.Text))             {                 MessageBox.Show(                     "Не правильный формат Email-адреса",                     "Ошибка",                     MessageBoxButtons.OK,                     MessageBoxIcon.Error                 );                  return;             }                          // В зависимости от операции вызываем ту или иную функцию, с экземпляром модели             if(operation == "save")             {                 Save(new AdminModel(Guid.NewGuid().ToString(), textBox.Text));             }else if(operation == "update")             {                 Update(new AdminModel(oldValue.Id, textBox.Text));             }         }     } }

Первое, на что можно обратить внимание это наследование от класса BaseService. Данный класс реализует все CRUD-операции, основываясь на механизме рефлексии. С его помощью код сервисов значительно упрощается, однако необходимо определять метод ShowDialog(), который вызывается при изменении данных в объекте или создании нового объекта для коллекции (create или update), вызывая при этом форму. На рисунке 13 представлен такой вызов, однако для объекта коллекции Host.

Рисунок 13 - Вызываемая форма при изменении данных объекта в коллекции
Полное определение абстрактного класса BaseService
namespace oodb_desktop_client.services {     /// <summary>     /// Абстрактный класс определяющий базовые CRUD-операции для работы с таблицами     /// </summary>     public abstract class BaseService     {         public DataGridView dataGridView;         public BaseService(DataGridView data)         {             dataGridView = data;         }          /// <summary>         /// Получение всех данных из таблицы         /// </summary>         /// <typeparam name="T">Тип данных</typeparam>         /// <param name="domainPath">Путь к домену таблицы</param>         /// <param name="columnName">Название столбца первичного ключа</param>         public void GetAll<T>(string domainPath, string columnName) where T : IdModel         {             var url = $"{ApiUrl.BASE_URL}{ApiUrl.MONGO_DOMAIN}{domainPath}{ApiUrl.GET_ALL}";              var httpRequest = (HttpWebRequest)WebRequest.Create(url);             var httpResponse = (HttpWebResponse)httpRequest.GetResponse();              using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))             {                 var result = streamReader.ReadToEnd();                 var list = JsonConvert.DeserializeObject<List<T>>(result.ToString());                  foreach (T item in list)                 {                     if (GridViewUtil.GetIndexByValue(dataGridView, columnName, item.Id) < 0)                     {                         var fields = ReflectionUtil.getFields(item);                          Action action = () => dataGridView.Rows.Add(fields.ToArray());                         dataGridView.Invoke(action);                     }                 }             }              httpResponse.Close();         }          /// <summary>         /// Изменение данных         /// </summary>         /// <param name="body">Данные для изменения</param>         public void Update<T>(T body, string domainPath, string columnName) where T : IdModel         {             var url = $"{ApiUrl.BASE_URL}{ApiUrl.MONGO_DOMAIN}{domainPath}{ApiUrl.UPDATE}";              var httpRequest = (HttpWebRequest)WebRequest.Create(url);             httpRequest.Method = "POST";             httpRequest.ContentType = "application/json";              var data = JsonConvert.SerializeObject(body);              using (var streamWriter = new StreamWriter(httpRequest.GetRequestStream()))             {                 streamWriter.Write(data);             }              var httpResponse = (HttpWebResponse)httpRequest.GetResponse();             object output = null;              using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))             {                 var result = streamReader.ReadToEnd();                 output = (object)JsonConvert.DeserializeObject<T>(result.ToString());                  if (((T)output).Id == null)                 {                     MessageBox.Show(                         (JsonConvert.DeserializeObject<MessageModel>(result.ToString())).message,                         "Ошибка",                          MessageBoxButtons.OK,                         MessageBoxIcon.Error                     );                 }                 else                 {                     T model = ((T)output);                     var index = GridViewUtil.GetIndexByValue(dataGridView, columnName, model.Id);                     var fields = ReflectionUtil.getFields(model);                      Action action = () =>                     {                         for (var i = 0; i < fields.Count; i++)                         {                             dataGridView.Rows[index].Cells[i].Value = fields[i];                         }                     };                      dataGridView.Invoke(action);                 }             }              httpResponse.Close();         }          /// <summary>         /// Сохранение данных         /// </summary>         /// <param name="body">Данные для сохранения</param>         public void Save<T>(T body, string domainPath) where T : IdModel         {             var url = $"{ApiUrl.BASE_URL}{ApiUrl.MONGO_DOMAIN}{domainPath}{ApiUrl.SAVE}";              var httpRequest = (HttpWebRequest)WebRequest.Create(url);             httpRequest.Method = "POST";             httpRequest.ContentType = "application/json";              var data = JsonConvert.SerializeObject(body);              using (var streamWriter = new StreamWriter(httpRequest.GetRequestStream()))             {                 streamWriter.Write(data);             }              var httpResponse = (HttpWebResponse)httpRequest.GetResponse();             object output = null;              using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))             {                 var result = streamReader.ReadToEnd();                 output = (object)JsonConvert.DeserializeObject<T>(result.ToString());                  if (((T)output).Id == null)                 {                     MessageBox.Show(                         (JsonConvert.DeserializeObject<MessageModel>(result.ToString())).message,                         "Ошибка",                          MessageBoxButtons.OK,                         MessageBoxIcon.Error                     );                 }                 else                 {                     var fields = ReflectionUtil.getFields((T)output);                      Action action = () => dataGridView.Rows.Add(fields.ToArray());                     dataGridView.Invoke(action);                 }             }              httpResponse.Close();         }          /// <summary>         /// Удаление записи         /// </summary>         /// <param name="admin">Данные для сохранения</param>         public void Delete<T>(string id, string domainPath, string columnId) where T : IdModel         {             var url = $"{ApiUrl.BASE_URL}{ApiUrl.MONGO_DOMAIN}{domainPath}{ApiUrl.DELETE}/{id}";              var httpRequest = (HttpWebRequest)WebRequest.Create(url);             httpRequest.Method = "POST";             httpRequest.ContentType = "application/json";              var httpResponse = (HttpWebResponse)httpRequest.GetResponse();             object output = null;              using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))             {                 var result = streamReader.ReadToEnd();                 output = (object)JsonConvert.DeserializeObject<T>(result.ToString());                  if (((T)output).Id == null)                 {                     MessageBox.Show(                         (JsonConvert.DeserializeObject<MessageModel>(result.ToString())).message,                         "Ошибка",                          MessageBoxButtons.OK,                         MessageBoxIcon.Error                     );                 }                 else                 {                     var index = GridViewUtil.GetIndexByValue(dataGridView, columnId, id);                     Action action = () => dataGridView.Rows.RemoveAt(index);                     dataGridView.Invoke(action);                 }             }              httpResponse.Close();         }     } }

Генерация данных с использованием Bogus

Для генерации данных из клиента была использована библиотека Bogus, которую можно установить с помощью пакетного менеджера NuGet.

Использование Bogus в рамках текущего приложения будет представлено на примере генератора AdminGenerator:

namespace oodb_desktop_client.generators { /// <summary> /// Класс генератора объектов коллекции Admin /// </summary> public class AdminGenerator     { /// <summary> /// Сервис Admin /// </summary>         private AdminService _adminService;          public AdminGenerator(AdminService adminService)         {             _adminService = adminService;         }  /// <summary> /// Генерация администраторов /// </summary> /// <param name="count">Количество генерируемых записей</param> /// <param name="progress">Прогресс генерации</param> public void GenerateAdmin( int count,// Количество сгенерированных элементов IProgress<ProgressInfo> progress    // Прогресс генерации ) { // Создание экземпляра класса Faker var faker = new Faker("ru");  // Процесс генерации объектов for(var i = 0; i < count; i++)             { // Получение рандомного почтового адреса var email = faker.Internet.Email();  // Сохранение объекта в ООБД _adminService.Save(new AdminModel("", email), ApiUrl.ADMIN);  // Обновление текстовой информации var info = $"Сгенерировано записей: {i}";  // Отправка изменений о прогрессе генерации записей progress?.Report(new ProgressInfo { value = i + 1, info = info });  // Пауза Thread.Sleep(1); } } } }

Генерация определённого количества записей происходит в методе GenerateAdmin. В данном методе используется экземпляр объекта Faker из Bogus, который генерирует случайный почтовый адрес пользователя, а затем через сервис AdminService происходит сохранение сгенерированной модели в базу данных.

Ссылка на репозиторий проекта.

Выводы

В данной статье были рассмотрены результаты разработки системы, состоящей из множества сервисов, каждый из которых работает со своей базой данных.

Была спроектирована модель базы данных по описанию предметной области в реляционном представлении, она же и служит ориентиром в обозначении отношений между коллекциями объектов.

Спроектированная архитектура системы полностью соответствует полученным результатам.

Два сервиса осуществляют работу с объектно-ориентированными базами данных Perst и db4o, один сервис взаимодействует с MongoDB, которая была развёрнута в контейнере вместе с веб-интерфейсом Mongo Express.

Клиентское приложение было разработано с помощью .NET Framework 4.8. Оно взаимодействует с основным сервисом (oodb-main-server) и посредством этого взаимодействия получает доступ к сервису oodb-mongo-server.

Было также дано описание проблеме с поддержкой Perst новыми версиями .NET, и описание решения данной проблемы в контексте избранного архитектурного паттерна (микросервисная архитектура).

Список использованных источников

  1. Репозиторий основного сервиса: oodb-main-server

  2. Репозиторий сервиса для взаимодействия с Perst: oodb-perst-server

  3. Репозиторий сервиса для взаимодействия с MongoDB: oodb-mongo-server

  4. Репозиторий клиентского приложения: oodb-desktop-client

Список рекомендуемых источников

  1. Введение в объектно-ориентированные базы данных

  2. Perst — высокопроизводительная ООБД

  3. Создание Web API приложения с использованием .NET Core + MongoDB .NET Driver


ссылка на оригинал статьи https://habr.com/ru/post/721066/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *