diff --git a/Tests/Task5_DeleteUserTests.cs b/Tests/Task5_DeleteUserTests.cs
index 2851605..168ca5e 100644
--- a/Tests/Task5_DeleteUserTests.cs
+++ b/Tests/Task5_DeleteUserTests.cs
@@ -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;
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index b2a360a..c53e3a7 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/Tests/UsersApiTestsBase.cs b/Tests/UsersApiTestsBase.cs
index 06705c5..6bb853b 100644
--- a/Tests/UsersApiTestsBase.cs
+++ b/Tests/UsersApiTestsBase.cs
@@ -96,13 +96,13 @@ protected async Task 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");
diff --git a/WebApi.MinimalApi/Controllers/UsersController.cs b/WebApi.MinimalApi/Controllers/UsersController.cs
index e6720ca..4f62568 100644
--- a/WebApi.MinimalApi/Controllers/UsersController.cs
+++ b/WebApi.MinimalApi/Controllers/UsersController.cs
@@ -1,6 +1,12 @@
-using Microsoft.AspNetCore.Mvc;
+using System.Net.Mime;
+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;
+using WebApi.MinimalApi.Samples;
namespace WebApi.MinimalApi.Controllers;
@@ -8,20 +14,202 @@ namespace WebApi.MinimalApi.Controllers;
[ApiController]
public class UsersController : Controller
{
+ private readonly IUserRepository userRepository;
+ private readonly IMapper mapper;
+ private readonly LinkGenerator linkGenerator;
+ private ISwaggerDescriptionsForUsersController swaggerDescriptionsForUsersControllerImplementation;
+
// Чтобы ASP.NET положил что-то в userRepository требуется конфигурация
- public UsersController(IUserRepository userRepository)
+ public UsersController(IUserRepository userRepository, IMapper mapper, LinkGenerator linkGenerator)
{
+ this.userRepository = userRepository;
+ this.mapper = mapper;
+ this.linkGenerator = linkGenerator;
}
- [HttpGet("{userId}")]
- public ActionResult GetUserById([FromRoute] Guid userId)
+ [HttpGet("{userId:guid}", Name = nameof(GetUserById))]
+ [HttpHead("{userId}")]
+ [Produces("application/json", "application/xml")]
+ [SwaggerResponse(200, "OK", typeof(UserDto))]
+ [SwaggerResponse(404, "Пользователь не найден")]
+ public ActionResult GetUserById(Guid userId)
{
- throw new NotImplementedException();
+ var user = userRepository.FindById(userId);
+ if (user is null)
+ return NotFound();
+
+ return Ok(mapper.Map(user));
}
[HttpPost]
- public IActionResult CreateUser([FromBody] object user)
+ [Consumes("application/json")]
+ [Produces("application/json", "application/xml")]
+ [SwaggerResponse(201, "Пользователь создан")]
+ [SwaggerResponse(400, "Некорректные входные данные")]
+ [SwaggerResponse(422, "Ошибка при проверке")]
+ public IActionResult CreateUser([FromBody] UserToCreateDto user)
+ {
+ if (user is null)
+ {
+ return BadRequest();
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return UnprocessableEntity(ModelState);
+ }
+
+ var userEntity = userRepository.Insert(mapper.Map(user));
+
+ return CreatedAtRoute(
+ nameof(GetUserById),
+ new { userId = userEntity.Id },
+ userEntity.Id);
+ }
+
+ [HttpPut("{userId}")]
+ [Consumes("application/json")]
+ [Produces("application/json", "application/xml")]
+ [SwaggerResponse(201, "Пользователь создан")]
+ [SwaggerResponse(204, "Пользователь обновлен")]
+ [SwaggerResponse(400, "Некорректные входные данные")]
+ [SwaggerResponse(422, "Ошибка при проверке")]
+ public IActionResult UpdateUser(UserToUpdateDto? userToUpdateDto, string userId)
+ {
+ if (!Guid.TryParse(userId, out var userIdGuid) || userToUpdateDto is null)
+ {
+ return BadRequest();
+ }
+ if (!ModelState.IsValid)
+ {
+ return UnprocessableEntity(ModelState);
+ }
+
+ var userEntity = mapper.Map(userToUpdateDto, new UserEntity(userIdGuid));
+
+ userRepository.UpdateOrInsert(userEntity, out var isInserted);
+
+ return isInserted
+ ? CreatedAtRoute(
+ nameof(GetUserById),
+ new { userId = userEntity.Id },
+ userEntity.Id)
+ : NoContent();
+ }
+
+ [HttpPatch("{userId:guid}")]
+ [Consumes("application/json-patch+json")]
+ [Produces("application/json", "application/xml")]
+ [SwaggerResponse(204, "Пользователь обновлен")]
+ [SwaggerResponse(400, "Некорректные входные данные")]
+ [SwaggerResponse(404, "Пользователь не найден")]
+ [SwaggerResponse(422, "Ошибка при проверке")]
+ public IActionResult PartiallyUpdateUser([FromBody] JsonPatchDocument patchDoc, Guid userId)
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest();
+ }
+
+ var user = userRepository.FindById(userId);
+
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ var userToUpdateDto = mapper.Map(user);
+
+
+ patchDoc.ApplyTo(userToUpdateDto, ModelState);
+ TryValidateModel(userToUpdateDto);
+
+ if (!ModelState.IsValid)
+ {
+ return UnprocessableEntity(ModelState);
+ }
+
+ var updatedUserEntity = mapper.Map(userToUpdateDto, new UserEntity(userId));
+ userRepository.Update(updatedUserEntity);
+
+ return NoContent();
+ }
+
+ [HttpDelete("{userId:guid}")]
+ [Produces("application/json", "application/xml")]
+ [SwaggerResponse(204, "Пользователь удален")]
+ [SwaggerResponse(404, "Пользователь не найден")]
+ public IActionResult DeleteUser(Guid userId)
+ {
+ if (userRepository.FindById(userId) is null)
+ {
+ return NotFound();
+ }
+
+ userRepository.Delete(userId);
+
+ return NoContent();
+ }
+
+ [HttpHead("{userId:guid}")]
+ [HttpHead("{userId}")]
+ [Produces("application/json", "application/xml")]
+ [SwaggerResponse(200, "OK", typeof(UserDto))]
+ [SwaggerResponse(404, "Пользователь не найден")]
+ public IActionResult Head(Guid userId)
+ {
+ var user = userRepository.FindById(userId);
+ if (user is null)
+ return NotFound();
+
+ HttpContext.Response.ContentType = "application/json; charset=utf-8";
+ return Ok();
+ }
+
+ [HttpGet(Name = nameof(GetUsers))]
+ [Produces("application/json", "application/xml")]
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public IActionResult GetUsers([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
- throw new NotImplementedException();
+ if (pageNumber < 1) pageNumber = 1;
+ if (pageSize < 1) pageSize = 1;
+ if (pageSize > 20) pageSize = 20;
+
+ var pageList = userRepository.GetPage(pageNumber, pageSize);
+ var users = mapper.Map>(pageList);
+
+ var totalCount = pageList.TotalCount;
+ var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
+
+ var paginationHeader = new
+ {
+ previousPageLink = pageNumber > 1 ? linkGenerator
+ .GetUriByRouteValues(
+ HttpContext,
+ "GetUsers",
+ new
+ {
+ pageNumber = pageNumber - 1,
+ pageSize
+ })
+ : null,
+ nextPageLink = linkGenerator.GetUriByRouteValues(HttpContext, "GetUsers", new { pageNumber = pageNumber + 1, pageSize }),
+ totalCount = totalCount,
+ pageSize = pageSize,
+ currentPage = pageNumber,
+ totalPages = totalPages
+ };
+
+ Response.Headers.Append("X-Pagination", JsonConvert.SerializeObject(paginationHeader));
+ return Ok(users);
+ }
+
+ [HttpOptions]
+ [SwaggerResponse(200, "OK")]
+ public IActionResult Options()
+ {
+ Response.Headers.Append("Allow", "POST, GET, OPTIONS");
+
+ return Ok();
}
}
\ No newline at end of file
diff --git a/WebApi.MinimalApi/Models/UserToCreateDto.cs b/WebApi.MinimalApi/Models/UserToCreateDto.cs
new file mode 100644
index 0000000..0ac6002
--- /dev/null
+++ b/WebApi.MinimalApi/Models/UserToCreateDto.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+
+namespace WebApi.MinimalApi.Models;
+
+public class UserToCreateDto
+{
+ [Required]
+ [RegularExpression("^[0-9\\p{L}]*$", ErrorMessage = "Login should contain only letters or digits")]
+ public string Login { get; set; }
+
+ [DefaultValue("John")]
+ public string FirstName { get; set; }
+
+ [DefaultValue("Doe")]
+ public string LastName { get; set; }
+}
\ No newline at end of file
diff --git a/WebApi.MinimalApi/Models/UserToUpdateDto.cs b/WebApi.MinimalApi/Models/UserToUpdateDto.cs
new file mode 100644
index 0000000..7d9120a
--- /dev/null
+++ b/WebApi.MinimalApi/Models/UserToUpdateDto.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+
+namespace WebApi.MinimalApi.Models;
+
+public class UserToUpdateDto
+{
+ [Required]
+ [RegularExpression("^[0-9\\p{L}]*$", ErrorMessage = "Login should contain only letters or digits")]
+ public string Login { get; set; }
+
+ [Required]
+ public string FirstName { get; set; }
+
+ [Required]
+ public string LastName { get; set; }
+}
\ No newline at end of file
diff --git a/WebApi.MinimalApi/Program.cs b/WebApi.MinimalApi/Program.cs
index e824d81..6846798 100644
--- a/WebApi.MinimalApi/Program.cs
+++ b/WebApi.MinimalApi/Program.cs
@@ -1,11 +1,50 @@
+using System.Buffers;
+using System.Reflection;
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using WebApi.MinimalApi.Domain;
+using WebApi.MinimalApi.Models;
+
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://localhost:5000");
-builder.Services.AddControllers()
+builder.Services.AddControllers(options =>
+ {
+ // Этот OutputFormatter позволяет возвращать данные в XML, если требуется.
+ options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
+ options.OutputFormatters.Insert(0, new
+ NewtonsoftJsonOutputFormatter(new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver()
+ }, ArrayPool.Shared, options));
+ // Эта настройка позволяет отвечать кодом 406 Not Acceptable на запросы неизвестных форматов.
+ options.ReturnHttpNotAcceptable = true;
+ // Эта настройка приводит к игнорированию заголовка Accept, когда он содержит */*
+ // Здесь она нужна, чтобы в этом случае ответ возвращался в формате JSON
+ options.RespectBrowserAcceptHeader = true;
+ })
.ConfigureApiBehaviorOptions(options => {
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
+ })
+ .AddNewtonsoftJson(options =>
+ {
+ options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
+ options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Populate;
});
+builder.Services.AddAutoMapper(cfg =>
+{
+ cfg.CreateMap()
+ .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.LastName} {src.FirstName}"));
+
+ cfg.CreateMap();
+ cfg.CreateMap();
+ cfg.CreateMap();
+}, Array.Empty());
+
+builder.Services.AddSingleton();
+
var app = builder.Build();
app.MapControllers();
diff --git a/WebApi.MinimalApi/WebApi.MinimalApi.csproj b/WebApi.MinimalApi/WebApi.MinimalApi.csproj
index 2089def..b73d4c9 100644
--- a/WebApi.MinimalApi/WebApi.MinimalApi.csproj
+++ b/WebApi.MinimalApi/WebApi.MinimalApi.csproj
@@ -21,6 +21,7 @@
+
diff --git a/web-api.sln.DotSettings b/web-api.sln.DotSettings
index b5ef7ae..3744498 100644
--- a/web-api.sln.DotSettings
+++ b/web-api.sln.DotSettings
@@ -19,6 +19,10 @@
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy>
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" />
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
@@ -69,4 +73,5 @@
True
True
True
+ True
True
\ No newline at end of file