Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Гнидец, Шаров, Городничая #39

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Tests/Task5_DeleteUserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public async Task Test4_Code404_WhenAlreadyCreatedAndDeleted()
lastName = "Condenado"
});

DeleteUser(createdUserId);
await DeleteUser(createdUserId);

var request = new HttpRequestMessage();
request.Method = HttpMethod.Delete;
Expand Down
2 changes: 1 addition & 1 deletion Tests/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="nunit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnitLite" Version="4.1.0" />
</ItemGroup>

Expand Down
4 changes: 2 additions & 2 deletions Tests/UsersApiTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ protected async Task<string> CreateUser(object user)
return createdUserId;
}

protected void DeleteUser(string userId)
protected async Task DeleteUser(string userId)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Delete;
request.RequestUri = BuildUsersByIdUri(userId);
request.Headers.Add("Accept", "*/*");
var response = HttpClient.Send(request);
var response = await HttpClient.SendAsync(request);

response.StatusCode.Should().Be(HttpStatusCode.NoContent);
response.ShouldNotHaveHeader("Content-Type");
Expand Down
8 changes: 8 additions & 0 deletions WebApi.MinimalApi/Controllers/HttpConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace WebApi.MinimalApi.Controllers;

public static class HttpConstants
{
public const string ContentTypeJsonHeader = "application/json; charset=utf-8";

public const string AllowHeader = "Allow";
}
232 changes: 225 additions & 7 deletions WebApi.MinimalApi/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,245 @@
using Microsoft.AspNetCore.Mvc;
using AutoMapper;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.Annotations;
using WebApi.MinimalApi.Domain;
using WebApi.MinimalApi.Models;

namespace WebApi.MinimalApi.Controllers;

[Route("api/[controller]")]
[ApiController]
[Produces("application/json", "application/xml")]
public class UsersController : Controller
{
// Чтобы ASP.NET положил что-то в userRepository требуется конфигурация
public UsersController(IUserRepository userRepository)
private readonly IUserRepository userRepository;
private readonly IMapper mapper;
private readonly LinkGenerator linkGenerator;
private static readonly string[] UsersAllowedMethods = { "POST", "GET", "OPTIONS" };

public UsersController(IUserRepository userRepository, IMapper mapper,
LinkGenerator linkGenerator)
{
this.userRepository = userRepository ??
throw new ArgumentException("Null reference", nameof(userRepository));
this.mapper = mapper;
this.linkGenerator = linkGenerator;
}

[HttpGet("{userId}")]
/// <summary>
/// Получить пользователя
/// </summary>
/// <param name="userId">Идентификатор пользователя</param>
[HttpHead("{userId}")]
[HttpGet("{userId}", Name = nameof(GetUserById))]
[SwaggerResponse(200, "OK", typeof(UserDto))]
[SwaggerResponse(404, "Пользователь не найден")]
public ActionResult<UserDto> GetUserById([FromRoute] Guid userId)
{
throw new NotImplementedException();
var user = userRepository.FindById(userId);

if (user is null)
{
return NotFound();
}

if (!HttpContext.Request.Method.Equals("head", StringComparison.OrdinalIgnoreCase))
{
return Ok(mapper.Map<UserDto>(user));
}

Response.ContentType = HttpConstants.ContentTypeJsonHeader;
return Ok();
}

/// <summary>
/// Создать пользователя
/// </summary>
/// <remarks>
/// Пример запроса:
///
/// POST /api/users
/// {
/// "login": "johndoe375",
/// "firstName": "John",
/// "lastName": "Doe"
/// }
///
/// </remarks>
/// <param name="user">Данные для создания пользователя</param>
[HttpPost]
public IActionResult CreateUser([FromBody] object user)
[Consumes("application/json")]
[SwaggerResponse(201, "Пользователь создан")]
[SwaggerResponse(400, "Некорректные входные данные")]
[SwaggerResponse(422, "Ошибка при проверке")]
public IActionResult CreateUser([FromBody] CreateUserRequest? user)
{
if (user is null)
{
return BadRequest();
}

if (!ModelState.IsValid)
{
return UnprocessableEntity(ModelState);
}

if (!user.Login.All(char.IsLetterOrDigit))
{
ModelState.AddModelError(nameof(user.Login), "Login has invalid chars");
return UnprocessableEntity(ModelState);
}

var entity = userRepository.Insert(mapper.Map<UserEntity>(user));


return CreatedAtRoute(
nameof(GetUserById),
new { userId = entity.Id },
entity.Id);
}

/// <summary>
/// Обновить пользователя
/// </summary>
/// <param name="userId">Идентификатор пользователя</param>
/// <param name="user">Обновленные данные пользователя</param>
[HttpPut("{userId}")]
[Consumes("application/json")]
[SwaggerResponse(201, "Пользователь создан")]
[SwaggerResponse(204, "Пользователь обновлен")]
[SwaggerResponse(400, "Некорректные входные данные")]
[SwaggerResponse(422, "Ошибка при проверке")]
public IActionResult UpdateUser([FromBody] UpdateUserRequest? user, [FromRoute] Guid userId)
{
if (user is null || userId == Guid.Empty)
{
return BadRequest();
}

if (!ModelState.IsValid)
{
return UnprocessableEntity(ModelState);
}

var entity = mapper.Map(new UserEntity(userId), mapper.Map<UserEntity>(user));
userRepository.UpdateOrInsert(entity, out var isInserted);
if (isInserted)
{
return CreatedAtRoute(
nameof(GetUserById),
new { userId = entity.Id },
entity.Id);
}

return NoContent();
}

/// <summary>
/// Частично обновить пользователя
/// </summary>
/// <param name="userId">Идентификатор пользователя</param>
/// <param name="patchDoc">JSON Patch для пользователя</param>
[HttpPatch("{userId}")]
[Consumes("application/json-patch+json")]
[SwaggerResponse(204, "Пользователь обновлен")]
[SwaggerResponse(400, "Некорректные входные данные")]
[SwaggerResponse(404, "Пользователь не найден")]
[SwaggerResponse(422, "Ошибка при проверке")]
public IActionResult PatchUser([FromBody] JsonPatchDocument<PatchUserRequest>? patchDoc, [FromRoute] Guid userId)
{
if (patchDoc is null)
{
return BadRequest();
}

if (userId == Guid.Empty)
{
return NotFound();
}

var patch = new PatchUserRequest();
patchDoc.ApplyTo(patch);

if (!TryValidateModel(patch) || !ModelState.IsValid)
{
return UnprocessableEntity(ModelState);
}

var userEntity = mapper.Map(new UserEntity(userId), mapper.Map<UserEntity>(patch));
userRepository.Update(userEntity);
if (userRepository.FindById(userId) is null)
{
return NotFound();
}

return NoContent();
}

/// <summary>
/// Удалить пользователя
/// </summary>
/// <param name="userId">Идентификатор пользователя</param>
[HttpDelete("{userId}")]
[SwaggerResponse(204, "Пользователь удален")]
[SwaggerResponse(404, "Пользователь не найден")]
public IActionResult DeleteUser([FromRoute] Guid userId)
{
if (userId == Guid.Empty)
{
return NotFound();
}

if (userRepository.FindById(userId) == null)
{
return NotFound();
}

userRepository.Delete(userId);
return NoContent();
}

/// <summary>
/// Получить пользователей
/// </summary>
/// <param name="pageNumber">Номер страницы, по умолчанию 1</param>
/// <param name="pageSize">Размер страницы, по умолчанию 20</param>
/// <response code="200">OK</response>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<UserDto>), 200)]
public IActionResult GetUsers([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
pageNumber = pageNumber < 1 ? 1 : pageNumber;
pageSize = pageSize < 1 ? 1 : pageSize > 20 ? 20 : pageSize;

var pageList = userRepository.GetPage(pageNumber, pageSize);
var users = mapper.Map<IList<UserDto>>(pageList);
var paginationHeader = new
{
previousPageLink = pageList.HasPrevious
? linkGenerator.GetUriByRouteValues(HttpContext, "", new { pageNumber = pageNumber - 1, pageSize })
: null,
nextPageLink = pageList.HasNext
? linkGenerator.GetUriByRouteValues(HttpContext, "", new { pageNumber = pageNumber + 1, pageSize })
: null,
totalCount = pageList.TotalCount,
pageSize = pageList.PageSize,
currentPage = pageList.CurrentPage,
totalPages = pageList.TotalPages,
};
Response.Headers.Append("X-Pagination", JsonConvert.SerializeObject(paginationHeader));
return Ok(users);
}

/// <summary>
/// Опции по запросам о пользователях
/// </summary>
[HttpOptions]
[SwaggerResponse(200, "OK")]
public IActionResult GetUsersOptions()
{
throw new NotImplementedException();
Response.Headers.Append(HttpConstants.AllowHeader, UsersAllowedMethods);
return Ok();
}
}
7 changes: 7 additions & 0 deletions WebApi.MinimalApi/Domain/ServiceCollectionExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace WebApi.MinimalApi.Domain;

public static class ServiceCollectionExtension
{
public static IServiceCollection AddRepositories(this IServiceCollection services) =>
services.AddSingleton<IUserRepository, InMemoryUserRepository>();
}
20 changes: 20 additions & 0 deletions WebApi.MinimalApi/Models/ApiDtosMappingProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using AutoMapper;
using WebApi.MinimalApi.Domain;

namespace WebApi.MinimalApi.Models;

public class ApiDtosMappingProfile : Profile
{
public ApiDtosMappingProfile()
{
CreateMap<UserEntity, UserDto>()
.ForMember(x => x.FullName,
x => x.MapFrom(entity => $"{entity.LastName} {entity.FirstName}"));

CreateMap<CreateUserRequest, UserEntity>();

CreateMap<UpdateUserRequest, UserEntity>();

CreateMap<PatchUserRequest, UserEntity>();
}
}
16 changes: 16 additions & 0 deletions WebApi.MinimalApi/Models/CreateUserRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace WebApi.MinimalApi.Models;

public class CreateUserRequest
{
[Required]
public string Login { get; set; } = default!;

[DefaultValue("John")]
public string FirstName { get; set; } = default!;

[DefaultValue("Doe")]
public string LastName { get; set; } = default!;
}
16 changes: 16 additions & 0 deletions WebApi.MinimalApi/Models/PatchUserRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;

namespace WebApi.MinimalApi.Models;

public class PatchUserRequest
{
[Required]
[RegularExpression("^[0-9\\p{L}]*$", ErrorMessage = "Login should contain only letters or digits")]
public string Login { get; set; } = default!;

[Required]
public string FirstName { get; set; } = default!;

[Required]
public string LastName { get; set; } = default!;
}
16 changes: 16 additions & 0 deletions WebApi.MinimalApi/Models/UpdateUserRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;

namespace WebApi.MinimalApi.Models;

public class UpdateUserRequest
{
[Required]
[RegularExpression("^[0-9\\p{L}]*$", ErrorMessage = "Login should contain only letters or digits")]
public string Login { get; set; } = default!;

[Required]
public string FirstName { get; set; } = default!;

[Required]
public string LastName { get; set; } = default!;
}
6 changes: 5 additions & 1 deletion WebApi.MinimalApi/Models/UserDto.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
namespace WebApi.MinimalApi.Models;

public class UserDto
{
{
public Guid Id { get; set; }

public string Login { get; set; }

public string FullName { get; set; }

public int GamesPlayed { get; set; }

public Guid? CurrentGameId { get; set; }
}
Loading