diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4e6a45b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/server/bin/Debug/net6.0/server.dll", + "args": [], + "cwd": "${workspaceFolder}/server", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4db21f2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "appService.defaultWebAppToDeploy": "/subscriptions/d031910a-7ca1-44c2-9bb1-0bdb42cd9f43/microsoft.web/sites/subscriptions/d031910a-7ca1-44c2-9bb1-0bdb42cd9f43/resourceGroups/StevanFreeborn/providers/Microsoft.Web/sites/criminalmindsapi", + "appService.deploySubpath": "server" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..5239770 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/server/server.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/server/server.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/server/server.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/client/src/App.js b/client/src/App.js index 4ec4478..ccfadae 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -55,7 +55,7 @@ export default class App extends Component { let url = '/api/seasons'; if (process.env.NODE_ENV === 'production') { - url = 'https://criminalminds-server.azurewebsites.net' + url; + url = 'https://criminalmindsapi.azurewebsites.net' + url; } const response = await fetch(url); diff --git a/server/Controllers/v1/CharactersController.cs b/server/Controllers/v1/CharactersController.cs new file mode 100644 index 0000000..9e1c3e6 --- /dev/null +++ b/server/Controllers/v1/CharactersController.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; +using server.Models; +using server.Persistence.Repositories; + +namespace server.Controllers.v1 +{ + [ApiController] + [ApiVersion("1.0")] + [Route("/api/characters")] + [Produces("application/json")] + public class CharactersController : ControllerBase + { + private readonly ICharacterRepository _characterRepository; + + public CharactersController(ICharacterRepository characterRepository) + { + _characterRepository = characterRepository; + } + + /// + /// Gets a collection of characters. + /// + /// Filter parameters passed from query string. + /// Returns the collection of characters requested. + /// Not a valid request. + /// Failed to get characters. + /// Returns a collection of characters. + [MapToApiVersion("1.0")] + [HttpGet] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task>> GetCharactersAsync([FromQuery] CharacterFilter? filter) + { + try + { + var character = await _characterRepository.GetCharactersAsync(filter); + return Ok(character); + } + catch (Exception e) + { + Console.WriteLine(e); + return Problem(detail: "Failed to get characters", statusCode: 500); + } + } + + /// + /// Gets a character by its id. + /// + /// The id of the character being requested. + /// Returns the character requested. + /// Not a valid request. + /// Character with the given id not found. + /// Failed to get character. + /// Returns a character. + [MapToApiVersion("1.0")] + [HttpGet("{id}")] + [ProducesResponseType(typeof(Character), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task> GetCharacterByIdAsync(string id) + { + + if (!ObjectId.TryParse(id, out _)) + { + ModelState.AddModelError(nameof(id), $"{id} is not a valid id"); + return ValidationProblem(); + } + + try + { + var quote = await _characterRepository.GetCharacterByIdAsync(id); + + return quote == null ? + Problem(detail: $"Could not find character {id}", statusCode: 404) : + Ok(quote); + } + catch (Exception e) + { + Console.WriteLine(e); + return Problem(detail: "Failed to get character", statusCode: 500); + } + } + } +} diff --git a/server/Controllers/v1/QuotesController.cs b/server/Controllers/v1/QuotesController.cs index 08de272..e8f838c 100644 --- a/server/Controllers/v1/QuotesController.cs +++ b/server/Controllers/v1/QuotesController.cs @@ -46,7 +46,15 @@ namespace server.Controllers.v1 } } - // TODO: Add xml comments + /// + /// Gets a quote by its id. + /// + /// The id of the quote being requested. + /// Returns the quote requested. + /// Not a valid request. + /// Quote with the given id not found. + /// Failed to get quote. + /// Returns a quote. [MapToApiVersion("1.0")] [HttpGet("{id}")] [ProducesResponseType(typeof(Quote), StatusCodes.Status200OK)] diff --git a/server/Models/Character.cs b/server/Models/Character.cs index 46fada7..15e18ab 100644 --- a/server/Models/Character.cs +++ b/server/Models/Character.cs @@ -12,6 +12,12 @@ namespace server.Models [BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; } + /// + /// The character's full name. + /// + [BsonElement("fullName")] + public string FullName => $"{FirstName} {LastName}"; + /// /// The character's first name. /// @@ -24,6 +30,12 @@ namespace server.Models [BsonElement("lastName")] public string? LastName { get; set; } + /// + /// The full name of the actor who potrayed the character. + /// + [BsonElement("actorFullName")] + public string ActorFullName => $"{ActorFirstName} {ActorLastName}"; + /// /// The first name of the actor who potrayed the character. /// diff --git a/server/Models/CharacterFilter.cs b/server/Models/CharacterFilter.cs new file mode 100644 index 0000000..35b0ef0 --- /dev/null +++ b/server/Models/CharacterFilter.cs @@ -0,0 +1,20 @@ +namespace server.Models +{ + public class CharacterFilter + { + /// + /// Used to filter characters by their name. + /// + public string? Name { get; set; } = null; + + /// + /// Used to filter characters by the actor's name. + /// + public string? ActorName { get; set; } = null; + + /// + /// Used to filter characters by a season. + /// + public int? Season { get; set; } = null; + } +} diff --git a/server/Persistence/Repositories/CharacterRepository.cs b/server/Persistence/Repositories/CharacterRepository.cs new file mode 100644 index 0000000..5838d55 --- /dev/null +++ b/server/Persistence/Repositories/CharacterRepository.cs @@ -0,0 +1,59 @@ +using server.Models; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace server.Persistence.Repositories +{ + public class CharacterRepository : ICharacterRepository + { + private readonly IDbContext _context; + + public CharacterRepository(IDbContext context) + { + _context = context; + } + + public async Task> GetCharactersAsync(CharacterFilter filter) + { + try + { + var query = _context.Characters.AsQueryable(); + + if (filter?.Name != null) + { + query = query.Where(character => character.FullName.ToLower().Contains(filter.Name.ToLower())); + } + + if (filter?.ActorName != null) + { + query = query.Where(character => character.ActorFullName.ToLower().Contains(filter.ActorName.ToLower())); + } + + if (filter?.Season != null) + { + query = query.Where(character => character.Seasons.Any(season => season == filter.Season)); + } + + return await query.ToListAsync(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public async Task GetCharacterByIdAsync(string id) + { + try + { + return await _context.Characters.Find(character => character.Id == id).SingleOrDefaultAsync(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + } +} diff --git a/server/Persistence/Repositories/ICharacterRepository.cs b/server/Persistence/Repositories/ICharacterRepository.cs new file mode 100644 index 0000000..b9b74a5 --- /dev/null +++ b/server/Persistence/Repositories/ICharacterRepository.cs @@ -0,0 +1,10 @@ +using server.Models; + +namespace server.Persistence.Repositories +{ + public interface ICharacterRepository + { + Task> GetCharactersAsync(CharacterFilter filter); + Task GetCharacterByIdAsync(string id); + } +} diff --git a/server/Persistence/Seed/Data/characters.json b/server/Persistence/Seed/Data/characters.json new file mode 100644 index 0000000..c1c10b1 --- /dev/null +++ b/server/Persistence/Seed/Data/characters.json @@ -0,0 +1,351 @@ +[ + { + "FirstName": "Jason", + "LastName": "Gideon", + "ActorFirstName": "Mandy", + "ActorLastName": "Patinkin", + "Seasons": [ + 1, + 2, + 3, + 10, + 15 + ], + "FirstEpisode": "Extreme Aggressor", + "LastEpisode": "In Name and Blood", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/jason-gideon.png" + }, + { + "FirstName": "Derek", + "LastName": "Morgan", + "ActorFirstName": "Shemar", + "ActorLastName": "Moore", + "Seasons": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ], + "FirstEpisode": "Extreme Aggressor", + "LastEpisode": "A Beautiful Disaster", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/derek-morgan.png" + }, + { + "FirstName": "Aaron", + "LastName": "Hotchner", + "ActorFirstName": "Thomas", + "ActorLastName": "Gibson", + "Seasons": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "FirstEpisode": "Extreme Aggressor", + "LastEpisode": "Sick Day", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/aaron-hotchner.png" + }, + { + "FirstName": "Spencer", + "LastName": "Reid", + "ActorFirstName": "Matthew", + "ActorLastName": "Gubler", + "Seasons": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ], + "FirstEpisode": "Extreme Aggressor", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/spencer-reid.png" + }, + { + "FirstName": "Max", + "LastName": "Ryan", + "ActorFirstName": "Geoff", + "ActorLastName": "Pierson", + "Seasons": [ + 1 + ], + "FirstEpisode": "Unfinished Business", + "LastEpisode": "Unfinished Business", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/max-ryan.png" + }, + { + "FirstName": "Elle", + "LastName": "Greenaway", + "ActorFirstName": "Lola", + "ActorLastName": "Glaudini", + "Seasons": [ + 1, + 2 + ], + "FirstEpisode": "Extreme Aggressor", + "LastEpisode": "The Boogeyman", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/elle-greenaway.png" + }, + { + "FirstName": "Jennifer", + "LastName": "Jareau", + "ActorFirstName": "A.J.", + "ActorLastName": "Cook", + "Seasons": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ], + "FirstEpisode": "Complusion", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/jennifer-jareau.png" + }, + { + "FirstName": "Emily", + "LastName": "Prentiss", + "ActorFirstName": "Paget", + "ActorLastName": "Brewster", + "Seasons": [ + 2, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 12, + 13, + 14, + 15 + ], + "FirstEpisode": "The Last Word", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/emily-prentiss.png" + }, + { + "FirstName": "David", + "LastName": "Rossi", + "ActorFirstName": "Joe", + "ActorLastName": "Mantegna", + "Seasons": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ], + "FirstEpisode": "About Face", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/david-rossi.png" + }, + { + "FirstName": "Penelope", + "LastName": "Garcia", + "ActorFirstName": "Kirsten", + "ActorLastName": "Vangsness", + "Seasons": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ], + "FirstEpisode": "Extreme Aggressor", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/penelope-garcia.png" + }, + { + "FirstName": "Ian", + "LastName": "Doyle", + "ActorFirstName": "Timothy", + "ActorLastName": "Murphy", + "Seasons": [ + 6, + 7 + ], + "FirstEpisode": "The Thirteenth Step", + "LastEpisode": "It Takes a Village", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/ian-doyle.png" + }, + { + "FirstName": "Ashley", + "LastName": "Seaver", + "ActorFirstName": "Rachel", + "ActorLastName": "Nichols", + "Seasons": [ + 6 + ], + "FirstEpisode": "What Happens at Home", + "LastEpisode": "Supply & Demand", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/ashley-seaver.png" + }, + { + "FirstName": "Alex", + "LastName": "Blake", + "ActorFirstName": "Jeanne", + "ActorLastName": "Tripplehorn", + "Seasons": [ + 8, + 9 + ], + "FirstEpisode": "The Silencer", + "LastEpisode": "Demons", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/alex-blake.png" + }, + { + "FirstName": "Kate", + "LastName": "Callahan", + "ActorFirstName": "Jennifer", + "ActorLastName": "Hewitt", + "Seasons": [ + 10 + ], + "FirstEpisode": "X", + "LastEpisode": "The Hunt", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/kate-callahan.png" + }, + { + "FirstName": "Jack", + "LastName": "Garrett", + "ActorFirstName": "Gary", + "ActorLastName": "Sinise", + "Seasons": [ + 10 + ], + "FirstEpisode": "Beyond Borders", + "LastEpisode": "Beyond Borders", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/jack-garrett.png" + }, + { + "FirstName": "Tara", + "LastName": "Lewis", + "ActorFirstName": "Aisha", + "ActorLastName": "Tyler", + "Seasons": [ + 11, + 12, + 13, + 14, + 15 + ], + "FirstEpisode": "The Job", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/tara-lewis.png" + }, + { + "FirstName": "Luke", + "LastName": "Alvez", + "ActorFirstName": "Adam", + "ActorLastName": "Rodriguez", + "Seasons": [ + 12, + 13, + 14, + 15 + ], + "FirstEpisode": "The Crimson King", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/luke-alvez.png" + }, + { + "FirstName": "Stephen", + "LastName": "Walker", + "ActorFirstName": "Damon", + "ActorLastName": "Gupton", + "Seasons": [ + 12, + 13 + ], + "FirstEpisode": "Scarecrow", + "LastEpisode": "Wheels Up", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/stephen-walker.png" + }, + { + "FirstName": "Matthew", + "LastName": "Simmons", + "ActorFirstName": "Daniel", + "ActorLastName": "Henney", + "Seasons": [ + 13, + 14, + 15 + ], + "FirstEpisode": "Beyond Borders", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/matthew-simmons.png" + }, + { + "FirstName": "Krystall", + "LastName": "Richards", + "ActorFirstName": "Gail", + "ActorLastName": "O'Grady", + "Seasons": [ + 8, + 13, + 14, + 15 + ], + "FirstEpisode": "The Dance of Love", + "LastEpisode": "And in the End", + "Image": "https://criminalmindsapi.stevanfreeborn.com/characters/krystall-richards.png" + } +] \ No newline at end of file diff --git a/server/Persistence/Seed/Seeder.cs b/server/Persistence/Seed/Seeder.cs index 521bb7a..d48a210 100644 --- a/server/Persistence/Seed/Seeder.cs +++ b/server/Persistence/Seed/Seeder.cs @@ -9,6 +9,7 @@ namespace server.Persistence.Seed private readonly IMongoCollection _seasons; private readonly IMongoCollection _episodes; private readonly IMongoCollection _quotes; + private readonly IMongoCollection _characters; public Seeder() { @@ -23,6 +24,7 @@ namespace server.Persistence.Seed _seasons = database.GetCollection(config.GetSection("MongoDBSettings:SeasonsCollection").Value); _episodes = database.GetCollection(config.GetSection("MongoDBSettings:EpisodesCollection").Value); _quotes = database.GetCollection(config.GetSection("MongoDBSettings:QuotesCollection").Value); + _characters = database.GetCollection(config.GetSection("MongoDBSettings:CharactersCollection").Value); } public async Task SeedSeasonsAsync() @@ -37,7 +39,7 @@ namespace server.Persistence.Seed if(seasons != null) { await _seasons.InsertManyAsync(seasons); - Console.WriteLine($"Seeded databases with seasons from {fileName}"); + Console.WriteLine($"Seeded databases with {nameof(seasons)} from {fileName}"); } } @@ -53,13 +55,13 @@ namespace server.Persistence.Seed if (episodes != null) { await _episodes.InsertManyAsync(episodes); - Console.WriteLine($"Seeded databases with episodes from {fileName}"); + Console.WriteLine($"Seeded databases with {nameof(episodes)} from {fileName}"); } } public async Task SeedQuotesAsync() { - await _quotes.DeleteManyAsync(quotes => true); + await _quotes.DeleteManyAsync(quote => true); const string fileName = "quotes.json"; var quotesFilePath = Path.Combine(AppContext.BaseDirectory, $"Persistence/Seed/Data/{fileName}"); @@ -69,7 +71,24 @@ namespace server.Persistence.Seed if (quotes != null) { await _quotes.InsertManyAsync(quotes); - Console.WriteLine($"Seeded databases with quotes from {fileName}"); + Console.WriteLine($"Seeded databases with {nameof(quotes)} from {fileName}"); + } + + } + + public async Task SeedCharactersAsync() + { + await _characters.DeleteManyAsync(character => true); + + const string fileName = "characters.json"; + var charactersFilePath = Path.Combine(AppContext.BaseDirectory, $"Persistence/Seed/Data/{fileName}"); + var charactersJson = await File.ReadAllTextAsync(charactersFilePath); + var characters = JsonSerializer.Deserialize>(charactersJson); + + if (characters != null) + { + await _characters.InsertManyAsync(characters); + Console.WriteLine($"Seeded databases with {nameof(characters)} from {fileName}"); } } diff --git a/server/Program.cs b/server/Program.cs index baa6bb0..22027c5 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -1,125 +1,28 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Options; -using server.Filters; -using server.Options; -using server.Persistence; -using server.Persistence.Repositories; using server.Persistence.Seed; -using System.Reflection; -using AspNetCoreRateLimit; +using server.Setup; -if (args.Length == 2 && args[0].ToLower() == "seed") +if (args.Length > 0 && args[0].ToLower() == "seed") { var seeder = new Seeder(); - if (args[1].ToLower() == "seasons") - { - await seeder.SeedSeasonsAsync(); - } + if (args.Contains("seasons")) await seeder.SeedSeasonsAsync(); - if (args[1].ToLower() == "episodes") - { - await seeder.SeedEpisodesAsync(); - } + if (args.Contains("episodes")) await seeder.SeedEpisodesAsync(); - if (args[1].ToLower() == "quotes") - { - await seeder.SeedQuotesAsync(); - } + if (args.Contains("quotes")) await seeder.SeedQuotesAsync(); + + if (args.Contains("characters")) await seeder.SeedCharactersAsync(); +} +else +{ + var app = WebApplication + .CreateBuilder(args) + .SetupDb() + .SetupMvC() + .SetupSwagger() + .Build(); + + app.SetupMiddleware().Run(); } -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddMemoryCache(); - -builder.Services.Configure( - builder.Configuration.GetSection("IpRateLimiting")); - -builder.Services.Configure( - builder.Configuration.GetSection("IpRateLimitPolicies")); - -builder.Services.AddInMemoryRateLimiting(); - -builder.Services.AddSingleton(); - -builder.Services.Configure( - builder.Configuration.GetSection("MongoDBSettings")); - -builder.Services.AddSingleton(sp => - sp.GetRequiredService>().Value); - -builder.Services.AddSingleton(); - -builder.Services.AddScoped(); - -builder.Services.AddScoped(); - -builder.Services.AddScoped(); - -builder.Services.AddControllers(); - -builder.Services.AddEndpointsApiExplorer(); - -builder.Services.AddSwaggerGen(options => -{ - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - - options.IncludeXmlComments(xmlPath); - options.OperationFilter(); -}); - -builder.Services.ConfigureOptions(); - -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.UseStaticFiles(); - -app.UseSwagger(options => -{ - options.RouteTemplate = "/{documentName}/docs.json"; -}); - -app.UseSwaggerUI(options => -{ - var provider = app.Services.GetRequiredService(); - - 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.UseAuthorization(); - -app.MapControllers(); - -app.UseIpRateLimiting(); - -app.Run(); - diff --git a/server/Properties/ServiceDependencies/criminalminds-server - Web Deploy/profile.arm.json b/server/Properties/ServiceDependencies/criminalmindsapi - Web Deploy/profile.arm.json similarity index 98% rename from server/Properties/ServiceDependencies/criminalminds-server - Web Deploy/profile.arm.json rename to server/Properties/ServiceDependencies/criminalmindsapi - Web Deploy/profile.arm.json index 4b91ec2..9d32f2f 100644 --- a/server/Properties/ServiceDependencies/criminalminds-server - Web Deploy/profile.arm.json +++ b/server/Properties/ServiceDependencies/criminalmindsapi - Web Deploy/profile.arm.json @@ -21,7 +21,7 @@ }, "resourceName": { "type": "string", - "defaultValue": "criminalminds-server", + "defaultValue": "criminalmindsapi", "metadata": { "description": "Name of the main resource to be created by this template." } diff --git a/server/Setup/Middleware.cs b/server/Setup/Middleware.cs new file mode 100644 index 0000000..7799abf --- /dev/null +++ b/server/Setup/Middleware.cs @@ -0,0 +1,43 @@ +using AspNetCoreRateLimit; +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace server.Setup +{ + public static class Middleware + { + public static WebApplication SetupMiddleware(this WebApplication app) + { + app.UseSwagger(options => + { + options.RouteTemplate = "/{documentName}/docs.json"; + }); + + app.UseSwaggerUI(options => + { + var provider = app.Services.GetRequiredService(); + + 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.UseAuthorization(); + + app.MapControllers(); + + app.UseIpRateLimiting(); + + return app; + } + } +} diff --git a/server/Setup/Services.cs b/server/Setup/Services.cs new file mode 100644 index 0000000..1ba5bab --- /dev/null +++ b/server/Setup/Services.cs @@ -0,0 +1,86 @@ +using AspNetCoreRateLimit; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.Options; +using server.Persistence; +using server.Persistence.Repositories; +using server.SwaggerOptions; +using server.SwaggerOptions.Filters; +using System.Reflection; + +namespace server.Setup +{ + public static class Services + { + public static WebApplicationBuilder SetupDb(this WebApplicationBuilder builder) + { + builder.Services.Configure( + builder.Configuration.GetSection("MongoDBSettings")); + + builder.Services.AddSingleton(sp => + sp.GetRequiredService>().Value); + + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + + return builder; + } + + public static WebApplicationBuilder SetupMvC(this WebApplicationBuilder builder) + { + builder.Services.AddMemoryCache(); + + builder.Services.Configure( + builder.Configuration.GetSection("IpRateLimiting")); + + builder.Services.Configure( + builder.Configuration.GetSection("IpRateLimitPolicies")); + + builder.Services.AddInMemoryRateLimiting(); + + builder.Services.AddSingleton(); + + builder.Services.AddControllers(); + + builder.Services.AddEndpointsApiExplorer(); + + 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"; + }); + + return builder; + } + + public static WebApplicationBuilder SetupSwagger(this WebApplicationBuilder builder) + { + builder.Services.AddSwaggerGen(options => + { + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + + options.IncludeXmlComments(xmlPath); + options.OperationFilter(); + }); + + builder.Services.ConfigureOptions(); + + return builder; + } + } +} diff --git a/server/Options/ConfigureSwaggerOptions.cs b/server/SwaggerOptions/ConfigureSwaggerOptions.cs similarity index 97% rename from server/Options/ConfigureSwaggerOptions.cs rename to server/SwaggerOptions/ConfigureSwaggerOptions.cs index 8d2a8aa..a919623 100644 --- a/server/Options/ConfigureSwaggerOptions.cs +++ b/server/SwaggerOptions/ConfigureSwaggerOptions.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace server.Options +namespace server.SwaggerOptions { public class ConfigureSwaggerOptions : IConfigureOptions { diff --git a/server/Filters/ApiVersionOperationFilter.cs b/server/SwaggerOptions/Filters/ApiVersionOperationFilter.cs similarity index 94% rename from server/Filters/ApiVersionOperationFilter.cs rename to server/SwaggerOptions/Filters/ApiVersionOperationFilter.cs index b49cd1c..4445471 100644 --- a/server/Filters/ApiVersionOperationFilter.cs +++ b/server/SwaggerOptions/Filters/ApiVersionOperationFilter.cs @@ -2,7 +2,7 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace server.Filters +namespace server.SwaggerOptions.Filters { public class ApiVersionOperationFilter : IOperationFilter {