Создание Restful CRUD API с помощью HarperDB и .Net Core

Создайте набор API для отдыха с помощью .Net Core и HarperDB. Мы будем использовать последнюю версию HarperDB.Net.Клиент Пакет Nuget для связи с экземпляром облачного сервера HarperDB через Асп.Нет Основной проект веб-API

Основное внимание в этом руководстве будет уделено сервисному уровню, который фактически имеет дело с реализацией. Код для остальной части проекта будет опубликован и кратко объяснен, включая модели, контроллеры и т. д., поскольку он аналогичен любому другому приложению .Net Core.

  • Направляйтесь к Студия HarperDB и создайте бесплатную учетную запись
  • После создания войдите в систему, используя свои учетные данные, создайте новую организацию и введите данные.

изображение.png

  • Дождитесь завершения процесса, а затем выберите только что созданную организацию и щелкните вкладку «Создать новый экземпляр HarperDB Cloud».

изображение.png

  • Затем выберите вариант экземпляра AWS или Verizon.

изображение.png

  • Выберите облачного партнера, сейчас я буду использовать AWS, а затем нажмите «Информация об экземпляре».

изображение.png

  • Укажите имя экземпляра, а также имя пользователя и пароль для доступа к экземпляру.

изображение.png

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

изображение.png

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

HarperDb.Net.Client package предоставляет различные полезные методы, с помощью которых мы можем легко выполнять все операции CRUD на нашем сервере HarperDB.

Давайте сначала перечислим поддерживаемые операции

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

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

namespace HarperNetClient
{ public interface IHarperClientAsync { Task<IRestResponse> BulkOperationAsync<T>(string dataSource, string operationType = "csv_url_load", string actionType = "insert"); Task<IRestResponse> CreateAttributeAsync(string schema, string table, string attribute); Task<IRestResponse> CreateRecordAsync<T>(T itemToCreate); Task<IRestResponse> CreateSchemaAsync(string schema); Task<IRestResponse> CreateTableAsync(string table, string schema, string hashAttribute = "id"); Task<IRestResponse> DescribeSchemaAsync(string schema); Task<IRestResponse> DescribeTableAsync(string table, string schema); Task<IRestResponse> DropAttributeAsync(string table, string schema, string attribute); Task<IRestResponse> DropSchemaAsync(string schema); Task<IRestResponse> DropTableAsync(string table, string schema); Task<IRestResponse> ExecuteQueryAsync(string sqlQuery); Task<IRestResponse> GetByIdAsync(string id); Task<IRestResponse> UpdateRecordAsync<T>(T itemToUpdate);
    }
}

Наша конечная цель — создать набор остальных API, используя Асп.Нет Ядро и HarperDB. API, который мы создаем, будет состоять из двух основных объектов: сообщений и комментариев.

Сообщение будет иметь несколько свойств, таких как Content, PostedBy и т. д. Каждое сообщение может иметь один или несколько комментариев, имеющих некоторые собственные свойства. Это могут быть и ответы на комментарии. Конечный объект будет примерно таким, как показано ниже.

[{ "id": "668e65fe-133b-44ef-a3db-b488b5990c90", "content": "Will Eric Ten Hag be a success at Manchester United", "postedBy": "mufc_fan", "postedAt": "2022-05-11T15:16:05.5078365+05:30", "imageURL": " "commentsThread": [ { "id": "370189e5-d613-43f9-a9a5-7f72f6b0c7d1", "comment": "Yes he will be", "postedAt": "2022-05-11T15:17:54.4697461+05:30", "postedBy": "always_Red", "postId": "668e65fe-133b-44ef-a3db-b488b5990c90", "parentCommentId": null, "commentReplies": [ { "id": "33653eb2-5e8f-4554-81f9-b079fafc36a5", "comment": "Totally rebuilt his squad at was great in UCL", "postedAt": "2022-05-11T15:19:29.4973336+05:30", "postedBy": "mufc_always", "postId": null, "parentCommentId": "370189e5-d613-43f9-a9a5-7f72f6b0c7d1" }, { "id": "86a1692a-ac8b-4266-93ae-198efb8bfd34", "comment": "Totally agree, his philosophy at Ajax proves he can be successful", "postedAt": "2022-05-11T15:19:00.6457227+05:30", "postedBy": "mufc_Red", "postId": null, "parentCommentId": "370189e5-d613-43f9-a9a5-7f72f6b0c7d1" }] }, { "id": "9636c45e-3156-4cfc-aa60-41da72ce6c7d", "comment": "Hopefully !!!", "postedAt": "2022-05-11T15:20:01.289686+05:30", "postedBy": "string", "postId": "668e65fe-133b-44ef-a3db-b488b5990c90", "parentCommentId": null, "commentReplies": []
      }
    ]
  }
]

Для этого нам понадобится решение Visual Studio с двумя проектами. Один простой Асп.Нет Проект на основе Core Web API, а другой — дополнительное приложение библиотеки классов для общих моделей, файлов и т. д.

изображение.png

Наряду с пакетом HarperDB мы также будем использовать несколько других пакетов, все из которых перечислены ниже и могут быть добавлены с помощью диспетчера пакетов NuGet в Visual Studio.

  • Автокартограф 11.0.1
  • AutoMapper.Extensions.Microsoft.DependencyInjection 11.0.0
  • HarperDB.Net.Client 1.1.0
  • Newtonsoft.Json 13.0.1
  • Swashbuckle.AspNetCore 5.6.3

Чтобы подключиться к серверу HarperDB, нам понадобится InstanceURl вместе с AuthToken. Вернемся к разделу конфигурации нашего сервера HarperDB, чтобы получить обе эти детали.

изображение.png

HarperDb.Net.Client пакет имеет класс с именем Харпердбконфигуратион который содержит свойства, необходимые для подключения к серверу.

Мы будем обновлять файл appsetting.json, чтобы добавить вышеупомянутые параметры в новое свойство с именем ConnectionString, а затем создадим несколько методов, чтобы получить их в любом месте.

"ConnectionString": { "InstanceUrl": " "AuthToken": "YOUR_AUTH_TOKEN_HERE"
  }

Примечание. В идеале следует использовать службу диспетчера секретов, такую ​​как Azure KeyVault, AWS KMS и т. д., для хранения учетных данных/токенов в реальных приложениях. Мы используем здесь файл AppSettings напрямую только для этой демонстрации.

Теперь создайте интерфейс с именем IHarperConfiguration и реализующий его класс с именем HarperConfiguration.

using HarperNetClient.models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Mozab.Service.Comments.Services
{ public interface IHarperConfiguration {
        HarperDbConfiguration GetHarperConfigurations();
    }
}
using HarperNetClient.models;
using Microsoft.Extensions.Configuration; namespace Mozab.Service.Comments.Services
{ public class HarperConfigurations : IHarperConfiguration { private IConfiguration _config; public HarperConfigurations(IConfiguration configs) { _config = configs; } public HarperDbConfiguration GetHarperConfigurations() { var dbConfigs = _config.GetSection("ConnectionString").Get<HarperDbConfiguration>(); return dbConfigs;
        }
    }
}

Цель этого интерфейса — предоставить метод GetHarperConfiguration всем службам через DependencyInjection. Этот метод считывает конфигурации, ранее сохраненные в файле appsettings.json.

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

public HarperClientAsync(HarperDbConfiguration config);
public HarperClientAsync(HarperDbConfiguration config, string table);

Первым параметром этого конструктора является параметр, возвращаемый GetHarperConfiguration метод, реализованный ранее.

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

namespace Mozab.SharedUtilities
{ public class Constants { public const string CommentCannotBeEmpty = "Cannot add an empty comment"; public const string UserNotFound = "No User found for the given id"; public const string PostNotFound = "No Posts found for the given id"; public const string InvalidCommentId = "No Comment found for the given id"; public const string InvalidParentCommentId = "No parent comment found for the given id"; public const string NoCommentForTheJab = "No Comment found for the given Jab"; public const string InvalidTableOrSchemaName = "Table or Schema name cannot be empty"; public const string SchemaAlreadyExists = "Schema with the same name already exists"; public const string TableAlreadyExists = "Table with the same name already exists"; public const string TableNotFound = "No Table found for the given name in the schema"; public const string SchemaNotFound = "No Schema found for the given name";
    }
}
using System; namespace Mozab.SharedUtilities
{ public class CustomException: Exception { public CustomException() { } public CustomException(string message) : base(message)
        {

        }
    }
}

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

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

using System;
using System.Collections.Generic; namespace Mozab.SharedUtilities.Models
{ public class Posts { public string id { get; set; } public string Content { get; set; } public string PostedBy { get; set; } public DateTime PostedAt { get; set; } public string ImageURL { get; set; } } public class PostVM { public string Content { get; set; } public string PostedBy { get; set; } public string ImageURL { get; set; } } public class PostComment { public string id { get; set; } public string Comment { get; set; } public DateTime PostedAt { get; set; } public string PostedBy { get; set; } public string PostId { get; set; } public string ParentCommentId { get; set; } } public class PostCommentVM { public string Comment { get; set; } public string PostedBy { get; set; } public string PostId { get; set; } public string ParentCommentId { get; set; } } public class PostsWithComments { public Posts Post { get; set; } public List<CommentsThread> PostComments { get; set; } } public class PostResponse { public string id { get; set; } public string Content { get; set; } public string PostedBy { get; set; } public DateTime PostedAt { get; set; } public string ImageURL { get; set; } public List<CommentResponse> CommentsThread { get; set; } } public class CommentsThread { public PostComment Comment { get; set; } public List<PostComment> CommentReplies { get; set; } } public class CommentResponse { public string id { get; set; } public string Comment { get; set; } public DateTime PostedAt { get; set; } public string PostedBy { get; set; } public string PostId { get; set; } public string ParentCommentId { get; set; } public List<PostComment> CommentReplies { get; set; }
    }
}

Мы будем использовать пакет AutoMapper для автоматического сопоставления всех объектов одного типа с другим. Нам это понадобится, поскольку ViewModel и фактические модели отличаются.

Например, чтобы позже создать новую публикацию, запрос API будет использовать модель PostVM, в то время как сущность HarperDB будет иметь тип Post с добавленными параметрами, такими как Id и PostedAt, которые нам не нужны в качестве входных данных от пользователя.

using AutoMapper;
using Mozab.SharedUtilities.Models; namespace Mozab.Service.Comments
{ public class ProfileMapping: Profile { public ProfileMapping() { CreateMap<PostVM, Posts>().ReverseMap(); CreateMap<PostCommentVM, PostComment>().ReverseMap(); CreateMap<Posts, PostResponse>().ReverseMap(); CreateMap<PostComment, CommentResponse>().ReverseMap();

        }
    }
}

Нам просто нужно создать новый класс и настроить Maps и ReverseMaps для типов объектов, которые мы хотим, чтобы AutoMapper преобразовывал для нас.

Также класс нужно добавить в startup.cs, чтобы он был везде доступен для инжекта.

var mapperConfig = new MapperConfiguration(m => { m.AddProfile(new ProfileMapping()); }); IMapper mapper = mapperConfig.CreateMapper();
    services.AddSingleton(mapper);

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

Контроллеры сообщений и комментариев будут иметь соответствующие методы для выполнения операций CRUD над сущностями. Ниже приведен скриншот документации swagger с окончательным списком API, который у нас будет в конце этой реализации.

изображение.png

using Microsoft.AspNetCore.Mvc;
using Mozab.Service.Services;
using Mozab.SharedUtilities;
using System;
using System.Threading.Tasks; namespace Mozab.Service.Controllers
{ [Route("api/[controller]")] [ApiController] public class SchemaController : ControllerBase { private ISchemaService _service; public SchemaController(ISchemaService service) { _service = service; } [HttpPost] public async Task<IActionResult> CreateSchema(string schema) { try { var response = await _service.CreateSchema(schema); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpPost("table")] public async Task<IActionResult> CreateTable(string table, string schema) { try { var response = await _service.CreateTable(table, schema); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpDelete("table")] public async Task<IActionResult> DeleteTable(string table, string schema) { try { var response = await _service.DeleteTable(table, schema); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpDelete] public async Task<IActionResult> DeleteSchema(string schema) { try { var response = await _service.DeleteSchema(schema); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpPost("table/attribute")] public async Task<IActionResult> CreateAttribute(string table, string schema, string attribute) { try { var response = await _service.CreateAttribute(table, schema, attribute); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpDelete("table/attribute")] public async Task<IActionResult> DeleteAttribute(string table, string schema, string attribute) { try { var response = await _service.DeleteAttribute(table, schema, attribute); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!");
            }
        }
    }
}

Постконтроллер

using Microsoft.AspNetCore.Mvc;
using Mozab.Service.Services;
using Mozab.SharedUtilities;
using Mozab.SharedUtilities.Models;
using System;
using System.Threading.Tasks; namespace Mozab.Service.Controllers
{ [Route("api/[controller]")] [ApiController] public class PostsController : ControllerBase { private IPostService _service; public PostsController(IPostService service) { _service = service; } [HttpGet] public async Task<IActionResult> GetAllPosts() { try { var response = await _service.GetAllPosts(); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpGet("{id}")] public async Task<IActionResult> GetPostById(string id) { try { var response = await _service.GetPostById(id); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpPost] public async Task<IActionResult> AddNewPost(PostVM postToAdd) { try { var response = await _service.AddNewPost(postToAdd); return Created("", response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpPut("{id}")] public async Task<IActionResult> UpdatePost(string id, PostVM postToUpdate) { try { var response = await _service.UpdatePostById(id, postToUpdate); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpDelete("{id}")] public async Task<IActionResult> DeletePostById(string id) { try { var response = await _service.DeletePostById(id); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!");
            }
        }

    }
}
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Mozab.Service.Comments.Services;
using Mozab.SharedUtilities;
using Mozab.SharedUtilities.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks; namespace Mozab.Service.Comments.Controllers
{ [Route("api/[controller]")] [ApiController] public class CommentsController : ControllerBase { private ICommentsService _service; public CommentsController(ICommentsService service) { _service = service; } [HttpGet("{id}")] public async Task<IActionResult> GetCommentById(string id) { try { var response = await _service.GetCommentById(id); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpGet("post/{id}")] public async Task<IActionResult> GetCommentsByPost(string id) { try { var response = await _service.GetAllCommentsByPost(id); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpPost] public async Task<IActionResult> CreateComment(PostCommentVM commentToAdd) { try { var response = await _service.AddNewComment(commentToAdd); return Created("", response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpPut("{id}")] public async Task<IActionResult> UpdateComment(string id, PostCommentVM postComment) { try { var response = await _service.UpdateComment(id, postComment); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpDelete("{id}")] public async Task<IActionResult> DeleteCommentById(string id) { try { var response = await _service.DeleteCommentById(id); return Ok(response); } catch (CustomException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return BadRequest("Something went wrong!"); } } [HttpDelete("post/{id}")] public async Task<IActionResult> DeleteCommentByPost(string id) { try { var response = await _service.DeleteCommentByPost(id); return Ok(response); } catch(CustomException ex) { return BadRequest(ex.Message); } catch(Exception ex) { return BadRequest("Something went wrong!");
            }
        }
    }
}

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

using System.Threading.Tasks; namespace Mozab.Service.Services
{ public interface ISchemaService { public Task<string> CreateSchema(string schemaName); public Task<string> CreateTable(string tableName, string schemaName); public Task<string> DeleteSchema(string schema); public Task<string> DeleteTable(string table, string schema); public Task<string> CreateAttribute(string schema, string table, string attribute); public Task<string> DeleteAttribute(string table, string schema, string attribute);
    }
}

IPostService

using Mozab.SharedUtilities.Models;
using System.Collections.Generic;
using System.Threading.Tasks; namespace Mozab.Service.Services
{ public interface IPostService { Task<List<PostResponse>> GetAllPosts(); Task<PostResponse> GetPostById(string zabId); Task<SharedUtilities.Models.Posts> AddNewPost(PostVM zabToAdd); Task<SharedUtilities.Models.Posts> UpdatePostById(string id, PostVM zabToUpdate); Task<bool> DeletePostById(string id);
    }
}
using Mozab.SharedUtilities.Models;
using System.Collections.Generic;
using System.Threading.Tasks; namespace Mozab.Service.Comments.Services
{ public interface ICommentsService { public Task<PostComment> AddNewComment(PostCommentVM comment); public Task<List<CommentResponse>> GetAllCommentsByPost(string postId); public Task<CommentResponse> GetCommentById(string commentId); public Task<PostComment> UpdateComment(string id, PostCommentVM comment); public Task<bool> DeleteCommentById(string commentId); public Task<bool> DeleteCommentByPost(string postId);
    }
}

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

services.AddSingleton<IHarperConfiguration, HarperConfigurations>();
services.AddSingleton<ICommentsService, CommentsService>();
services.AddSingleton<ISchemaService, SchemaService>();
services.AddSingleton<IPostService, PostService>();
using HarperNetClient;
using HarperNetClient.models;
using Mozab.Service.Comments.Services;
using Mozab.SharedUtilities;
using Newtonsoft.Json;
using System.Threading.Tasks;
using Constants = Mozab.SharedUtilities.Constants; namespace Mozab.Service.Services
{ public class SchemaService : ISchemaService { private IHarperClientAsync _client; private IHarperConfiguration _config; private HarperDbConfiguration _dbConfigs; public SchemaService(IHarperConfiguration configs) { _config = configs; _dbConfigs = _config.GetHarperConfigurations(); _client = new HarperClientAsync(_dbConfigs);
        }
}

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

_dbConfig объект будет использовать созданную ранее службу HarperConfiguration для получения сведений об экземпляре HarperDB из файла appsettings.json.

_клиент object будет фактическим объектом для класса HarperClientAsync, который будет предоставлять все асинхронные методы, необходимые для взаимодействия с HarperDB. Этот класс принимает объект HarperDbConfiguration, определяющий детали экземпляра, с которым необходимо установить соединение.

public async Task<string> CreateSchema(string schemaName) { try { if (string.IsNullOrEmpty(schemaName)) throw new CustomException(Constants.InvalidTableOrSchemaName); var schemaExists = await CheckIfSchemaExists(schemaName); if (schemaExists) throw new CustomException(Constants.SchemaAlreadyExists); var response = await _client.CreateSchemaAsync(schemaName); if (response.IsSuccessful) _dbConfigs.Schema = schemaName; return JsonConvert.DeserializeObject<Content>(response.Content).Message; } catch { throw; } } private async Task<bool> CheckIfSchemaExists(string schema) { try { var response = await _client.DescribeSchemaAsync(schema); return response.StatusCode == System.Net.HttpStatusCode.OK ? true : false; } catch { throw;
            }
        }
  • Чтобы создать новую схему, мы будем использовать методы из пакета. Один будет использоваться для проверки того, существует ли уже схема с таким же именем или нет, а другой фактически создает схему.
  • ОписатьSchemaAsync метод принимает строку, определяющую имя схемы, и возвращает сведения о ней.
  • Ниже приведен скриншот ответа HarperDB для обоих сценариев, когда схема существует и когда ее нет.

изображение.png

  • Когда схема не найдена, мы получаем от HarperDB код состояния NotFound с соответствующим сообщением в свойстве Content. Если он найден, мы возвращаем StatusCode of Ok
  • Если схема не найдена, мы будем использовать CreateSchemaAsync способ создания новой схемы.
  • Ответ успешной транзакции в этом случае будет примерно таким:

изображение.png

  • Пакет NuGet уже содержит имя класса Content, которое можно использовать для анализа ответа и получения фактического сообщения, возвращаемого API HarperDB.

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

Методы CheckIfTableExists и CreateTableAsync будет использоваться для того же.

using HarperNetClient;
using HarperNetClient.models;
using Mozab.Service.Comments.Services;
using Mozab.SharedUtilities;
using Newtonsoft.Json;
using System.Threading.Tasks;
using Constants = Mozab.SharedUtilities.Constants; namespace Mozab.Service.Services
{ public class SchemaService : ISchemaService { private IHarperClientAsync _client; private IHarperConfiguration _config; private HarperDbConfiguration _dbConfigs; public SchemaService(IHarperConfiguration configs) { _config = configs; _dbConfigs = _config.GetHarperConfigurations(); _client = new HarperClientAsync(_dbConfigs); } public async Task<string> CreateSchema(string schemaName) { try { if (string.IsNullOrEmpty(schemaName)) throw new CustomException(Constants.InvalidTableOrSchemaName); var schemaExists = await CheckIfSchemaExists(schemaName); if (schemaExists) throw new CustomException(Constants.SchemaAlreadyExists); var response = await _client.CreateSchemaAsync(schemaName); if (response.IsSuccessful) _dbConfigs.Schema = schemaName; return JsonConvert.DeserializeObject<Content>(response.Content).Message; } catch { throw; } } public async Task<string> CreateTable(string tableName, string schemaName) { try { if (string.IsNullOrEmpty(tableName) || string.IsNullOrEmpty(schemaName)) throw new CustomException(Constants.InvalidTableOrSchemaName); _dbConfigs.Schema = schemaName; var tableExists = await CheckIfTableExists(tableName, schemaName); if (tableExists) throw new CustomException(Constants.TableAlreadyExists); var response = await _client.CreateTableAsync(tableName, schemaName); return JsonConvert.DeserializeObject<Content>(response.Content).Message; } catch { throw; } } public async Task<string> CreateAttribute(string schema, string table, string attribute) { try { if (string.IsNullOrEmpty(table) || string.IsNullOrEmpty(schema)) throw new CustomException(Constants.InvalidTableOrSchemaName); _dbConfigs.Schema = schema; if (string.IsNullOrEmpty(attribute)) throw new CustomException("Attribute name cannot be empty"); var tableExists = await CheckIfTableExists(table, schema); if (!tableExists) throw new CustomException(Constants.TableNotFound); var response = await _client.CreateAttributeAsync(table, schema, attribute); return JsonConvert.DeserializeObject<Content>(response.Content).Message; } catch { throw; } } public async Task<string> DeleteAttribute(string table, string schema, string attribute) { try { if (string.IsNullOrEmpty(table) || string.IsNullOrEmpty(schema)) throw new CustomException(Constants.InvalidTableOrSchemaName); _dbConfigs.Schema = schema; if (string.IsNullOrEmpty(attribute)) throw new CustomException("Attribute name cannot be empty"); var tableExists = await CheckIfTableExists(table, schema); if (!tableExists) throw new CustomException(Constants.TableNotFound); var response = await _client.DropAttributeAsync(table, schema, attribute); return JsonConvert.DeserializeObject<Content>(response.Content).Message; } catch { throw; } } public async Task<string> DeleteSchema(string schema) { try { if (string.IsNullOrEmpty(schema)) throw new CustomException(Constants.InvalidTableOrSchemaName); _dbConfigs.Schema = schema; var schemaExists = await CheckIfSchemaExists(schema); if (!schemaExists) throw new CustomException(Constants.SchemaNotFound); var response = await _client.DropSchemaAsync(schema); return JsonConvert.DeserializeObject<Content>(response.Content).Message; } catch { throw; } } public async Task<string> DeleteTable(string table, string schema) { try { if (string.IsNullOrEmpty(table) || string.IsNullOrEmpty(schema)) throw new CustomException(Constants.InvalidTableOrSchemaName); _dbConfigs.Schema = schema; var tableExists = await CheckIfTableExists(table, schema); if (!tableExists) throw new CustomException(Constants.TableNotFound); var response = await _client.DropTableAsync(table, schema); return JsonConvert.DeserializeObject<Content>(response.Content).Message; } catch { throw; } } private async Task<bool> CheckIfSchemaExists(string schema) { try { var response = await _client.DescribeSchemaAsync(schema); return response.StatusCode == System.Net.HttpStatusCode.OK ? true : false; } catch { throw; } } private async Task<bool> CheckIfTableExists(string table, string schema) { try { var response = await _client.DescribeTableAsync(table, schema); return response.StatusCode == System.Net.HttpStatusCode.OK ? true : false; } catch { throw;
            }
        }

    }
}

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

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

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

Первоначальная конфигурация как CommentService, так и более поздней PostService остается такой же, как и SchemaService, поскольку для нее также необходимо определить все эти три свойства.

Единственная разница здесь будет заключаться в том, что мы также определяем имена схемы и таблицы. Схема будет частью класса HarperDbConfiguration, а имя таблицы можно будет передать в качестве параметра конструктору класса HarperClientAsync.

using AutoMapper;
using HarperNetClient;
using HarperNetClient.models;
using Mozab.SharedUtilities;
using Mozab.SharedUtilities.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Constants = Mozab.SharedUtilities.Constants; namespace Mozab.Service.Comments.Services
{ public class CommentsService : ICommentsService { private IHarperClientAsync _client; private IHarperConfiguration _config; private HarperDbConfiguration _dbConfigs; private string Table_Name = "comments"; private IMapper _mapper; public CommentsService(IHarperConfiguration configs, IMapper mapper) { _config = configs; _dbConfigs = _config.GetHarperConfigurations(); _dbConfigs.Schema = "Mozabs"; _client = new HarperClientAsync(_dbConfigs, Table_Name); _mapper = mapper;

        }
}
public async Task<PostComment> AddNewComment(PostCommentVM commentToAdd) { try { if (string.IsNullOrEmpty(commentToAdd.Comment)) throw new CustomException(Constants.CommentCannotBeEmpty); if (!string.IsNullOrEmpty(commentToAdd.ParentCommentId)) { if (CheckIfCommentExists(commentToAdd.ParentCommentId) == null) throw new CustomException(Constants.InvalidParentCommentId); } var comment = _mapper.Map<PostComment>(commentToAdd); comment.PostedAt = DateTime.Now; var response = await _client.CreateRecordAsync<PostComment>(comment); var insertedCommentId = JsonConvert.DeserializeObject<Content>(response.Content).Inserted_Hashes[0]; comment.id = insertedCommentId; return comment; } catch { throw;
            }
        }
  • Общий метод CreateRecordAsync используется для создания новой записи в базе данных.
  • Возвращаемый ответ содержит идентификатор только что созданного поста вместе с кодом статуса Ok.
  • Идентификатор можно получить из вставленные_хэши свойство содержимого в объекте ответа
public async Task<CommentResponse> GetCommentById(string commentId) { try { if (string.IsNullOrEmpty(commentId)) throw new CustomException(Constants.InvalidCommentId); string query = $"SELECT * FROM {_dbConfigs.Schema}.{Table_Name} WHERE id = \"{commentId}\" OR ParentCommentId = \"{commentId}\" "; var response = await _client.ExecuteQueryAsync(query); var commentsResponse = JsonConvert.DeserializeObject<List<PostComment>>(response.Content); if (commentsResponse != null && commentsResponse.Count > 0) { var parentComment = commentsResponse.Where(x => x.id == commentId).ToList()[0]; var childComments = commentsResponse.Where(x => x.id != commentId).ToList(); var comments = _mapper.Map<CommentResponse>(parentComment); comments.CommentReplies = childComments; return comments; } else { return new CommentResponse(); } } catch { throw;
            }
        }
  • Теперь, когда мы передаем идентификатор комментария, нам нужно получить фактический комментарий, а также любые ответы на этот комментарий.
  • Несмотря на то, что у нас есть доступ к методу GetRecordByID, поскольку у нас есть специальное требование, мы можем использовать возможности HarperDB для использования SQL-запроса к схеме типа JSON и получения нужных записей.
  • ExecuteQueryAsync Здесь используется метод, когда мы передаем простой запрос для получения комментариев с заданным идентификатором и всех других комментариев, которые также имеют свой ParentCommentID.

изображение.png

public async Task<PostComment> UpdateComment(string id, PostCommentVM commentToUpdate) { try { var comment = _mapper.Map<PostComment>(commentToUpdate); if (CheckIfCommentExists(id) == null) throw new CustomException(Constants.InvalidCommentId); comment.id = id; var response = await _client.UpdateRecordAsync<PostComment>(comment); if (response.IsSuccessful) { return comment; } else { return null; } } catch { throw; } } public async Task<PostComment> CheckIfCommentExists(string commentId) { try { var response = await _client.GetByIdAsync(commentId); var comment = (JsonConvert.DeserializeObject<List<PostComment>>(response.Content)); if (comment != null && comment.Count > 0) return comment[0]; else return null; } catch { throw;
            }

        }
  • Перед обновлением комментария мы воспользуемся доступным методом GetByIdAsync чтобы убедиться, что комментарий существует для данного идентификатора
  • Если это правда, мы перейдем к Упдейрекордасинк метод и передать измененный объект, который будет обновлен в базе данных
  • В случае успеха часть Content в ответе будет иметь идентификатор комментария, обновленный в Update_Hashes свойство
public async Task<bool> DeleteCommentById(string commentId) { try { if (CheckIfCommentExists(commentId) == null) throw new CustomException(Constants.InvalidCommentId); string queryToDeleteMainComment = $"DELETE FROM {_dbConfigs.Schema}.{Table_Name} WHERE id = \"{commentId}\""; var response = await _client.ExecuteQueryAsync(queryToDeleteMainComment); if (response.IsSuccessful) { string queryToDeleteCommentReplies = $"DELETE FROM {_dbConfigs.Schema}.{Table_Name} WHERE ParentCommentId = \"{commentId}\""; response = await _client.ExecuteQueryAsync(queryToDeleteCommentReplies); } return response.IsSuccessful; } catch { throw;
            }
        }
  • В HarperDB нет прямого метода удаления сущности. Вместо этого мы можем использовать ExecuteQueryAsync еще раз и передайте ему запрос на удаление.
  • Здесь мы сначала выполняем запрос на удаление комментария по идентификатору, а затем еще один запрос на удаление всех ответов на него.

У нас есть еще два метода, реализованных в CommentService, которые используют метод ExecuteQueryAsync для получения всех комментариев по PostId, а другой — для удаления всех комментариев по PostId. Оба этих метода повторно используют функции GET и DELETE, созданные выше.

using AutoMapper;
using HarperNetClient;
using HarperNetClient.models;
using Mozab.SharedUtilities;
using Mozab.SharedUtilities.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Constants = Mozab.SharedUtilities.Constants; namespace Mozab.Service.Comments.Services
{ public class CommentsService : ICommentsService { private IHarperClientAsync _client; private IHarperConfiguration _config; private HarperDbConfiguration _dbConfigs; private string Table_Name = "comments"; private IMapper _mapper; public CommentsService(IHarperConfiguration configs, IMapper mapper) { _config = configs; _dbConfigs = _config.GetHarperConfigurations(); _dbConfigs.Schema = "Mozabs"; _client = new HarperClientAsync(_dbConfigs, Table_Name); _mapper = mapper; } public async Task<PostComment> AddNewComment(PostCommentVM commentToAdd) { try { if (string.IsNullOrEmpty(commentToAdd.Comment)) throw new CustomException(Constants.CommentCannotBeEmpty); if (!string.IsNullOrEmpty(commentToAdd.ParentCommentId)) { if (CheckIfCommentExists(commentToAdd.ParentCommentId) == null) throw new CustomException(Constants.InvalidParentCommentId); } var comment = _mapper.Map<PostComment>(commentToAdd); comment.PostedAt = DateTime.Now; var response = await _client.CreateRecordAsync<PostComment>(comment); var insertedCommentId = JsonConvert.DeserializeObject<Content>(response.Content).Inserted_Hashes[0]; comment.id = insertedCommentId; return comment; } catch { throw; } } public async Task<bool> DeleteCommentById(string commentId) { try { if (CheckIfCommentExists(commentId) == null) throw new CustomException(Constants.InvalidCommentId); string queryToDeleteMainComment = $"DELETE FROM {_dbConfigs.Schema}.{Table_Name} WHERE id = \"{commentId}\""; var response = await _client.ExecuteQueryAsync(queryToDeleteMainComment); if (response.IsSuccessful) { string queryToDeleteCommentReplies = $"DELETE FROM {_dbConfigs.Schema}.{Table_Name} WHERE ParentCommentId = \"{commentId}\""; response = await _client.ExecuteQueryAsync(queryToDeleteCommentReplies); } return response.IsSuccessful; } catch { throw; } } public async Task<bool> DeleteCommentByPost(string postId) { try { if (CheckIfCommentsExistsForPost(postId) == null) throw new CustomException(Constants.NoCommentForTheJab); string queryToGetMainComments = $"SELECT id FROM {_dbConfigs.Schema}.{Table_Name} WHERE PostId = \"{postId}\" "; var response = await _client.ExecuteQueryAsync(queryToGetMainComments); var mainComments = (JsonConvert.DeserializeObject<List<PostComment>>(response.Content)); foreach (var pc in mainComments) { await DeleteCommentById(pc.id); } return response.IsSuccessful; } catch { throw; } } public async Task<List<CommentResponse>> GetAllCommentsByPost(string postId) { try { string query = $"SELECT * FROM {_dbConfigs.Schema}.{Table_Name} WHERE PostId = \"{postId}\""; var response = await _client.ExecuteQueryAsync(query); var mainComments = (JsonConvert.DeserializeObject<List<PostComment>>(response.Content)); var threads = new List<CommentResponse>(); foreach (var pc in mainComments) { threads.Add(await GetCommentById(pc.id)); } return threads; } catch { throw; } } public async Task<CommentResponse> GetCommentById(string commentId) { try { if (string.IsNullOrEmpty(commentId)) throw new CustomException(Constants.InvalidCommentId); string query = $"SELECT * FROM {_dbConfigs.Schema}.{Table_Name} WHERE id = \"{commentId}\" OR ParentCommentId = \"{commentId}\" "; var response = await _client.ExecuteQueryAsync(query); var commentsResponse = JsonConvert.DeserializeObject<List<PostComment>>(response.Content); if (commentsResponse != null && commentsResponse.Count > 0) { var parentComment = commentsResponse.Where(x => x.id == commentId).ToList()[0]; var childComments = commentsResponse.Where(x => x.id != commentId).ToList(); var comments = _mapper.Map<CommentResponse>(parentComment); comments.CommentReplies = childComments; return comments; } else { return new CommentResponse(); } } catch { throw; } } public async Task<PostComment> UpdateComment(string id, PostCommentVM commentToUpdate) { try { var comment = _mapper.Map<PostComment>(commentToUpdate); if (CheckIfCommentExists(id) == null) throw new CustomException(Constants.InvalidCommentId); comment.id = id; var response = await _client.UpdateRecordAsync<PostComment>(comment); if (response.IsSuccessful) { return comment; } else { return null; } } catch { throw; } } public async Task<PostComment> CheckIfCommentExists(string commentId) { try { var response = await _client.GetByIdAsync(commentId); var comment = (JsonConvert.DeserializeObject<List<PostComment>>(response.Content)); if (comment != null && comment.Count > 0) return comment[0]; else return null; } catch { throw; } } public async Task<PostComment> CheckIfCommentsExistsForPost(string postId) { try { string query = $"SELECT * FROM {_dbConfigs.Schema}.{Table_Name} WHERE PostId = \"{postId}\""; var response = await _client.ExecuteQueryAsync(query); var comment = (JsonConvert.DeserializeObject<List<PostComment>>(response.Content)); if (comment != null && comment.Count > 0) return comment[0]; else return null; } catch { throw;
            }
        }

    }
}

Постсервис

using AutoMapper;
using HarperNetClient;
using HarperNetClient.models;
using Mozab.Service.Comments.Services;
using Mozab.SharedUtilities;
using Mozab.SharedUtilities.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Constants = Mozab.SharedUtilities.Constants; namespace Mozab.Service.Services
{ public class PostService: IPostService { private IHarperClientAsync _client; private IHarperConfiguration _config; private HarperDbConfiguration _dbConfigs; private string Table_Name = "posts"; private IMapper _mapper; private ICommentsService _comments; public PostService(IHarperConfiguration configs, IMapper mapper, ICommentsService service) { _config = configs; _dbConfigs = _config.GetHarperConfigurations(); _dbConfigs.Schema = "Mozabs"; _client = new HarperClientAsync(_dbConfigs, Table_Name); _mapper = mapper; _comments = service;
        }
}

Создать сообщение

public async Task<SharedUtilities.Models.Posts> AddNewPost(PostVM postToAdd) { try { if (string.IsNullOrEmpty(postToAdd.Content)) throw new CustomException("Posts content cannot be empty"); var post = _mapper.Map<Posts>(postToAdd); post.PostedAt = DateTime.Now; var response = await _client.CreateRecordAsync<Posts>(post); var insertedCommentId = JsonConvert.DeserializeObject<Content>(response.Content).Inserted_Hashes[0]; post.id = insertedCommentId; return post; } catch { throw;
            }
        }
  • Общий метод CreateRecordAsync используется для создания новой записи в базе данных.
  • Возвращаемый ответ содержит идентификатор только что созданного поста вместе с кодом статуса Ok.
  • Идентификатор можно получить из вставленные_хэши свойство содержимого в объекте ответа

изображение.png

Все остальные операции CRUD аналогичны реализации, выполненной в CommentService с точки зрения связи с HarperDB. Единственное изменение заключается в бизнес-логике, поскольку мы вызываем несколько методов CommentService из PostService для получения комментариев к определенному сообщению.

using AutoMapper;
using HarperNetClient;
using HarperNetClient.models;
using Mozab.Service.Comments.Services;
using Mozab.SharedUtilities;
using Mozab.SharedUtilities.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Constants = Mozab.SharedUtilities.Constants; namespace Mozab.Service.Services
{ public class PostService: IPostService { private IHarperClientAsync _client; private IHarperConfiguration _config; private HarperDbConfiguration _dbConfigs; private string Table_Name = "posts"; private IMapper _mapper; private ICommentsService _comments; public PostService(IHarperConfiguration configs, IMapper mapper, ICommentsService service) { _config = configs; _dbConfigs = _config.GetHarperConfigurations(); _dbConfigs.Schema = "Mozabs"; _client = new HarperClientAsync(_dbConfigs, Table_Name); _mapper = mapper; _comments = service; } public async Task<SharedUtilities.Models.Posts> AddNewPost(PostVM postToAdd) { try { if (string.IsNullOrEmpty(postToAdd.Content)) throw new CustomException("Posts content cannot be empty"); var post = _mapper.Map<Posts>(postToAdd); post.PostedAt = DateTime.Now; var response = await _client.CreateRecordAsync<Posts>(post); var insertedCommentId = JsonConvert.DeserializeObject<Content>(response.Content).Inserted_Hashes[0]; post.id = insertedCommentId; return post; } catch { throw; } } public async Task<bool> DeletePostById(string id) { try { if (CheckIfPostExist(id) == null) throw new CustomException(Constants.PostNotFound); string query = $"DELETE FROM {_dbConfigs.Schema}.{Table_Name} WHERE id = \"{id}\""; var response = await _client.ExecuteQueryAsync(query); if (response.IsSuccessful) { await _comments.DeleteCommentByPost(id); } return response.IsSuccessful; } catch { throw; } } public async Task<List<PostResponse>> GetAllPosts() { try { string query = $"SELECT * FROM {_dbConfigs.Schema}.{Table_Name}"; var response = await _client.ExecuteQueryAsync(query); var postResponse = JsonConvert.DeserializeObject<List<Posts>>(response.Content); if (postResponse != null && postResponse.Count > 0) { var posts = new List<PostResponse>(); foreach (var pr in postResponse) { posts.Add(await GetPostById(pr.id)); } return posts; } else { return new List<PostResponse>(); } } catch { throw; } } public async Task<PostResponse> GetPostById(string postId) { try { if (string.IsNullOrEmpty(postId)) throw new CustomException(Constants.PostNotFound); string query = $"SELECT * FROM {_dbConfigs.Schema}.{Table_Name} WHERE id = \"{postId}\" "; var response = await _client.ExecuteQueryAsync(query); var postResponse = JsonConvert.DeserializeObject<List<Posts>>(response.Content); if (postResponse != null && postResponse.Count > 0) { var commentThread = await _comments.GetAllCommentsByPost(postResponse[0].id); var postsData = _mapper.Map<PostResponse>(postResponse[0]); postsData.CommentsThread = commentThread; return postsData; } else { return new PostResponse(); } } catch { throw; } } public async Task<SharedUtilities.Models.Posts> UpdatePostById(string id, PostVM postToUpdate) { try { var post = _mapper.Map<Posts>(postToUpdate); if (CheckIfPostExist(id) == null) throw new CustomException(Constants.PostNotFound); post.id = id; var response = await _client.UpdateRecordAsync<Posts>(post); if (response.IsSuccessful) { return post; } else { return null; } } catch { throw; } } public async Task<SharedUtilities.Models.Posts> CheckIfPostExist(string id) { try { var response = await _client.GetByIdAsync(id); var post = (JsonConvert.DeserializeObject<List<Posts>>(response.Content)); if (post != null && post.Count > 0) return post[0]; else return null; } catch { throw;
            }
        }
    }
}

HarperDB.Net.Клиент Пакет Nuget предоставляет различные методы, которые использовались для создания базового приложения CRUD.

Большая часть этого пакета и HarperDB заключается в том, что мы можем игнорировать все методы и использовать только метод ExecuteQueryAsync для выполнения наших задач, если мы более знакомы с SQL-запросами.

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

Репозиторий на гитхабе

Пакет NuGet

Документация HarperDB и примеры кода

Коллекция почтальона

Похожие записи

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

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