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.Valid)
322 return paginationResult.EarlyOut;
324 var queriedResults = paginationResult
326 .Skip((page - 1) * pageSize)
330 List<TModel> pagedResults;
331 if (queriedResults.Provider is IAsyncQueryProvider)
333 totalResults = await paginationResult.Results.CountAsync(cancellationToken);
334 pagedResults = await queriedResults
335 .ToListAsync(cancellationToken);
339 totalResults = paginationResult.Results.Count();
340 pagedResults = [.. queriedResults];
343 ICollection<TResultModel> finalResults;
344 if (typeof(TResultModel).IsAssignableFrom(typeof(TModel)))
345 finalResults = pagedResults.Cast<TResultModel>().ToList();
347 finalResults = pagedResults
349 .Select(x => x.ToApi())
352 if (resultTransformer !=
null)
353 foreach (var finalResult
in finalResults)
354 await resultTransformer(finalResult);
356 var carryTheOne = totalResults % pageSize != 0
362 Content = finalResults,
364 TotalPages = (ushort)(totalResults / pageSize) + carryTheOne,
365 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.
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,...
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 .
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.
For creating and accessing authentication contexts.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
HeaderErrorTypes
Types of individual ApiHeaders errors.