Введение
Прежде чем начать, я оставлю ссылку на репо этого проекта, откуда я беру коды здесь, в репо блога.
Также я хотел бы оставить ссылку на драйвер mongo, если вы хотите сделать в локальной среде необходимо, чтобы mongo был установлен на вашей машине скачать mongo.
С чего начать?
После того как вы создали базу данных в mongo и коллекцию, в которой будет производиться crud, в случае с двумя CRUD, которые я сделал, я создал две коллекции, одну из книг, а другую из людей, в терминале mongo я выполнил соответствующие команды:
db.createCollection('Books')
db.createCollection('Users')
правильно создавая коллекции, добавьте настройки в свой
// no arquivo appsettings.json
"LibraryDatabase": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "Library", // Nome do DB
"BooksCollectionName": "Books", // Nome das coleções conforme o comando acima
"UsersCollectionName": "Users"
},
файл appsettings.json
Также создайте класс, в котором будут содержаться настройки, которые вы поместили в JSON:
public class MongoDBSettings
{
public string ConnectionString { get; set; }
public string DataBaseName { get; set; }
public string BooksCollectionName { get; set; }
public string UsersCollectionName { get; set; }
}
Наконец, вам также нужно сделать инъекцию зависимостей в вашей программе с помощью этих переменных окружения, это делается в Program.cs
builder.Services.Configure<MongoDBSettings>(
builder.Configuration.GetSection("LibraryDatabase")); // Pega a seção do config conforme o seu nome
Когда проект настроен, вы, должно быть, задаетесь вопросом, как будет осуществляться моделирование?
Моделирование данных
При использовании mongo, когда речь идет о разработке, ваша производительность не имеет себе равных. Для создания модели данных Book разработчику необходимо создать модель, которая представляет собой не что иное, как класс, в котором вы будете представлять эти данные.
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace WebApplicationCRUDExample.Models;
public class Book
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public string Summary { get; set; }
public string CoverURL { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}
Книжная модель
модель пользователя не сильно отличается:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace WebApplicationCRUDExample.Models;
public class User
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string Name { get; set; }
public List<string>? UserLikes { get; set; }
}
Модель пользователя
Создание услуг
Хорошим примером является использование сервисов для подключения к базе данных, чтобы каждый класс нес только одну ответственность.
Служба Book, называемая Library, выглядит следующим образом:
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using WebApplicationCRUDExample.Models;
using WebApplicationCRUDExample.Services.DB;
namespace WebApplicationCRUDExample.Services;
public class LibraryService
{
private readonly IMongoCollection<Book> _booksCollection;
public LibraryService(
IOptions<MongoDBSettings> library)
{
var mongoClient = new MongoClient(
library.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(
library.Value.DataBaseName);
_booksCollection = mongoDatabase.GetCollection<Book>(
library.Value.BooksCollectionName);
}
public async Task<List<Book>> GetBookAsync()
{
return await _booksCollection.Find(_ => true).ToListAsync();
}
public async Task<Book?> GetBookByIdAsync(string id)
{
return await _booksCollection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
public async Task CreateBookAsync(Book newBook)
{
await _booksCollection.InsertOneAsync(newBook);
}
public async Task UpdateBookAsync(string id, Book updatedBook)
{
await _booksCollection.ReplaceOneAsync(x => x.Id == id, updatedBook);
}
public async Task RemoveBookAsync(string id)
{
await _booksCollection.DeleteOneAsync(x => x.Id == id);
}
}
LibraryService.cs
Это просто использование драйвера mongo для внесения изменений.
Если вы когда-нибудь измените схему модели, все будет в порядке, так как сервис просто делает мост между программой и базой данных.
Служба пользователей будет иметь очень похожую структуру:
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using WebApplicationCRUDExample.Models;
using WebApplicationCRUDExample.Services.DB;
namespace WebApplicationCRUDExample.Services;
public class UserService
{
private readonly IMongoCollection<User> _usersCollection;
public UserService(
IOptions<MongoDBSettings> library)
{
var mongoClient = new MongoClient(
library.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(
library.Value.DataBaseName);
_usersCollection = mongoDatabase.GetCollection<User>(
library.Value.UsersCollectionName);
}
public async Task<List<User>> GetUserAsync()
{
return await _usersCollection.Find(_ => true).ToListAsync();
}
public async Task<User?> GetUserByIdAsync(string id)
{
return await _usersCollection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
public async Task CreateUserAsync(User newUser)
{
await _usersCollection.InsertOneAsync(newUser);
}
public async Task UpdateUserAsync(string id, User updatedUser)
{
await _usersCollection.ReplaceOneAsync(x => x.Id == id, updatedUser);
}
public async Task RemoveUserAsync(string id)
{
await _usersCollection.DeleteOneAsync(x => x.Id == id);
}
}
UserService.cs
Поскольку каждый сервис используется с инъекцией зависимостей контроллера, необходимо объявить его и в билдере, я сделал это следующим образом в Program.cs :
builder.Services.AddSingleton<LibraryService>();
builder.Services.AddSingleton<UserService>();
Program.cs
Создание контроллеров
Мы создали банк, мост между банком и программой, теперь мы собираемся создать переднюю часть программы, ту часть, которая соединяется с внешним миром. Контроллер библиотеки будет выглядеть следующим образом:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WebApplicationCRUDExample.Models;
using WebApplicationCRUDExample.Services;
namespace WebApplicationCRUDExample.Controllers;
[ApiController]
[Route("api/[controller]")]
public class LibraryController : Controller
{
private readonly LibraryService _libraryService;
public LibraryController(LibraryService libraryService)
{
_libraryService = libraryService;
}
[HttpGet("/books")]
[Authorize] // já irei explicar o que é authorize
public async Task<List<Book>> GetBooks()
{
return await _libraryService.GetBookAsync();
}
[HttpGet("/books/{id:length(24)}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<Book>> GetBookById(string id)
{
var book = await _libraryService.GetBookByIdAsync(id);
if (book is null) return NotFound();
return book;
}
[HttpPost("/books/")]
[Authorize]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> PostBook(Book book)
{
await _libraryService.CreateBookAsync(book);
return CreatedAtAction(nameof(GetBookById), new {id = book.Id}, book);
}
[HttpPut("/books/{id:length(24)}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> UpdateBook(string id, Book updatedBook)
{
var oldBook = await _libraryService.GetBookByIdAsync(id);
if (oldBook is null) return NotFound();
await _libraryService.UpdateBookAsync(id, updatedBook);
return NoContent();
}
[HttpDelete("/books/{id:length(24)}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> DeleteBook(string id)
{
var book = await _libraryService.GetBookByIdAsync(id);
if (book is null) return NotFound();
await _libraryService.RemoveBookAsync(id);
return NoContent();
}
}
LibraryController.cs
Если вы можете заметить, в этом api мы используем маршрут api/books/…. Поскольку это грубый процесс без бизнес-правил, цель состоит в том, чтобы быть как можно более простым.
Что можно поставить под сомнение, так это функцию CreatedAtAction(nameof(GetBookyId), new {id = book.Id}, book), которая дает get при постинге.
Потому что драйвер C# mongo не имеет возможности возвращать новую запись.
Контроллер пользователя также очень похож:
using Microsoft.AspNetCore.Mvc;
using WebApplicationCRUDExample.Models;
using WebApplicationCRUDExample.Services;
namespace WebApplicationCRUDExample.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UserController : Controller
{
private readonly UserService _userService;
private readonly LibraryService _libraryService;
public UserController(UserService userService, LibraryService libraryService)
{
_userService = userService;
_libraryService = libraryService;
}
[HttpGet("/users/")]
public async Task<List<User>> GetUsers()
{
return await _userService.GetUserAsync();
}
[HttpGet("/users/{id:length(24)}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<User>> GetUserById(string id)
{
var user = await _userService.GetUserByIdAsync(id);
if (user is null) return NotFound();
return user;
}
[HttpGet("/users/{id:length(24)}/likes")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<List<Book>>> GetUserLikes(string id)
{
var user = await _userService.GetUserByIdAsync(id);
var bookList = new List<Book>();
if (user is null) return NotFound();
if (user.UserLikes is null) return BadRequest();
foreach (var bookId in user.UserLikes)
{
var book = await _libraryService.GetBookByIdAsync(bookId);
if (book is not null) bookList.Add(book);
}
return bookList;
}
[HttpPost("/users/")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> PostUser(User user)
{
await _userService.CreateUserAsync(user);
return CreatedAtAction(nameof(GetUserById), new {id = user.Id}, user);
}
[HttpPut("/users/{id:length(24)}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> UpdateUser(string id, User updatedUser)
{
var oldUser = await _userService.GetUserByIdAsync(id);
if (oldUser is null) return NotFound();
await _userService.UpdateUserAsync(id, updatedUser);
return NoContent();
}
[HttpDelete("/users/{id:length(24)}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> DeleteUser(string id)
{
var user = await _userService.GetUserByIdAsync(id);
if (user is null) return NotFound();
await _userService.RemoveUserAsync(id);
return NoContent();
}
}
UserController.cs
Если вы не используете декоратор [Authorize] в Library Controller, вы уже можете протестировать его, а также увидеть, что отношения «нравится пользователю» работают, если вы передаете ID книги, пользователь может «понравиться» другим книгам.
Как установить аутентификацию с помощью JWT в моих сервисах?
Для создания Auth необходимо загрузить пакеты Nuget
dotnet add package Microsoft.AspNetCore.Authentication
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
После этого создайте ключ, который будет паролем шифрования для вашего JWT. Для этого рекомендуется использовать appsettings.json, но чтобы показать другой способ, который также возможен для конфигурации вашего приложения, мы будем использовать статический шаблон класса Settings.
В нашем случае класс будет выглядеть следующим образом:
public static class Settings
{
public static string Secret = "FeWENgwGTUe2vz5Vtfnc64MrwkeNM56D";
}
Теперь, чтобы использовать его, мы можем сделать это, как в данном случае, в нашей службе Static Auth:
public static class AuthService
{
public static string GenerateToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(Settings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Name),
}),
Expires = DateTime.UtcNow.AddHours(24),
SigningCredentials =
new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
Поскольку это статический сервис, нам не нужно добавлять его в Program.cs, но нам нужно добавить в swagger возможность поместить предъявителя в наш тестовый заголовок.
Необходимо будет добавить некоторые настройки в ваш Program.cs.
Чтобы выровнять этот файл Program.cs, следуйте приведенным ниже инструкциям:
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using WebApplicationCRUDExample;
using WebApplicationCRUDExample.Services;
using WebApplicationCRUDExample.Services.DB;
#region Builder
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.Configure<MongoDBSettings>(
builder.Configuration.GetSection("LibraryDatabase"));
builder.Services.AddSingleton<LibraryService>();
builder.Services.AddSingleton<UserService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(setup =>
{
// Include 'SecurityScheme' to use JWT Authentication
var jwtSecurityScheme = new OpenApiSecurityScheme
{
Scheme = "bearer",
BearerFormat = "JWT",
Name = "JWT Authentication",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Description = "Put **_ONLY_** your JWT Bearer token on textbox below!",
Reference = new OpenApiReference
{
Id = JwtBearerDefaults.AuthenticationScheme,
Type = ReferenceType.SecurityScheme
}
};
setup.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme);
setup.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{jwtSecurityScheme, Array.Empty<string>()}
});
});
var key = Encoding.ASCII.GetBytes(Settings.Secret);
builder.Services
.AddAuthentication(auth =>
{
auth.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(bearer =>
{
bearer.RequireHttpsMetadata = false;
bearer.SaveToken = true;
bearer.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
}
);
#endregion
#region App
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
#endregion
Наконец, чтобы использовать auth, нам просто нужно создать контроллер входа, в котором мы сможем пройти весь этот процесс:
using Microsoft.AspNetCore.Mvc;
using WebApplicationCRUDExample.Services;
namespace WebApplicationCRUDExample.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : Controller
{
private readonly UserService _userService;
public AuthController(UserService userService)
{
_userService = userService;
}
[HttpPost("/auth/")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<dynamic>> Authenticate([FromBody] string id)
{
// Obtem esse id via email/hash e usa para logar o user;
var user = await _userService.GetUserByIdAsync(id);
if (user is null) return NotFound();
var token = AuthService.GenerateToken(user);
return new
{
user, token
};
}
}
В данном случае, поскольку я был только PoC, я оставил в качестве формы входа, Id’s hit, это форма, используемая в приложениях, где для входа пользователю необходимо ввести свой email и выполнить Two-factor.
Но это легко можно было бы сделать обычным способом с помощью электронной почты и пароля.
Использование авторизации
Теперь самое интересное, так как проект настроен и у нас уже есть маршрут аутентификации, мы можем использовать [Authorize] в маршрутах, которые должны быть аутентифицированы, если пользователь не является таковым по умолчанию, .NET вернет сообщение not authorized, как в следующем примере:
[HttpGet("/books/{id:length(24)}")]
[Authorize] // precisa estar logado
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<Book>> GetBookById(string id)
{
var book = await _libraryService.GetBookByIdAsync(id);
if (book is null) return NotFound();
return book;
}
Очень круто, не правда ли?
Всю эту часть авторизации я сделал, следуя этому очень хорошему руководству, предоставленному Андре Балтьери в его блоге. Если вы хотите посмотреть первоисточник, то он находится здесь
Следующие шаги
В этом проекте, где я буду продолжать объяснять и писать о некоторых чудесах C#, я в следующий раз немного расскажу о тестах в нашем crud и о том, как мы можем делать тесты.