diff --git a/351002/Yemyalyanenka/RestApiTask/.dockerignore b/351002/Yemyalyanenka/RestApiTask/.dockerignore new file mode 100644 index 000000000..fe1152bdb --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/Controllers/ArticlesController.cs b/351002/Yemyalyanenka/RestApiTask/Controllers/ArticlesController.cs new file mode 100644 index 000000000..ce005ee60 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Controllers/ArticlesController.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Mvc; +using RestApiTask.Models.DTOs; +using RestApiTask.Services.Interfaces; +using AutoMapper; + +namespace RestApiTask.Controllers; + +[ApiController] +[Route("articles")] +public class ArticlesController : ControllerBase +{ + private readonly IArticleService _service; + private readonly IMapper _mapper; + + public ArticlesController(IArticleService service, IMapper mapper) + { + _service = service; + _mapper = mapper; + } + + [HttpGet] + public async Task>> GetAll() => Ok(await _service.GetAllAsync()); + + [HttpGet("{id}")] + public async Task> GetById(long id) => Ok(await _service.GetByIdAsync(id)); + + [HttpPost] + public async Task> Create([FromBody] ArticleRequestTo request) + { + var result = await _service.CreateAsync(request); + return StatusCode(201, result); + } + + [HttpPut] + public async Task> Update([FromBody] ArticleResponseTo responseDto) + { + var requestDto = _mapper.Map(responseDto); + var result = await _service.UpdateAsync(responseDto.Id, requestDto); + return Ok(result); + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + await _service.DeleteAsync(id); + return NoContent(); + } + +} \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/Controllers/MarkersController.cs b/351002/Yemyalyanenka/RestApiTask/Controllers/MarkersController.cs new file mode 100644 index 000000000..0557ac4bf --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Controllers/MarkersController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using RestApiTask.Models.DTOs; +using RestApiTask.Services.Interfaces; +using AutoMapper; + +namespace RestApiTask.Controllers; + +[ApiController] +[Route("markers")] +public class MarkersController : ControllerBase +{ + private readonly IMarkerService _service; + private readonly IMapper _mapper; + + public MarkersController(IMarkerService service, IMapper mapper) + { + _service = service; + _mapper = mapper; + } + + [HttpGet] + public async Task>> GetAll() => Ok(await _service.GetAllAsync()); + + [HttpGet("{id}")] + public async Task> GetById(long id) => Ok(await _service.GetByIdAsync(id)); + + [HttpPost] + public async Task> Create([FromBody] MarkerRequestTo request) + { + var result = await _service.CreateAsync(request); + return StatusCode(201, result); + } + + [HttpPut] + public async Task> Update([FromBody] MarkerResponseTo responseDto) + { + var requestDto = _mapper.Map(responseDto); + var result = await _service.UpdateAsync(responseDto.Id, requestDto); + return Ok(result); + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + await _service.DeleteAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/Controllers/MessagesController.cs b/351002/Yemyalyanenka/RestApiTask/Controllers/MessagesController.cs new file mode 100644 index 000000000..8a36745a2 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Controllers/MessagesController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using RestApiTask.Models.DTOs; +using RestApiTask.Services.Interfaces; +using AutoMapper; + +namespace RestApiTask.Controllers; + +[ApiController] +[Route("messages")] +public class MessagesController : ControllerBase +{ + private readonly IMessageService _service; + private readonly IMapper _mapper; + + public MessagesController(IMessageService service, IMapper mapper) + { + _service = service; + _mapper = mapper; + } + + [HttpGet] + public async Task>> GetAll() => Ok(await _service.GetAllAsync()); + + [HttpGet("{id}")] + public async Task> GetById(long id) => Ok(await _service.GetByIdAsync(id)); + + [HttpPost] + public async Task> Create([FromBody] MessageRequestTo request) + { + var result = await _service.CreateAsync(request); + return StatusCode(201, result); + } + + [HttpPut] + public async Task> Update([FromBody] MessageResponseTo responseDto) + { + var requestDto = _mapper.Map(responseDto); + var result = await _service.UpdateAsync(responseDto.Id, requestDto); + return Ok(result); + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + await _service.DeleteAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/Controllers/WritersController.cs b/351002/Yemyalyanenka/RestApiTask/Controllers/WritersController.cs new file mode 100644 index 000000000..1732eae77 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Controllers/WritersController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using RestApiTask.Models.DTOs; +using RestApiTask.Services.Interfaces; +using AutoMapper; + +namespace RestApiTask.Controllers; + +[ApiController] +[Route("writers")] +public class WritersController : ControllerBase +{ + private readonly IWriterService _service; + private readonly IMapper _mapper; + + public WritersController(IWriterService service, IMapper mapper) + { + _service = service; + _mapper = mapper; + } + + [HttpGet] + public async Task>> GetAll() => Ok(await _service.GetAllAsync()); + + [HttpGet("{id}")] + public async Task> GetById(long id) => Ok(await _service.GetByIdAsync(id)); + + [HttpPost] + public async Task> Create([FromBody] WriterRequestTo request) + { + var result = await _service.CreateAsync(request); + return StatusCode(201, result); + } + + [HttpPut] // Без {id} в URL + public async Task> Update([FromBody] WriterResponseTo responseDto) + { + var requestDto = _mapper.Map(responseDto); + var result = await _service.UpdateAsync(responseDto.Id, requestDto); + return Ok(result); + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + await _service.DeleteAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/Dockerfile b/351002/Yemyalyanenka/RestApiTask/Dockerfile new file mode 100644 index 000000000..1faaa28a9 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Dockerfile @@ -0,0 +1,29 @@ +# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки. + +# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки) +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 + + +# Этот этап используется для сборки проекта службы +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["RestApiTask.csproj", "."] +RUN dotnet restore "./RestApiTask.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "./RestApiTask.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./RestApiTask.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "RestApiTask.dll"] \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/Infrastructure/ExceptionMiddleware.cs b/351002/Yemyalyanenka/RestApiTask/Infrastructure/ExceptionMiddleware.cs new file mode 100644 index 000000000..e5a37dcc4 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Infrastructure/ExceptionMiddleware.cs @@ -0,0 +1,46 @@ +using RestApiTask.Infrastructure.Exceptions; +using RestApiTask.Models; + +namespace RestApiTask.Infrastructure; + +public class ExceptionMiddleware +{ + private readonly RequestDelegate _next; + public ExceptionMiddleware(RequestDelegate next) => _next = next; + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + + // Перехват 404/405, если они не вызвали исключение (защита от HTML) + if ((context.Response.StatusCode == 404 || context.Response.StatusCode == 405) && !context.Response.HasStarted) + { + context.Response.ContentType = "application/json"; + var response = new ErrorResponse("Resource or Method not found", $"{context.Response.StatusCode}00"); + await context.Response.WriteAsJsonAsync(response); + } + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + private static Task HandleExceptionAsync(HttpContext context, Exception ex) + { + var (statusCode, subCode) = ex switch + { + NotFoundException => (StatusCodes.Status404NotFound, "01"), + ValidationException => (StatusCodes.Status400BadRequest, "01"), + _ => (StatusCodes.Status500InternalServerError, "00") + }; + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = statusCode; + + var response = new ErrorResponse(ex.Message, $"{statusCode}{subCode}"); + return context.Response.WriteAsJsonAsync(response); + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Infrastructure/Exceptions/NotFoundException.cs b/351002/Yemyalyanenka/RestApiTask/Infrastructure/Exceptions/NotFoundException.cs new file mode 100644 index 000000000..3d3d69895 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Infrastructure/Exceptions/NotFoundException.cs @@ -0,0 +1,10 @@ +namespace RestApiTask.Infrastructure.Exceptions +{ + public class NotFoundException : Exception + { + public NotFoundException(string message) : base(message) + { + + } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Infrastructure/Exceptions/ValidationException.cs b/351002/Yemyalyanenka/RestApiTask/Infrastructure/Exceptions/ValidationException.cs new file mode 100644 index 000000000..7e9dd8ebe --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Infrastructure/Exceptions/ValidationException.cs @@ -0,0 +1,10 @@ +namespace RestApiTask.Infrastructure.Exceptions +{ + public class ValidationException : Exception + { + public ValidationException(string message) : base(message) + { + + } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Mappings/MappingProfile.cs b/351002/Yemyalyanenka/RestApiTask/Mappings/MappingProfile.cs new file mode 100644 index 000000000..bd6ef032e --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Mappings/MappingProfile.cs @@ -0,0 +1,36 @@ +using AutoMapper; +using RestApiTask.Models.DTOs; +using RestApiTask.Models.Entities; + +namespace RestApiTask.Mappings +{ + + public class MappingProfile : Profile + { + public MappingProfile() + { + // Writer Mappings + CreateMap(); + CreateMap(); + + // Article Mappings + CreateMap() + .ForMember(dest => dest.Created, opt => opt.MapFrom(_ => DateTime.UtcNow)) + .ForMember(dest => dest.Modified, opt => opt.MapFrom(_ => DateTime.UtcNow)); + CreateMap(); + + // Marker Mappings + CreateMap(); + CreateMap(); + + // Message Mappings + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/DTOs/ArticleRequestTo.cs b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/ArticleRequestTo.cs new file mode 100644 index 000000000..57a871ff5 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/ArticleRequestTo.cs @@ -0,0 +1,4 @@ +namespace RestApiTask.Models.DTOs +{ + public record ArticleRequestTo(long WriterId, string Title, string Content); +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/DTOs/ArticleResponseTo.cs b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/ArticleResponseTo.cs new file mode 100644 index 000000000..8037d355b --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/ArticleResponseTo.cs @@ -0,0 +1,12 @@ +namespace RestApiTask.Models.DTOs +{ + public class ArticleResponseTo + { + public long Id { get; set; } + public long WriterId { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MarkerRequestTo.cs b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MarkerRequestTo.cs new file mode 100644 index 000000000..11bb5347b --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MarkerRequestTo.cs @@ -0,0 +1,4 @@ +namespace RestApiTask.Models.DTOs +{ + public record MarkerRequestTo(string Name); +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MarkerResponseTo.cs b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MarkerResponseTo.cs new file mode 100644 index 000000000..5d6fbdc4b --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MarkerResponseTo.cs @@ -0,0 +1,8 @@ +namespace RestApiTask.Models.DTOs +{ + public class MarkerResponseTo + { + public long Id { get; set; } + public string Name { get; set; } = string.Empty; + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MessageRequestTo.cs b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MessageRequestTo.cs new file mode 100644 index 000000000..092015c83 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MessageRequestTo.cs @@ -0,0 +1,4 @@ +namespace RestApiTask.Models.DTOs +{ + public record MessageRequestTo(long ArticleId, string Content); +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MessageResponseTo.cs b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MessageResponseTo.cs new file mode 100644 index 000000000..8ca790d2c --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/MessageResponseTo.cs @@ -0,0 +1,9 @@ +namespace RestApiTask.Models.DTOs +{ + public class MessageResponseTo + { + public long Id { get; set; } + public long ArticleId { get; set; } + public string Content { get; set; } = string.Empty; + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/DTOs/WriterRequestTo.cs b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/WriterRequestTo.cs new file mode 100644 index 000000000..5aa956cef --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/WriterRequestTo.cs @@ -0,0 +1,4 @@ +namespace RestApiTask.Models.DTOs +{ + public record WriterRequestTo(string Login, string Password, string Firstname, string Lastname); +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/DTOs/WriterResponseTo.cs b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/WriterResponseTo.cs new file mode 100644 index 000000000..1becb04b5 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/DTOs/WriterResponseTo.cs @@ -0,0 +1,12 @@ +namespace RestApiTask.Models.DTOs +{ + public class WriterResponseTo + { + public long Id { get; set; } + public string Login { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Firstname { get; set; } = string.Empty; + public string Lastname { get; set; } = string.Empty; + } + +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/Entities/Article.cs b/351002/Yemyalyanenka/RestApiTask/Models/Entities/Article.cs new file mode 100644 index 000000000..70dd090b5 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/Entities/Article.cs @@ -0,0 +1,15 @@ +namespace RestApiTask.Models.Entities +{ + public class Article : IHasId + { + public long Id { get; set; } + public long WriterId { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + + // Для связи многие-ко-многим + public List MarkerIds { get; set; } = new(); + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/Entities/IHasId.cs b/351002/Yemyalyanenka/RestApiTask/Models/Entities/IHasId.cs new file mode 100644 index 000000000..5730a9f7b --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/Entities/IHasId.cs @@ -0,0 +1,7 @@ +namespace RestApiTask.Models.Entities +{ + public interface IHasId + { + long Id { get; set; } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/Entities/Marker.cs b/351002/Yemyalyanenka/RestApiTask/Models/Entities/Marker.cs new file mode 100644 index 000000000..f3abcff67 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/Entities/Marker.cs @@ -0,0 +1,9 @@ +namespace RestApiTask.Models.Entities +{ + public class Marker : IHasId + { + public long Id { get; set; } + public string Name { get; set; } = string.Empty; + + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/Entities/Message.cs b/351002/Yemyalyanenka/RestApiTask/Models/Entities/Message.cs new file mode 100644 index 000000000..1a25bb129 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/Entities/Message.cs @@ -0,0 +1,9 @@ +namespace RestApiTask.Models.Entities +{ + public class Message : IHasId + { + public long Id { get; set; } + public long ArticleId { get; set; } + public string Content { get; set; } = string.Empty; + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/Entities/Writer.cs b/351002/Yemyalyanenka/RestApiTask/Models/Entities/Writer.cs new file mode 100644 index 000000000..466ac5e5f --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/Entities/Writer.cs @@ -0,0 +1,12 @@ +namespace RestApiTask.Models.Entities +{ + public class Writer : IHasId + { + public long Id { get; set; } + public string Login { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Firstname { get; set; } = string.Empty; + public string Lastname { get; set; } = string.Empty; + } + +} diff --git a/351002/Yemyalyanenka/RestApiTask/Models/ErrorResponse.cs b/351002/Yemyalyanenka/RestApiTask/Models/ErrorResponse.cs new file mode 100644 index 000000000..ff01e7002 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Models/ErrorResponse.cs @@ -0,0 +1,4 @@ +namespace RestApiTask.Models +{ + public record ErrorResponse(string ErrorMessage, string ErrorCode); +} diff --git a/351002/Yemyalyanenka/RestApiTask/Program.cs b/351002/Yemyalyanenka/RestApiTask/Program.cs new file mode 100644 index 000000000..eba515bf5 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Program.cs @@ -0,0 +1,102 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Routing; +using RestApiTask.Infrastructure; +using RestApiTask.Mappings; +using RestApiTask.Models.Entities; +using RestApiTask.Repositories; +using RestApiTask.Services; +using RestApiTask.Services.Interfaces; + +namespace RestApiTask; + +// api/v1.0 +public class RoutePrefixConvention : IApplicationModelConvention +{ + private readonly AttributeRouteModel _routePrefix; + public RoutePrefixConvention(IRouteTemplateProvider route) => _routePrefix = new AttributeRouteModel(route); + + public void Apply(ApplicationModel application) + { + foreach (var controller in application.Controllers) + { + var selectors = controller.Selectors.Where(s => s.AttributeRouteModel != null).ToList(); + if (selectors.Any()) + { + foreach (var selector in selectors) + { + selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel); + } + } + else + { + controller.Selectors.Add(new SelectorModel { AttributeRouteModel = _routePrefix }); + } + } + } +} + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // + builder.Services.AddControllers(options => + { + options.Conventions.Insert(0, new RoutePrefixConvention(new RouteAttribute("api/v1.0"))); + }); + + // (Singleton InMemory) + builder.Services.AddSingleton, InMemoryRepository>(); + builder.Services.AddSingleton, InMemoryRepository
>(); + builder.Services.AddSingleton, InMemoryRepository>(); + builder.Services.AddSingleton, InMemoryRepository>(); + + // + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // AutoMapper 13 + var configExpression = new MapperConfigurationExpression(); + configExpression.AddProfile(); + var mapperConfig = new MapperConfiguration(configExpression); + IMapper mapper = mapperConfig.CreateMapper(); + builder.Services.AddSingleton(mapper); + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + var app = builder.Build(); + + // 1. Middleware ( ) + app.UseMiddleware(); + + // 2. ( ) + using (var scope = app.Services.CreateScope()) + { + var writerRepo = scope.ServiceProvider.GetRequiredService>(); + writerRepo.AddAsync(new Writer + { + Login = "yevgeny2006@gmail.com", + Firstname = "", + Lastname = "", + Password = "securePassword123" + }).GetAwaiter().GetResult(); + } + + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAuthorization(); + app.MapControllers(); + app.Run(); + } +} \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/Properties/launchSettings.json b/351002/Yemyalyanenka/RestApiTask/Properties/launchSettings.json new file mode 100644 index 000000000..4fce49511 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:24110" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": false + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6172", + "sslPort": 0 + } + } +} \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/Repositories/IRepository.cs b/351002/Yemyalyanenka/RestApiTask/Repositories/IRepository.cs new file mode 100644 index 000000000..3dc9588bd --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Repositories/IRepository.cs @@ -0,0 +1,13 @@ +using RestApiTask.Models.Entities; + +namespace RestApiTask.Repositories +{ + public interface IRepository where T : class, IHasId + { + Task> GetAllAsync(); + Task GetByIdAsync(long id); + Task AddAsync(T entity); + Task UpdateAsync(T entity); + Task DeleteAsync(long id); + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Repositories/InMemoryRepository.cs b/351002/Yemyalyanenka/RestApiTask/Repositories/InMemoryRepository.cs new file mode 100644 index 000000000..6f1804304 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Repositories/InMemoryRepository.cs @@ -0,0 +1,45 @@ +using RestApiTask.Models.Entities; +using System.Collections.Concurrent; + +namespace RestApiTask.Repositories; + +public class InMemoryRepository : IRepository where T : class, IHasId +{ + private readonly ConcurrentDictionary _storage = new(); + private long _currentId = 0; + + public Task> GetAllAsync() + { + return Task.FromResult(_storage.Values.AsEnumerable()); + } + + public Task GetByIdAsync(long id) + { + _storage.TryGetValue(id, out var entity); + return Task.FromResult(entity); + } + + public Task AddAsync(T entity) + { + // Атомарно увеличиваем ID + long id = Interlocked.Increment(ref _currentId); + entity.Id = id; + _storage[id] = entity; + return Task.FromResult(entity); + } + + public Task UpdateAsync(T entity) + { + if (_storage.ContainsKey(entity.Id)) + { + _storage[entity.Id] = entity; + return Task.FromResult(entity); + } + throw new KeyNotFoundException($"Entity with id {entity.Id} not found."); + } + + public Task DeleteAsync(long id) + { + return Task.FromResult(_storage.TryRemove(id, out _)); + } +} \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/RestApiTask.csproj b/351002/Yemyalyanenka/RestApiTask/RestApiTask.csproj new file mode 100644 index 000000000..65a7957fc --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/RestApiTask.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + Linux + . + + + + + + + + + + + + + + + diff --git a/351002/Yemyalyanenka/RestApiTask/RestApiTask.http b/351002/Yemyalyanenka/RestApiTask/RestApiTask.http new file mode 100644 index 000000000..4756ec949 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/RestApiTask.http @@ -0,0 +1,6 @@ +@RestApiTask_HostAddress = http://localhost:5183 + +GET {{RestApiTask_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/351002/Yemyalyanenka/RestApiTask/RestApiTask.sln b/351002/Yemyalyanenka/RestApiTask/RestApiTask.sln new file mode 100644 index 000000000..d5f8cf29e --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/RestApiTask.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36327.8 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestApiTask", "RestApiTask.csproj", "{E54AF6E0-0DF9-4FC8-A5D4-EA694985B628}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E54AF6E0-0DF9-4FC8-A5D4-EA694985B628}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E54AF6E0-0DF9-4FC8-A5D4-EA694985B628}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E54AF6E0-0DF9-4FC8-A5D4-EA694985B628}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E54AF6E0-0DF9-4FC8-A5D4-EA694985B628}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1C9C7BB4-8A8A-4E27-A464-EB4150AD2377} + EndGlobalSection +EndGlobal diff --git a/351002/Yemyalyanenka/RestApiTask/Services/ArticleService.cs b/351002/Yemyalyanenka/RestApiTask/Services/ArticleService.cs new file mode 100644 index 000000000..66ede3227 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Services/ArticleService.cs @@ -0,0 +1,62 @@ +using AutoMapper; +using RestApiTask.Infrastructure.Exceptions; +using RestApiTask.Models.DTOs; +using RestApiTask.Models.Entities; +using RestApiTask.Repositories; +using RestApiTask.Services.Interfaces; + +namespace RestApiTask.Services +{ + public class ArticleService : IArticleService + { + private readonly IRepository
_repo; + private readonly IRepository _writerRepo; + private readonly IMapper _mapper; + + public ArticleService(IRepository
repo, IRepository writerRepo, IMapper mapper) + { + _repo = repo; + _writerRepo = writerRepo; + _mapper = mapper; + } + + public async Task> GetAllAsync() => + _mapper.Map>(await _repo.GetAllAsync()); + + public async Task GetByIdAsync(long id) + { + var entity = await _repo.GetByIdAsync(id) ?? throw new NotFoundException("Article not found"); + return _mapper.Map(entity); + } + + public async Task CreateAsync(ArticleRequestTo request) + { + await Validate(request); + var entity = _mapper.Map
(request); + entity.Created = entity.Modified = DateTime.UtcNow; + return _mapper.Map(await _repo.AddAsync(entity)); + } + + public async Task UpdateAsync(long id, ArticleRequestTo request) + { + var existing = await _repo.GetByIdAsync(id) ?? throw new NotFoundException("Article not found"); + await Validate(request); + _mapper.Map(request, existing); + existing.Modified = DateTime.UtcNow; + await _repo.UpdateAsync(existing); + return _mapper.Map(existing); + } + + public async Task DeleteAsync(long id) + { + if (!await _repo.DeleteAsync(id)) throw new NotFoundException("Article not found"); + } + + private async Task Validate(ArticleRequestTo r) + { + if (r.Title.Length < 2 || r.Title.Length > 64) throw new ValidationException("Title: 2-64 chars"); + if (r.Content.Length < 4 || r.Content.Length > 2048) throw new ValidationException("Content: 4-2048 chars"); + if (await _writerRepo.GetByIdAsync(r.WriterId) == null) throw new ValidationException("Invalid WriterId"); + } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IArticleService.cs b/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IArticleService.cs new file mode 100644 index 000000000..8ef2e7915 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IArticleService.cs @@ -0,0 +1,14 @@ +using RestApiTask.Models.DTOs; + +namespace RestApiTask.Services.Interfaces +{ + public interface IArticleService + { + Task> GetAllAsync(); + Task GetByIdAsync(long id); + Task CreateAsync(ArticleRequestTo request); + Task UpdateAsync(long id, ArticleRequestTo request); + Task DeleteAsync(long id); + } + +} diff --git a/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IMarkerService.cs b/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IMarkerService.cs new file mode 100644 index 000000000..198a4029f --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IMarkerService.cs @@ -0,0 +1,14 @@ +using RestApiTask.Models.DTOs; + +namespace RestApiTask.Services.Interfaces +{ + public interface IMarkerService + { + Task> GetAllAsync(); + Task GetByIdAsync(long id); + Task CreateAsync(MarkerRequestTo request); + Task UpdateAsync(long id, MarkerRequestTo request); + Task DeleteAsync(long id); + } + +} diff --git a/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IMessageService.cs b/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IMessageService.cs new file mode 100644 index 000000000..a77eda8b0 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IMessageService.cs @@ -0,0 +1,13 @@ +using RestApiTask.Models.DTOs; + +namespace RestApiTask.Services.Interfaces +{ + public interface IMessageService + { + Task> GetAllAsync(); + Task GetByIdAsync(long id); + Task CreateAsync(MessageRequestTo request); + Task UpdateAsync(long id, MessageRequestTo request); + Task DeleteAsync(long id); + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IWriterService.cs b/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IWriterService.cs new file mode 100644 index 000000000..b9609c118 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Services/Interfaces/IWriterService.cs @@ -0,0 +1,14 @@ +using RestApiTask.Models.DTOs; + +namespace RestApiTask.Services.Interfaces +{ + public interface IWriterService + { + Task> GetAllAsync(); + Task GetByIdAsync(long id); + Task CreateAsync(WriterRequestTo request); + Task UpdateAsync(long id, WriterRequestTo request); + Task DeleteAsync(long id); + } + +} diff --git a/351002/Yemyalyanenka/RestApiTask/Services/MarkerService.cs b/351002/Yemyalyanenka/RestApiTask/Services/MarkerService.cs new file mode 100644 index 000000000..c515a9f6d --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Services/MarkerService.cs @@ -0,0 +1,51 @@ +using AutoMapper; +using RestApiTask.Infrastructure.Exceptions; +using RestApiTask.Models.DTOs; +using RestApiTask.Models.Entities; +using RestApiTask.Repositories; +using RestApiTask.Services.Interfaces; + +namespace RestApiTask.Services +{ + public class MarkerService : IMarkerService + { + private readonly IRepository _repo; + private readonly IMapper _mapper; + + public MarkerService(IRepository repo, IMapper mapper) + { + _repo = repo; + _mapper = mapper; + } + + public async Task> GetAllAsync() => + _mapper.Map>(await _repo.GetAllAsync()); + + public async Task GetByIdAsync(long id) + { + var entity = await _repo.GetByIdAsync(id) ?? throw new NotFoundException("Marker not found"); + return _mapper.Map(entity); + } + + public async Task CreateAsync(MarkerRequestTo request) + { + if (request.Name.Length < 2 || request.Name.Length > 32) throw new ValidationException("Name: 2-32 chars"); + var entity = _mapper.Map(request); + return _mapper.Map(await _repo.AddAsync(entity)); + } + + public async Task UpdateAsync(long id, MarkerRequestTo request) + { + var existing = await _repo.GetByIdAsync(id) ?? throw new NotFoundException("Marker not found"); + if (request.Name.Length < 2 || request.Name.Length > 32) throw new ValidationException("Name: 2-32 chars"); + _mapper.Map(request, existing); + await _repo.UpdateAsync(existing); + return _mapper.Map(existing); + } + + public async Task DeleteAsync(long id) + { + if (!await _repo.DeleteAsync(id)) throw new NotFoundException("Marker not found"); + } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Services/MessageService.cs b/351002/Yemyalyanenka/RestApiTask/Services/MessageService.cs new file mode 100644 index 000000000..aff5a74bb --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Services/MessageService.cs @@ -0,0 +1,59 @@ +using AutoMapper; +using RestApiTask.Infrastructure.Exceptions; +using RestApiTask.Models.DTOs; +using RestApiTask.Models.Entities; +using RestApiTask.Repositories; +using RestApiTask.Services.Interfaces; + +namespace RestApiTask.Services +{ + public class MessageService : IMessageService + { + private readonly IRepository _repo; + private readonly IRepository
_articleRepo; + private readonly IMapper _mapper; + + public MessageService(IRepository repo, IRepository
articleRepo, IMapper mapper) + { + _repo = repo; + _articleRepo = articleRepo; + _mapper = mapper; + } + + public async Task> GetAllAsync() => + _mapper.Map>(await _repo.GetAllAsync()); + + public async Task GetByIdAsync(long id) + { + var entity = await _repo.GetByIdAsync(id) ?? throw new NotFoundException("Message not found"); + return _mapper.Map(entity); + } + + public async Task CreateAsync(MessageRequestTo request) + { + await Validate(request); + var entity = _mapper.Map(request); + return _mapper.Map(await _repo.AddAsync(entity)); + } + + public async Task UpdateAsync(long id, MessageRequestTo request) + { + var existing = await _repo.GetByIdAsync(id) ?? throw new NotFoundException("Message not found"); + await Validate(request); + _mapper.Map(request, existing); + await _repo.UpdateAsync(existing); + return _mapper.Map(existing); + } + + public async Task DeleteAsync(long id) + { + if (!await _repo.DeleteAsync(id)) throw new NotFoundException("Message not found"); + } + + private async Task Validate(MessageRequestTo r) + { + if (r.Content.Length < 2 || r.Content.Length > 2048) throw new ValidationException("Content: 2-2048 chars"); + if (await _articleRepo.GetByIdAsync(r.ArticleId) == null) throw new ValidationException("Invalid ArticleId"); + } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/Services/WriterService.cs b/351002/Yemyalyanenka/RestApiTask/Services/WriterService.cs new file mode 100644 index 000000000..f885c8ccd --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/Services/WriterService.cs @@ -0,0 +1,58 @@ +using AutoMapper; +using RestApiTask.Infrastructure.Exceptions; +using RestApiTask.Models.DTOs; +using RestApiTask.Models.Entities; +using RestApiTask.Repositories; +using RestApiTask.Services.Interfaces; + +namespace RestApiTask.Services; + +public class WriterService : IWriterService +{ + private readonly IRepository _repo; + private readonly IMapper _mapper; + + public WriterService(IRepository repo, IMapper mapper) + { + _repo = repo; + _mapper = mapper; + } + + public async Task> GetAllAsync() => + _mapper.Map>(await _repo.GetAllAsync()); + + public async Task GetByIdAsync(long id) + { + var entity = await _repo.GetByIdAsync(id) ?? throw new NotFoundException("Writer not found"); + return _mapper.Map(entity); + } + + public async Task CreateAsync(WriterRequestTo request) + { + Validate(request); + var entity = _mapper.Map(request); + return _mapper.Map(await _repo.AddAsync(entity)); + } + + public async Task UpdateAsync(long id, WriterRequestTo request) + { + var existing = await _repo.GetByIdAsync(id) ?? throw new NotFoundException("Writer not found"); + Validate(request); + _mapper.Map(request, existing); + await _repo.UpdateAsync(existing); + return _mapper.Map(existing); + } + + public async Task DeleteAsync(long id) + { + if (!await _repo.DeleteAsync(id)) throw new NotFoundException("Writer not found"); + } + + private void Validate(WriterRequestTo r) + { + if (r.Login.Length < 2 || r.Login.Length > 64) throw new ValidationException("Login: 2-64 chars"); + if (r.Password.Length < 8 || r.Password.Length > 128) throw new ValidationException("Password: 8-128 chars"); + if (r.Firstname.Length < 2 || r.Firstname.Length > 64) throw new ValidationException("Firstname: 2-64 chars"); + if (r.Lastname.Length < 2 || r.Lastname.Length > 64) throw new ValidationException("Lastname: 2-64 chars"); + } +} \ No newline at end of file diff --git a/351002/Yemyalyanenka/RestApiTask/WeatherForecast.cs b/351002/Yemyalyanenka/RestApiTask/WeatherForecast.cs new file mode 100644 index 000000000..f1e909e56 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace RestApiTask +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/appsettings.Development.json b/351002/Yemyalyanenka/RestApiTask/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/351002/Yemyalyanenka/RestApiTask/appsettings.json b/351002/Yemyalyanenka/RestApiTask/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/351002/Yemyalyanenka/RestApiTask/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}