2using System.Collections.Generic;
3using System.Globalization;
7using System.Threading.Tasks;
9using Microsoft.AspNetCore.Http;
10using Microsoft.AspNetCore.Mvc;
11using Microsoft.EntityFrameworkCore;
12using Microsoft.EntityFrameworkCore.Query;
13using Microsoft.Extensions.Logging;
14using Microsoft.Net.Http.Headers;
71 protected ILogger<ApiController>
Logger {
get; }
95 ILogger<ApiController> logger,
98 DatabaseContext = databaseContext ??
throw new ArgumentNullException(nameof(databaseContext));
99 AuthenticationContext = authenticationContext ??
throw new ArgumentNullException(nameof(authenticationContext));
100 ApiHeadersProvider = apiHeadersProvider ??
throw new ArgumentNullException(nameof(apiHeadersProvider));
101 Logger = logger ??
throw new ArgumentNullException(nameof(logger));
108#pragma warning disable CA1506
109 protected override async ValueTask<IActionResult?>
HookExecuteAction(Func<Task> executeAction, CancellationToken cancellationToken)
111 ArgumentNullException.ThrowIfNull(executeAction);
121 if (errorCase !=
null)
124 if (ModelState?.IsValid ==
false)
126 var errorMessages = ModelState
127 .SelectMany(x => x.Value!.Errors)
128 .Select(x => x.ErrorMessage)
134 .Where(x => !x.EndsWith(
" field is required.", StringComparison.Ordinal));
136 if (errorMessages.Any())
140 AdditionalData = String.Join(Environment.NewLine, errorMessages),
156 var isGet = HttpMethods.IsGet(Request.Method);
161 "Starting API request: Version: {clientApiVersion}. {userAgentHeaderName}: {clientUserAgent}",
163 HeaderNames.UserAgent,
166 else if (Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgents))
168 "Starting unauthorized API request. {userAgentHeaderName}: {allUserAgents}",
169 HeaderNames.UserAgent,
173 "Starting unauthorized API request. No {userAgentHeaderName}!",
174 HeaderNames.UserAgent);
176 await executeAction();
181#pragma warning restore CA1506
202 Logger.LogTrace(ex,
"System identities not implemented!");
218 protected ObjectResult
Created(
object payload) =>
StatusCode((
int)HttpStatusCode.Created, payload);
225 protected ObjectResult
RateLimit(RateLimitExceededException rateLimitException)
227 ArgumentNullException.ThrowIfNull(rateLimitException);
229 Logger.LogWarning(rateLimitException,
"Exceeded GitHub rate limit!");
231 var secondsString = Math.Ceiling(rateLimitException.GetRetryAfterTimeSpan().TotalSeconds).ToString(CultureInfo.InvariantCulture);
232 Response.Headers.Add(HeaderNames.RetryAfter, secondsString);
241 protected virtual ValueTask<IActionResult?>
ValidateRequest(CancellationToken cancellationToken)
242 => ValueTask.FromResult<IActionResult?>(
null);
251 if (headersException ==
null)
252 throw new InvalidOperationException(
"Expected a header parse exception!");
256 AdditionalData = headersException.Message,
260 return this.
StatusCode(HttpStatusCode.NotAcceptable, errorMessage);
262 return BadRequest(errorMessage);
277 Func<TModel, ValueTask>? resultTransformer,
280 CancellationToken cancellationToken) => PaginatedImpl(
300 Func<TApiModel, ValueTask>? resultTransformer,
303 CancellationToken cancellationToken)
325 Func<TResultModel, ValueTask>? resultTransformer,
328 CancellationToken cancellationToken)
330 ArgumentNullException.ThrowIfNull(queryGenerator);
332 if (pageQuery <= 0 || pageSizeQuery <= 0)
339 AdditionalData = $
"Maximum page size: {MaximumPageSize}",
342 var page = pageQuery ?? 1;
344 var paginationResult = await queryGenerator();
345 if (!paginationResult.Valid)
346 return paginationResult.EarlyOut;
348 var queriedResults = paginationResult
350 .Skip((page - 1) * pageSize)
354 List<TModel> pagedResults;
355 if (queriedResults.Provider is IAsyncQueryProvider)
357 totalResults = await paginationResult.Results.CountAsync(cancellationToken);
358 pagedResults = await queriedResults
359 .ToListAsync(cancellationToken);
363 totalResults = paginationResult.Results.Count();
364 pagedResults = queriedResults.ToList();
367 ICollection<TResultModel> finalResults;
368 if (typeof(TModel) == typeof(TResultModel))
369 finalResults = (List<TResultModel>)(
object)pagedResults;
371 finalResults = pagedResults
373 .Select(x => x.ToApi())
376 if (resultTransformer !=
null)
377 foreach (var finalResult
in finalResults)
378 await resultTransformer(finalResult);
380 var carryTheOne = totalResults % pageSize != 0
386 Content = finalResults,
388 TotalPages = (ushort)(totalResults / pageSize) + carryTheOne,
389 TotalItems = totalResults,
virtual ? long Id
The ID of the entity.
Metadata about a server instance.
Represents an error message returned by the server.
Represents a paginated set of models.
Base class for all API style controllers.
Base Controller for API functions.
async ValueTask< IActionResult > PaginatedImpl< TModel, TResultModel >(Func< ValueTask< PaginatableResult< TModel > > > queryGenerator, Func< TResultModel, ValueTask >? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken)
Generates a paginated response.
const ushort MaximumPageSize
Maximum size of Paginated<TModel> results.
new NotFoundObjectResult NotFound()
Generic 404 response.
ObjectResult RequiresPosixSystemIdentity(NotImplementedException ex)
Generic 501 response.
IActionResult HeadersIssue(HeadersException headersException)
Response for missing/Invalid headers.
override async ValueTask< IActionResult?> HookExecuteAction(Func< Task > executeAction, CancellationToken cancellationToken)
Hook for executing a request.A ValueTask<TResult> resulting in an IActionResult that,...
new ObjectResult Unauthorized()
Generic 401 response.
readonly bool requireHeaders
If ApiHeaders are required.
StatusCodeResult StatusCode(HttpStatusCode statusCode)
Strongly type calls to ControllerBase.StatusCode(int).
virtual ValueTask< IActionResult?> ValidateRequest(CancellationToken cancellationToken)
Performs validation a request.
const ushort DefaultPageSize
Default size of Paginated<TModel> results.
ValueTask< IActionResult > Paginated< TModel >(Func< ValueTask< PaginatableResult< TModel > > > queryGenerator, Func< TModel, ValueTask >? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken)
Generates a paginated response.
ObjectResult RateLimit(RateLimitExceededException rateLimitException)
429 response for a given rateLimitException .
ObjectResult Created(object payload)
Generic 201 response with a given payload .
ILogger< ApiController > Logger
The ILogger for the ApiController.
ApiController(IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, IApiHeadersProvider apiHeadersProvider, ILogger< ApiController > logger, bool requireHeaders)
Initializes a new instance of the ApiController class.
ValueTask< IActionResult > Paginated< TModel, TApiModel >(Func< ValueTask< PaginatableResult< TModel > > > queryGenerator, Func< TApiModel, ValueTask >? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken)
Generates a paginated response.
Helper for returning paginated models.
Backend abstract implementation of IDatabaseContext.
Instance? Instance
The Models.Instance the InstancePermissionSet belongs to.
User User
The authenticated user.
InstancePermissionSet? InstancePermissionSet
The User's effective Models.InstancePermissionSet if applicable.
bool Valid
If the IAuthenticationContext is for a valid login.
Helpers for manipulating the Serilog.Context.LogContext.
const string InstanceIdContextProperty
The Serilog.Context.LogContext property name for Models.Instance Api.Models.EntityId....
const string UserIdContextProperty
The Serilog.Context.LogContext property name for Models.Instance Api.Models.EntityId....
const string RequestPathContextProperty
The Serilog.Context.LogContext property name for Models.User Api.Models.EntityId.Ids.
Represents the currently authenticated Models.User.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
HeaderErrorTypes
Types of individual ApiHeaders errors.