Merge pull request #6 from StevanFreeborn/master

Deploy master to production.
This commit is contained in:
StevanFreeborn
2022-06-23 23:58:47 -05:00
committed by GitHub
50 changed files with 9292 additions and 104 deletions
+10
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

+6 -1
View File
@@ -8,7 +8,12 @@ Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "client", "client\client.esp
{1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC} = {1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC} {1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC} = {1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC}
EndProjectSection EndProjectSection
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
-57
View File
@@ -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);
}
}
}
}
+80
View File
@@ -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", ""));
}
}
}
}
+27
View File
@@ -5,31 +5,58 @@ namespace server.Models
{ {
public class Character public class Character
{ {
/// <summary>
/// Identifier for the character.
/// </summary>
[BsonId] [BsonId]
[BsonRepresentation(BsonType.ObjectId)] [BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; } public string? Id { get; set; }
/// <summary>
/// The character's first name.
/// </summary>
[BsonElement("firstName")] [BsonElement("firstName")]
public string? FirstName { get; set; } public string? FirstName { get; set; }
/// <summary>
/// The character's last name.
/// </summary>
[BsonElement("lastName")] [BsonElement("lastName")]
public string? LastName { get; set; } public string? LastName { get; set; }
/// <summary>
/// The first name of the actor who potrayed the character.
/// </summary>
[BsonElement("actorFirstName")] [BsonElement("actorFirstName")]
public string? ActorFirstName { get; set; } public string? ActorFirstName { get; set; }
/// <summary>
/// The last name of the actor who potrayed the character.
/// </summary>
[BsonElement("actorLastName")] [BsonElement("actorLastName")]
public string? ActorLastName { get; set; } public string? ActorLastName { get; set; }
/// <summary>
/// The seasons in which the character appeared.
/// </summary>
[BsonElement("seasons")] [BsonElement("seasons")]
public int[]? Seasons { get; set; } public int[]? Seasons { get; set; }
/// <summary>
/// The first episode where the character appeared.
/// </summary>
[BsonElement("firstEpisode")] [BsonElement("firstEpisode")]
public string? FirstEpisode { get; set; } public string? FirstEpisode { get; set; }
/// <summary>
/// The last episode where the character appeared.
/// </summary>
[BsonElement("lastEpisode")] [BsonElement("lastEpisode")]
public string? LastEpisode { get; set; } public string? LastEpisode { get; set; }
/// <summary>
/// A link to an image of the character.
/// </summary>
[BsonElement("image")] [BsonElement("image")]
public string? Image { get; set; } public string? Image { get; set; }
} }
+31
View File
@@ -5,34 +5,65 @@ namespace server.Models
{ {
public class Episode public class Episode
{ {
/// <summary>
/// Identifier for the episode.
/// </summary>
[BsonId] [BsonId]
[BsonRepresentation(BsonType.ObjectId)] [BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; } public string? Id { get; set; }
/// <summary>
/// Season number for the episode.
/// </summary>
[BsonElement("season")] [BsonElement("season")]
public int? Season { get; set; } public int? Season { get; set; }
/// <summary>
/// The episde number relative to the entire series.
/// </summary>
[BsonElement("numberInSeries")] [BsonElement("numberInSeries")]
public int? NumberInSeries { get; set; } public int? NumberInSeries { get; set; }
/// <summary>
/// The episode number relative to its season.
/// </summary>
[BsonElement("numberInSeason")] [BsonElement("numberInSeason")]
public int? NumberInSeason { get; set; } public int? NumberInSeason { get; set; }
/// <summary>
/// The title of the episode.
/// </summary>
[BsonElement("title")] [BsonElement("title")]
public string? Title { get; set; } public string? Title { get; set; }
/// <summary>
/// A brief summary of the episode.
/// </summary>
[BsonElement("summary")] [BsonElement("summary")]
public string? Summary { get; set; } public string? Summary { get; set; }
/// <summary>
/// The director of the episode.
/// </summary>
[BsonElement("directedBy")] [BsonElement("directedBy")]
public string? DirectedBy { get; set; } public string? DirectedBy { get; set; }
/// <summary>
/// The writer of the episode.
/// </summary>
[BsonElement("writtenBy")] [BsonElement("writtenBy")]
public string[]? WrittenBy { get; set; } public string[]? WrittenBy { get; set; }
/// <summary>
/// Date the episode aired.
/// </summary>
[BsonElement("airDate")] [BsonElement("airDate")]
[BsonDateTimeOptions(DateOnly = true)]
public DateTime? AirDate { get; set; } public DateTime? AirDate { get; set; }
/// <summary>
/// Number of us viewers in millions for the episode.
/// </summary>
[BsonElement("usViewersInMillions")] [BsonElement("usViewersInMillions")]
public double? UsViewersInMillions { get; set; } public double? UsViewersInMillions { get; set; }
+50
View File
@@ -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;
}
}
-12
View File
@@ -1,12 +0,0 @@
namespace server.Models
{
public class ErrorResponse
{
public string Error { get; set; }
public ErrorResponse(string error)
{
Error = error;
}
}
}
+18
View File
@@ -5,22 +5,40 @@ namespace server.Models
{ {
public class Quote public class Quote
{ {
/// <summary>
/// Identifier for the quote.
/// </summary>
[BsonId] [BsonId]
[BsonRepresentation(BsonType.ObjectId)] [BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; } public string? Id { get; set; }
/// <summary>
/// The season number during which the quote was said.
/// </summary>
[BsonElement("season")] [BsonElement("season")]
public int? Season { get; set; } public int? Season { get; set; }
/// <summary>
/// The episode number during which the quote was said.
/// </summary>
[BsonElement("episode")] [BsonElement("episode")]
public int Episode { get; set; } public int Episode { get; set; }
/// <summary>
/// The quote text.
/// </summary>
[BsonElement("text")] [BsonElement("text")]
public string? Text { get; set; } public string? Text { get; set; }
/// <summary>
/// The source of the quote.
/// </summary>
[BsonElement("source")] [BsonElement("source")]
public string? Source { get; set; } public string? Source { get; set; }
/// <summary>
/// The narrator of the quote.
/// </summary>
[BsonElement("narrator")] [BsonElement("narrator")]
public string? Narrator { get; set; } public string? Narrator { get; set; }
} }
+32
View File
@@ -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;
}
}
+17
View File
@@ -5,20 +5,37 @@ namespace server.Models
{ {
public class Season public class Season
{ {
/// <summary>
/// Identifier for the season.
/// </summary>
[BsonId] [BsonId]
[BsonRepresentation(BsonType.ObjectId)] [BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; } public string? Id { get; set; }
/// <summary>
/// The season number.
/// </summary>
[BsonElement("seasonNumber")] [BsonElement("seasonNumber")]
public int? SeasonNumber { get; set; } public int? SeasonNumber { get; set; }
/// <summary>
/// The number of episodes in the season.
/// </summary>
[BsonElement("numberOfEpisodes")] [BsonElement("numberOfEpisodes")]
public int? NumberOfEpisodes { get; set; } public int? NumberOfEpisodes { get; set; }
/// <summary>
/// The date the season first aired.
/// </summary>
[BsonElement("dateFirstAired")] [BsonElement("dateFirstAired")]
[BsonDateTimeOptions(DateOnly = true)]
public DateTime? DateFirstAired { get; set; } public DateTime? DateFirstAired { get; set; }
/// <summary>
/// The date the season last aired.
/// </summary>
[BsonElement("dateLastAired")] [BsonElement("dateLastAired")]
[BsonDateTimeOptions(DateOnly = true)]
public DateTime? DateLastAired { get; set; } public DateTime? DateLastAired { get; set; }
} }
} }
+45
View File
@@ -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 -7
View File
@@ -1,10 +1,4 @@
using System; namespace server.Persistence
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace server.Persistence
{ {
public class DatabaseSettings : IDatabaseSettings public class DatabaseSettings : IDatabaseSettings
{ {
+1 -7
View File
@@ -1,10 +1,4 @@
using System; using MongoDB.Driver;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.OpenApi.Writers;
using MongoDB.Driver;
using server.Models; using server.Models;
namespace server.Persistence namespace server.Persistence
+1 -7
View File
@@ -1,10 +1,4 @@
using System; namespace server.Persistence
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace server.Persistence
{ {
public interface IDatabaseSettings public interface IDatabaseSettings
{ {
+1 -6
View File
@@ -1,9 +1,4 @@
using System; using MongoDB.Driver;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Driver;
using server.Models; using server.Models;
namespace server.Persistence 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;
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+92
View File
@@ -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"
}
]
+77
View File
@@ -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}");
}
}
}
}
+89 -6
View File
@@ -1,10 +1,49 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models; using server.Filters;
using server.Options;
using server.Persistence; using server.Persistence;
using server.Persistence.Repositories; 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); 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.Services.Configure<DatabaseSettings>(
builder.Configuration.GetSection("MongoDBSettings")); builder.Configuration.GetSection("MongoDBSettings"));
@@ -15,22 +54,62 @@ builder.Services.AddSingleton<IDbContext, DbContext>();
builder.Services.AddScoped<ISeasonRepository, SeasonRepository>(); builder.Services.AddScoped<ISeasonRepository, SeasonRepository>();
builder.Services.AddScoped<IEpisodeRepository, EpisodeRepository>();
builder.Services.AddScoped<IQuoteRepository, QuoteRepository>();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); 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(); 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(); app.UseHttpsRedirection();
@@ -39,4 +118,8 @@ app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.UseIpRateLimiting();
app.Run(); app.Run();
-1
View File
@@ -11,7 +11,6 @@
"profiles": { "profiles": {
"server": { "server": {
"commandName": "Project", "commandName": "Project",
"launchUrl": "swagger",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
+8
View File
@@ -4,9 +4,17 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="MongoDB.Driver" Version="2.16.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
</ItemGroup> </ItemGroup>