SubsystemOperationTimeRequest implements IValidatableObject;

Add Controller.ValidationBadRequest(..)
This commit is contained in:
ngfrolov 2023-09-28 16:25:29 +05:00
parent ab166487fb
commit 772360cb6e
Signed by untrusted user who does not match committer: ng.frolov
GPG Key ID: E99907A0357B29A7
14 changed files with 96 additions and 65 deletions

View File

@ -1,4 +1,4 @@
using AsbCloudApp.ValidationAttributes;
using AsbCloudApp.Validation;
using System;
using System.ComponentModel.DataAnnotations;

View File

@ -22,25 +22,5 @@ namespace AsbCloudApp.Exceptions
{
ParamName = paramName;
}
/// <summary>
/// преобразование в объект валидации
/// </summary>
/// <returns></returns>
public object ToValidationErrorObject()
=> MakeValidationError(ParamName, Message);
/// <summary>
/// фабрика объекта валидации
/// </summary>
/// <param name="paramName"></param>
/// <param name="errors"></param>
/// <returns></returns>
public static object MakeValidationError(string paramName, params string[] errors)
=> new
{
name = paramName,
errors,
};
}
}

View File

@ -1,4 +1,4 @@
using AsbCloudApp.ValidationAttributes;
using AsbCloudApp.Validation;
using System;
namespace AsbCloudApp.Requests;

View File

@ -1,23 +1,27 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace AsbCloudApp.Requests
{
/// <summary>
/// класс с фильтрами для запроса
/// </summary>
public class SubsystemOperationTimeRequest: RequestBase
public class SubsystemOperationTimeRequest: RequestBase, IValidatableObject
{
private static readonly DateTime validationMinDate = new DateTime(2020,01,01,0,0,0,DateTimeKind.Utc);
/// <summary>
/// идентификатор скважины
/// </summary>
[Required]
public int IdWell { get; set; }
/// <summary>
/// идентификатор подсистемы
/// </summary>
public IEnumerable<int>? IdsSubsystems { get; set; }
public IEnumerable<int> IdsSubsystems { get; set; } = Enumerable.Empty<int>();
/// <summary>
/// Больше или равно дате
@ -58,5 +62,32 @@ namespace AsbCloudApp.Requests
/// Режим выборки элементов
/// </summary>
public int SelectMode { get; set; } = SelectModeOuter;
/// <inheritdoc/>
public IEnumerable<ValidationResult> 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;
}
}
}

View File

@ -1,7 +1,7 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace AsbCloudApp.ValidationAttributes
namespace AsbCloudApp.Validation
{
/// <summary>
/// Атрибут валидации даты-времени

View File

@ -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

View File

@ -1,4 +1,4 @@
using AsbCloudApp.ValidationAttributes;
using AsbCloudApp.Validation;
using System;
using Xunit;

View File

@ -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;

View File

@ -83,18 +83,14 @@ public class NotificationController : ControllerBase
/// <returns></returns>
[HttpGet("{idNotification}")]
[ProducesResponseType(typeof(NotificationDto), (int)System.Net.HttpStatusCode.OK)]
public async Task<IActionResult> GetAsync([Required] int idNotification,
[ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)]
public async Task<IActionResult> 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);
}
/// <summary>

View File

@ -128,7 +128,7 @@ namespace AsbCloudWebApi.Controllers.SAUB
public async Task<IActionResult> 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();

View File

@ -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
/// </summary>
[HttpGet("stat")]
[ProducesResponseType(typeof(IEnumerable<SubsystemStatDto>), (int)System.Net.HttpStatusCode.OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)]
public async Task<IActionResult> 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
/// </summary>
[HttpGet("operationTime")]
[ProducesResponseType(typeof(IEnumerable<SubsystemOperationTimeDto>), (int)System.Net.HttpStatusCode.OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)]
public async Task<IActionResult> 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<IActionResult> 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<bool> IsValidRequest(SubsystemOperationTimeRequest request, CancellationToken token)
/// <summary>
/// Валидирует запрос и бросает исключение ArgumentInvalidException
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
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;
}
}
}

View File

@ -55,6 +55,7 @@ namespace AsbCloudWebApi.Controllers
/// <param name="token"></param>
/// <returns></returns>
[HttpPut("{key}")]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)]
public virtual async Task<ActionResult<int>> 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
/// <param name="token"></param>
/// <returns></returns>
[HttpDelete("{key}")]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)System.Net.HttpStatusCode.BadRequest)]
public virtual async Task<ActionResult<int>> 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);
}

View File

@ -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)
/// <summary>
/// <para>
/// Returns BadRequest with ValidationProblemDetails as body
/// </para>
/// <para>
/// Используйте этот метод только если валидацию нельзя сделать через
/// атрибуты валидации или IValidatableObject модели.
/// </para>
/// </summary>
/// <param name="controller"></param>
/// <param name="paramName"></param>
/// <param name="error"></param>
/// <returns></returns>
public static BadRequestObjectResult ValidationBadRequest(this ControllerBase controller, string paramName, string error)
{
return controller.BadRequest(new
{
name = paramName,
errors,
});
var errors = new Dictionary<string, string[]> {
{ paramName, new[]{ error } }
};
var problem = new ValidationProblemDetails(errors);
return controller.BadRequest(problem);
}
public static MvcOptions UseDateOnlyTimeOnlyStringConverters(this MvcOptions options)

View File

@ -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<string, string[]> {
{ ex.ParamName, new[]{ ex.Message } }
};
var problem = new ValidationProblemDetails(errors);
var buffer = System.Text.Json.JsonSerializer.Serialize(problem);
return buffer;
}
}