diff --git a/6,1.png b/6,1.png new file mode 100644 index 0000000..91f2f11 Binary files /dev/null and b/6,1.png differ diff --git a/6,2.png b/6,2.png new file mode 100644 index 0000000..93a021d Binary files /dev/null and b/6,2.png differ diff --git a/6,3.png b/6,3.png new file mode 100644 index 0000000..6ef3032 Binary files /dev/null and b/6,3.png differ diff --git a/6,4.png b/6,4.png new file mode 100644 index 0000000..7b0eec8 Binary files /dev/null and b/6,4.png differ diff --git a/6,5.png b/6,5.png new file mode 100644 index 0000000..7b0eec8 Binary files /dev/null and b/6,5.png differ diff --git a/6,6.png b/6,6.png new file mode 100644 index 0000000..be819f1 Binary files /dev/null and b/6,6.png differ diff --git a/TaskHub/Api/Api.csproj b/TaskHub/Api/Api.csproj index a6946fa..c8bbecd 100644 --- a/TaskHub/Api/Api.csproj +++ b/TaskHub/Api/Api.csproj @@ -1,7 +1,7 @@ - + - net10.0 + net8.0 enable enable @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TaskHub/Api/Controllers/Tasks/Request/CreateTaskRequest.cs b/TaskHub/Api/Controllers/Tasks/Request/CreateTaskRequest.cs new file mode 100644 index 0000000..9507259 --- /dev/null +++ b/TaskHub/Api/Controllers/Tasks/Request/CreateTaskRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Tasks.Request +{ + public class CreateTaskRequest + { + public string? Title { get; set; } + public Guid UserId { get; set; } + } +} diff --git a/TaskHub/Api/Controllers/Tasks/Request/SetTaskTitleRequest.cs b/TaskHub/Api/Controllers/Tasks/Request/SetTaskTitleRequest.cs new file mode 100644 index 0000000..7e9d631 --- /dev/null +++ b/TaskHub/Api/Controllers/Tasks/Request/SetTaskTitleRequest.cs @@ -0,0 +1,7 @@ +namespace Api.Controllers.Tasks.Request +{ + public class SetTaskTitleRequest + { + public string? Title { get; set; } + } +} diff --git a/TaskHub/Api/Controllers/Tasks/Response/TaskResponse.cs b/TaskHub/Api/Controllers/Tasks/Response/TaskResponse.cs new file mode 100644 index 0000000..f21d312 --- /dev/null +++ b/TaskHub/Api/Controllers/Tasks/Response/TaskResponse.cs @@ -0,0 +1,18 @@ +namespace Api.Controllers.Tasks.Response +{ + public class TaskResponse + { + public Guid Id { get; } + public string? Title { get; } + public Guid CreatedByUserId { get; } + public DateTimeOffset CreatedUtc { get; } + + public TaskResponse(Guid id, string? title, Guid createdByUserId, DateTimeOffset createdUtc) + { + Id = id; + Title = title; + CreatedByUserId = createdByUserId; + CreatedUtc = createdUtc; + } + } +} diff --git a/TaskHub/Api/Controllers/Tasks/TasksController.cs b/TaskHub/Api/Controllers/Tasks/TasksController.cs new file mode 100644 index 0000000..d7f50fb --- /dev/null +++ b/TaskHub/Api/Controllers/Tasks/TasksController.cs @@ -0,0 +1,99 @@ +using Api.Controllers.Tasks.Request; +using Api.Controllers.Tasks.Response; +using Api.Filters; +using Api.UseCases.Tasks.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers.Tasks; + +[Route("tasks")] +[StudentInfoHeadersFilter] +[RequestLoggingFilter] +public class TasksController : ControllerBase +{ + private readonly IManageTaskUseCase _useCase; + + public TasksController(IManageTaskUseCase useCase) + { + _useCase = useCase; + } + + [HttpPost] + [ValidateCreateTaskRequestFilter] + public async Task> Create(CreateTaskRequest request, CancellationToken ct) + { + var result = await _useCase.CreateTaskAsync(request.Title, request.UserId, ct); + return Created("", result); + } + + [HttpGet] + public async Task>> GetAll(CancellationToken ct) + { + return Ok(await _useCase.GetAllTasksAsync(ct)); + } + + [HttpGet("{id}")] + [FromRouteTaskId] + public async Task> Get( + [FromRouteTaskId] string id, + CancellationToken ct) + { + var guidObj = HttpContext.Items["TaskId"]; + + if (guidObj == null) + { + return BadRequest("Идентификатор задачи не задан"); + } + + var guid = (Guid)guidObj; + var result = await _useCase.GetTaskByIdAsync(guid, ct); + + if (result == null) return NotFound(); + return Ok(result); + } + + [HttpPut("{id}/title")] + [FromRouteTaskId] + [ValidateSetTaskTitleRequestFilter] + public async Task SetTitle( + [FromRouteTaskId] string id, + SetTaskTitleRequest request, + CancellationToken ct) + { + var guidObj = HttpContext.Items["TaskId"]; + + if (guidObj == null) + { + return BadRequest("Идентификатор задачи не задан"); + } + + var guid = (Guid)guidObj; + await _useCase.SetTaskTitleAsync(guid, request.Title!, ct); + return NoContent(); + } + + [HttpDelete("{id}")] + [FromRouteTaskId] + public async Task Delete( + [FromRouteTaskId] string id, + CancellationToken ct) + { + var guidObj = HttpContext.Items["TaskId"]; + + if (guidObj == null) + { + return BadRequest("Идентификатор задачи не задан"); + } + + var guid = (Guid)guidObj; + var deleted = await _useCase.DeleteTaskByIdAsync(guid, ct); + return deleted ? NoContent() : NotFound(); + } + + [HttpDelete] + public async Task DeleteAll(CancellationToken ct) + { + await _useCase.DeleteAllTasksAsync(ct); + return NoContent(); + } +} \ No newline at end of file diff --git a/TaskHub/Api/Controllers/Users/UsersController.cs b/TaskHub/Api/Controllers/Users/UsersController.cs index 10a433a..b139e87 100644 --- a/TaskHub/Api/Controllers/Users/UsersController.cs +++ b/TaskHub/Api/Controllers/Users/UsersController.cs @@ -1,3 +1,4 @@ +using Api.Filters; using Api.Controllers.Users.Request; using Api.Controllers.Users.Response; using Api.UseCases.Users.Interfaces; @@ -5,6 +6,9 @@ namespace Api.Controllers.Users; +[ResponseTimeHeader] +[StudentInfoHeaders] + /// /// Контроллер работы с пользователями /// @@ -29,6 +33,7 @@ public UsersController(IManageUserUseCase userUseCase) /// Токен отмены /// Созданный пользователь [HttpPost] + [SetName] public async Task> CreateUserAsync( [FromBody] CreateUserRequest? request, CancellationToken cancellationToken) diff --git a/TaskHub/Api/Filters/FromRouteTaskIdAttribute.cs b/TaskHub/Api/Filters/FromRouteTaskIdAttribute.cs new file mode 100644 index 0000000..e80dc16 --- /dev/null +++ b/TaskHub/Api/Filters/FromRouteTaskIdAttribute.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Api.Filters; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter)] +public class FromRouteTaskIdAttribute : Attribute, IActionFilter +{ + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.RouteData.Values.TryGetValue("id", out var idObj)) + { + context.Result = new BadRequestObjectResult("Идентификатор задачи не задан"); + return; + } + + string? idString = idObj?.ToString(); + + if (string.IsNullOrWhiteSpace(idString)) + { + context.Result = new BadRequestObjectResult("Идентификатор задачи не задан"); + return; + } + + if (!Guid.TryParse(idString, out var guid)) + { + context.Result = new BadRequestObjectResult("Идентификатор задачи имеет некорректный формат"); + return; + } + + context.HttpContext.Items["TaskId"] = guid; + } + + public void OnActionExecuted(ActionExecutedContext context) { } +} \ No newline at end of file diff --git a/TaskHub/Api/Filters/RequestLoggingFilter.cs b/TaskHub/Api/Filters/RequestLoggingFilter.cs new file mode 100644 index 0000000..12ce868 --- /dev/null +++ b/TaskHub/Api/Filters/RequestLoggingFilter.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace Api.Filters; + +public class RequestLoggingFilter : Attribute, IActionFilter +{ + private Stopwatch _stopwatch; + private ILogger _logger; + + public void OnActionExecuting(ActionExecutingContext context) + { + var loggerFactory = context.HttpContext.RequestServices.GetRequiredService(); + _logger = loggerFactory.CreateLogger(); + + _stopwatch = Stopwatch.StartNew(); + var httpMethod = context.HttpContext.Request.Method; + var path = context.HttpContext.Request.Path; + + _logger.LogInformation("начало выполнения: {Method} {Path}", httpMethod, path); + } + + public void OnActionExecuted(ActionExecutedContext context) + { + _stopwatch.Stop(); + var statusCode = context.HttpContext.Response.StatusCode; + var elapsedMs = _stopwatch.ElapsedMilliseconds; + + _logger.LogInformation("завершение: статус {StatusCode}, время {ElapsedMs} мс", statusCode, elapsedMs); + } +} \ No newline at end of file diff --git a/TaskHub/Api/Filters/ResponseTimeHeaderAttribute.cs b/TaskHub/Api/Filters/ResponseTimeHeaderAttribute.cs new file mode 100644 index 0000000..79b610a --- /dev/null +++ b/TaskHub/Api/Filters/ResponseTimeHeaderAttribute.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using System.Diagnostics; + +namespace Api.Filters +{ + public class ResponseTimeHeaderAttribute : ActionFilterAttribute + { + private Stopwatch _stopwatch; + + public override void OnActionExecuting(ActionExecutingContext context) + { + _stopwatch = Stopwatch.StartNew(); + } + + public override void OnActionExecuted(ActionExecutedContext context) + { + _stopwatch.Stop(); + + context.HttpContext.Response.Headers["X-Response-Time-Ms"] = + _stopwatch.ElapsedMilliseconds.ToString(); + } + } +} diff --git a/TaskHub/Api/Filters/SetNameAttribute.cs b/TaskHub/Api/Filters/SetNameAttribute.cs new file mode 100644 index 0000000..523ccb1 --- /dev/null +++ b/TaskHub/Api/Filters/SetNameAttribute.cs @@ -0,0 +1,6 @@ +namespace Api.Filters +{ + public class SetNameAttribute : ValidateUserRequestAttribute + { + } +} diff --git a/TaskHub/Api/Filters/StudentInfoHeadersAttribute.cs b/TaskHub/Api/Filters/StudentInfoHeadersAttribute.cs new file mode 100644 index 0000000..35488c3 --- /dev/null +++ b/TaskHub/Api/Filters/StudentInfoHeadersAttribute.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Api.Filters +{ + public class StudentInfoHeadersAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(ActionExecutedContext context) + { + var headers = context.HttpContext.Request.Headers; + + headers["X-Student-Name"] = "Tumashova Marina"; + headers["X-Student-Group"] = "RI-240912"; + } + } +} diff --git a/TaskHub/Api/Filters/StudentInfoHeadersFilter.cs b/TaskHub/Api/Filters/StudentInfoHeadersFilter.cs new file mode 100644 index 0000000..ff3fc5b --- /dev/null +++ b/TaskHub/Api/Filters/StudentInfoHeadersFilter.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Api.Filters; + +public class StudentInfoHeadersFilter : Attribute, IActionFilter +{ + public void OnActionExecuting(ActionExecutingContext context) + { + context.HttpContext.Response.Headers.Append("X-Student-Name", "Tumashova Marina"); + context.HttpContext.Response.Headers.Append("X-Student-Group", "RI-240912"); + } + + public void OnActionExecuted(ActionExecutedContext context) { } +} \ No newline at end of file diff --git a/TaskHub/Api/Filters/TaskIdModelBinder.cs b/TaskHub/Api/Filters/TaskIdModelBinder.cs new file mode 100644 index 0000000..15c1b35 --- /dev/null +++ b/TaskHub/Api/Filters/TaskIdModelBinder.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Api.Filters; + +public class TaskIdModelBinder : IModelBinder +{ + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var value = bindingContext.ValueProvider.GetValue("id"); + + if (value == ValueProviderResult.None) + { + bindingContext.ModelState.AddModelError("id", "Идентификатор задачи не задан"); + bindingContext.Result = ModelBindingResult.Failed(); + return Task.CompletedTask; + } + + var idString = value.FirstValue; + + if (string.IsNullOrWhiteSpace(idString)) + { + bindingContext.ModelState.AddModelError("id", "Идентификатор задачи не задан"); + bindingContext.Result = ModelBindingResult.Failed(); + return Task.CompletedTask; + } + + if (!Guid.TryParse(idString, out var guid)) + { + bindingContext.ModelState.AddModelError("id", "Идентификатор задачи имеет некорректный формат"); + bindingContext.Result = ModelBindingResult.Failed(); + return Task.CompletedTask; + } + + bindingContext.Result = ModelBindingResult.Success(guid); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/TaskHub/Api/Filters/ValidateCreateTaskRequestFilter.cs b/TaskHub/Api/Filters/ValidateCreateTaskRequestFilter.cs new file mode 100644 index 0000000..5138f5d --- /dev/null +++ b/TaskHub/Api/Filters/ValidateCreateTaskRequestFilter.cs @@ -0,0 +1,44 @@ +using Api.Controllers.Tasks.Request; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Api.Filters; + +public class ValidateCreateTaskRequestFilter : Attribute, IActionFilter +{ + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ActionArguments.TryGetValue("request", out var requestObj)) + { + context.Result = new BadRequestObjectResult("тело запроса отсутствует"); + return; + } + + if (requestObj == null) + { + context.Result = new BadRequestObjectResult("тело запроса отсутствует"); + return; + } + + var request = requestObj as CreateTaskRequest; + if (request == null) + { + context.Result = new BadRequestObjectResult("тело запроса отсутствует"); + return; + } + + if (request.UserId == Guid.Empty) + { + context.Result = new BadRequestObjectResult("идентификатор пользователя не задан"); + return; + } + + if (string.IsNullOrWhiteSpace(request.Title)) + { + context.Result = new BadRequestObjectResult("название задачи не задано"); + return; + } + } + + public void OnActionExecuted(ActionExecutedContext context) { } +} \ No newline at end of file diff --git a/TaskHub/Api/Filters/ValidateSetTaskTitleRequestFilter.cs b/TaskHub/Api/Filters/ValidateSetTaskTitleRequestFilter.cs new file mode 100644 index 0000000..62edbda --- /dev/null +++ b/TaskHub/Api/Filters/ValidateSetTaskTitleRequestFilter.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Api.Filters; + +public class ValidateSetTaskTitleRequestFilter : Attribute, IActionFilter +{ + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ActionArguments.TryGetValue("request", out var requestObj)) + { + context.Result = new BadRequestObjectResult("тело запроса отсутствует"); + return; + } + + if (requestObj == null) + { + context.Result = new BadRequestObjectResult("тело запроса отсутствует"); + return; + } + } + + public void OnActionExecuted(ActionExecutedContext context) { } +} \ No newline at end of file diff --git a/TaskHub/Api/Filters/ValidateUserRequestAttribute.cs b/TaskHub/Api/Filters/ValidateUserRequestAttribute.cs new file mode 100644 index 0000000..8d44de0 --- /dev/null +++ b/TaskHub/Api/Filters/ValidateUserRequestAttribute.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Reflection; + +namespace Api.Filters +{ + public class ValidateUserRequestAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + var request = context.ActionArguments.Values.FirstOrDefault(); + + if (request == null) + { + context.Result = new BadRequestObjectResult("нет тела запроса"); + return; + } + + var nameProperty = request.GetType().GetProperty("Name"); + + if (nameProperty == null) + { + return; + } + + var nameValue = nameProperty.GetValue(request)?.ToString(); + + if (string.IsNullOrWhiteSpace(nameValue)) + { + context.Result = new BadRequestObjectResult("нет имени"); + } + } + } +} diff --git a/TaskHub/Api/Middleware/ResponseTimeMiddleware.cs.cs b/TaskHub/Api/Middleware/ResponseTimeMiddleware.cs.cs new file mode 100644 index 0000000..1f1b9d1 --- /dev/null +++ b/TaskHub/Api/Middleware/ResponseTimeMiddleware.cs.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; + +namespace Api.Middleware +{ + public class ResponseTimeMiddleware + { + private readonly RequestDelegate _next; + + public ResponseTimeMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + var stopwatch = Stopwatch.StartNew(); + + context.Response.OnStarting(() => + { + stopwatch.Stop(); + context.Response.Headers["X-Response-Time-Ms"] = stopwatch.ElapsedMilliseconds.ToString(); + return Task.CompletedTask; + }); + + await _next(context); + } + } +} diff --git a/TaskHub/Api/Middleware/StudentInfoMiddleware.cs b/TaskHub/Api/Middleware/StudentInfoMiddleware.cs new file mode 100644 index 0000000..23db0ab --- /dev/null +++ b/TaskHub/Api/Middleware/StudentInfoMiddleware.cs @@ -0,0 +1,24 @@ +namespace Api.Middleware +{ + public class StudentInfoMiddleware + { + private readonly RequestDelegate _next; + + public StudentInfoMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + context.Response.OnStarting(() => + { + context.Response.Headers["X-Student-Name"] = "Tumashova Marina"; + context.Response.Headers["X-Student-Group"] = "RI-240912"; + return Task.CompletedTask; + }); + + await _next(context); + } + } +} diff --git a/TaskHub/Api/Services/DisposedService.cs b/TaskHub/Api/Services/DisposedService.cs new file mode 100644 index 0000000..9a2ce8d --- /dev/null +++ b/TaskHub/Api/Services/DisposedService.cs @@ -0,0 +1,18 @@ +namespace Api.Services +{ + public abstract class DisposedService : IHasInstanceId, IDisposable + { + public Guid InstanceId { get; } = Guid.NewGuid(); + + protected DisposedService() + { + Console.WriteLine($"{GetType().Name} CREATED:{InstanceId}"); + } + + public void Dispose() + { + Console.WriteLine($"{GetType().Name} DISPOSED: {InstanceId}"); + } + } +} + diff --git a/TaskHub/Api/Services/IHasInstanceId.cs b/TaskHub/Api/Services/IHasInstanceId.cs new file mode 100644 index 0000000..d5abf99 --- /dev/null +++ b/TaskHub/Api/Services/IHasInstanceId.cs @@ -0,0 +1,7 @@ +namespace Api.Services +{ + public interface IHasInstanceId + { + Guid InstanceId { get; } + } +} diff --git a/TaskHub/Api/Services/ServiceProviderExtensions.cs b/TaskHub/Api/Services/ServiceProviderExtensions.cs new file mode 100644 index 0000000..9e0da57 --- /dev/null +++ b/TaskHub/Api/Services/ServiceProviderExtensions.cs @@ -0,0 +1,17 @@ +namespace Api.Services +{ + public static class ServiceProviderExtensions + { + public static void CompareServices(this IServiceProvider provider) + where T : IHasInstanceId + { + var first = provider.GetRequiredService(); + var second = provider.GetRequiredService(); + + Console.WriteLine($"Service: {typeof(T).Name}"); + Console.WriteLine($"First: {first.InstanceId}"); + Console.WriteLine($"Second: {second.InstanceId}"); + Console.WriteLine($"Same instance: {ReferenceEquals(first, second)}"); + } + } +} diff --git a/TaskHub/Api/Services/Services.cs b/TaskHub/Api/Services/Services.cs new file mode 100644 index 0000000..7a9a5eb --- /dev/null +++ b/TaskHub/Api/Services/Services.cs @@ -0,0 +1,20 @@ +namespace Api.Services +{ + public interface ISingletonService1 : IHasInstanceId { } + public interface ISingletonService2 : IHasInstanceId { } + + public interface IScopedService1 : IHasInstanceId { } + public interface IScopedService2 : IHasInstanceId { } + + public interface ITransientService1 : IHasInstanceId { } + public interface ITransientService2 : IHasInstanceId { } + + public class SingletonService1 : DisposedService, ISingletonService1 { } + public class SingletonService2 : DisposedService, ISingletonService2 { } + + public class ScopedService1 : DisposedService, IScopedService1 { } + public class ScopedService2 : DisposedService, IScopedService2 { } + + public class TransientService1 : DisposedService, ITransientService1 { } + public class TransientService2 : DisposedService, ITransientService2 { } +} diff --git a/TaskHub/Api/StartUp.cs b/TaskHub/Api/StartUp.cs index 9220ca4..0d2fcc8 100644 --- a/TaskHub/Api/StartUp.cs +++ b/TaskHub/Api/StartUp.cs @@ -1,5 +1,9 @@ +using Api.Middleware; +using Api.Services; using Api.UseCases.Users; using Api.UseCases.Users.Interfaces; +using Api.UseCases.Tasks; +using Api.UseCases.Tasks.Interfaces; using Dal; using Logic; using Microsoft.OpenApi.Models; @@ -38,7 +42,8 @@ public void ConfigureServices(IServiceCollection services) services.AddLogic(); services.AddScoped(); - + services.AddScoped(); + services.AddCors(options => { options.AddDefaultPolicy(builder => @@ -61,6 +66,15 @@ public void ConfigureServices(IServiceCollection services) Version = "v1" }); }); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddScoped(); + services.AddScoped(); + + services.AddTransient(); + services.AddTransient(); } /// @@ -69,18 +83,53 @@ public void ConfigureServices(IServiceCollection services) /// Построитель приложения public void Configure(IApplicationBuilder app) { - if (Environment.IsDevelopment()) + using (var scope1 = app.ApplicationServices.CreateScope()) + { + var provider = scope1.ServiceProvider; + + Console.WriteLine("SCOPE 1"); + + provider.CompareServices(); + provider.CompareServices(); + + provider.CompareServices(); + provider.CompareServices(); + + provider.CompareServices(); + provider.CompareServices(); + } + + using (var scope2 = app.ApplicationServices.CreateScope()) { + var provider = scope2.ServiceProvider; + + Console.WriteLine("SCOPE 2"); + + provider.CompareServices(); + provider.CompareServices(); + + provider.CompareServices(); + provider.CompareServices(); + + provider.CompareServices(); + provider.CompareServices(); + } + + //if (Environment.IsDevelopment()) + //{ app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "TaskHub API v1"); }); - } + //} app.UseRouting(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/TaskHub/Api/UseCases/Tasks/Interfaces/IManageTaskUseCase.cs b/TaskHub/Api/UseCases/Tasks/Interfaces/IManageTaskUseCase.cs new file mode 100644 index 0000000..9572d65 --- /dev/null +++ b/TaskHub/Api/UseCases/Tasks/Interfaces/IManageTaskUseCase.cs @@ -0,0 +1,19 @@ +using Api.Controllers.Tasks.Response; + +namespace Api.UseCases.Tasks.Interfaces +{ + public interface IManageTaskUseCase + { + Task CreateTaskAsync(string? title, Guid userId, CancellationToken cancellationToken); + + Task> GetAllTasksAsync(CancellationToken cancellationToken); + + Task GetTaskByIdAsync(Guid id, CancellationToken cancellationToken); + + Task SetTaskTitleAsync(Guid id, string title, CancellationToken cancellationToken); + + Task DeleteTaskByIdAsync(Guid id, CancellationToken cancellationToken); + + Task DeleteAllTasksAsync(CancellationToken cancellationToken); + } +} diff --git a/TaskHub/Api/UseCases/Tasks/ManageTaskUseCase.cs b/TaskHub/Api/UseCases/Tasks/ManageTaskUseCase.cs new file mode 100644 index 0000000..eb93d89 --- /dev/null +++ b/TaskHub/Api/UseCases/Tasks/ManageTaskUseCase.cs @@ -0,0 +1,57 @@ +using Api.Controllers.Tasks.Response; +using Api.UseCases.Tasks.Interfaces; +using Logic.Tasks.Services.Interfaces; + +namespace Api.UseCases.Tasks; + +internal sealed class ManageTaskUseCase : IManageTaskUseCase +{ + private readonly ITaskService _taskService; + + public ManageTaskUseCase(ITaskService taskService) + { + _taskService = taskService; + } + + public async Task CreateTaskAsync(string? title, Guid userId, CancellationToken cancellationToken) + { + var task = await _taskService.CreateTaskAsync(title, userId, cancellationToken); + + return new TaskResponse(task.Id, task.Title, task.CreatedByUserId, task.CreatedUtc); + } + + public async Task> GetAllTasksAsync(CancellationToken cancellationToken) + { + var tasks = await _taskService.GetAllTasksAsync(cancellationToken); + + return tasks + .Select(x => new TaskResponse(x.Id, x.Title, x.CreatedByUserId, x.CreatedUtc)) + .ToList() + .AsReadOnly(); + } + + public async Task GetTaskByIdAsync(Guid id, CancellationToken cancellationToken) + { + var task = await _taskService.GetTaskByIdAsync(id, cancellationToken); + + if (task == null) + return null; + + return new TaskResponse(task.Id, task.Title, task.CreatedByUserId, task.CreatedUtc); + } + + public async Task SetTaskTitleAsync(Guid id, string title, CancellationToken cancellationToken) + { + await _taskService.SetTaskTitleAsync(id, title, cancellationToken); + } + + public async Task DeleteTaskByIdAsync(Guid id, CancellationToken cancellationToken) + { + return await _taskService.DeleteTaskByIdAsync(id, cancellationToken); + } + + public async Task DeleteAllTasksAsync(CancellationToken cancellationToken) + { + await _taskService.DeleteAllTasksAsync(cancellationToken); + } +} diff --git a/TaskHub/Dal/Dal.csproj b/TaskHub/Dal/Dal.csproj index edb5ba8..1c690cf 100644 --- a/TaskHub/Dal/Dal.csproj +++ b/TaskHub/Dal/Dal.csproj @@ -1,14 +1,14 @@  - net10.0 + net8.0 enable enable - - + + diff --git a/TaskHub/Dal/DalStartUp.cs b/TaskHub/Dal/DalStartUp.cs index f6dfa82..81a8690 100644 --- a/TaskHub/Dal/DalStartUp.cs +++ b/TaskHub/Dal/DalStartUp.cs @@ -1,6 +1,7 @@ using Dal.Context; using Dal.Repositories; using Dal.Repositories.Interfaces; +using Dal.Repositories.Tasks; using DatabaseLibrary; using Microsoft.Extensions.DependencyInjection; @@ -18,6 +19,8 @@ public static class DalStartUp public static void AddDal(this IServiceCollection services) { services.AddDatabase(); + services.AddDatabase(); services.AddScoped(); + services.AddScoped(); } } diff --git a/TaskHub/Dal/Entities/TaskEntity.cs b/TaskHub/Dal/Entities/TaskEntity.cs new file mode 100644 index 0000000..9f8bcba --- /dev/null +++ b/TaskHub/Dal/Entities/TaskEntity.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dal.Entities +{ + public class TaskEntity + { + public Guid Id { get; set; } + public string? Title { get; set; } + public Guid CreatedByUserId { get; set; } + public DateTimeOffset CreatedUtc { get; set; } + } +} diff --git a/TaskHub/Dal/Migrations/Tasks/20260405200703_CreateTasks.Designer.cs b/TaskHub/Dal/Migrations/Tasks/20260405200703_CreateTasks.Designer.cs new file mode 100644 index 0000000..b47dfff --- /dev/null +++ b/TaskHub/Dal/Migrations/Tasks/20260405200703_CreateTasks.Designer.cs @@ -0,0 +1,50 @@ +// +using System; +using Dal; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Dal.Migrations.Tasks +{ + [DbContext(typeof(TaskDbContext))] + [Migration("20260405200703_CreateTasks")] + partial class CreateTasks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Dal.Entities.TaskEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskHub/Dal/Migrations/Tasks/20260405200703_CreateTasks.cs b/TaskHub/Dal/Migrations/Tasks/20260405200703_CreateTasks.cs new file mode 100644 index 0000000..abce812 --- /dev/null +++ b/TaskHub/Dal/Migrations/Tasks/20260405200703_CreateTasks.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Dal.Migrations.Tasks +{ + /// + public partial class CreateTasks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tasks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "text", nullable: true), + CreatedByUserId = table.Column(type: "uuid", nullable: false), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tasks", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Tasks"); + } + } +} diff --git a/TaskHub/Dal/Migrations/Tasks/TaskDbContextModelSnapshot.cs b/TaskHub/Dal/Migrations/Tasks/TaskDbContextModelSnapshot.cs new file mode 100644 index 0000000..9256b64 --- /dev/null +++ b/TaskHub/Dal/Migrations/Tasks/TaskDbContextModelSnapshot.cs @@ -0,0 +1,47 @@ +// +using System; +using Dal; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Dal.Migrations.Tasks +{ + [DbContext(typeof(TaskDbContext))] + partial class TaskDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Dal.Entities.TaskEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskHub/Dal/Migrations/Users/20260405203233_CreateUsers.Designer.cs b/TaskHub/Dal/Migrations/Users/20260405203233_CreateUsers.Designer.cs new file mode 100644 index 0000000..bd48a9a --- /dev/null +++ b/TaskHub/Dal/Migrations/Users/20260405203233_CreateUsers.Designer.cs @@ -0,0 +1,50 @@ +// +using System; +using Dal.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Dal.Migrations.Users +{ + [DbContext(typeof(UserDbContext))] + [Migration("20260405203233_CreateUsers")] + partial class CreateUsers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Dal.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("LastActivityUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_activity_utc"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("users", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskHub/Dal/Migrations/Users/20260405203233_CreateUsers.cs b/TaskHub/Dal/Migrations/Users/20260405203233_CreateUsers.cs new file mode 100644 index 0000000..2932ed2 --- /dev/null +++ b/TaskHub/Dal/Migrations/Users/20260405203233_CreateUsers.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Dal.Migrations.Users +{ + /// + public partial class CreateUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + last_activity_utc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/TaskHub/Dal/Migrations/Users/UserDbContextModelSnapshot.cs b/TaskHub/Dal/Migrations/Users/UserDbContextModelSnapshot.cs new file mode 100644 index 0000000..d673cf2 --- /dev/null +++ b/TaskHub/Dal/Migrations/Users/UserDbContextModelSnapshot.cs @@ -0,0 +1,47 @@ +// +using System; +using Dal.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Dal.Migrations.Users +{ + [DbContext(typeof(UserDbContext))] + partial class UserDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Dal.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("LastActivityUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_activity_utc"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("users", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskHub/Dal/Repositories/Tasks/ITaskRepository.cs b/TaskHub/Dal/Repositories/Tasks/ITaskRepository.cs new file mode 100644 index 0000000..cf428b5 --- /dev/null +++ b/TaskHub/Dal/Repositories/Tasks/ITaskRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dal.Entities; + +namespace Dal.Repositories.Tasks +{ + public interface ITaskRepository + { + Task CreateAsync(TaskEntity task); + Task> GetAllAsync(); + Task GetByIdAsync(Guid id); + Task UpdateTitleAsync(Guid id, string title); + Task DeleteAsync(Guid id); + Task DeleteAllAsync(); + } +} diff --git a/TaskHub/Dal/Repositories/Tasks/TaskRepository.cs b/TaskHub/Dal/Repositories/Tasks/TaskRepository.cs new file mode 100644 index 0000000..087cef7 --- /dev/null +++ b/TaskHub/Dal/Repositories/Tasks/TaskRepository.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dal.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Dal.Repositories.Tasks +{ + public class TaskRepository : ITaskRepository + { + private readonly TaskDbContext _context; + + public TaskRepository(TaskDbContext context) + { + _context = context; + } + + public async Task CreateAsync(TaskEntity task) + { + _context.Tasks.Add(task); + await _context.SaveChangesAsync(); + return task; + } + + public async Task> GetAllAsync() + { + return await _context.Tasks.ToListAsync(); + } + + public async Task GetByIdAsync(Guid id) + { + return await _context.Tasks.FindAsync(id); + } + + public async Task UpdateTitleAsync(Guid id, string title) + { + var task = await _context.Tasks.FindAsync(id); + if (task != null) + { + task.Title = title; + await _context.SaveChangesAsync(); + } + } + + public async Task DeleteAsync(Guid id) + { + var task = await _context.Tasks.FindAsync(id); + if (task == null) return false; + + _context.Tasks.Remove(task); + await _context.SaveChangesAsync(); + return true; + } + + public async Task DeleteAllAsync() + { + _context.Tasks.RemoveRange(_context.Tasks); + await _context.SaveChangesAsync(); + } + } +} diff --git a/TaskHub/Dal/TaskDbContext.cs b/TaskHub/Dal/TaskDbContext.cs new file mode 100644 index 0000000..42caa0c --- /dev/null +++ b/TaskHub/Dal/TaskDbContext.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dal.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Dal +{ + public class TaskDbContext : DbContext + { + public TaskDbContext(DbContextOptions options) + : base(options) { } + + public DbSet Tasks { get; set; } + } +} diff --git a/TaskHub/Libs/DatabaseLibrary/DatabaseLibrary.csproj b/TaskHub/Libs/DatabaseLibrary/DatabaseLibrary.csproj index 0c323c8..012f0c1 100644 --- a/TaskHub/Libs/DatabaseLibrary/DatabaseLibrary.csproj +++ b/TaskHub/Libs/DatabaseLibrary/DatabaseLibrary.csproj @@ -1,14 +1,14 @@  - net10.0 + net8.0 enable enable - - + + diff --git a/TaskHub/Libs/LoggingLibrary/LoggingLibrary.csproj b/TaskHub/Libs/LoggingLibrary/LoggingLibrary.csproj index bf06657..ff7786d 100644 --- a/TaskHub/Libs/LoggingLibrary/LoggingLibrary.csproj +++ b/TaskHub/Libs/LoggingLibrary/LoggingLibrary.csproj @@ -1,15 +1,15 @@  - net10.0 + net8.0 enable enable - + - + diff --git a/TaskHub/Logic/Logic.csproj b/TaskHub/Logic/Logic.csproj index a38dfe4..2be5253 100644 --- a/TaskHub/Logic/Logic.csproj +++ b/TaskHub/Logic/Logic.csproj @@ -5,11 +5,11 @@ - + - net10.0 + net8.0 enable enable diff --git a/TaskHub/Logic/LogicStartUp.cs b/TaskHub/Logic/LogicStartUp.cs index 6fc3e70..5d0a99c 100644 --- a/TaskHub/Logic/LogicStartUp.cs +++ b/TaskHub/Logic/LogicStartUp.cs @@ -1,4 +1,6 @@ -using Logic.Users.Services; +using Logic.Tasks.Services; +using Logic.Tasks.Services.Interfaces; +using Logic.Users.Services; using Logic.Users.Services.Interfaces; using Microsoft.Extensions.DependencyInjection; @@ -16,5 +18,6 @@ public static class LogicStartUp public static void AddLogic(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); } } \ No newline at end of file diff --git a/TaskHub/Logic/Tasks/Services/Interfaces/ITaskService.cs b/TaskHub/Logic/Tasks/Services/Interfaces/ITaskService.cs new file mode 100644 index 0000000..69069af --- /dev/null +++ b/TaskHub/Logic/Tasks/Services/Interfaces/ITaskService.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dal.Entities; + +namespace Logic.Tasks.Services.Interfaces; + +public interface ITaskService +{ + Task CreateTaskAsync(string? title, Guid userId, CancellationToken cancellationToken); + + Task> GetAllTasksAsync(CancellationToken cancellationToken); + + Task GetTaskByIdAsync(Guid id, CancellationToken cancellationToken); + + Task SetTaskTitleAsync(Guid id, string title, CancellationToken cancellationToken); + + Task DeleteTaskByIdAsync(Guid id, CancellationToken cancellationToken); + + Task DeleteAllTasksAsync(CancellationToken cancellationToken); +} diff --git a/TaskHub/Logic/Tasks/Services/TaskService.cs b/TaskHub/Logic/Tasks/Services/TaskService.cs new file mode 100644 index 0000000..bbda82d --- /dev/null +++ b/TaskHub/Logic/Tasks/Services/TaskService.cs @@ -0,0 +1,45 @@ +using Dal.Entities; +using Dal.Repositories.Interfaces; +using Dal.Repositories.Tasks; +using Logic.Tasks.Services.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Logic.Tasks.Services; + +public class TaskService : ITaskService +{ + private readonly ITaskRepository _repo; + + public TaskService(ITaskRepository repo) + { + _repo = repo; + } + + public Task CreateTaskAsync(string? title, Guid userId, CancellationToken cancellationToken) + => _repo.CreateAsync(new TaskEntity + { + Id = Guid.NewGuid(), + Title = title, + CreatedByUserId = userId, + CreatedUtc = DateTimeOffset.UtcNow + }); + + public Task> GetAllTasksAsync(CancellationToken cancellationToken) + => _repo.GetAllAsync(); + + public Task GetTaskByIdAsync(Guid id, CancellationToken cancellationToken) + => _repo.GetByIdAsync(id); + + public Task SetTaskTitleAsync(Guid id, string title, CancellationToken cancellationToken) + => _repo.UpdateTitleAsync(id, title); + + public Task DeleteTaskByIdAsync(Guid id, CancellationToken cancellationToken) + => _repo.DeleteAsync(id); + + public Task DeleteAllTasksAsync(CancellationToken cancellationToken) + => _repo.DeleteAllAsync(); +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8d3ee0f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + postgres: + image: postgres:15 + container_name: taskhub_postgres + environment: + POSTGRES_DB: taskhub + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: \ No newline at end of file