diff --git a/AsbCloudApp/Data/WellOperationDto.cs b/AsbCloudApp/Data/WellOperationDto.cs index 31b50243..f2a2d65f 100644 --- a/AsbCloudApp/Data/WellOperationDto.cs +++ b/AsbCloudApp/Data/WellOperationDto.cs @@ -1,4 +1,4 @@ -using AsbCloudApp.ValidationAttributes; +using AsbCloudApp.Validation; using System; using System.ComponentModel.DataAnnotations; diff --git a/AsbCloudApp/Exceptions/ArgumentInvalidException.cs b/AsbCloudApp/Exceptions/ArgumentInvalidException.cs index 987401bc..af239d00 100644 --- a/AsbCloudApp/Exceptions/ArgumentInvalidException.cs +++ b/AsbCloudApp/Exceptions/ArgumentInvalidException.cs @@ -22,25 +22,5 @@ namespace AsbCloudApp.Exceptions { ParamName = paramName; } - - /// - /// преобразование в объект валидации - /// - /// - public object ToValidationErrorObject() - => MakeValidationError(ParamName, Message); - - /// - /// фабрика объекта валидации - /// - /// - /// - /// - public static object MakeValidationError(string paramName, params string[] errors) - => new - { - name = paramName, - errors, - }; } } diff --git a/AsbCloudApp/Requests/ReportParametersRequest.cs b/AsbCloudApp/Requests/ReportParametersRequest.cs index 465662e1..7a9061a5 100644 --- a/AsbCloudApp/Requests/ReportParametersRequest.cs +++ b/AsbCloudApp/Requests/ReportParametersRequest.cs @@ -1,4 +1,4 @@ -using AsbCloudApp.ValidationAttributes; +using AsbCloudApp.Validation; using System; namespace AsbCloudApp.Requests; diff --git a/AsbCloudApp/Requests/SubsystemOperationTimeRequest.cs b/AsbCloudApp/Requests/SubsystemOperationTimeRequest.cs index 11aafa3c..9080b596 100644 --- a/AsbCloudApp/Requests/SubsystemOperationTimeRequest.cs +++ b/AsbCloudApp/Requests/SubsystemOperationTimeRequest.cs @@ -1,23 +1,27 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; namespace AsbCloudApp.Requests { /// /// класс с фильтрами для запроса /// - public class SubsystemOperationTimeRequest: RequestBase + public class SubsystemOperationTimeRequest: RequestBase, IValidatableObject { + private static readonly DateTime validationMinDate = new DateTime(2020,01,01,0,0,0,DateTimeKind.Utc); + /// /// идентификатор скважины /// [Required] public int IdWell { get; set; } + /// /// идентификатор подсистемы /// - public IEnumerable? IdsSubsystems { get; set; } + public IEnumerable IdsSubsystems { get; set; } = Enumerable.Empty(); /// /// Больше или равно дате @@ -58,5 +62,32 @@ namespace AsbCloudApp.Requests /// Режим выборки элементов /// public int SelectMode { get; set; } = SelectModeOuter; + + /// + public IEnumerable Validate(ValidationContext validationContext) + { + if (GtDate.HasValue && GtDate < validationMinDate) + yield return new ValidationResult( + $"Должно быть больше {validationMinDate:O})", + new[] { nameof(GtDate) }); + + if (LtDate.HasValue && GtDate.HasValue) + { + if (LtDate < GtDate) + yield return new ValidationResult( + $"{nameof(LtDate)} должно быть больше {nameof(GtDate)}. ({LtDate:O} < {GtDate:O})", + new[] { nameof(LtDate), nameof(GtDate) }); + } + + if (LtDepth.HasValue && GtDepth.HasValue) + { + if (LtDepth < GtDepth) + yield return new ValidationResult( + $"{nameof(LtDepth)} должно быть больше {nameof(GtDepth)}. ({LtDepth:O} < {GtDepth:O})", + new[] { nameof(LtDepth), nameof(GtDepth) }); + } + + yield break; + } } } diff --git a/AsbCloudApp/ValidationAttributes/DateValidationAttribute.cs b/AsbCloudApp/Validation/DateValidationAttribute.cs similarity index 98% rename from AsbCloudApp/ValidationAttributes/DateValidationAttribute.cs rename to AsbCloudApp/Validation/DateValidationAttribute.cs index 3ad6fce0..925739a4 100644 --- a/AsbCloudApp/ValidationAttributes/DateValidationAttribute.cs +++ b/AsbCloudApp/Validation/DateValidationAttribute.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; -namespace AsbCloudApp.ValidationAttributes +namespace AsbCloudApp.Validation { /// /// Атрибут валидации даты-времени diff --git a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeService.cs b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeService.cs index 0bb5acd6..1b26b03f 100644 --- a/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeService.cs +++ b/AsbCloudInfrastructure/Services/Subsystems/SubsystemOperationTimeService.cs @@ -345,7 +345,7 @@ namespace AsbCloudInfrastructure.Services.Subsystems .Where(o => o.IdTelemetry == well.IdTelemetry) .AsNoTracking(); - if (request.IdsSubsystems?.Any() == true) + if (request.IdsSubsystems.Any()) query = query.Where(o => request.IdsSubsystems.Contains(o.IdSubsystem)); // # Dates range condition diff --git a/AsbCloudWebApi.Tests/AttributeTest/DateValidationAttributeTest.cs b/AsbCloudWebApi.Tests/AttributeTest/DateValidationAttributeTest.cs index 3e4990a2..35c446e0 100644 --- a/AsbCloudWebApi.Tests/AttributeTest/DateValidationAttributeTest.cs +++ b/AsbCloudWebApi.Tests/AttributeTest/DateValidationAttributeTest.cs @@ -1,4 +1,4 @@ -using AsbCloudApp.ValidationAttributes; +using AsbCloudApp.Validation; using System; using Xunit; diff --git a/AsbCloudWebApi/Controllers/DrillingProgramController.cs b/AsbCloudWebApi/Controllers/DrillingProgramController.cs index fc4ad8ea..435f0b50 100644 --- a/AsbCloudWebApi/Controllers/DrillingProgramController.cs +++ b/AsbCloudWebApi/Controllers/DrillingProgramController.cs @@ -119,10 +119,10 @@ namespace AsbCloudWebApi.Controllers return Forbid(); if (files.Count > 1) - return BadRequest(ArgumentInvalidException.MakeValidationError(nameof(files), "only 1 file can be uploaded")); + throw new ArgumentInvalidException("only 1 file can be uploaded", nameof(files)); if (files.Count == 0) - return BadRequest(ArgumentInvalidException.MakeValidationError(nameof(files), "at list 1 file should be uploaded")); + throw new ArgumentInvalidException("at list 1 file should be uploaded", nameof(files)); var fileName = files[0].FileName; diff --git a/AsbCloudWebApi/Controllers/NotificationController.cs b/AsbCloudWebApi/Controllers/NotificationController.cs index 2d37693d..3332e012 100644 --- a/AsbCloudWebApi/Controllers/NotificationController.cs +++ b/AsbCloudWebApi/Controllers/NotificationController.cs @@ -83,18 +83,14 @@ public class NotificationController : ControllerBase /// [HttpGet("{idNotification}")] [ProducesResponseType(typeof(NotificationDto), (int)System.Net.HttpStatusCode.OK)] - public async Task GetAsync([Required] int idNotification, + [ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)] + public async Task GetAsync([Required] int idNotification, CancellationToken cancellationToken) { - var notification = await notificationRepository.GetOrDefaultAsync(idNotification, cancellationToken); + var notification = await notificationRepository.GetOrDefaultAsync(idNotification, cancellationToken) + ?? throw new ArgumentInvalidException("Уведомление не найдено", nameof(idNotification)); - if (notification is null) - { - return BadRequest(ArgumentInvalidException.MakeValidationError(nameof(idNotification), - "Уведомление не найдено")); - } - - return Ok(notification); + return Ok(notification); } /// diff --git a/AsbCloudWebApi/Controllers/SAUB/DetectedOperationController.cs b/AsbCloudWebApi/Controllers/SAUB/DetectedOperationController.cs index 0a16bcfd..2f644988 100644 --- a/AsbCloudWebApi/Controllers/SAUB/DetectedOperationController.cs +++ b/AsbCloudWebApi/Controllers/SAUB/DetectedOperationController.cs @@ -128,7 +128,7 @@ namespace AsbCloudWebApi.Controllers.SAUB public async Task ExportAsync(int? idWell, int? idCluster, CancellationToken token) { if (idCluster is null && idWell is null) - return this.MakeBadRequest(nameof(idWell), $"One of {nameof(idWell)} or {nameof(idCluster)} mast be set."); + return this.ValidationBadRequest(nameof(idWell), $"One of {nameof(idWell)} or {nameof(idCluster)} mast be set."); int? idCompany = User.GetCompanyId(); diff --git a/AsbCloudWebApi/Controllers/Subsystems/SubsystemOperationTimeController.cs b/AsbCloudWebApi/Controllers/Subsystems/SubsystemOperationTimeController.cs index 1c7279b7..f7bc7afd 100644 --- a/AsbCloudWebApi/Controllers/Subsystems/SubsystemOperationTimeController.cs +++ b/AsbCloudWebApi/Controllers/Subsystems/SubsystemOperationTimeController.cs @@ -1,5 +1,6 @@ using AsbCloudApp.Data; using AsbCloudApp.Data.Subsystems; +using AsbCloudApp.Exceptions; using AsbCloudApp.Requests; using AsbCloudApp.Services; using AsbCloudApp.Services.Subsystems; @@ -50,12 +51,12 @@ namespace AsbCloudWebApi.Controllers.Subsystems /// [HttpGet("stat")] [ProducesResponseType(typeof(IEnumerable), (int)System.Net.HttpStatusCode.OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)] public async Task GetStatAsync([FromQuery] SubsystemOperationTimeRequest request, CancellationToken token) { if (!await UserHasAccesToWellAsync(request.IdWell, token)) return Forbid(); - if (!await IsValidRequest(request, token)) - return BadRequest("Запрашиваемый диапазон должен заканчиваться за 2 часа до текущего времени(после приведения к UTC)."); + await CustomValidate(request, token); var subsystemResult = await subsystemOperationTimeService.GetStatAsync(request, token); return Ok(subsystemResult); } @@ -125,15 +126,14 @@ namespace AsbCloudWebApi.Controllers.Subsystems /// [HttpGet("operationTime")] [ProducesResponseType(typeof(IEnumerable), (int)System.Net.HttpStatusCode.OK)] - + [ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)] public async Task GetOperationTimeAsync( [FromQuery] SubsystemOperationTimeRequest request, CancellationToken token) { if (!await UserHasAccesToWellAsync(request.IdWell, token)) return Forbid(); - if (!await IsValidRequest(request, token)) - return BadRequest("Запрашиваемый диапазон должен заканчиваться за 2 часа до текущего времени(после приведения к UTC)."); + await CustomValidate(request, token); var result = await subsystemOperationTimeService.GetOperationTimeAsync(request, token); return Ok(result); @@ -148,14 +148,14 @@ namespace AsbCloudWebApi.Controllers.Subsystems [HttpDelete] [Permission] [ProducesResponseType(typeof(int), (int)System.Net.HttpStatusCode.OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)] public async Task DeleteAsync( [FromQuery] SubsystemOperationTimeRequest request, CancellationToken token) { if (!await UserHasAccesToWellAsync(request.IdWell, token)) return Forbid(); - if (!await IsValidRequest(request, token)) - return BadRequest("Запрашиваемый диапазон должен заканчиваться за 2 часа до текущего времени(после приведения к UTC)."); + await CustomValidate(request, token); var result = await subsystemOperationTimeService.DeleteAsync(request, token); return Ok(result); } @@ -180,20 +180,24 @@ namespace AsbCloudWebApi.Controllers.Subsystems return true; return false; } - protected async Task IsValidRequest(SubsystemOperationTimeRequest request, CancellationToken token) + + /// + /// Валидирует запрос и бросает исключение ArgumentInvalidException + /// + /// + /// + /// + /// + private async Task CustomValidate(SubsystemOperationTimeRequest request, CancellationToken token) { var well = await wellService.GetOrDefaultAsync(request.IdWell, token); if (well is not null && request.LtDate.HasValue) { - var ltDate = (DateTimeOffset)request.LtDate; - var utcDateRequest = ltDate.ToRemoteDateTime(well.Timezone.Hours); - + var ltDate = request.LtDate.Value; + var utcDateRequest = ltDate.ToUtcDateTimeOffset(well.Timezone.Hours); if (utcDateRequest.AddHours(2) > DateTime.UtcNow) - { - return false; - } + throw new ArgumentInvalidException("Запрашиваемый диапазон должен заканчиваться за 2 часа до текущего времени", nameof(request.LtDate)); } - return true; } } } diff --git a/AsbCloudWebApi/Controllers/UserSettingsController.cs b/AsbCloudWebApi/Controllers/UserSettingsController.cs index e3e47327..db3f6f2e 100644 --- a/AsbCloudWebApi/Controllers/UserSettingsController.cs +++ b/AsbCloudWebApi/Controllers/UserSettingsController.cs @@ -55,6 +55,7 @@ namespace AsbCloudWebApi.Controllers /// /// [HttpPut("{key}")] + [ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)] public virtual async Task> UpsertAsync(string key, [FromBody] System.Text.Json.JsonDocument value, CancellationToken token) { var userId = User.GetUserId(); @@ -63,7 +64,7 @@ namespace AsbCloudWebApi.Controllers var result = await service.UpsertAsync((int)userId, key, value, token).ConfigureAwait(false); if (result < 0) - return BadRequest(ArgumentInvalidException.MakeValidationError(nameof(key), "not found")); + throw new ArgumentInvalidException("not found", nameof(key)); return Ok(result); } @@ -74,6 +75,7 @@ namespace AsbCloudWebApi.Controllers /// /// [HttpDelete("{key}")] + [ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)] public virtual async Task> DeleteAsync(string key, CancellationToken token) { var userId = User.GetUserId(); @@ -82,7 +84,7 @@ namespace AsbCloudWebApi.Controllers var result = await service.DeleteAsync((int)userId, key, token).ConfigureAwait(false); if (result < 0) - return BadRequest(ArgumentInvalidException.MakeValidationError(nameof(key), "not found")); + throw new ArgumentInvalidException("not found", nameof(key)); return Ok(result); } diff --git a/AsbCloudWebApi/Extentions.cs b/AsbCloudWebApi/Extentions.cs index 021594fc..ba87612f 100644 --- a/AsbCloudWebApi/Extentions.cs +++ b/AsbCloudWebApi/Extentions.cs @@ -1,6 +1,7 @@ using AsbCloudApp.Data.User; using AsbCloudWebApi.Converters; using System; +using System.Collections.Generic; using System.ComponentModel; using System.Security.Claims; @@ -30,13 +31,26 @@ namespace Microsoft.AspNetCore.Mvc : null; } - public static IActionResult MakeBadRequest(this ControllerBase controller, string paramName, params string[] errors) + /// + /// + /// Returns BadRequest with ValidationProblemDetails as body + /// + /// + /// Используйте этот метод только если валидацию нельзя сделать через + /// атрибуты валидации или IValidatableObject модели. + /// + /// + /// + /// + /// + /// + public static BadRequestObjectResult ValidationBadRequest(this ControllerBase controller, string paramName, string error) { - return controller.BadRequest(new - { - name = paramName, - errors, - }); + var errors = new Dictionary { + { paramName, new[]{ error } } + }; + var problem = new ValidationProblemDetails(errors); + return controller.BadRequest(problem); } public static MvcOptions UseDateOnlyTimeOnlyStringConverters(this MvcOptions options) diff --git a/AsbCloudWebApi/Middlewares/SimplifyExceptionsMiddleware.cs b/AsbCloudWebApi/Middlewares/SimplifyExceptionsMiddleware.cs index f7ff53e8..3180f41d 100644 --- a/AsbCloudWebApi/Middlewares/SimplifyExceptionsMiddleware.cs +++ b/AsbCloudWebApi/Middlewares/SimplifyExceptionsMiddleware.cs @@ -1,8 +1,9 @@ // Ignore Spelling: Middlewares - using AsbCloudApp.Exceptions; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace AsbCloudWebApi.Middlewares @@ -55,8 +56,11 @@ namespace AsbCloudWebApi.Middlewares private static string MakeJsonBody(ArgumentInvalidException ex) { - object error = ex.ToValidationErrorObject(); - var buffer = System.Text.Json.JsonSerializer.Serialize(error); + var errors = new Dictionary { + { ex.ParamName, new[]{ ex.Message } } + }; + var problem = new ValidationProblemDetails(errors); + var buffer = System.Text.Json.JsonSerializer.Serialize(problem); return buffer; } }