Merge pull request #6 from StevanFreeborn/master
Deploy master to production.
@@ -0,0 +1,10 @@
|
||||
[*.cs]
|
||||
|
||||
# CS0472: The result of the expression is always the same since a value of this type is never equal to 'null'
|
||||
dotnet_diagnostic.CS0472.severity = none
|
||||
|
||||
# CS8604: Possible null reference argument.
|
||||
dotnet_diagnostic.CS8604.severity = none
|
||||
|
||||
# CS1591: Missing XML comment for publicly visible type or member
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 357 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 564 KiB |
@@ -8,7 +8,12 @@ Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "client", "client\client.esp
|
||||
{1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC} = {1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server", "server\server.csproj", "{1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "server", "server\server.csproj", "{1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2FE01B2A-2F23-477B-BC6A-1E71781A61DB}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using server.Models;
|
||||
using server.Persistence.Repositories;
|
||||
|
||||
namespace server.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/seasons")]
|
||||
[Produces("application/json")]
|
||||
public class SeasonsController : ControllerBase
|
||||
{
|
||||
private readonly ISeasonRepository _seasonRepository;
|
||||
|
||||
public SeasonsController(ISeasonRepository seasonRepository)
|
||||
{
|
||||
_seasonRepository = seasonRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<Season>), 200)]
|
||||
[ProducesResponseType(500)]
|
||||
public async Task<ActionResult<List<Season>>> GetSeasonsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var seasons = await _seasonRepository.GetSeasonsAsync();
|
||||
return Ok(seasons);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to get seasons");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{number:int}")]
|
||||
[ProducesResponseType(typeof(Season), 200)]
|
||||
[ProducesResponseType(typeof(ErrorResponse), 404)]
|
||||
[ProducesResponseType(500)]
|
||||
public async Task<ActionResult<Season>> GetSeasonByNumberAsync(int number)
|
||||
{
|
||||
try
|
||||
{
|
||||
var season = await _seasonRepository.GetSeasonByNumberAsync(number);
|
||||
|
||||
if (season == null) return NotFound(new ErrorResponse($"Could not find season number {number}"));
|
||||
|
||||
return Ok(season);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to get season");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using server.Models;
|
||||
using server.Persistence.Repositories;
|
||||
|
||||
namespace server.Controllers.v1
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("/api/episodes")]
|
||||
[Produces("application/json")]
|
||||
public class EpisodesController : ControllerBase
|
||||
{
|
||||
private readonly IEpisodeRepository _episodeRepository;
|
||||
|
||||
public EpisodesController(IEpisodeRepository episodeRepository)
|
||||
{
|
||||
_episodeRepository = episodeRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of episodes.
|
||||
/// </summary>
|
||||
/// <param name="filter">Filter parameters passed from query string.</param>
|
||||
/// <response code="200">Returns the collection of episodes requested.</response>
|
||||
/// <response code="400">Not a valid request.</response>
|
||||
/// <response code="500">Failed to get episodes.</response>
|
||||
/// <returns>Returns a collection of episodes.</returns>
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<Episode>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<List<Episode>>> GetEpisodesAsync([FromQuery] EpisodeFilter? filter)
|
||||
{
|
||||
try
|
||||
{
|
||||
var seasons = await _episodeRepository.GetEpisodesAsync(filter);
|
||||
return Ok(seasons);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return Problem(detail: "Failed to get episodes", statusCode: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an episode by its number in the series.
|
||||
/// </summary>
|
||||
/// <param name="number">The number of the episode in the series.</param>
|
||||
/// <response code="200">Returns the episode requested.</response>
|
||||
/// <response code="400">Not a valid request.</response>
|
||||
/// <response code="404">Unable to find an episode with the provided number.</response>
|
||||
/// <response code="500">Failed to get episode.</response>
|
||||
/// <returns>Returns the episode requested.</returns>
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("{number:int}")]
|
||||
[ProducesResponseType(typeof(Episode), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<Episode>> GetEpisodeByNumberAsync(int number)
|
||||
{
|
||||
try
|
||||
{
|
||||
var episode = await _episodeRepository.GetEpisodeByNumberAsync(number);
|
||||
|
||||
return episode == null ?
|
||||
Problem(detail: $"Could not find episode {number}", statusCode: 404) :
|
||||
Ok(episode);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return Problem(detail: "Failed to get episode", statusCode: 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using MongoDB.Bson;
|
||||
using server.Models;
|
||||
using server.Persistence.Repositories;
|
||||
|
||||
namespace server.Controllers.v1
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("/api/quotes")]
|
||||
[Produces("application/json")]
|
||||
public class QuotesController : ControllerBase
|
||||
{
|
||||
private readonly IQuoteRepository _quoteRepository;
|
||||
|
||||
public QuotesController(IQuoteRepository quoteRepository)
|
||||
{
|
||||
_quoteRepository = quoteRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of quotes.
|
||||
/// </summary>
|
||||
/// <param name="filter">Filter parameters passed from query string.</param>
|
||||
/// <response code="200">Returns the collection of quotes requested.</response>
|
||||
/// <response code="400">Not a valid request.</response>
|
||||
/// <response code="500">Failed to get quotes.</response>
|
||||
/// <returns>Returns a collection of quotes.</returns>
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<Quote>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<List<Quote>>> GetQuotesAsync([FromQuery] QuoteFilter? filter)
|
||||
{
|
||||
try
|
||||
{
|
||||
var quotes = await _quoteRepository.GetQuotesAsync(filter);
|
||||
return Ok(quotes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return Problem(detail: "Failed to get quotes", statusCode: 500);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add xml comments
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(Quote), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<Quote>> GetQuoteByIdAsync(string id)
|
||||
{
|
||||
|
||||
if (!ObjectId.TryParse(id, out _))
|
||||
{
|
||||
ModelState.AddModelError(nameof(id), $"{id} is not a valid id");
|
||||
return ValidationProblem();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var quote = await _quoteRepository.GetQuoteByIdAsync(id);
|
||||
|
||||
return quote == null ?
|
||||
Problem(detail: $"Could not find quote {id}", statusCode: 404) :
|
||||
Ok(quote);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return Problem(detail: "Failed to get quote", statusCode: 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using server.Models;
|
||||
using server.Persistence.Repositories;
|
||||
|
||||
namespace server.Controllers.v1
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("/api/seasons")]
|
||||
[Produces("application/json")]
|
||||
public class SeasonsController : ControllerBase
|
||||
{
|
||||
private readonly ISeasonRepository _seasonRepository;
|
||||
|
||||
public SeasonsController(ISeasonRepository seasonRepository)
|
||||
{
|
||||
_seasonRepository = seasonRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of seasons.
|
||||
/// </summary>
|
||||
/// <response code="200">Returns the collection of seasons requested.</response>
|
||||
/// <response code="500">Failed to get seasons.</response>
|
||||
/// <returns>Returns a collection of seasons.</returns>
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<Season>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<List<Season>>> GetSeasonsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var seasons = await _seasonRepository.GetSeasonsAsync();
|
||||
return Ok(seasons);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return Problem(detail: "Failed to get seasons", statusCode: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a season by its number.
|
||||
/// </summary>
|
||||
/// <response code="200">Returns the season requested.</response>
|
||||
/// <response code="404">Could not find a seaon with number provided.</response>
|
||||
/// <response code="500">Failed to get season.</response>
|
||||
/// <returns>Returns the season requested</returns>
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("{number:int}")]
|
||||
[ProducesResponseType(typeof(Season), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<Season>> GetSeasonByNumberAsync(int number)
|
||||
{
|
||||
try
|
||||
{
|
||||
var season = await _seasonRepository.GetSeasonByNumberAsync(number);
|
||||
|
||||
return season == null ?
|
||||
Problem(detail: $"Could not find season {number}", statusCode: 404) :
|
||||
Ok(season);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return Problem(detail: "Failed to get season", statusCode: 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace server.Filters
|
||||
{
|
||||
public class ApiVersionOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var headerParameter = operation.Parameters.Where(p => p.Name == "x-api-version").SingleOrDefault();
|
||||
|
||||
if (headerParameter != null)
|
||||
{
|
||||
headerParameter.Description = "Header value that identifies target version of api.";
|
||||
headerParameter.Schema.Default = new OpenApiString(context.DocumentName.ToLower().Replace("v", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,58 @@ namespace server.Models
|
||||
{
|
||||
public class Character
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier for the character.
|
||||
/// </summary>
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The character's first name.
|
||||
/// </summary>
|
||||
[BsonElement("firstName")]
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The character's last name.
|
||||
/// </summary>
|
||||
[BsonElement("lastName")]
|
||||
public string? LastName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first name of the actor who potrayed the character.
|
||||
/// </summary>
|
||||
[BsonElement("actorFirstName")]
|
||||
public string? ActorFirstName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last name of the actor who potrayed the character.
|
||||
/// </summary>
|
||||
[BsonElement("actorLastName")]
|
||||
public string? ActorLastName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The seasons in which the character appeared.
|
||||
/// </summary>
|
||||
[BsonElement("seasons")]
|
||||
public int[]? Seasons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first episode where the character appeared.
|
||||
/// </summary>
|
||||
[BsonElement("firstEpisode")]
|
||||
public string? FirstEpisode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last episode where the character appeared.
|
||||
/// </summary>
|
||||
[BsonElement("lastEpisode")]
|
||||
public string? LastEpisode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A link to an image of the character.
|
||||
/// </summary>
|
||||
[BsonElement("image")]
|
||||
public string? Image { get; set; }
|
||||
}
|
||||
|
||||
@@ -5,34 +5,65 @@ namespace server.Models
|
||||
{
|
||||
public class Episode
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier for the episode.
|
||||
/// </summary>
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Season number for the episode.
|
||||
/// </summary>
|
||||
[BsonElement("season")]
|
||||
public int? Season { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The episde number relative to the entire series.
|
||||
/// </summary>
|
||||
[BsonElement("numberInSeries")]
|
||||
public int? NumberInSeries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The episode number relative to its season.
|
||||
/// </summary>
|
||||
[BsonElement("numberInSeason")]
|
||||
public int? NumberInSeason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of the episode.
|
||||
/// </summary>
|
||||
[BsonElement("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A brief summary of the episode.
|
||||
/// </summary>
|
||||
[BsonElement("summary")]
|
||||
public string? Summary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The director of the episode.
|
||||
/// </summary>
|
||||
[BsonElement("directedBy")]
|
||||
public string? DirectedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The writer of the episode.
|
||||
/// </summary>
|
||||
[BsonElement("writtenBy")]
|
||||
public string[]? WrittenBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date the episode aired.
|
||||
/// </summary>
|
||||
[BsonElement("airDate")]
|
||||
[BsonDateTimeOptions(DateOnly = true)]
|
||||
public DateTime? AirDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of us viewers in millions for the episode.
|
||||
/// </summary>
|
||||
[BsonElement("usViewersInMillions")]
|
||||
public double? UsViewersInMillions { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace server.Models
|
||||
{
|
||||
public class EpisodeFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Use to filter episodes by season number.
|
||||
/// </summary>
|
||||
public int? Season { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter episodes by those who have an air date on or after this value.
|
||||
/// </summary>
|
||||
public DateTime? StartDate { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter episodes by those who have an air date on or before this value.
|
||||
/// </summary>
|
||||
public DateTime? EndDate { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter episodes by episode title.
|
||||
/// </summary>
|
||||
public string? Title { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter episodes by those whose summary contains this value.
|
||||
/// </summary>
|
||||
public string? SummaryKeyword { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter episodes by director's name.
|
||||
/// </summary>
|
||||
public string? DirectedBy { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter episodes by writer's name.
|
||||
/// </summary>
|
||||
public string? WrittenBy { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter episodes by those who have millions of us viewers equal to or more than this value.
|
||||
/// </summary>
|
||||
public double? ViewersRangeStart { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter episodes by those who have millions of us viewers equal to or less than this value.
|
||||
/// </summary>
|
||||
public double? ViewersRangeEnd { get; set; } = null;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace server.Models
|
||||
{
|
||||
public class ErrorResponse
|
||||
{
|
||||
public string Error { get; set; }
|
||||
|
||||
public ErrorResponse(string error)
|
||||
{
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,40 @@ namespace server.Models
|
||||
{
|
||||
public class Quote
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier for the quote.
|
||||
/// </summary>
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The season number during which the quote was said.
|
||||
/// </summary>
|
||||
[BsonElement("season")]
|
||||
public int? Season { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The episode number during which the quote was said.
|
||||
/// </summary>
|
||||
[BsonElement("episode")]
|
||||
public int Episode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The quote text.
|
||||
/// </summary>
|
||||
[BsonElement("text")]
|
||||
public string? Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The source of the quote.
|
||||
/// </summary>
|
||||
[BsonElement("source")]
|
||||
public string? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The narrator of the quote.
|
||||
/// </summary>
|
||||
[BsonElement("narrator")]
|
||||
public string? Narrator { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace server.Models
|
||||
{
|
||||
public class QuoteFilter
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter quotes by season.
|
||||
/// </summary>
|
||||
public int? Season { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter quotes by episode.
|
||||
/// </summary>
|
||||
public int? Episode { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter quotes by a key word.
|
||||
/// </summary>
|
||||
public string? TextKeyword { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter quotes by source.
|
||||
/// </summary>
|
||||
public string? Source { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Use to filter quote by narrator.
|
||||
/// </summary>
|
||||
public string? Narrator { get; set; } = null;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,37 @@ namespace server.Models
|
||||
{
|
||||
public class Season
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier for the season.
|
||||
/// </summary>
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The season number.
|
||||
/// </summary>
|
||||
[BsonElement("seasonNumber")]
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of episodes in the season.
|
||||
/// </summary>
|
||||
[BsonElement("numberOfEpisodes")]
|
||||
public int? NumberOfEpisodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date the season first aired.
|
||||
/// </summary>
|
||||
[BsonElement("dateFirstAired")]
|
||||
[BsonDateTimeOptions(DateOnly = true)]
|
||||
public DateTime? DateFirstAired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date the season last aired.
|
||||
/// </summary>
|
||||
[BsonElement("dateLastAired")]
|
||||
[BsonDateTimeOptions(DateOnly = true)]
|
||||
public DateTime? DateLastAired { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace server.Options
|
||||
{
|
||||
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IApiVersionDescriptionProvider _provider;
|
||||
|
||||
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
|
||||
{
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
foreach (var description in _provider.ApiVersionDescriptions)
|
||||
{
|
||||
var versionInfo = CreateVersionInfo(description);
|
||||
|
||||
options.SwaggerDoc(description.GroupName, versionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenApiInfo CreateVersionInfo(ApiVersionDescription description)
|
||||
{
|
||||
var info = new OpenApiInfo
|
||||
{
|
||||
Title = "criminalmindsapi",
|
||||
Version = description.ApiVersion.ToString(),
|
||||
Description = "An api that provides information about the Criminal Minds series.",
|
||||
Contact = new OpenApiContact
|
||||
{
|
||||
Name = "Stevan Freeborn",
|
||||
Email = "stevan.freeborn@gmail.com",
|
||||
Url = new Uri("https://stevanfreeborn.com")
|
||||
}
|
||||
};
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace server.Persistence
|
||||
namespace server.Persistence
|
||||
{
|
||||
public class DatabaseSettings : IDatabaseSettings
|
||||
{
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.OpenApi.Writers;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver;
|
||||
using server.Models;
|
||||
|
||||
namespace server.Persistence
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace server.Persistence
|
||||
namespace server.Persistence
|
||||
{
|
||||
public interface IDatabaseSettings
|
||||
{
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver;
|
||||
using server.Models;
|
||||
|
||||
namespace server.Persistence
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver.Linq;
|
||||
using server.Models;
|
||||
|
||||
namespace server.Persistence.Repositories
|
||||
{
|
||||
public class EpisodeRepository : IEpisodeRepository
|
||||
{
|
||||
private readonly IDbContext _context;
|
||||
|
||||
public EpisodeRepository(IDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<Episode>> GetEpisodesAsync(EpisodeFilter? filter)
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = _context.Episodes.AsQueryable();
|
||||
|
||||
if (filter?.Season != null)
|
||||
{
|
||||
query = query.Where(episode => episode.Season == filter.Season);
|
||||
}
|
||||
|
||||
if (filter?.StartDate != null)
|
||||
{
|
||||
query = query.Where(episode => episode.AirDate >= filter.StartDate);
|
||||
}
|
||||
|
||||
if (filter?.EndDate != null)
|
||||
{
|
||||
query = query.Where(episode => episode.AirDate <= filter.EndDate);
|
||||
}
|
||||
|
||||
if (filter?.Title != null)
|
||||
{
|
||||
query = query.Where(episode => episode.Title!.ToLower().Contains(filter.Title.ToLower()));
|
||||
}
|
||||
|
||||
if (filter?.SummaryKeyword != null)
|
||||
{
|
||||
query = query.Where(episode => episode.Summary!.ToLower().Contains(filter.SummaryKeyword.ToLower()));
|
||||
}
|
||||
|
||||
if (filter?.DirectedBy != null)
|
||||
{
|
||||
query = query.Where(episode => episode.DirectedBy!.ToLower().Contains(filter.DirectedBy.ToLower()));
|
||||
}
|
||||
|
||||
if (filter?.WrittenBy != null)
|
||||
{
|
||||
query = query.Where(episode => episode.WrittenBy.Any(e => e.ToLower().Contains(filter.WrittenBy.ToLower())));
|
||||
}
|
||||
|
||||
if (filter?.ViewersRangeStart != null)
|
||||
{
|
||||
query = query.Where(episode => episode.UsViewersInMillions >= filter.ViewersRangeStart);
|
||||
}
|
||||
|
||||
if (filter?.ViewersRangeEnd != null)
|
||||
{
|
||||
query = query.Where(episode => episode.UsViewersInMillions <= filter.ViewersRangeEnd);
|
||||
}
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Episode> GetEpisodeByNumberAsync(int number)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _context.Episodes.Find(episode => episode.NumberInSeries == number).SingleOrDefaultAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using server.Models;
|
||||
|
||||
namespace server.Persistence.Repositories
|
||||
{
|
||||
public interface IEpisodeRepository
|
||||
{
|
||||
Task<List<Episode>> GetEpisodesAsync(EpisodeFilter filter);
|
||||
Task<Episode> GetEpisodeByNumberAsync(int number);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using server.Models;
|
||||
|
||||
namespace server.Persistence.Repositories
|
||||
{
|
||||
public interface IQuoteRepository
|
||||
{
|
||||
Task<List<Quote>> GetQuotesAsync(QuoteFilter filter);
|
||||
Task<Quote> GetQuoteByIdAsync(string id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver.Linq;
|
||||
using server.Models;
|
||||
|
||||
namespace server.Persistence.Repositories
|
||||
{
|
||||
public class QuoteRepository : IQuoteRepository
|
||||
{
|
||||
private readonly IDbContext _context;
|
||||
|
||||
public QuoteRepository(IDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<Quote>> GetQuotesAsync(QuoteFilter filter)
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = _context.Quotes.AsQueryable();
|
||||
|
||||
if (filter?.Season != null)
|
||||
{
|
||||
query = query.Where(quote => quote.Season == filter.Season);
|
||||
}
|
||||
|
||||
if (filter?.Episode != null)
|
||||
{
|
||||
query = query.Where(quote => quote.Episode == filter.Episode);
|
||||
}
|
||||
|
||||
if (filter?.TextKeyword != null)
|
||||
{
|
||||
query = query.Where(quote => quote.Text!.ToLower().Contains(filter.TextKeyword.ToLower()));
|
||||
}
|
||||
|
||||
if (filter?.Source != null)
|
||||
{
|
||||
query = query.Where(quote => quote.Source!.ToLower().Contains(filter.Source.ToLower()));
|
||||
}
|
||||
|
||||
if (filter?.Narrator != null)
|
||||
{
|
||||
query = query.Where(quote => quote.Narrator!.ToLower().Contains(filter.Narrator.ToLower()));
|
||||
}
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Quote> GetQuoteByIdAsync(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _context.Quotes.Find(quote => quote.Id == id).SingleOrDefaultAsync();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"SeasonNumber": 1,
|
||||
"NumberOfEpisodes": 22,
|
||||
"DateFirstAired": "2005-09-22",
|
||||
"DateLastAired": "2006-05-10"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 2,
|
||||
"NumberOfEpisodes": 23,
|
||||
"DateFirstAired": "2006-09-20",
|
||||
"DateLastAired": "2007-05-16"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 3,
|
||||
"NumberOfEpisodes": 20,
|
||||
"DateFirstAired": "2007-09-26",
|
||||
"DateLastAired": "2008-05-21"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 4,
|
||||
"NumberOfEpisodes": 26,
|
||||
"DateFirstAired": "2008-09-24",
|
||||
"DateLastAired": "2009-05-20"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 5,
|
||||
"NumberOfEpisodes": 23,
|
||||
"DateFirstAired": "2009-09-23",
|
||||
"DateLastAired": "2010-05-26"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 6,
|
||||
"NumberOfEpisodes": 24,
|
||||
"DateFirstAired": "2010-09-22",
|
||||
"DateLastAired": "2011-05-18"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 7,
|
||||
"NumberOfEpisodes": 24,
|
||||
"DateFirstAired": "2011-09-21",
|
||||
"DateLastAired": "2012-05-16"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 8,
|
||||
"NumberOfEpisodes": 24,
|
||||
"DateFirstAired": "2012-09-26",
|
||||
"DateLastAired": "2013-05-22"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 9,
|
||||
"NumberOfEpisodes": 24,
|
||||
"DateFirstAired": "2013-09-25",
|
||||
"DateLastAired": "2014-05-14"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 10,
|
||||
"NumberOfEpisodes": 23,
|
||||
"DateFirstAired": "2014-10-01",
|
||||
"DateLastAired": "2015-05-06"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 11,
|
||||
"NumberOfEpisodes": 22,
|
||||
"DateFirstAired": "2015-09-30",
|
||||
"DateLastAired": "2016-05-04"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 12,
|
||||
"NumberOfEpisodes": 22,
|
||||
"DateFirstAired": "2016-09-28",
|
||||
"DateLastAired": "2017-05-10"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 13,
|
||||
"NumberOfEpisodes": 22,
|
||||
"DateFirstAired": "2017-09-27",
|
||||
"DateLastAired": "2018-04-18"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 14,
|
||||
"NumberOfEpisodes": 15,
|
||||
"DateFirstAired": "2018-10-03",
|
||||
"DateLastAired": "2019-02-06"
|
||||
},
|
||||
{
|
||||
"SeasonNumber": 15,
|
||||
"NumberOfEpisodes": 10,
|
||||
"DateFirstAired": "2020-01-08",
|
||||
"DateLastAired": "2020-02-19"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,77 @@
|
||||
using MongoDB.Driver;
|
||||
using server.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace server.Persistence.Seed
|
||||
{
|
||||
public class Seeder
|
||||
{
|
||||
private readonly IMongoCollection<Season> _seasons;
|
||||
private readonly IMongoCollection<Episode> _episodes;
|
||||
private readonly IMongoCollection<Quote> _quotes;
|
||||
|
||||
public Seeder()
|
||||
{
|
||||
IConfiguration config = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
IMongoClient client = new MongoClient(config.GetSection("MongoDBSettings:ConnectionString").Value);
|
||||
|
||||
IMongoDatabase database = client.GetDatabase(config.GetSection("MongoDBSettings:DatabaseName").Value);
|
||||
|
||||
_seasons = database.GetCollection<Season>(config.GetSection("MongoDBSettings:SeasonsCollection").Value);
|
||||
_episodes = database.GetCollection<Episode>(config.GetSection("MongoDBSettings:EpisodesCollection").Value);
|
||||
_quotes = database.GetCollection<Quote>(config.GetSection("MongoDBSettings:QuotesCollection").Value);
|
||||
}
|
||||
|
||||
public async Task SeedSeasonsAsync()
|
||||
{
|
||||
await _seasons.DeleteManyAsync(season => true);
|
||||
|
||||
const string fileName = "seasons.json";
|
||||
var seasonsFilePath = Path.Combine(AppContext.BaseDirectory, $"Persistence/Seed/Data/{fileName}");
|
||||
var seasonsJson = await File.ReadAllTextAsync(seasonsFilePath);
|
||||
var seasons = JsonSerializer.Deserialize<List<Season>>(seasonsJson);
|
||||
|
||||
if(seasons != null)
|
||||
{
|
||||
await _seasons.InsertManyAsync(seasons);
|
||||
Console.WriteLine($"Seeded databases with seasons from {fileName}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SeedEpisodesAsync()
|
||||
{
|
||||
await _episodes.DeleteManyAsync(episode => true);
|
||||
|
||||
const string fileName = "episodes.json";
|
||||
var episodesFilePath = Path.Combine(AppContext.BaseDirectory, $"Persistence/Seed/Data/{fileName}");
|
||||
var episodesJson = await File.ReadAllTextAsync(episodesFilePath);
|
||||
var episodes = JsonSerializer.Deserialize<List<Episode>>(episodesJson);
|
||||
|
||||
if (episodes != null)
|
||||
{
|
||||
await _episodes.InsertManyAsync(episodes);
|
||||
Console.WriteLine($"Seeded databases with episodes from {fileName}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SeedQuotesAsync()
|
||||
{
|
||||
await _quotes.DeleteManyAsync(quotes => true);
|
||||
|
||||
const string fileName = "quotes.json";
|
||||
var quotesFilePath = Path.Combine(AppContext.BaseDirectory, $"Persistence/Seed/Data/{fileName}");
|
||||
var quotesJson = await File.ReadAllTextAsync(quotesFilePath);
|
||||
var quotes = JsonSerializer.Deserialize<List<Quote>>(quotesJson);
|
||||
|
||||
if (quotes != null)
|
||||
{
|
||||
await _quotes.InsertManyAsync(quotes);
|
||||
Console.WriteLine($"Seeded databases with quotes from {fileName}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,49 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Versioning;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using server.Filters;
|
||||
using server.Options;
|
||||
using server.Persistence;
|
||||
using server.Persistence.Repositories;
|
||||
using server.Persistence.Seed;
|
||||
using System.Reflection;
|
||||
using AspNetCoreRateLimit;
|
||||
|
||||
if (args.Length == 2 && args[0].ToLower() == "seed")
|
||||
{
|
||||
var seeder = new Seeder();
|
||||
|
||||
if (args[1].ToLower() == "seasons")
|
||||
{
|
||||
await seeder.SeedSeasonsAsync();
|
||||
}
|
||||
|
||||
if (args[1].ToLower() == "episodes")
|
||||
{
|
||||
await seeder.SeedEpisodesAsync();
|
||||
}
|
||||
|
||||
if (args[1].ToLower() == "quotes")
|
||||
{
|
||||
await seeder.SeedQuotesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
builder.Services.Configure<IpRateLimitOptions>(
|
||||
builder.Configuration.GetSection("IpRateLimiting"));
|
||||
|
||||
builder.Services.Configure<IpRateLimitPolicies>(
|
||||
builder.Configuration.GetSection("IpRateLimitPolicies"));
|
||||
|
||||
builder.Services.AddInMemoryRateLimiting();
|
||||
|
||||
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
|
||||
|
||||
builder.Services.Configure<DatabaseSettings>(
|
||||
builder.Configuration.GetSection("MongoDBSettings"));
|
||||
|
||||
@@ -15,22 +54,62 @@ builder.Services.AddSingleton<IDbContext, DbContext>();
|
||||
|
||||
builder.Services.AddScoped<ISeasonRepository, SeasonRepository>();
|
||||
|
||||
builder.Services.AddScoped<IEpisodeRepository, EpisodeRepository>();
|
||||
|
||||
builder.Services.AddScoped<IQuoteRepository, QuoteRepository>();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "criminalmindsapi", Version = "v1" });
|
||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||
|
||||
options.IncludeXmlComments(xmlPath);
|
||||
options.OperationFilter<ApiVersionOperationFilter>();
|
||||
});
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
|
||||
|
||||
builder.Services.AddApiVersioning(config =>
|
||||
{
|
||||
config.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
config.AssumeDefaultVersionWhenUnspecified = true;
|
||||
config.ReportApiVersions = true;
|
||||
config.ApiVersionReader = new HeaderApiVersionReader("x-api-version");
|
||||
});
|
||||
|
||||
builder.Services.AddVersionedApiExplorer(config =>
|
||||
{
|
||||
config.GroupNameFormat = "'v'VVV";
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseSwaggerUI(c =>
|
||||
app.UseSwagger(options =>
|
||||
{
|
||||
c.SwaggerEndpoint("/swagger/v1/swagger.json", "criminalmindsapi v1");
|
||||
options.RouteTemplate = "/{documentName}/docs.json";
|
||||
});
|
||||
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
|
||||
|
||||
foreach (var description in provider.ApiVersionDescriptions)
|
||||
{
|
||||
var url = $"/{description.GroupName}/docs.json";
|
||||
var name = $"criminalmindsapi v{description.ApiVersion}";
|
||||
|
||||
options.RoutePrefix = String.Empty;
|
||||
options.SwaggerEndpoint(url, name);
|
||||
options.EnableTryItOutByDefault();
|
||||
options.DisplayRequestDuration();
|
||||
options.DocumentTitle = "criminalmindsapi";
|
||||
}
|
||||
});
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
@@ -39,4 +118,8 @@ app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.UseIpRateLimiting();
|
||||
|
||||
app.Run();
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"profiles": {
|
||||
"server": {
|
||||
"commandName": "Project",
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
|
||||
@@ -4,9 +4,17 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="4.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.16.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||