mirror of
https://github.com/XFox111/MuiCharts.git
synced 2026-04-22 06:51:05 +03:00
Added ASP.NET backend with SQLite
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace MuiCharts.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for API controllers that provides common functionality and error handling.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the derived controller.</typeparam>
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public abstract class ApiControllerBase<T>(ILogger<T> logger)
|
||||
: ControllerBase where T : ApiControllerBase<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the logger instance used for logging.
|
||||
/// </summary>
|
||||
protected ILogger<T> Logger { get; } = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the response for a list of errors.
|
||||
/// </summary>
|
||||
/// <param name="errors">The list of errors.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the response.</returns>
|
||||
protected IActionResult Problem(List<Error> errors)
|
||||
{
|
||||
if (errors.All(error => error.Type == ErrorType.Validation))
|
||||
{
|
||||
ModelStateDictionary modelState = new();
|
||||
|
||||
foreach (Error error in errors)
|
||||
modelState.AddModelError(error.Code, error.Description);
|
||||
|
||||
return ValidationProblem(modelState);
|
||||
}
|
||||
|
||||
Error firstError = errors[0];
|
||||
|
||||
Logger.LogError("An error occured during request processing: {Error}", firstError);
|
||||
|
||||
int statusCode = firstError.Type switch
|
||||
{
|
||||
ErrorType.Validation => StatusCodes.Status400BadRequest,
|
||||
ErrorType.Unauthorized => StatusCodes.Status401Unauthorized,
|
||||
ErrorType.Forbidden => StatusCodes.Status403Forbidden,
|
||||
ErrorType.NotFound => StatusCodes.Status404NotFound,
|
||||
ErrorType.Conflict => StatusCodes.Status409Conflict,
|
||||
_ => StatusCodes.Status500InternalServerError
|
||||
};
|
||||
|
||||
return Problem(
|
||||
statusCode: statusCode,
|
||||
detail: firstError.Description
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace MuiCharts.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for handling errors.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class ErrorController: ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the HTTP GET request for the error endpoint.
|
||||
/// </summary>
|
||||
/// <returns>An IActionResult representing the error response.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult Error() => Problem();
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MuiCharts.Contracts.Point;
|
||||
using MuiCharts.Domain.Models;
|
||||
using MuiCharts.Domain.Repositories;
|
||||
|
||||
namespace MuiCharts.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing points.
|
||||
/// </summary>
|
||||
public class PointsController(
|
||||
ILogger<PointsController> logger,
|
||||
IPointRepository pointRepository
|
||||
) : ApiControllerBase<PointsController>(logger)
|
||||
{
|
||||
private readonly IPointRepository _repository = pointRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new point.
|
||||
/// </summary>
|
||||
/// <param name="request">The new point model.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType<Point>(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> CreatePointAsync(UpsertPointRequest request)
|
||||
{
|
||||
Logger.LogInformation("Creating point with name {Name} and height {Height}", request.Name, request.Height);
|
||||
|
||||
Point point = new()
|
||||
{
|
||||
Id = default,
|
||||
Name = request.Name,
|
||||
Height = request.Height
|
||||
};
|
||||
|
||||
ErrorOr<Point> createResult = await _repository.AddPointAsync(point);
|
||||
|
||||
if (createResult.IsError)
|
||||
return Problem(createResult.Errors);
|
||||
|
||||
Logger.LogInformation("Point created with id {Id}", createResult.Value.Id);
|
||||
|
||||
return CreatedAtPointResult(createResult.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an array of points based on the provided IDs.
|
||||
/// </summary>
|
||||
/// <param name="ids">The array of point IDs.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpPost("Array")]
|
||||
[ProducesResponseType<Point[]>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<Point[]>(StatusCodes.Status206PartialContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> GetPointsArrayAsync(int[] ids)
|
||||
{
|
||||
Logger.LogInformation("Getting points with ids {Ids}", ids);
|
||||
|
||||
IQueryable<Point> query = await _repository.GetPointsRangeAsync();
|
||||
|
||||
PointResponse[] points = [
|
||||
.. query
|
||||
.Where(point => ids.Contains(point.Id))
|
||||
.Select(point => MapPointResponse(point))
|
||||
];
|
||||
|
||||
if (points.Length == 0)
|
||||
{
|
||||
Logger.LogInformation("No points found with ids {Ids}", ids);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (points.Length != ids.Length)
|
||||
{
|
||||
Logger.LogInformation("Not all points found with ids {Ids}", ids);
|
||||
return StatusCode(StatusCodes.Status206PartialContent, points);
|
||||
}
|
||||
|
||||
Logger.LogInformation("Returning {Count} points", points.Length);
|
||||
|
||||
return Ok(points);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a range of points based on the specified page and count.
|
||||
/// </summary>
|
||||
/// <param name="request">The request object containing the page and count parameters.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<GetPointsResponse>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> GetPointsAsync([FromQuery] GetPointsRequest request)
|
||||
{
|
||||
Logger.LogInformation("Getting points with page {Page} and count {Count}", request.Page, request.Count);
|
||||
|
||||
IQueryable<Point> query = await _repository.GetPointsRangeAsync();
|
||||
|
||||
PointResponse[] points = [
|
||||
.. query
|
||||
.Skip((request.Page - 1) * request.Count)
|
||||
.Take(request.Count)
|
||||
.Select(point => MapPointResponse(point))
|
||||
];
|
||||
|
||||
GetPointsResponse response = new(
|
||||
points,
|
||||
query.Count(),
|
||||
points.Length,
|
||||
request.Page
|
||||
);
|
||||
|
||||
Logger.LogInformation("Returning {Count} points", response.Count);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a point with the specified ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the point to retrieve.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType<Point>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> GetPointAsync(int id)
|
||||
{
|
||||
Logger.LogInformation("Getting point with id {Id}", id);
|
||||
|
||||
ErrorOr<Point> getResult = await _repository.GetPointAsync(id);
|
||||
|
||||
if (getResult.IsError)
|
||||
return Problem(getResult.Errors);
|
||||
|
||||
Logger.LogInformation("Returning point with id {Id}", id);
|
||||
|
||||
return Ok(MapPointResponse(getResult.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upserts a point with the specified ID and request data.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the point.</param>
|
||||
/// <param name="request">The request data for the point.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType<Point>(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> UpsertPointAsync(int id, UpsertPointRequest request)
|
||||
{
|
||||
Logger.LogInformation("Upserting point with id {Id}", id);
|
||||
|
||||
Point point = new()
|
||||
{
|
||||
Id = id,
|
||||
Name = request.Name,
|
||||
Height = request.Height
|
||||
};
|
||||
|
||||
ErrorOr<Point?> upsertResult = await _repository.AddOrUpdatePointAsync(point);
|
||||
|
||||
if (upsertResult.IsError)
|
||||
return Problem(upsertResult.Errors);
|
||||
|
||||
if (upsertResult.Value is Point value)
|
||||
{
|
||||
Logger.LogInformation("Point created with id {Id}", value.Id);
|
||||
return CreatedAtPointResult(value);
|
||||
}
|
||||
|
||||
Logger.LogInformation("Point updated with id {Id}", id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a point with the specified ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the point to delete.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> DeletePointAsync(int id)
|
||||
{
|
||||
Logger.LogInformation("Deleting point with id {Id}", id);
|
||||
|
||||
ErrorOr<Deleted> deleteResult = await _repository.DeletePointAsync(id);
|
||||
|
||||
if (deleteResult.IsError)
|
||||
return Problem(deleteResult.Errors);
|
||||
|
||||
Logger.LogInformation("Point deleted with id {Id}", id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private CreatedAtActionResult CreatedAtPointResult(Point point) =>
|
||||
CreatedAtAction(
|
||||
actionName: nameof(GetPointAsync),
|
||||
routeValues: new { id = point.Id },
|
||||
value: MapPointResponse(point)
|
||||
);
|
||||
|
||||
private static PointResponse MapPointResponse(Point value) =>
|
||||
new(
|
||||
value.Id,
|
||||
value.Name,
|
||||
value.Height
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MuiCharts.Contracts.Track;
|
||||
using MuiCharts.Domain.Models;
|
||||
using MuiCharts.Domain.Repositories;
|
||||
|
||||
namespace MuiCharts.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing tracks.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="trackRepository">The track repository.</param>
|
||||
public class TracksController(
|
||||
ILogger<TracksController> logger,
|
||||
ITrackRepository trackRepository
|
||||
) : ApiControllerBase<TracksController>(logger)
|
||||
{
|
||||
private readonly ITrackRepository _repository = trackRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new track.
|
||||
/// </summary>
|
||||
/// <param name="request">The request containing the track details.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType<TrackResponse>(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> CreateTrackAsync(UpsertTrackRequest request)
|
||||
{
|
||||
// TODO: Check if points exist
|
||||
Logger.LogInformation("Creating track with first ID {FirstId} and second ID {SecondId}", request.FirstId, request.SecondId);
|
||||
|
||||
if (request.FirstId == request.SecondId)
|
||||
return Problem([Error.Validation(description: "First ID and second ID cannot be the same.")]);
|
||||
|
||||
Track track = new()
|
||||
{
|
||||
FirstId = request.FirstId,
|
||||
SecondId = request.SecondId,
|
||||
Distance = request.Distance,
|
||||
Surface = request.Surface,
|
||||
MaxSpeed = request.MaxSpeed
|
||||
};
|
||||
|
||||
ErrorOr<Track> createResult = await _repository.AddTrackAsync(track);
|
||||
|
||||
if (createResult.IsError)
|
||||
return Problem(createResult.Errors);
|
||||
|
||||
Logger.LogInformation("Track created with first ID {FirstId} and second ID {SecondId}", createResult.Value.FirstId, createResult.Value.SecondId);
|
||||
|
||||
return CreatedAtTrackResult(createResult.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a track with the specified first ID and second ID.
|
||||
/// </summary>
|
||||
/// <param name="firstId">The first point ID of the track.</param>
|
||||
/// <param name="secondId">The second point ID of the track.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpGet("{firstId:int}/{secondId:int}")]
|
||||
[ProducesResponseType<TrackResponse>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> GetTrackAsync(int firstId, int secondId)
|
||||
{
|
||||
Logger.LogInformation("Retrieving track with first ID {FirstId} and second ID {SecondId}", firstId, secondId);
|
||||
|
||||
ErrorOr<Track> getResult = await _repository.GetTrackAsync(firstId, secondId);
|
||||
|
||||
if (getResult.IsError)
|
||||
return Problem(getResult.Errors);
|
||||
|
||||
Logger.LogInformation("Track retrieved with first ID {FirstId} and second ID {SecondId}", getResult.Value.FirstId, getResult.Value.SecondId);
|
||||
|
||||
return Ok(MapTrackResponse(getResult.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all tracks.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<TrackResponse>>(StatusCodes.Status200OK)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> GetAllTracksAsync()
|
||||
{
|
||||
Logger.LogInformation("Retrieving all tracks");
|
||||
|
||||
IQueryable<Track> tracks = await _repository.GetTracksRangeAsync();
|
||||
|
||||
Logger.LogInformation("All tracks retrieved");
|
||||
|
||||
return Ok(tracks.Select(MapTrackResponse));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upserts a track with the specified first point ID, second point ID, and request data.
|
||||
/// </summary>
|
||||
/// <param name="firstId">The first point ID of the track.</param>
|
||||
/// <param name="secondId">The second point ID of the track.</param>
|
||||
/// <param name="request">The request data containing the track details.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpPut("{firstId:int}/{secondId:int}")]
|
||||
[ProducesResponseType<TrackResponse>(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> UpsertTrackAsync(int firstId, int secondId, UpsertTrackRequest request)
|
||||
{
|
||||
// TODO: Check if points exist
|
||||
Logger.LogInformation("Upserting track with first ID {FirstId} and second ID {SecondId}", firstId, secondId);
|
||||
|
||||
Track track = new()
|
||||
{
|
||||
FirstId = firstId,
|
||||
SecondId = secondId,
|
||||
Distance = request.Distance,
|
||||
Surface = request.Surface,
|
||||
MaxSpeed = request.MaxSpeed
|
||||
};
|
||||
|
||||
ErrorOr<Track?> upsertResult = await _repository.AddOrUpdateTrackAsync(track);
|
||||
|
||||
if (upsertResult.IsError)
|
||||
return Problem(upsertResult.Errors);
|
||||
|
||||
if (upsertResult.Value is Track value)
|
||||
{
|
||||
Logger.LogInformation("Track created with first ID {FirstId} and second ID {SecondId}", value.FirstId, value.SecondId);
|
||||
return CreatedAtTrackResult(value);
|
||||
}
|
||||
|
||||
Logger.LogInformation("Track updated with first ID {FirstId} and second ID {SecondId}", firstId, secondId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a track with the specified first point ID and second point ID.
|
||||
/// </summary>
|
||||
/// <param name="firstId">The first point ID of the track to delete.</param>
|
||||
/// <param name="secondId">The second point ID of the track to delete.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the asynchronous operation result.</returns>
|
||||
[HttpDelete("{firstId:int}/{secondId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesDefaultResponseType(typeof(ProblemDetails))]
|
||||
public async Task<IActionResult> DeleteTrackAsync(int firstId, int secondId)
|
||||
{
|
||||
Logger.LogInformation("Deleting track with first ID {FirstId} and second ID {SecondId}", firstId, secondId);
|
||||
|
||||
ErrorOr<Deleted> deleteResult = await _repository.DeleteTrackAsync(firstId, secondId);
|
||||
|
||||
if (deleteResult.IsError)
|
||||
return Problem(deleteResult.Errors);
|
||||
|
||||
Logger.LogInformation("Track deleted with first ID {FirstId} and second ID {SecondId}", firstId, secondId);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private CreatedAtActionResult CreatedAtTrackResult(Track track) =>
|
||||
CreatedAtAction(
|
||||
actionName: nameof(GetTrackAsync),
|
||||
routeValues: new { firstId = track.FirstId, secondId = track.SecondId },
|
||||
value: MapTrackResponse(track)
|
||||
);
|
||||
|
||||
private static TrackResponse MapTrackResponse(Track track) =>
|
||||
new(
|
||||
track.FirstId,
|
||||
track.SecondId,
|
||||
track.Distance,
|
||||
track.Surface,
|
||||
track.MaxSpeed
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user