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

Сорокин Артём, Шманцарь Ангелина, Лежнин Владимир, Калинина Софья #43

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
202 changes: 195 additions & 7 deletions WebApi.MinimalApi/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,215 @@
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;

[Route("api/[controller]")]
[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<UserDto> 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<UserDto> GetUserById(Guid userId)
{
throw new NotImplementedException();
var user = userRepository.FindById(userId);
if (user is null)
return NotFound();

return Ok(mapper.Map<UserDto>(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<UserEntity>(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<UserToUpdateDto> patchDoc, Guid userId)
{
if (!ModelState.IsValid)
{
return BadRequest();
}

var user = userRepository.FindById(userId);

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

var userToUpdateDto = mapper.Map<UserToUpdateDto>(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<UserDto>), 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<IEnumerable<UserDto>>(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();
}
}
17 changes: 17 additions & 0 deletions WebApi.MinimalApi/Models/UserToCreateDto.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
17 changes: 17 additions & 0 deletions WebApi.MinimalApi/Models/UserToUpdateDto.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
41 changes: 40 additions & 1 deletion WebApi.MinimalApi/Program.cs
Original file line number Diff line number Diff line change
@@ -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<char>.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<UserEntity, UserDto>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.LastName} {src.FirstName}"));

cfg.CreateMap<UserToCreateDto, UserEntity>();
cfg.CreateMap<UserToUpdateDto, UserEntity>();
cfg.CreateMap<UserEntity, UserToUpdateDto>();
}, Array.Empty<Assembly>());

builder.Services.AddSingleton<IUserRepository, InMemoryUserRepository>();

var app = builder.Build();

app.MapControllers();
Expand Down
1 change: 1 addition & 0 deletions WebApi.MinimalApi/WebApi.MinimalApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="Swashbuckle" Version="5.6.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
Expand Down
5 changes: 5 additions & 0 deletions web-api.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=15b5b1f1_002D457c_002D4ca6_002Db278_002D5615aedc07d3/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=4a98fdf6_002D7d98_002D4f5a_002Dafeb_002Dea44ad98c70c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a0b4bc4d_002Dd13b_002D4a37_002Db37e_002Dc9c6864e4302/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="NAMESPACE" /&gt;&lt;Kind Name="CLASS" /&gt;&lt;Kind Name="STRUCT" /&gt;&lt;Kind Name="ENUM" /&gt;&lt;Kind Name="DELEGATE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=f9fce829_002De6f4_002D4cb2_002D80f1_002D5497c44f51df/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FCONSTANT/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FFUNCTION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
Expand Down Expand Up @@ -69,4 +73,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EFormat_002ESettingsUpgrade_002EAlignmentTabFillStyleMigration/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>