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

ФТ-403 Протасов/Медведевский #47

Open
wants to merge 1 commit 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
14 changes: 7 additions & 7 deletions Tests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ public static void Main()
var testsToRun = new string[]
{
typeof(Task1_GetUserByIdTests).FullName,
//typeof(Task2_CreateUserTests).FullName,
//typeof(Task3_UpdateUserTests).FullName,
//typeof(Task4_PartiallyUpdateUserTests).FullName,
//typeof(Task5_DeleteUserTests).FullName,
//typeof(Task6_HeadUserByIdTests).FullName,
//typeof(Task7_GetUsersTests).FullName,
//typeof(Task8_GetUsersOptionsTests).FullName,
typeof(Task2_CreateUserTests).FullName,
typeof(Task3_UpdateUserTests).FullName,
typeof(Task4_PartiallyUpdateUserTests).FullName,
typeof(Task5_DeleteUserTests).FullName,
typeof(Task6_HeadUserByIdTests).FullName,
typeof(Task7_GetUsersTests).FullName,
typeof(Task8_GetUsersOptionsTests).FullName,
};
new AutoRun().Execute(new[]
{
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 void 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
169 changes: 161 additions & 8 deletions WebApi.MinimalApi/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using AutoMapper;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using WebApi.MinimalApi.Domain;
using WebApi.MinimalApi.Models;

Expand All @@ -8,20 +14,167 @@ namespace WebApi.MinimalApi.Controllers;
[ApiController]
public class UsersController : Controller
{
// Чтобы ASP.NET положил что-то в userRepository требуется конфигурация
public UsersController(IUserRepository userRepository)
private readonly IUserRepository userRepository;
private readonly IMapper mapper;
private readonly LinkGenerator linkGenerator;
public UsersController(IUserRepository userRepository, IMapper mapper, LinkGenerator linkGenerator)
{
this.userRepository = userRepository;
this.mapper = mapper;
this.linkGenerator = linkGenerator;
}

[HttpGet("{userId}")]
[HttpHead("{userId}")]
[HttpGet("{userId}", Name = nameof(GetUserById))]
[Produces("application/json", "application/xml")]
public ActionResult<UserDto> GetUserById([FromRoute] Guid userId)
{
throw new NotImplementedException();
var possibleUser = userRepository.FindById(userId);

if (possibleUser == null)
return NotFound();

if (HttpContext.Request.Method == HttpMethods.Head)
{
HttpContext.Response.Headers.Add("Content-Type", "application/json; charset=utf-8");
return Ok();
}

var userDto = mapper.Map<UserDto>(possibleUser);
return Ok(userDto);
}

[HttpGet(Name = nameof(GetUsers))]
[Produces("application/json", "application/xml")]
public ActionResult<IEnumerable<UserDto>> GetUsers(
[FromQuery] [Range(1, int.MaxValue)] [DefaultValue(1)]
int pageNumber,
[FromQuery] [Range(1, 20)] [DefaultValue(10)]
int pageSize)
{
if (ModelState.GetFieldValidationState("pageNumber") == ModelValidationState.Invalid) pageNumber = 1;

if (ModelState.GetFieldValidationState("pageSize") == ModelValidationState.Invalid)
{
if (pageSize < 1) pageSize = 1;
else if (pageSize > 20) pageSize = 20;
}


var pageList = userRepository.GetPage(pageNumber, pageSize);
var users = mapper.Map<IEnumerable<UserDto>>(pageList);

var paginationHeader = new
{
previousPageLink = pageList.CurrentPage == 1
? null
: linkGenerator.GetUriByRouteValues(HttpContext, nameof(GetUsers), new { pageNumber = pageNumber - 1, pageSize }),
nextPageLink = pageList.CurrentPage == pageList.TotalPages
? null
: linkGenerator.GetUriByRouteValues(HttpContext, nameof(GetUsers), new { pageNumber = pageNumber + 1, pageSize }),
totalCount = pageList.TotalCount,
pageSize = pageList.PageSize,
currentPage = pageList.CurrentPage,
totalPages = pageList.TotalPages,
};

Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(paginationHeader));

return Ok(users);
}

[HttpPost]
public IActionResult CreateUser([FromBody] object user)
[Produces("application/json", "application/xml")]
public IActionResult CreateUser([FromBody] CreateUserDto user)
{
throw new NotImplementedException();
if (user == null)
return BadRequest();
if (user.Login == null)
{
return UnprocessableEntity(ModelState);
}

if (user.Login != null && !user.Login.All(char.IsLetterOrDigit))
ModelState.AddModelError("Login", "Login must contain only characters or digits");

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

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

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

[HttpPut("{userId}")]
[Produces("application/json", "application/xml")]
public IActionResult UpdateUser([FromRoute] Guid userId, UpdateUserDto user)
{
if (user == null || userId == Guid.Empty)
return BadRequest();

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

var userEntity = mapper.Map(user, new UserEntity(userId));

bool isInsert;
userRepository.UpdateOrInsert(userEntity, out isInsert);

if (isInsert)
return CreatedAtRoute(
nameof(GetUserById),
new { userId = userId },
userId);
return NoContent();
}

[HttpPatch("{userId}")]
[Produces("application/json", "application/xml")]
public IActionResult PartiallyUpdateUser([FromRoute] Guid userId, [FromBody] JsonPatchDocument<UpdateUserDto> patchDoc)
{
if (patchDoc == null)
return BadRequest();

var user = userRepository.FindById(userId);

if (user == null)
return NotFound();

var updateUserDto = mapper.Map<UpdateUserDto>(user);

patchDoc.ApplyTo(updateUserDto, ModelState);

TryValidateModel(updateUserDto);

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

return NoContent();
}

[HttpDelete("{userId}")]
public IActionResult DeleteUser([FromRoute] Guid userId)
{
var user = userRepository.FindById(userId);

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

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

[HttpOptions]
public IActionResult GetAllMethods()
{
Response.Headers.Add("Allow", new[] { "GET", "POST", "OPTIONS" });

return Ok();
}
}

14 changes: 14 additions & 0 deletions WebApi.MinimalApi/Models/CreateUserDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace WebApi.MinimalApi.Models;

public class CreateUserDto
{
[Required]
public string Login { get; set; }
[DefaultValue("John")]
public string FirstName { get; set; }
[DefaultValue("John")]
public string LastName { get; set; }
}
12 changes: 12 additions & 0 deletions WebApi.MinimalApi/Models/UpdateUserDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;

public class UpdateUserDto
{
[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; }
}
43 changes: 40 additions & 3 deletions WebApi.MinimalApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
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()
.ConfigureApiBehaviorOptions(options => {
builder.Services.AddControllers(options =>
{
// Этот OutputFormatter позволяет возвращать данные в XML, если требуется.
options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
// Эта настройка позволяет отвечать кодом 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.AddSingleton<IUserRepository, InMemoryUserRepository>();
builder.Services.AddAutoMapper(cfg =>
{
cfg.CreateMap<UserEntity, UserDto>()
.ForMember(
dto => dto.FullName,
opt => opt.MapFrom(entity => $"{entity.LastName} {entity.FirstName}")

);
cfg.CreateMap<CreateUserDto, UserEntity>();
cfg.CreateMap<UpdateUserDto, UserEntity>();
cfg.CreateMap<UserEntity, UpdateUserDto>();
}, new System.Reflection.Assembly[0]);

var app = builder.Build();

Expand Down
25 changes: 25 additions & 0 deletions WebApi.MinimalApi/WebApi.MinimalApi.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.MinimalApi", "WebApi.MinimalApi.csproj", "{3703745E-6723-4B0E-AD87-B087479676E5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3703745E-6723-4B0E-AD87-B087479676E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3703745E-6723-4B0E-AD87-B087479676E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3703745E-6723-4B0E-AD87-B087479676E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3703745E-6723-4B0E-AD87-B087479676E5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2935645E-E009-4793-BA79-B27B1E58681B}
EndGlobalSection
EndGlobal
Loading