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
201 protected ObjectResult
RateLimit(RateLimitExceededException rateLimitException)
203 ArgumentNullException.ThrowIfNull(rateLimitException);
205 Logger.LogWarning(rateLimitException,
"Exceeded GitHub rate limit!");
207 var secondsString = Math.Ceiling(rateLimitException.GetRetryAfterTimeSpan().TotalSeconds).ToString(CultureInfo.InvariantCulture);
208 Response.Headers.Add(HeaderNames.RetryAfter, secondsString);
217 protected virtual ValueTask<IActionResult?>
ValidateRequest(CancellationToken cancellationToken)
218 => ValueTask.FromResult<IActionResult?>(
null);
227 if (headersException ==
null)
228 throw new InvalidOperationException(
"Expected a header parse exception!");
232 AdditionalData = headersException.Message,
236 return this.
StatusCode(HttpStatusCode.NotAcceptable, errorMessage);
238 return BadRequest(errorMessage);
253 Func<TModel, ValueTask>? resultTransformer,
256 CancellationToken cancellationToken) => PaginatedImpl(
276 Func<TApiModel, ValueTask>? resultTransformer,
279 CancellationToken cancellationToken)
301 Func<TResultModel, ValueTask>? resultTransformer,
304 CancellationToken cancellationToken)
306 ArgumentNullException.ThrowIfNull(queryGenerator);
308 if (pageQuery <= 0 || pageSizeQuery <= 0)
315 AdditionalData = $
"Maximum page size: {MaximumPageSize}",
318 var page = pageQuery ?? 1;
320 var paginationResult = await queryGenerator();
321 if (paginationResult ==
null)
324 if (!paginationResult.Valid)
325 return paginationResult.EarlyOut;
327 var queriedResults = paginationResult
329 .Skip((page - 1) * pageSize)
333 List<TModel> pagedResults;
334 if (queriedResults.Provider is IAsyncQueryProvider)
336 totalResults = await paginationResult.Results.CountAsync(cancellationToken);
337 pagedResults = await queriedResults
338 .ToListAsync(cancellationToken);
342 totalResults = paginationResult.Results.Count();
343 pagedResults = [.. queriedResults];
346 ICollection<TResultModel> finalResults;
347 if (typeof(TResultModel).IsAssignableFrom(typeof(TModel)))
348 finalResults = pagedResults.Cast<TResultModel>().ToList();
350 finalResults = pagedResults
352 .Select(x => x.ToApi())
355 if (resultTransformer !=
null)
356 foreach (var finalResult
in finalResults)
357 await resultTransformer(finalResult);
359 var carryTheOne = totalResults % pageSize != 0
365 Content = finalResults,
367 TotalPages = (ushort)(totalResults / pageSize) + carryTheOne,
368 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.
ValueTask< IActionResult > Paginated< TModel >(Func< ValueTask< PaginatableResult< TModel >?> > queryGenerator, Func< TModel, 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.
IActionResult HeadersIssue(HeadersException headersException)
Response for missing/Invalid headers.
ValueTask< IActionResult > Paginated< TModel, TApiModel >(Func< ValueTask< PaginatableResult< TModel >?> > queryGenerator, Func< TApiModel, ValueTask >? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken)
Generates a paginated response.
override async ValueTask< IActionResult?> HookExecuteAction(Func< Task > executeAction, CancellationToken cancellationToken)
Hook for executing a request.A ValueTask<TResult> resulting in an IActionResult that,...
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.
ObjectResult RateLimit(RateLimitExceededException rateLimitException)
429 response for a given rateLimitException .
ILogger< ApiController > Logger
The ILogger for the ApiController.
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.
ApiController(IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, IApiHeadersProvider apiHeadersProvider, ILogger< ApiController > logger, bool requireHeaders)
Initializes a new instance of the ApiController class.
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.
For creating and accessing authentication contexts.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
HeaderErrorTypes
Types of individual ApiHeaders errors.