├── src
├── dwCheckApi
│ ├── dwDatabase.db-wal
│ ├── favicon.ico
│ ├── dwDatabase.db-shm
│ ├── wwwroot
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── mstile-150x150.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── browserconfig.xml
│ │ ├── site.webmanifest
│ │ ├── html_code.html
│ │ └── safari-pinned-tab.svg
│ ├── appsettings.json
│ ├── appsettings.Production.json
│ ├── web.config
│ ├── Controllers
│ │ ├── NotFoundController.cs
│ │ ├── VersionController.cs
│ │ ├── BaseController.cs
│ │ ├── SeriesController.cs
│ │ ├── CharactersController.cs
│ │ ├── DatabaseController.cs
│ │ └── BooksController.cs
│ ├── program.cs
│ ├── Helpers
│ │ ├── SecretChecker.cs
│ │ └── CommonHelpers.cs
│ ├── dwCheckApi.csproj
│ ├── SeedData
│ │ └── SeriesBookSeedData.json
│ ├── startup.cs
│ ├── ConfigureContainerExtensions.cs
│ └── ConfigureHttpPipelineExtension.cs
├── dwCheckApi.Entities
│ ├── dwCheckApi.Entities.csproj
│ ├── BaseAuditClass.cs
│ ├── BookCharacter.cs
│ ├── BookSeries.cs
│ ├── Series.cs
│ ├── Character.cs
│ └── Book.cs
├── dwCheckApi.DTO
│ ├── ViewModels
│ │ ├── BaseViewModel.cs
│ │ ├── BookCoverViewModel.cs
│ │ ├── CharacterViewModel.cs
│ │ ├── BookBaseViewModel.cs
│ │ ├── SeriesViewModel.cs
│ │ └── BookViewModel.cs
│ ├── dwCheckApi.DTO.csproj
│ └── Helpers
│ │ ├── CharacterViewModelHelper.cs
│ │ ├── SeriesViewModelHelpers.cs
│ │ └── BookViewModelHelper.cs
├── dwCheckApi.Common
│ ├── ValueNotFoundException.cs
│ ├── DatabaseConfiguration.cs
│ ├── CorsConfiguration.cs
│ ├── ConfigurationBase.cs
│ └── dwCheckApi.Common.csproj
├── dwCheckApi.Persistence
│ ├── Helpers
│ │ ├── BookSeriesSeedData.cs
│ │ ├── BookCharacterSeedData.cs
│ │ └── DatabaseSeeder.cs
│ ├── ChangeTrackerExtensions.cs
│ ├── DwContextFactory.cs
│ ├── ModelBuilderExtensions.cs
│ ├── dwContext.cs
│ ├── IDwContext.cs
│ ├── DwContextExtensions.cs
│ ├── dwCheckApi.Persistence.csproj
│ └── Migrations
│ │ ├── DwContextModelSnapshot.cs
│ │ ├── 20170826014619_InitialMigration.Designer.cs
│ │ └── 20170826014619_InitialMigration.cs
└── dwCheckApi.DAL
│ ├── ISeriesService.cs
│ ├── dwCheckApi.DAL.csproj
│ ├── ICharacterService.cs
│ ├── IDatabaseService.cs
│ ├── IBookService.cs
│ ├── DatabaseService.cs
│ ├── SeriesService.cs
│ ├── CharacterService.cs
│ └── BookService.cs
├── tests
├── dwCheckApi.Common.Tests
│ ├── Usings.cs
│ ├── appsettings.Tests.json
│ ├── TestableCorsConfiguration.cs
│ ├── TestableConfigurationBase.cs
│ ├── DatabaseConfigurationTests.cs
│ ├── ConfigurationBaseTests.cs
│ ├── CorsConfigurationBaseTests.cs
│ └── dwCheckApi.Common.Tests.csproj
└── dwCheckApi.Tests
│ ├── SeedData
│ └── TestBookSeedData.json
│ ├── Helpers
│ ├── CommonHelperTests.cs
│ └── SecretCheckerTests.cs
│ ├── ViewModelMappers
│ ├── CharacterViewModelMapperTests.cs
│ ├── SeriesViewModelMapperTests.cs
│ └── BookViewModelMapperTests.cs
│ ├── dwCheckApi.Tests.csproj
│ └── DatabaseSeederTests.cs
├── global.json
├── .dockerignore
├── dwCheckApi.sln.DotSettings
├── LICENSE
├── .github
└── workflows
│ ├── weekly-cleanup.yml
│ ├── pr-action.yml
│ └── ci-build-action.yml
├── Dockerfile
├── Code of Conduct.md
├── .gitignore
├── dwCheckApi.sln
└── README.md
/src/dwCheckApi/dwDatabase.db-wal:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dwCheckApi.Common.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "6.0.x"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/bin/
2 | **/obj/
3 | .idea/
4 | .vscode/
5 | *.md
6 | *.yml
7 | *.userprefs
--------------------------------------------------------------------------------
/src/dwCheckApi/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/favicon.ico
--------------------------------------------------------------------------------
/src/dwCheckApi/dwDatabase.db-shm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/dwDatabase.db-shm
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/wwwroot/favicon-16x16.png
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/wwwroot/favicon-32x32.png
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/wwwroot/mstile-150x150.png
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/wwwroot/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/wwwroot/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GaProgMan/dwCheckApi/HEAD/src/dwCheckApi/wwwroot/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/dwCheckApi.Entities/dwCheckApi.Entities.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 |
5 |
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/ViewModels/BaseViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace dwCheckApi.DTO.ViewModels
2 | {
3 | public abstract class BaseViewModel
4 | {
5 | public string Message { get; set; }
6 | }
7 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Entities/BaseAuditClass.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace dwCheckApi.Entities
4 | {
5 | public class BaseAuditClass
6 | {
7 | public DateTime Created { get; set; }
8 |
9 | public DateTime Modified { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Common/ValueNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace dwCheckApi.Common
4 | {
5 | public class ValueNotFoundException : Exception
6 | {
7 | public ValueNotFoundException(string message) : base(message)
8 | {
9 |
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/dwCheckApi.DTO.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/Helpers/BookSeriesSeedData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace dwCheckApi.Persistence.Helpers
4 | {
5 | public class SeriesBookSeedData
6 | {
7 | public string SeriesName { get; set; }
8 | public List BookNames { get; set; }
9 | }
10 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/Helpers/BookCharacterSeedData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace dwCheckApi.Persistence.Helpers
4 | {
5 | public class BookCharacterSeedData
6 | {
7 | public string BookName {get; set;}
8 | public List CharacterNames {get; set;}
9 | }
10 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/ViewModels/BookCoverViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace dwCheckApi.DTO.ViewModels
2 | {
3 | public class BookCoverViewModel : BaseViewModel
4 | {
5 | public int bookId { get; set; }
6 | public string BookCoverImage { get; set; }
7 | public bool BookImageIsBase64String { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/ISeriesService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using dwCheckApi.Entities;
3 |
4 | namespace dwCheckApi.DAL
5 | {
6 | public interface ISeriesService
7 | {
8 | Series GetById (int id);
9 | Series GetByName (string seriesName);
10 | IEnumerable Search(string searchKey);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/ViewModels/CharacterViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace dwCheckApi.DTO.ViewModels
4 | {
5 | public class CharacterViewModel : BaseViewModel
6 | {
7 | public string CharacterName { get; set; }
8 | public SortedDictionary Books { get; set; } = new();
9 | }
10 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Entities/BookCharacter.cs:
--------------------------------------------------------------------------------
1 | namespace dwCheckApi.Entities
2 | {
3 | public class BookCharacter : BaseAuditClass
4 | {
5 | public int BookId { get; set; }
6 | public virtual Book Book { get; set; }
7 | public int CharacterId {get; set; }
8 | public virtual Character Character { get; set; }
9 | }
10 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/ViewModels/BookBaseViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace dwCheckApi.DTO.ViewModels
2 | {
3 | public class BookBaseViewModel : BaseViewModel
4 | {
5 | public int BookId { get; set; }
6 | public int BookOrdinal { get; set; }
7 | public string BookName { get; set; }
8 | public string BookDescription { get; set; }
9 | }
10 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/dwCheckApi.DAL.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/dwCheckApi.Entities/BookSeries.cs:
--------------------------------------------------------------------------------
1 | namespace dwCheckApi.Entities
2 | {
3 | public class BookSeries : BaseAuditClass
4 | {
5 | public int BookId { get; set; }
6 | public virtual Book Book { get; set; }
7 |
8 | public int SeriesId { get; set; }
9 | public virtual Series Series { get; set; }
10 |
11 | public int Ordinal { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "dwCheckApiConnection": "Data Source=dwDatabase.db"
4 | },
5 | "Logging": {
6 | "IncludeScopes": false,
7 | "LogLevel": {
8 | "Default": "Debug",
9 | "System": "Information",
10 | "Microsoft": "Information"
11 | }
12 | },
13 | "CorsPolicy": {
14 | "name":"dwCheckApiCorsPolicy"
15 | }
16 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/appsettings.Production.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "dwCheckApiConnection": "Data Source=dwDatabase.db"
4 | },
5 | "Logging": {
6 | "IncludeScopes": false,
7 | "LogLevel": {
8 | "Default": "Debug",
9 | "System": "Information",
10 | "Microsoft": "Information"
11 | }
12 | },
13 | "CorsPolicy": {
14 | "name":"dwCheckApiCorsPolicy"
15 | }
16 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/ICharacterService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using dwCheckApi.Entities;
4 |
5 | namespace dwCheckApi.DAL
6 | {
7 | public interface ICharacterService
8 | {
9 | Character GetById (int id);
10 | Character GetByName (string characterName);
11 | IEnumerable> Search(string searchKey);
12 | }
13 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Common.Tests/appsettings.Tests.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "dwCheckApiConnection": "Data Source=dwDatabase.db"
4 | },
5 | "Logging": {
6 | "IncludeScopes": false,
7 | "LogLevel": {
8 | "Default": "Debug",
9 | "System": "Information",
10 | "Microsoft": "Information"
11 | }
12 | },
13 | "CorsPolicy": {
14 | "name":"dwCheckApiCorsPolicy"
15 | }
16 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/IDatabaseService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using dwCheckApi.Entities;
4 |
5 | namespace dwCheckApi.DAL
6 | {
7 | public interface IDatabaseService
8 | {
9 | bool ClearDatabase();
10 |
11 | int SeedDatabase();
12 |
13 | IEnumerable BooksWithoutCoverBytes();
14 |
15 | Task SaveAnyChanges();
16 | }
17 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Entities/Series.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.ObjectModel;
3 |
4 | namespace dwCheckApi.Entities
5 | {
6 | public class Series : BaseAuditClass
7 | {
8 | public int SeriesId { get; set; }
9 | public string SeriesName { get; set; }
10 | public virtual ICollection BookSeries { get; set; } = new Collection();
11 | }
12 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Entities/Character.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.ObjectModel;
3 |
4 | namespace dwCheckApi.Entities
5 | {
6 | public class Character : BaseAuditClass
7 | {
8 | public int CharacterId { get; set; }
9 | public string CharacterName { get; set; }
10 | public virtual ICollection BookCharacter { get; set; } = new Collection();
11 | }
12 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Common/DatabaseConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 |
3 | namespace dwCheckApi.Common
4 | {
5 | public class DatabaseConfiguration : ConfigurationBase
6 | {
7 | private string DbConnectionKey = "dwCheckApiConnection";
8 | public string GetDatabaseConnectionString()
9 | {
10 | return GetConfiguration().GetConnectionString(DbConnectionKey);
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/ViewModels/SeriesViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace dwCheckApi.DTO.ViewModels
4 | {
5 | public class SeriesViewModel : BaseViewModel
6 | {
7 | public SeriesViewModel()
8 | {
9 | BookNames = new List();
10 | }
11 |
12 | public int SeriesId { get; set; }
13 | public string SeriesName { get; set; }
14 | public List BookNames { get; set; }
15 | }
16 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Common.Tests/TestableCorsConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace dwCheckApi.Common.Tests
2 | {
3 | public abstract class TestableCorsConfiguration : CorsConfiguration
4 | {
5 | public string CallGetCorsPolicy(string? corsPolicyName = null)
6 | {
7 | if (!string.IsNullOrEmpty(corsPolicyName))
8 | {
9 | CorsPolicyKey = corsPolicyName;
10 | }
11 |
12 | return GetCorsPolicyName();
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/IBookService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using dwCheckApi.Entities;
3 |
4 | namespace dwCheckApi.DAL
5 | {
6 | public interface IBookService
7 | {
8 | // Search and Get
9 | Book FindById(int id);
10 | Book FindByOrdinal (int id);
11 | Book GetByName(string bookName);
12 | IEnumerable GetAll();
13 | IEnumerable Search(string searchKey);
14 | IEnumerable Series(int seriesId);
15 | }
16 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "DwCheckApi",
3 | "short_name": "DwCheckApi",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/src/dwCheckApi.Common/CorsConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace dwCheckApi.Common
2 | {
3 | public class CorsConfiguration : ConfigurationBase
4 | {
5 | protected string CorsPolicyKey = "CorsPolicy:name";
6 | public string GetCorsPolicyName()
7 | {
8 | var section = GetConfiguration().GetSection(CorsPolicyKey);
9 | if (section == null || string.IsNullOrEmpty(section.Value))
10 | {
11 | RaiseValueNotFoundException(CorsPolicyKey);
12 | }
13 | return section!.Value;
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/html_code.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/ViewModels/BookViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace dwCheckApi.DTO.ViewModels
4 | {
5 | public class BookViewModel : BookBaseViewModel
6 | {
7 | public BookViewModel()
8 | {
9 | Characters = new List();
10 | Series = new Dictionary();
11 | }
12 |
13 | public string BookIsbn10 { get; set; }
14 | public string BookIsbn13 { get; set; }
15 | public List Characters { get; set; }
16 | public Dictionary Series { get; set; }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/dwCheckApi.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
--------------------------------------------------------------------------------
/tests/dwCheckApi.Common.Tests/TestableConfigurationBase.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 |
3 | namespace dwCheckApi.Common.Tests
4 | {
5 | public abstract class TestableConfigurationBase : ConfigurationBase
6 | {
7 | public TestableConfigurationBase()
8 | {
9 | JsonFileName = "appsettings.Tests.json";
10 | }
11 | public IConfigurationRoot CallGetConfiguration()
12 | {
13 | return GetConfiguration();
14 | }
15 |
16 | public void CallRaiseValueNotFoundException(string key)
17 | {
18 | RaiseValueNotFoundException(key);
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Common.Tests/DatabaseConfigurationTests.cs:
--------------------------------------------------------------------------------
1 | using NSubstitute;
2 |
3 | namespace dwCheckApi.Common.Tests
4 | {
5 |
6 | public class DatabaseConfigurationTests
7 | {
8 | private readonly DatabaseConfiguration _databaseConfigurationMock = Substitute.For();
9 |
10 | [Fact]
11 | public void GetDatabaseConnectionString_ReturnsNonEmptyConnectionString()
12 | {
13 | var connectionString = _databaseConfigurationMock.GetDatabaseConnectionString();
14 | Assert.IsAssignableFrom(connectionString);
15 | Assert.NotEmpty(connectionString);
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Tests/SeedData/TestBookSeedData.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "BookName": "The Colour of Magic",
4 | "BookOrdinal": "1",
5 | "BookIsbn10": "086140324X",
6 | "BookIsbn13": "9780552138932",
7 | "BookDescription": "On a world supported on the back of a giant turtle (sex unknown), a gleeful, explosive, wickedly eccentric expedition sets out. There's an avaricious but inept wizard, a naive tourist whose luggage moves on hundreds of dear little legs, dragons who only exist if you believe in them, and of course THE EDGE of the planet ...",
8 | "BookCoverImageUrl": "https://wiki.lspace.org/File:Cover_The_Colour_Of_Magic.jpg"
9 | }
10 | ]
--------------------------------------------------------------------------------
/src/dwCheckApi/Controllers/NotFoundController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace dwCheckApi.Controllers
4 | {
5 | [Route("/")]
6 | [Produces("text/plain")]
7 | public class NotFoundController : BaseController
8 | {
9 | ///
10 | /// Used to get a string which represents all of the controller actions
11 | /// available for the API
12 | ///
13 | ///
14 | /// A string representing all of the controller actions available for the API
15 | ///
16 | [HttpGet]
17 | public string Get()
18 | {
19 | return IncorrectUseOfApi();
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/Helpers/CharacterViewModelHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using dwCheckApi.DTO.ViewModels;
3 |
4 | namespace dwCheckApi.DTO.Helpers
5 | {
6 | public static class CharacterViewModelHelpers
7 | {
8 | public static CharacterViewModel ConvertToViewModel (string characterName, Dictionary books = null)
9 | {
10 | var viewModel = new CharacterViewModel
11 | {
12 | CharacterName = characterName
13 | };
14 |
15 | if (books != null)
16 | {
17 | viewModel.Books = new SortedDictionary(books);
18 | }
19 |
20 | return viewModel;
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Common/ConfigurationBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Configuration;
3 |
4 | namespace dwCheckApi.Common
5 | {
6 | public abstract class ConfigurationBase
7 | {
8 | protected string JsonFileName = "appsettings.Production.json";
9 | protected IConfigurationRoot GetConfiguration()
10 | {
11 | return new ConfigurationBuilder()
12 | .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
13 | .AddJsonFile(JsonFileName)
14 | .Build();
15 | }
16 |
17 | protected void RaiseValueNotFoundException(string configurationKey)
18 | {
19 | throw new ValueNotFoundException($"appsettings key ({configurationKey}) could not be found.");
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Entities/Book.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.ObjectModel;
3 |
4 | namespace dwCheckApi.Entities
5 | {
6 | public class Book : BaseAuditClass
7 | {
8 | public int BookId { get; set; }
9 | public int BookOrdinal { get; set; }
10 | public string BookName { get; set; }
11 | public string BookIsbn10 { get; set; }
12 | public string BookIsbn13 { get; set; }
13 | public string BookDescription { get; set; }
14 | public byte[] BookCoverImage { get; set; }
15 | public string BookCoverImageUrl { get; set; }
16 | public virtual ICollection BookCharacter { get; set; } = new Collection();
17 | public virtual ICollection BookSeries { get; set; } = new Collection();
18 | }
19 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Tests/Helpers/CommonHelperTests.cs:
--------------------------------------------------------------------------------
1 | using dwCheckApi.Helpers;
2 | using Xunit;
3 |
4 | namespace dwCheckApi.Tests.Helpers
5 | {
6 | public class CommonHelperTests
7 | {
8 | [Fact]
9 | public void IncorrectUsageOfApi_Returns_NonNull_String()
10 | {
11 | // Arrange
12 |
13 | // Act
14 | var response = CommonHelpers.IncorrectUsageOfApi();
15 |
16 | // Assert
17 | Assert.NotEmpty(response);
18 | Assert.Contains("Incorrect usage of API", response);
19 | }
20 |
21 | [Fact]
22 | public void GetVersionNumber_Returns_NonNull_String()
23 | {
24 | // Arrange
25 |
26 | // Act
27 | var response = CommonHelpers.GetVersionNumber();
28 |
29 | // Assert
30 | Assert.NotEmpty(response);
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/Controllers/VersionController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using dwCheckApi.Helpers;
3 | using Microsoft.AspNetCore.Http;
4 |
5 | namespace dwCheckApi.Controllers
6 | {
7 | [Route("/[controller]")]
8 | [Produces("application/json")]
9 | public class VersionController : BaseController
10 | {
11 | ///
12 | /// Gets the semver formatted version number for the application
13 | ///
14 | ///
15 | /// A string representing the semver formatted version number for the application
16 | ///
17 | [HttpGet]
18 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
19 | public IActionResult Get()
20 | {
21 | return Ok(new SingleResult
22 | {
23 | Success = true,
24 | Result = CommonHelpers.GetVersionNumber()
25 | });
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/Helpers/SeriesViewModelHelpers.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using dwCheckApi.DTO.ViewModels;
4 | using dwCheckApi.Entities;
5 |
6 | namespace dwCheckApi.DTO.Helpers
7 | {
8 | public static class SeriesViewModelHelpers
9 | {
10 | public static SeriesViewModel ConvertToViewModel (Series dbModel)
11 | {
12 | var viewModel = new SeriesViewModel
13 | {
14 | SeriesId = dbModel.SeriesId,
15 | SeriesName = dbModel.SeriesName
16 | };
17 |
18 | foreach(var dbBook in dbModel.BookSeries.OrderBy(bs => bs.Ordinal))
19 | {
20 | viewModel.BookNames.Add(dbBook.Book.BookName ?? string.Empty);
21 | }
22 |
23 | return viewModel;
24 | }
25 |
26 | public static List ConvertToViewModels(List dbModels)
27 | {
28 | return dbModels.Select (ConvertToViewModel).ToList();
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/ChangeTrackerExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using dwCheckApi.Entities;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.ChangeTracking;
5 |
6 | namespace dwCheckApi.Persistence
7 | {
8 | public static class ChangeTrackerExtensions
9 | {
10 | public static void ApplyAuditInformation(this ChangeTracker changeTracker)
11 | {
12 | foreach (var entry in changeTracker.Entries())
13 | {
14 | if (entry.Entity is not BaseAuditClass baseAudit) continue;
15 |
16 | var now = DateTime.UtcNow;
17 | switch (entry.State)
18 | {
19 | case EntityState.Modified:
20 | baseAudit.Created = now;
21 | baseAudit.Modified = now;
22 | break;
23 |
24 | case EntityState.Added:
25 | baseAudit.Created = now;
26 | break;
27 | }
28 | }
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jamie Taylor
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/tests/dwCheckApi.Common.Tests/ConfigurationBaseTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 | using NSubstitute;
3 |
4 | namespace dwCheckApi.Common.Tests
5 | {
6 | public class ConfigurationBaseTests
7 | {
8 | private TestableConfigurationBase _configurationBaseMock = Substitute.For();
9 |
10 | [Fact]
11 | public void GetConfiguration_ReturnsIConfigurationRoot()
12 | {
13 | var result = _configurationBaseMock.CallGetConfiguration();
14 | Assert.IsAssignableFrom(result);
15 | }
16 |
17 | [Fact]
18 | public void RaiseValueNotFoundException_ThrowsValueNotFoundException()
19 | {
20 | const string keyToSearch = "NonExistentKey";
21 | var exception =
22 | Record.Exception(() => _configurationBaseMock.CallRaiseValueNotFoundException(keyToSearch));
23 | Assert.NotNull(exception);
24 | Assert.IsAssignableFrom(exception);
25 | Assert.Equal($"appsettings key ({keyToSearch}) could not be found.", exception.Message);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Common.Tests/CorsConfigurationBaseTests.cs:
--------------------------------------------------------------------------------
1 | using NSubstitute;
2 |
3 | namespace dwCheckApi.Common.Tests
4 | {
5 | public class CorsConfigurationBaseTests
6 | {
7 | private TestableCorsConfiguration _corsConfigurationMock = Substitute.For();
8 |
9 | [Fact]
10 | public void GetCorsPolicyName_ValidPolicyNameKey_ReturnsNonEmptyCorsPolicyName()
11 | {
12 | var policyName = _corsConfigurationMock.CallGetCorsPolicy();
13 | Assert.IsAssignableFrom(policyName);
14 | Assert.NotEmpty(policyName);
15 | }
16 |
17 | [Fact]
18 | public void GetCorsPolicyName_InvalidPolicyNameKey_RaisesValueNotFoundException()
19 | {
20 | const string nonsensePolicyName = "nonsense";
21 | _corsConfigurationMock = Substitute.For();
22 | var exception =
23 | Record.Exception(() => _corsConfigurationMock.CallGetCorsPolicy(nonsensePolicyName));
24 | Assert.NotNull(exception);
25 | Assert.IsAssignableFrom(exception);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/program.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.IO;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.Extensions.Hosting;
5 |
6 | namespace dwCheckApi
7 | {
8 | [ExcludeFromCodeCoverage]
9 | public class Program
10 | {
11 | public static void Main(string[] args)
12 | {
13 | var host = BuildWebHost(args);
14 |
15 | host.Run();
16 | }
17 |
18 | public static IHost BuildWebHost(string[] args) =>
19 | // Notes for CreateDefaultBuilder:
20 | // - loads IConfiguration from UserSecrets automatically when in Development env
21 | // - still loads IConfiguration from appsettings[envName].json
22 | // - adds Developer Exception page when in Development env
23 | Host.CreateDefaultBuilder(args)
24 | .ConfigureWebHostDefaults(webBuilder =>
25 | {
26 | webBuilder.UseStartup();
27 | })
28 | // might need this anyway
29 | .UseContentRoot(Directory.GetCurrentDirectory())
30 | .Build();
31 | }
32 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Tests/ViewModelMappers/CharacterViewModelMapperTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using dwCheckApi.DTO.Helpers;
5 | using Xunit;
6 |
7 | namespace dwCheckApi.Tests.ViewModelMappers
8 | {
9 | public class CharacterViewModelMapperTests
10 | {
11 | [Fact]
12 | public void Given_CharacterDbModel_Returns_ViewModel()
13 | {
14 | // Arrange
15 | var characterName = Guid.NewGuid().ToString();
16 | var books = new Dictionary
17 | {
18 | // intentionally added out of order ot test the ordering of the final Dictionary
19 | { 2, Guid.NewGuid().ToString() },
20 | { 1, Guid.NewGuid().ToString() }
21 | };
22 |
23 | // Act
24 | var response = CharacterViewModelHelpers.ConvertToViewModel(characterName, books);
25 |
26 | // Assert
27 | Assert.Equal(response.CharacterName, characterName);
28 | Assert.Equal(response.Books.Count, books.Count);
29 |
30 | var first = response.Books.First();
31 | Assert.Equal(1, first.Key);
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/DatabaseService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using dwCheckApi.Entities;
5 | using dwCheckApi.Persistence;
6 |
7 | namespace dwCheckApi.DAL
8 | {
9 | public class DatabaseService : IDatabaseService
10 | {
11 | private readonly DwContext _context;
12 | public DatabaseService(DwContext context)
13 | {
14 | _context = context;
15 | }
16 | public bool ClearDatabase()
17 | {
18 | var cleared = _context.Database.EnsureDeleted();
19 | var created = _context.Database.EnsureCreated();
20 | var entitiesAdded = _context.SaveChanges();
21 |
22 | return cleared && created && entitiesAdded == 0;
23 | }
24 |
25 | public int SeedDatabase()
26 | {
27 | return _context.EnsureSeedData();
28 | }
29 |
30 | public IEnumerable BooksWithoutCoverBytes()
31 | {
32 | return _context.Books.Where(b => b.BookCoverImage == null || b.BookCoverImage.Length == 0);
33 | }
34 |
35 | public async Task SaveAnyChanges()
36 | {
37 | return await _context.SaveChangesAsync();
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/Controllers/BaseController.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using dwCheckApi.Helpers;
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | namespace dwCheckApi.Controllers
6 | {
7 | public class BaseController : Controller
8 | {
9 | protected record SingleResult where T : class
10 | {
11 | public bool Success { get; set; }
12 | public T Result { get; set; }
13 | }
14 |
15 | protected record MultipleResult where T : class
16 | {
17 | public bool Success { get; set; }
18 | public List Result { get; set; }
19 | }
20 |
21 | protected static string IncorrectUseOfApi()
22 | {
23 | return CommonHelpers.IncorrectUsageOfApi();
24 | }
25 |
26 | protected IActionResult NotFoundResponse(string message = "Not Found")
27 | {
28 | return NotFound(new SingleResult{
29 | Success = false,
30 | Result = message
31 | });
32 | }
33 |
34 | protected static IActionResult ErrorResponse(int statusCode, string message = "Internal server error")
35 | {
36 | return new StatusCodeResult(statusCode);
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/DwContextFactory.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Design;
4 | using dwCheckApi.Common;
5 |
6 | namespace dwCheckApi.Persistence
7 | {
8 | [ExcludeFromCodeCoverage]
9 | ///
10 | /// This factory is provided so that the EF Core tools can build a full context
11 | /// without having to have access to where the DbContext is being created (i.e.
12 | /// in the UI layer).
13 | ///
14 | ///
15 | /// Please see the following URL for more information:
16 | /// https://docs.microsoft.com/en-us/ef/core/miscellaneous/configuring-dbcontext#using-idbcontextfactorytcontext
17 | ///
18 | public class DwContextFactory : IDesignTimeDbContextFactory
19 | {
20 | private static string DbConnectionString => new DatabaseConfiguration().GetDatabaseConnectionString();
21 |
22 | public DwContext CreateDbContext(string[] args)
23 | {
24 | var optionsBuilder = new DbContextOptionsBuilder();
25 |
26 | optionsBuilder.UseSqlite(DbConnectionString);
27 |
28 | return new DwContext(optionsBuilder.Options);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/ModelBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using dwCheckApi.Entities;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace dwCheckApi.Persistence
6 | {
7 | [ExcludeFromCodeCoverage]
8 | public static class ModelBuilderExtensions
9 | {
10 | ///
11 | /// Used to create the the primary keys for dwCheckApi's database model
12 | ///
13 | /// An instance of the to act on
14 | public static void AddPrimaryKeys(this ModelBuilder builder)
15 | {
16 | builder.Entity().ToTable("Books")
17 | .HasKey(b => b.BookId);
18 |
19 | builder.Entity().ToTable("Characters")
20 | .HasKey(c => c.CharacterId);
21 |
22 | builder.Entity().ToTable("Series")
23 | .HasKey(s => s.SeriesId);
24 |
25 | builder.Entity().ToTable("BookCharacters")
26 | .HasKey(x => new {x.BookId, x.CharacterId});
27 |
28 | builder.Entity().ToTable("BookSeries")
29 | .HasKey(x => new { x.BookId, x.SeriesId });
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Tests/Helpers/SecretCheckerTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using dwCheckApi.Helpers;
3 | using Xunit;
4 |
5 | namespace dwCheckApi.Tests.Helpers
6 | {
7 | public class SecretCheckerTests
8 | {
9 | [Theory]
10 | [InlineData(null, "something")]
11 | [InlineData("", "something")]
12 | [InlineData("something", null)]
13 | [InlineData("something", "")]
14 | public void CheckUserSuppliedSecretValue_A_Null_Returns_False(string userSuppliedValue, string secretValue)
15 | {
16 | // Arrange
17 |
18 | // Act
19 | var response = SecretChecker.CheckUserSuppliedSecretValue(userSuppliedValue, secretValue);
20 |
21 | // Assert
22 | Assert.False(response);
23 | }
24 |
25 | [Fact]
26 | public void CheckUserSuppliedSecretValue_Matching_Strings_Returns_True()
27 | {
28 | // Arrange
29 | string secretValue;
30 |
31 | var userSuppliedValue = secretValue = Guid.NewGuid().ToString();
32 |
33 | // Act
34 | var response = SecretChecker.CheckUserSuppliedSecretValue(userSuppliedValue, secretValue);
35 |
36 | // Assert
37 | Assert.True(response);
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/dwContext.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using dwCheckApi.Entities;
5 | using Microsoft.EntityFrameworkCore;
6 |
7 | namespace dwCheckApi.Persistence
8 | {
9 | [ExcludeFromCodeCoverage]
10 | public class DwContext : DbContext, IDwContext
11 | {
12 | public DwContext(DbContextOptions options) : base(options) { }
13 | public DwContext() { }
14 |
15 | protected override void OnModelCreating(ModelBuilder modelBuilder)
16 | {
17 | modelBuilder.AddPrimaryKeys();
18 | }
19 |
20 | public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess,
21 | CancellationToken cancellationToken = default(CancellationToken))
22 | {
23 | ChangeTracker.ApplyAuditInformation();
24 |
25 | return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
26 | }
27 |
28 | public DbSet Books { get; set; }
29 | public DbSet Characters { get; set; }
30 | public DbSet Series {get; set;}
31 | public DbSet BookCharacters { get; set; }
32 | public DbSet BookSeries { get; set; }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Common/dwCheckApi.Common.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 |
5 |
6 |
7 |
8 | all
9 | runtime; build; native; contentfiles; analyzers; buildtransitive
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/dwCheckApi/Helpers/SecretChecker.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | namespace dwCheckApi.Helpers
4 | {
5 | public static class SecretChecker
6 | {
7 | public static bool CheckUserSuppliedSecretValue(string userSuppliedValue, string secretValue)
8 | {
9 | if (string.IsNullOrWhiteSpace(userSuppliedValue) || string.IsNullOrWhiteSpace(secretValue))
10 | {
11 | return false;
12 | }
13 |
14 | return FixedTimeStringComparison(userSuppliedValue, secretValue);
15 | }
16 |
17 | ///
18 | /// Provides constant time comparison of two strings and
19 | ///
20 | /// The input string
21 | /// The string to compare to
22 | /// True if the provided strings are equal, false otherwise
23 | /// Based on the code found at https://vcsjones.dev/fixed-time-equals-dotnet-core/
24 | [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
25 | private static bool FixedTimeStringComparison(string input, string expected)
26 | {
27 | var result = 0;
28 | for (var i = 0; i < input.Length; i++) {
29 | result |= input[i] ^ expected[i];
30 | }
31 | return result == 0;
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/IDwContext.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using dwCheckApi.Entities;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace dwCheckApi.Persistence
7 | {
8 | public interface IDwContext
9 | {
10 | ///
11 | /// Asynchronously saves all changes made in the DwContext to the database.
12 | ///
13 | ///
14 | /// Indicates whether is called after the changes have
15 | /// been sent successfully to the database.
16 | ///
17 | /// A to observe while waiting for the task to complete.
18 | ///
19 | /// A task that represents the asynchronous save operation. The task result contains the
20 | /// number of state entries written to the database.
21 | ///
22 | Task SaveChangesAsync(bool acceptAllChangesOnSuccess = true,
23 | CancellationToken cancellationToken = default(CancellationToken));
24 |
25 | DbSet Books { get; set; }
26 | DbSet Characters { get; set; }
27 | DbSet Series { get; set; }
28 | DbSet BookCharacters { get; set; }
29 | DbSet BookSeries { get; set; }
30 | }
31 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Tests/dwCheckApi.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Unit tests for dwCheckApi.
4 | 5.0.0.0
5 | Alpha
6 | Jamie Taylor
7 | dwCheckApi-Tests
8 | net6.0
9 | false
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 | PreserveNewest
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/DwContextExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Linq;
3 | using dwCheckApi.Persistence.Helpers;
4 |
5 | namespace dwCheckApi.Persistence
6 | {
7 | public static class DwContextExtensions
8 | {
9 | public static int EnsureSeedData(this DwContext context)
10 | {
11 | var bookCount = default(int);
12 | var characterCount = default(int);
13 | var bookSeriesCount = default(int);
14 |
15 | // Because each of the following seed method needs to do a save
16 | // (the data they're importing is relational), we need to call
17 | // SaveAsync within each method.
18 | // So let's keep tabs on the counts as they come back
19 |
20 | var dbSeeder = new DatabaseSeeder(context);
21 | if (!context.Books.Any())
22 | {
23 | var pathToSeedData = Path.Combine(Directory.GetCurrentDirectory(), "SeedData", "BookSeedData.json");
24 | bookCount = dbSeeder.SeedBookEntitiesFromJson(pathToSeedData).Result;
25 | }
26 | if (!context.BookCharacters.Any())
27 | {
28 | characterCount = dbSeeder.SeedBookCharacterEntriesFromJson().Result;
29 | }
30 | if (!context.BookSeries.Any())
31 | {
32 | bookSeriesCount = dbSeeder.SeedBookSeriesEntriesFromJson().Result;
33 | }
34 |
35 | return bookCount + characterCount + bookSeriesCount;
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/dwCheckApi.Persistence.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 |
5 |
6 |
7 |
8 | all
9 | runtime; build; native; contentfiles; analyzers; buildtransitive
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.github/workflows/weekly-cleanup.yml:
--------------------------------------------------------------------------------
1 | name: 'weekly artifacts cleanup'
2 |
3 | ## Rather than this action firing on a push or PR, we want this action to fire
4 | ## on a regular interval. For that, we use a cron-string. This action currently
5 | ## fires automatically at 1 am UTC every day (or as close to it as possible).
6 | ## Check https://crontab.guru for examples and a cron string builder
7 | on:
8 | schedule:
9 | - cron: '0 1 * * *'
10 |
11 | jobs:
12 |
13 | ## We only one job: clean up any build artifacts which are more than 7 days
14 | ## old.
15 | ## This is because GitHub only allows us to have a certain amount of storage
16 | ## space for build artifacts on free accounts, and it's always a good idea
17 | ## to clean up after yourself
18 | delete-artifacts:
19 |
20 | ## Each job can run on different OS images. Even though the repo has a hard
21 | ## requirement on Windows for it's build job, this one can be run on a Linux
22 | ## image - we're specifying Ubuntu vLatest here.
23 | runs-on: ubuntu-latest
24 |
25 | steps:
26 |
27 | ## We only want one step, and we want it to use the following action.
28 | - uses: kolpav/purge-artifacts-action@v1
29 | with:
30 | ## GitHub will create and insert a token here for us. This token will
31 | ## allow the delete-artifacts job to reach into the GitHub settings
32 | ## for us and delete any artifacts which were created more than 7 days
33 | ## ago.
34 | token: ${{ secrets.GITHUB_TOKEN }}
35 | expire-in: 7days # Setting this to 0 will delete all artifacts
--------------------------------------------------------------------------------
/src/dwCheckApi/dwCheckApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A .NET Core WebApi project, utilizing SqlLite and EF Core, for searching Discworld Books and Characters.
5 |
6 | 6.0.0.0
7 | Jamie Taylor
8 | net6.0
9 | dwCheckApi
10 | Exe
11 | dwCheckApi
12 | 8dd69dfc-8bd6-46b4-9bec-186b9044a48d
13 | true
14 | $(NoWarn);1591
15 | true
16 |
17 |
18 |
19 | PreserveNewest
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/tests/dwCheckApi.Common.Tests/dwCheckApi.Common.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 | false
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | true
37 | PreserveNewest
38 | PreserveNewest
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/tests/dwCheckApi.Tests/DatabaseSeederTests.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 | using System.IO;
3 | using System;
4 | using System.Threading.Tasks;
5 | using dwCheckApi.Persistence;
6 | using dwCheckApi.Persistence.Helpers;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.EntityFrameworkCore.Diagnostics;
9 |
10 | namespace dwCheckApi.Tests
11 | {
12 | public class DatabaseSeederTests
13 | {
14 | private readonly DbContextOptions _contextOptions;
15 | public DatabaseSeederTests()
16 | {
17 | _contextOptions = new DbContextOptionsBuilder()
18 | .UseInMemoryDatabase("dwCheckApi.Tests.InMemoryContext")
19 | .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
20 | .Options;
21 | }
22 | [Fact]
23 | public async void DbSeeder_SeedBookData_NoDataSupplied_ShouldThrowException()
24 | {
25 | // Arrange
26 | await using var context = new DwContext(_contextOptions);
27 |
28 | // Act & Assert
29 | var dbSeeder = new DatabaseSeeder(context);
30 | var argEx = await Assert.ThrowsAsync(() =>
31 | dbSeeder.SeedBookEntitiesFromJson(string.Empty));
32 | }
33 |
34 | [Fact]
35 | public async Task DbSeeder_SeedBookData_DataSupplied_ShouldNotThrowException()
36 | {
37 | // Arrange
38 | await using var context = new DwContext(_contextOptions);
39 |
40 | var testJsonDirectory = Path.Combine(Directory.GetCurrentDirectory(), "SeedData");
41 | var pathToSeedData = Path.Combine(testJsonDirectory, "TestBookSeedData.json");
42 | var dbSeeder = new DatabaseSeeder(context);
43 |
44 | // Act
45 | var entitiesAdded = await dbSeeder.SeedBookEntitiesFromJson(pathToSeedData);
46 |
47 | // Assert
48 | Assert.NotEqual(0, entitiesAdded);
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/SeriesService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using dwCheckApi.Entities;
4 | using dwCheckApi.Persistence;
5 | using Microsoft.EntityFrameworkCore;
6 |
7 | namespace dwCheckApi.DAL
8 | {
9 | public class SeriesService : ISeriesService
10 | {
11 | private DwContext _dwContext;
12 |
13 | public SeriesService (DwContext dwContext)
14 | {
15 | _dwContext = dwContext;
16 | }
17 |
18 | Series ISeriesService.GetById(int id)
19 | {
20 | return BaseQuery().FirstOrDefault(s => s.SeriesId == id);
21 | }
22 |
23 | Series ISeriesService.GetByName(string seriesName)
24 | {
25 | if(string.IsNullOrWhiteSpace(seriesName))
26 | {
27 | // TODO : what here?
28 | return null;
29 | }
30 |
31 | seriesName = seriesName.ToLower();
32 |
33 | return BaseQuery().FirstOrDefault(ch => ch.SeriesName.ToLower() == seriesName);
34 | }
35 |
36 | IEnumerable ISeriesService.Search(string searchKey)
37 | {
38 | var blankSearchString = string.IsNullOrEmpty(searchKey);
39 |
40 | var results = BaseQuery();
41 |
42 | if (!blankSearchString)
43 | {
44 | searchKey = searchKey.ToLower();
45 | results = BaseQuery()
46 | .Where(ch => ch.SeriesName.ToLower().Contains(searchKey));
47 | }
48 |
49 | return results.OrderBy(ch => ch.SeriesName);
50 | }
51 |
52 | private IEnumerable BaseQuery()
53 | {
54 | // Explicit joins of entities is taken from here:
55 | // https://weblogs.asp.net/jeff/ef7-rc-navigation-properties-and-lazy-loading
56 | // At the time of committing 5da65e093a64d7165178ef47d5c21e8eeb9ae1fc, Entity
57 | // Framework Core had no built in support for Lazy Loading, so the above was
58 | // used on all DbSet queries.
59 | return _dwContext.Series
60 | .AsNoTracking()
61 | .Include(bookSeries => bookSeries.BookSeries)
62 | .ThenInclude(book => book.Book);
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/tests/dwCheckApi.Tests/ViewModelMappers/SeriesViewModelMapperTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using dwCheckApi.DTO.Helpers;
5 | using dwCheckApi.Entities;
6 | using Xunit;
7 |
8 | namespace dwCheckApi.Tests.ViewModelMappers
9 | {
10 | public class SeriesViewModelMapperTests
11 | {
12 | [Fact]
13 | public void Given_SeriesDbModel_Returns_ViewModel()
14 | {
15 | // Arrange
16 | var dbBook = new Book
17 | {
18 | BookName = Guid.NewGuid().ToString()
19 | };
20 | var dbSeries = new Series
21 | {
22 | SeriesId = 1,
23 | SeriesName = Guid.NewGuid().ToString(),
24 | BookSeries = new List
25 | {
26 | new()
27 | {
28 | Book = dbBook
29 | }
30 | }
31 | };
32 |
33 | // Act
34 | var viewModel = SeriesViewModelHelpers.ConvertToViewModel(dbSeries);
35 |
36 | // Assert
37 | Assert.Equal(dbSeries.SeriesId, viewModel.SeriesId);
38 | Assert.Equal(dbSeries.SeriesName, viewModel.SeriesName);
39 | Assert.Equal(dbSeries.BookSeries.First().Book.BookName, viewModel.BookNames.First());
40 | }
41 |
42 | [Fact]
43 | public void Given_ListOfSeriesDbModel_Returns_ListOfViewModel()
44 | {
45 | // Arrange
46 | var dbSeries = new List
47 | {
48 | new()
49 | {
50 | SeriesId = 1,
51 | SeriesName = Guid.NewGuid().ToString(),
52 | BookSeries = new List()
53 | }
54 | };
55 |
56 | // Act
57 | var viewModels = SeriesViewModelHelpers.ConvertToViewModels(dbSeries);
58 |
59 | // Assert
60 | Assert.NotNull(viewModels);
61 | Assert.NotEmpty(viewModels);
62 | Assert.Equal(dbSeries.Count, viewModels.Count);
63 |
64 | for (var i = 0; i < viewModels.Count; i++)
65 | {
66 | Assert.Equal(dbSeries[i].SeriesId, viewModels[i].SeriesId);
67 | Assert.Equal(dbSeries[i].SeriesName, viewModels[i].SeriesName);
68 | }
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/SeedData/SeriesBookSeedData.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "SeriesName": "Rincewind",
4 | "BookNames": [
5 | "The Colour of Magic",
6 | "The Light Fantastic",
7 | "Sourcery",
8 | "Eric",
9 | "Interesting Times",
10 | "The Last Continent",
11 | "The Last Hero",
12 | "Unseen Academicals"
13 | ]
14 | },
15 | {
16 | "SeriesName": "Witches",
17 | "BookNames": [
18 | "Equal Rites",
19 | "Wyrd Sisters",
20 | "Witches Abroad",
21 | "Lords and Ladies",
22 | "Maskerade",
23 | "Carpe Jugulum",
24 | "The Wee Free Men",
25 | "A Hat Full of Sky",
26 | "Wintersmith",
27 | "I Shall Wear Midnight",
28 | "The Shepherd's Crown"
29 | ]
30 | },
31 | {
32 | "SeriesName": "Tiffany Aching",
33 | "BookNames": [
34 | "The Wee Free Men",
35 | "A Hat Full of Sky",
36 | "Wintersmith",
37 | "I Shall Wear Midnight",
38 | "The Shepherd's Crown"
39 | ]
40 | },
41 | {
42 | "SeriesName": "Death",
43 | "BookNames": [
44 | "Mort",
45 | "Soul Music",
46 | "Reaper Man",
47 | "Hogfather",
48 | "Thief of Time"
49 | ]
50 | },
51 | {
52 | "SeriesName": "Ancient Civilisations",
53 | "BookNames":[
54 | "Pyramids",
55 | "Small Gods"
56 | ]
57 | },
58 | {
59 | "SeriesName": "Watch",
60 | "BookNames":[
61 | "Guards! Guards!",
62 | "Men at Arms",
63 | "Feet of Clay",
64 | "Jingo",
65 | "The Fifth Elephant",
66 | "Night Watch",
67 | "Thud!",
68 | "Snuff"
69 | ]
70 | },
71 | {
72 | "SeriesName": "Industrial Revolution",
73 | "BookNames":[
74 | "Moving Pictures",
75 | "The Truth",
76 | "Monstrous Regiment",
77 | "Going Postal",
78 | "Making Money",
79 | "Raising Steam"
80 | ]
81 | },
82 | {
83 | "SeriesName": "Moist von Lipwig",
84 | "BookNames":[
85 | "Going Postal",
86 | "Making Money",
87 | "Raising Steam"
88 | ]
89 | }
90 | ]
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
2 | # Set the working directory witin the container
3 | WORKDIR /build
4 |
5 | # Copy the sln and csproj files. These are the only files
6 | # required in order to restore
7 | COPY ./dwCheckApi.Common/dwCheckApi.Common.csproj ./dwCheckApi.Common/dwCheckApi.Common.csproj
8 | COPY ./dwCheckApi.DAL/dwCheckApi.DAL.csproj ./dwCheckApi.DAL/dwCheckApi.DAL.csproj
9 | COPY ./dwCheckApi.DTO/dwCheckApi.DTO.csproj ./dwCheckApi.DTO/dwCheckApi.DTO.csproj
10 | COPY ./dwCheckApi.Entities/dwCheckApi.Entities.csproj ./dwCheckApi.Entities/dwCheckApi.Entities.csproj
11 | COPY ./dwCheckApi.Persistence/dwCheckApi.Persistence.csproj ./dwCheckApi.Persistence/dwCheckApi.Persistence.csproj
12 | COPY ./dwCheckApi.Tests/dwCheckApi.Tests.csproj ./dwCheckApi.Tests/dwCheckApi.Tests.csproj
13 | COPY ./dwCheckApi/dwCheckApi.csproj ./dwCheckApi/dwCheckApi.csproj
14 | COPY ./dwCheckApi.sln ./dwCheckApi.sln
15 | COPY ./global.json ./global.json
16 |
17 | # Restore all packages
18 | RUN dotnet restore --force --no-cache
19 |
20 | # Copy the remaining source
21 | COPY ./dwCheckApi.Common/ ./dwCheckApi.Common/
22 | COPY ./dwCheckApi.DAL/ ./dwCheckApi.DAL/
23 | COPY ./dwCheckApi.DTO/ ./dwCheckApi.DTO/
24 | COPY ./dwCheckApi.Entities/ ./dwCheckApi.Entities/
25 | COPY ./dwCheckApi.Persistence/ ./dwCheckApi.Persistence/
26 | COPY ./dwCheckApi.Tests/ ./dwCheckApi.Tests/
27 | COPY ./dwCheckApi/ ./dwCheckApi/
28 |
29 | # Build the source code
30 | RUN dotnet build --configuration Release --no-restore
31 |
32 | # Install the dotnet ef global tool
33 | ## The following was taken from https://itnext.io/database-development-in-docker-with-entity-framework-core-95772714626f
34 | RUN dotnet tool install -g dotnet-ef
35 | ENV PATH $PATH:/root/.dotnet/tools
36 |
37 | # Ensure that we generate and migrate the database
38 | WORKDIR ./dwCheckApi.Persistence
39 | RUN dotnet ef database update
40 |
41 | # # Run all tests
42 | WORKDIR ./dwCheckApi.Tests
43 | RUN dotnet test --configuration Release --no-build
44 |
45 | # Publish application
46 | WORKDIR ../dwCheckApi
47 | RUN dotnet publish dwCheckApi.csproj --configuration Release --no-restore --no-build --output "../dist"
48 |
49 | # Copy the created database
50 | WORKDIR ..
51 | RUN cp ./dwCheckApi/dwDatabase.db ./dist/dwDatabase.db
52 |
53 | # Build runtime image
54 | FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS app
55 | WORKDIR /app
56 | COPY --from=build /dist .
57 | ENV ASPNETCORE_URLS http://+:5000
58 |
59 | ENTRYPOINT ["dotnet", "dwCheckApi.dll"]
60 |
--------------------------------------------------------------------------------
/Code of Conduct.md:
--------------------------------------------------------------------------------
1 | This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available from [http://contributor-covenant.org/version/1/3/0/](http://contributor-covenant.org/version/1/3/0/)
2 |
3 | # Contributor Code of Conduct
4 |
5 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
6 |
7 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
8 |
9 | Examples of unacceptable behavior by participants include:
10 |
11 | - The use of sexualized language or imagery
12 | - Personal attacks
13 | -Trolling or insulting/derogatory comments
14 | -Public or private harassment
15 | -Publishing other's private information, such as physical or electronic addresses, without explicit permission
16 | - Other unethical or unprofessional conduct
17 | -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
18 |
19 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
20 |
21 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
22 |
23 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at jamiegaprogmancom. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.
24 |
25 | This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available from [http://contributor-covenant.org/version/1/3/0/](http://contributor-covenant.org/version/1/3/0/)
--------------------------------------------------------------------------------
/src/dwCheckApi/startup.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Linq;
3 | using ClacksMiddleware.Extensions;
4 | using dwCheckApi.Helpers;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.ResponseCompression;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using Microsoft.Extensions.Hosting;
11 | using Microsoft.Extensions.Logging;
12 |
13 | namespace dwCheckApi
14 | {
15 | [ExcludeFromCodeCoverage]
16 | public class Startup
17 | {
18 | public Startup(IConfiguration configuration)
19 | {
20 | Configuration = configuration;
21 | }
22 |
23 | public IConfiguration Configuration { get; }
24 |
25 | // This method gets called by the runtime. Use this method to add services to the container.
26 | public void ConfigureServices(IServiceCollection services)
27 | {
28 | services.AddResponseCaching();
29 | services.AddResponseCompression(options =>
30 | {
31 | options.Providers.Add();
32 | options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
33 | {
34 | "text/plain", "application/json"
35 | });
36 | });
37 |
38 | services.AddControllers();
39 | services.AddCorsPolicy();
40 | services.AddDbContext();
41 | services.AddTransientServices();
42 | services.AddSwagger($"v{CommonHelpers.GetVersionNumber()}");
43 | }
44 |
45 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
46 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
47 | {
48 | if (env.IsDevelopment())
49 | {
50 | app.UseDeveloperExceptionPage();
51 | app.EnsureDatabaseIsSeeded(false);
52 | }
53 |
54 | app.UseRouting();
55 |
56 | app.UseResponseCaching();
57 | app.UseResponseCompression();
58 | app.GnuTerryPratchett();
59 | app.UseCorsPolicy();
60 | app.UseStaticFiles();
61 |
62 | app.UseSwagger($"/swagger/v{CommonHelpers.GetVersionNumber()}/swagger.json",
63 | $"dwCheckApi {CommonHelpers.GetVersionNumber()}");
64 |
65 | app.UseEndpoints(endpoints =>
66 | {
67 | endpoints.MapControllers();
68 | });
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DTO/Helpers/BookViewModelHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using dwCheckApi.DTO.ViewModels;
5 | using dwCheckApi.Entities;
6 |
7 | namespace dwCheckApi.DTO.Helpers
8 | {
9 | public static class BookViewModelHelpers
10 | {
11 | public static BookViewModel ConvertToViewModel (Book dbModel)
12 | {
13 | var viewModel = new BookViewModel
14 | {
15 | BookId = dbModel.BookId,
16 | BookOrdinal = dbModel.BookOrdinal,
17 | BookName = dbModel.BookName,
18 | BookIsbn10 = dbModel.BookIsbn10,
19 | BookIsbn13 = dbModel.BookIsbn13,
20 | BookDescription = dbModel.BookDescription
21 | };
22 |
23 | foreach (var bc in dbModel.BookCharacter)
24 | {
25 | viewModel.Characters.Add(bc.Character.CharacterName ?? string.Empty);
26 | }
27 |
28 | foreach(var series in dbModel.BookSeries)
29 | {
30 | viewModel.Series.Add(series.SeriesId, series.Series.SeriesName ?? string.Empty);
31 | }
32 |
33 | return viewModel;
34 | }
35 |
36 | public static List ConvertToViewModels(List dbModel)
37 | {
38 | return dbModel.Select(ConvertToViewModel).ToList();
39 | }
40 |
41 | public static List ConvertToBaseViewModels(List dbModel)
42 | {
43 | return dbModel.Select(ConvertToBaseViewModel).ToList();
44 | }
45 |
46 | public static BookCoverViewModel ConvertToBookCoverViewModel(Book dbModel)
47 | {
48 | return new BookCoverViewModel
49 | {
50 | bookId = dbModel.BookId,
51 | BookCoverImage = GetBookImage(dbModel),
52 | BookImageIsBase64String = ContainsImageData(dbModel),
53 | };
54 | }
55 |
56 | private static BookBaseViewModel ConvertToBaseViewModel(Book dbModel)
57 | {
58 | var viewModel = new BookBaseViewModel
59 | {
60 | BookId = dbModel.BookId,
61 | BookOrdinal = dbModel.BookOrdinal,
62 | BookName = dbModel.BookName,
63 | BookDescription = dbModel.BookDescription
64 | };
65 |
66 |
67 | return viewModel;
68 | }
69 |
70 | private static bool ContainsImageData(Book dbModel)
71 | {
72 | return dbModel.BookCoverImage?.Length > 0;
73 | }
74 |
75 | private static string GetBookImage(Book dbModel)
76 | {
77 | return ContainsImageData(dbModel)
78 | ? Convert.ToBase64String(dbModel.BookCoverImage)
79 | : dbModel.BookCoverImageUrl;
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/CharacterService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using dwCheckApi.Entities;
4 | using dwCheckApi.Persistence;
5 | using Microsoft.EntityFrameworkCore;
6 |
7 | namespace dwCheckApi.DAL
8 | {
9 | public class CharacterService : ICharacterService
10 | {
11 | private readonly DwContext _dwContext;
12 |
13 | public CharacterService (DwContext dwContext)
14 | {
15 | _dwContext = dwContext;
16 | }
17 |
18 | public IEnumerable> Search(string searchKey)
19 | {
20 | var results = BaseQueryForCharacterNames();
21 |
22 | var blankSearchString = string.IsNullOrEmpty(searchKey);
23 | if (!blankSearchString)
24 | {
25 | searchKey = searchKey.ToLower();
26 | results = results
27 | .Where(bc => bc.Character.CharacterName.ToLower().Contains(searchKey));
28 | }
29 |
30 | return results.GroupBy(bc => bc.Character.CharacterName);
31 | }
32 |
33 | public Character GetById (int id)
34 | {
35 | return BaseQuery()
36 | .FirstOrDefault(character => character.CharacterId == id);
37 | }
38 |
39 | public Character GetByName(string characterName)
40 | {
41 | if(string.IsNullOrWhiteSpace(characterName))
42 | {
43 | // TODO : what here?
44 | return null;
45 | }
46 |
47 | characterName = characterName.ToLower();
48 |
49 | return BaseQuery().FirstOrDefault(ch => ch.CharacterName.ToLower() == characterName);
50 | }
51 |
52 | private IEnumerable BaseQuery()
53 | {
54 | // Explicit joins of entities is taken from here:
55 | // https://weblogs.asp.net/jeff/ef7-rc-navigation-properties-and-lazy-loading
56 | // At the time of committing 5da65e093a64d7165178ef47d5c21e8eeb9ae1fc, Entity
57 | // Framework Core had no built in support for Lazy Loading, so the above was
58 | // used on all DbSet queries.
59 | return _dwContext.Characters
60 | .AsNoTracking()
61 | .Include(character => character.BookCharacter)
62 | .ThenInclude(bookCharacter => bookCharacter.Book);
63 | }
64 |
65 | private IEnumerable BaseQueryForCharacterNames()
66 | {
67 | // Explicit joins of entities is taken from here:
68 | // https://weblogs.asp.net/jeff/ef7-rc-navigation-properties-and-lazy-loading
69 | // At the time of committing 5da65e093a64d7165178ef47d5c21e8eeb9ae1fc, Entity
70 | // Framework Core had no built in support for Lazy Loading, so the above was
71 | // used on all DbSet queries.
72 | return _dwContext.BookCharacters
73 | .Include(bc => bc.Character)
74 | .Include(bc => bc.Book)
75 | .AsNoTracking();
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.DAL/BookService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using dwCheckApi.Entities;
4 | using dwCheckApi.Persistence;
5 | using Microsoft.EntityFrameworkCore;
6 |
7 | namespace dwCheckApi.DAL
8 | {
9 | public class BookService : IBookService
10 | {
11 | private readonly DwContext _dwContext;
12 |
13 | public BookService (DwContext dwContext)
14 | {
15 | _dwContext = dwContext;
16 | }
17 |
18 | public Book FindById(int id)
19 | {
20 | return BaseQuery()
21 | .FirstOrDefault(book => book.BookId == id);
22 | }
23 |
24 | public Book FindByOrdinal (int id)
25 | {
26 | return BaseQuery()
27 | .FirstOrDefault(book => book.BookOrdinal == id);
28 | }
29 |
30 | public Book GetByName(string bookName)
31 | {
32 | if (string.IsNullOrEmpty(bookName))
33 | {
34 | // TODO: replace this if check with a Guard clause
35 | return null;
36 | }
37 |
38 | bookName = bookName.ToLower();
39 |
40 | return BaseQuery().FirstOrDefault(book => book.BookName.ToLower() == (bookName));
41 | }
42 |
43 | public IEnumerable GetAll()
44 | {
45 | return BaseQuery();
46 | }
47 |
48 | public IEnumerable Search(string searchKey)
49 | {
50 | var blankSearchString = string.IsNullOrWhiteSpace(searchKey);
51 |
52 | var results = BaseQuery();
53 |
54 | if (!blankSearchString)
55 | {
56 | searchKey = searchKey.ToLower();
57 | results = results
58 | .Where(book => book.BookName.ToLower().Contains(searchKey)
59 | || book.BookDescription.ToLower().Contains(searchKey)
60 | || book.BookIsbn10.ToLower().Contains(searchKey)
61 | || book.BookIsbn13.ToLower().Contains(searchKey));
62 | }
63 |
64 |
65 | return results.OrderBy(book => book.BookOrdinal);
66 | }
67 |
68 | public IEnumerable Series(int seriesId)
69 | {
70 | return BaseQuery()
71 | .Where(book => book.BookSeries.Select(series => series.SeriesId).Contains(seriesId))
72 | .OrderBy(book => book.BookOrdinal);
73 | }
74 |
75 | private IEnumerable BaseQuery()
76 | {
77 | // Explicit joins of entities is taken from here:
78 | // https://weblogs.asp.net/jeff/ef7-rc-navigation-properties-and-lazy-loading
79 | // At the time of committing 5da65e093a64d7165178ef47d5c21e8eeb9ae1fc, Entity
80 | // Framework Core had no built in support for Lazy Loading, so the above was
81 | // used on all DbSet queries.
82 | return _dwContext.Books
83 | .AsNoTracking()
84 | .Include(book => book.BookCharacter)
85 | .ThenInclude(bookCharacter => bookCharacter.Character)
86 | .Include(book => book.BookSeries)
87 | .ThenInclude(bookSeries => bookSeries.Series);
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/Helpers/CommonHelpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 |
4 | namespace dwCheckApi.Helpers
5 | {
6 | public static class CommonHelpers
7 | {
8 | /// Hard code this list for now, as .NET Core 1.0
9 | /// doesn't have reflection
10 | public static string IncorrectUsageOfApi()
11 | {
12 | var sb = new System.Text.StringBuilder();
13 | sb.Append($"Incorrect usage of API{Environment.NewLine}");
14 |
15 | sb.Append($"The following functions are available for Books:{Environment.NewLine}");
16 | sb.Append($"\t'/Books/GetByOrdinal' - Returns a single book, by it's ordinal (release order){Environment.NewLine}");
17 | sb.Append($"\t'/Books/GetByName - Returns a single book whose name matches the name passed in (?bookName=) {Environment.NewLine}");
18 | sb.Append($"\t'/Books/Search' - Searches all Books for a search string (?searchString=){Environment.NewLine}");
19 |
20 | sb.Append($"The following functions are available for Characters:{Environment.NewLine}");
21 | sb.Append($"\t'/Characters/Get' - Returns a single character by it's ID (set in the database){Environment.NewLine}");
22 | sb.Append($"\t'/Characters/GetByName' - Returns a single Character my their name (?characterName=), must match exactly{Environment.NewLine}");
23 | sb.Append($"\t'/Characters/Search' - Searches all Characters for a search string (?searchString=){Environment.NewLine}");
24 |
25 | sb.Append($"The following functions are available for Series:{Environment.NewLine}");
26 | sb.Append($"\t'/Series/Get' - Returns a single Series by it's ID (set in the database){Environment.NewLine}");
27 | sb.Append($"\t'/Series/GetByName' - Returns a single Series my it's name (?seriesName=), must match exactly{Environment.NewLine}");
28 | sb.Append($"\t'/Series/Search' - Searches all Series for a search string (?searchString=){Environment.NewLine}");
29 |
30 | sb.Append($"The following functions are available for the Database itself:{Environment.NewLine}");
31 | sb.Append($"\t'/Database/ApplyBookCoverArt' - Looks through the database for books without cover art and gets the Base64 string which represents the book cover art{Environment.NewLine}");
32 | sb.Append($"\t'/Database/DropData' - Useful for dropping all data from the database{Environment.NewLine}");
33 | sb.Append($"\t'/Database/SeedData - Useful for seeding all data (read from a series of JSON files){Environment.NewLine}");
34 |
35 | sb.Append($"The following functions are available for the application itself:{Environment.NewLine}");
36 | sb.Append($"\t'/Version - Returns the semver formatted version string for this application{Environment.NewLine}");
37 | sb.Append($"\t'/swagger - Returns Swagger formatted API documentation for the application{Environment.NewLine}");
38 |
39 | return sb.ToString();
40 | }
41 |
42 | public static string GetVersionNumber()
43 | {
44 | return Assembly.GetEntryAssembly()!
45 | .GetCustomAttribute()!
46 | .InformationalVersion;
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/ConfigureContainerExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 | using dwCheckApi.Common;
5 | using dwCheckApi.DAL;
6 | using dwCheckApi.Helpers;
7 | using dwCheckApi.Persistence;
8 | using Microsoft.EntityFrameworkCore;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using Microsoft.OpenApi.Models;
11 |
12 | namespace dwCheckApi
13 | {
14 | ///
15 | /// This class is based on some of the suggestions bty K. Scott Allen in
16 | /// his NDC 2017 talk https://www.youtube.com/watch?v=6Fi5dRVxOvc
17 | ///
18 | public static class ConfigureContainerExtensions
19 | {
20 | private static string DbConnectionString => new DatabaseConfiguration().GetDatabaseConnectionString();
21 | private static string CorsPolicyName => new CorsConfiguration().GetCorsPolicyName();
22 |
23 | public static void AddDbContext(this IServiceCollection serviceCollection,
24 | string connectionString = null)
25 | {
26 | serviceCollection.AddDbContext(options =>
27 | options.UseSqlite(connectionString ?? DbConnectionString));
28 | }
29 |
30 | public static void AddTransientServices(this IServiceCollection serviceCollection)
31 | {
32 | serviceCollection.AddTransient();
33 | serviceCollection.AddTransient();
34 | serviceCollection.AddTransient();
35 | serviceCollection.AddTransient();
36 | }
37 |
38 | public static void AddCorsPolicy(this IServiceCollection serviceCollection, string corsPolicyName = null)
39 | {
40 | serviceCollection.AddCors(options =>
41 | {
42 | options.AddPolicy(corsPolicyName ?? CorsPolicyName,
43 | builder =>
44 | builder.WithOrigins("localhost")
45 | .AllowAnyMethod()
46 | .AllowAnyHeader()
47 | .AllowCredentials());
48 | });
49 | }
50 |
51 | ///
52 | /// Used to register and add the Swagger generator to the service Collection
53 | ///
54 | ///
55 | /// The which is used in the Container
56 | ///
57 | /// The version number for the application
58 | ///
59 | /// Whether or not to include XmlDocumentation (defaults to True)
60 | ///
61 | ///
62 | /// includeXmlDocumentation requires:
63 | ///
64 | /// bin\Debug\net6.0\dwCheckApi.xml
65 | ///
66 | /// for debug builds and:
67 | ///
68 | /// bin\Release\net6.0\dwCheckApi.xml
69 | ///
70 | ///
71 | public static void AddSwagger(this IServiceCollection serviceCollection, string versionNumberString,
72 | bool includeXmlDocumentation = true)
73 | {
74 | // Register the Swagger generator, defining one or more Swagger documents
75 | serviceCollection.AddSwaggerGen(options =>
76 | {
77 | options.SwaggerDoc($"v{CommonHelpers.GetVersionNumber()}",
78 | new OpenApiInfo
79 | {
80 | Title = "dwCheckApi",
81 | Version = $"v{CommonHelpers.GetVersionNumber()}",
82 | Description = "A simple APi to get the details on Books, Characters and Series within a canon of novels",
83 | Contact = new OpenApiContact
84 | {
85 | Name = "Jamie Taylor",
86 | Email = "",
87 | Url = new Uri("https://dotnetcore.show")
88 | }
89 | }
90 | );
91 |
92 | if (!includeXmlDocumentation) return;
93 | // Set the comments path for the Swagger JSON and UI.
94 | var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
95 | options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
96 | });
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/Helpers/DatabaseSeeder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Newtonsoft.Json;
7 | using dwCheckApi.Entities;
8 |
9 | namespace dwCheckApi.Persistence.Helpers
10 | {
11 | public class DatabaseSeeder
12 | {
13 | private readonly IDwContext _context;
14 |
15 | public DatabaseSeeder(IDwContext context)
16 | {
17 | _context = context;
18 | }
19 |
20 | public async Task SeedBookEntitiesFromJson(string filePath)
21 | {
22 | if (string.IsNullOrWhiteSpace(filePath))
23 | {
24 | throw new ArgumentException($"Value of {filePath} must be supplied to {nameof(SeedBookEntitiesFromJson)}");
25 | }
26 | if (!File.Exists(filePath))
27 | {
28 | throw new ArgumentException($"The file { filePath} does not exist");
29 | }
30 | var dataSet = await File.ReadAllTextAsync(filePath);
31 | var seedData = JsonConvert.DeserializeObject>(dataSet);
32 |
33 | // ensure that we only get the distinct books (based on their name)
34 | var distinctSeedData = seedData.GroupBy(b => b.BookName).Select(b => b.First());
35 |
36 | _context.Books.AddRange(distinctSeedData);
37 | return await _context.SaveChangesAsync();
38 | }
39 |
40 | public async Task SeedBookCharacterEntriesFromJson()
41 | {
42 | var filePath = Path.Combine(Directory.GetCurrentDirectory(), "SeedData", "BookCharacterSeedData.json");
43 | if (File.Exists(filePath))
44 | {
45 | var dataSet = await File.ReadAllTextAsync(filePath);
46 | var seedData = JsonConvert.DeserializeObject>(dataSet);
47 |
48 | foreach(var seedBook in seedData)
49 | {
50 | var dbBook = _context.Books.Single(b => b.BookName == seedBook.BookName);
51 |
52 | foreach (var seedChar in seedBook.CharacterNames)
53 | {
54 | var dbChar = _context.Characters.FirstOrDefault(c => c.CharacterName == seedChar);
55 | if (dbChar == null)
56 | {
57 | dbChar = new Character{
58 | CharacterName = seedChar
59 | };
60 | }
61 | _context.BookCharacters.Add(new BookCharacter
62 | {
63 | Book = dbBook,
64 | Character = dbChar
65 | });
66 | }
67 | }
68 | return await _context.SaveChangesAsync();
69 | }
70 |
71 | return default(int);
72 | }
73 |
74 | public async Task SeedBookSeriesEntriesFromJson()
75 | {
76 | var filePath = Path.Combine(Directory.GetCurrentDirectory(), "SeedData", "SeriesBookSeedData.json");
77 | if (File.Exists(filePath))
78 | {
79 | var dataSet = await File.ReadAllTextAsync(filePath);
80 | var seedData = JsonConvert.DeserializeObject>(dataSet);
81 |
82 | var entitiesToAdd = new List();
83 | foreach (var seedSeries in seedData)
84 | {
85 | var dbSeries = _context.Series.FirstOrDefault(s => s.SeriesName == seedSeries.SeriesName);
86 | if (dbSeries == null)
87 | {
88 | dbSeries = new Series
89 | {
90 | SeriesName = seedSeries.SeriesName
91 | };
92 | }
93 |
94 | for(var ordinal = 0; ordinal < seedSeries.BookNames.Count; ordinal++)
95 | {
96 | var dbBook = _context.Books.Single(b => b.BookName == seedSeries.BookNames[ordinal]);
97 | entitiesToAdd.Add(new BookSeries
98 | {
99 | Series = dbSeries,
100 | Book = dbBook,
101 | Ordinal = ordinal
102 | });
103 | }
104 | }
105 |
106 | _context.BookSeries.AddRange(entitiesToAdd);
107 | return await _context.SaveChangesAsync();
108 | }
109 | return default(int);
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/Controllers/SeriesController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using System.Linq;
3 | using dwCheckApi.DAL;
4 | using dwCheckApi.DTO.Helpers;
5 | using dwCheckApi.DTO.ViewModels;
6 | using Microsoft.AspNetCore.Http;
7 |
8 | namespace dwCheckApi.Controllers
9 | {
10 | [Route("/[controller]")]
11 | [Produces("application/json")]
12 | public class SeriesController : BaseController
13 | {
14 | private readonly ISeriesService _seriesService;
15 |
16 | public SeriesController(ISeriesService seriesService)
17 | {
18 | _seriesService = seriesService;
19 | }
20 |
21 | ///
22 | /// Used to get a Series record by its ID
23 | ///
24 | /// The ID of the Series Record
25 | ///
26 | /// If a Series record can be found, then a
27 | /// is returned, which contains a .
28 | /// If no record can be found, then an is returned
29 | ///
30 | [HttpGet("Get/{id}")]
31 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
32 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
33 | public IActionResult GetById(int id)
34 | {
35 | var dbSeries = _seriesService.GetById(id);
36 | if (dbSeries == null)
37 | {
38 | return NotFoundResponse("Not found");
39 | }
40 |
41 | return Ok(new SingleResult
42 | {
43 | Success = true,
44 | Result = SeriesViewModelHelpers.ConvertToViewModel(dbSeries)
45 | });
46 | }
47 |
48 | ///
49 | /// Used to get a Series record by its name
50 | ///
51 | /// The name of the Series record to return
52 | ///
53 | /// If a Series record can be found, then a
54 | /// is returned, which contains a .
55 | /// If no record can be found, then an is returned
56 | ///
57 | [HttpGet("GetByName")]
58 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
59 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
60 | public IActionResult GetByName(string seriesName)
61 | {
62 | if (string.IsNullOrWhiteSpace(seriesName))
63 | {
64 | return NotFoundResponse("Series name is required");
65 | }
66 |
67 | var series = _seriesService.GetByName(seriesName);
68 |
69 | if (series == null)
70 | {
71 | return NotFoundResponse("No Series found");
72 | }
73 |
74 | return Ok(new SingleResult
75 | {
76 | Success = true,
77 | Result = SeriesViewModelHelpers.ConvertToViewModel(series)
78 | });
79 | }
80 |
81 | ///
82 | /// Used to search Series records by their name
83 | ///
84 | /// The string to use when searching for Series
85 | ///
86 | /// If a Series records can be found, then a
87 | /// is returned, which contains a collection of .
88 | /// If no record can be found, then an is returned
89 | ///
90 | [HttpGet("Search")]
91 | [ProducesResponseType(typeof(MultipleResult), StatusCodes.Status200OK)]
92 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
93 | public IActionResult Search(string searchString)
94 | {
95 | var series = _seriesService
96 | .Search(searchString).ToList();
97 |
98 | if (!series.Any())
99 | {
100 | return NotFoundResponse($"No series found for supplied search string: {searchString}");
101 | }
102 |
103 | return Ok(new MultipleResult
104 | {
105 | Success = true,
106 | Result = SeriesViewModelHelpers.ConvertToViewModels(series.ToList())
107 | });
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/Migrations/DwContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 |
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using System;
6 | using System.Diagnostics.CodeAnalysis;
7 |
8 | namespace dwCheckApi.Persistence.Migrations
9 | {
10 | [ExcludeFromCodeCoverage]
11 | [DbContext(typeof(DwContext))]
12 | partial class DwContextModelSnapshot : ModelSnapshot
13 | {
14 | protected override void BuildModel(ModelBuilder modelBuilder)
15 | {
16 | #pragma warning disable 612, 618
17 | modelBuilder
18 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
19 |
20 | modelBuilder.Entity("dwCheckApi.Entities.Book", b =>
21 | {
22 | b.Property("BookId")
23 | .ValueGeneratedOnAdd();
24 |
25 | b.Property("BookCoverImage");
26 |
27 | b.Property("BookCoverImageUrl");
28 |
29 | b.Property("BookDescription");
30 |
31 | b.Property("BookIsbn10");
32 |
33 | b.Property("BookIsbn13");
34 |
35 | b.Property("BookName");
36 |
37 | b.Property("BookOrdinal");
38 |
39 | b.Property("Created");
40 |
41 | b.Property("Modified");
42 |
43 | b.HasKey("BookId");
44 |
45 | b.ToTable("Books");
46 | });
47 |
48 | modelBuilder.Entity("dwCheckApi.Entities.BookCharacter", b =>
49 | {
50 | b.Property("BookId");
51 |
52 | b.Property("CharacterId");
53 |
54 | b.Property("Created");
55 |
56 | b.Property("Modified");
57 |
58 | b.HasKey("BookId", "CharacterId");
59 |
60 | b.HasIndex("CharacterId");
61 |
62 | b.ToTable("BookCharacters");
63 | });
64 |
65 | modelBuilder.Entity("dwCheckApi.Entities.BookSeries", b =>
66 | {
67 | b.Property("BookId");
68 |
69 | b.Property("SeriesId");
70 |
71 | b.Property("Created");
72 |
73 | b.Property("Modified");
74 |
75 | b.Property("Ordinal");
76 |
77 | b.HasKey("BookId", "SeriesId");
78 |
79 | b.HasIndex("SeriesId");
80 |
81 | b.ToTable("BookSeries");
82 | });
83 |
84 | modelBuilder.Entity("dwCheckApi.Entities.Character", b =>
85 | {
86 | b.Property("CharacterId")
87 | .ValueGeneratedOnAdd();
88 |
89 | b.Property("CharacterName");
90 |
91 | b.Property("Created");
92 |
93 | b.Property("Modified");
94 |
95 | b.HasKey("CharacterId");
96 |
97 | b.ToTable("Characters");
98 | });
99 |
100 | modelBuilder.Entity("dwCheckApi.Entities.Series", b =>
101 | {
102 | b.Property("SeriesId")
103 | .ValueGeneratedOnAdd();
104 |
105 | b.Property("Created");
106 |
107 | b.Property("Modified");
108 |
109 | b.Property("SeriesName");
110 |
111 | b.HasKey("SeriesId");
112 |
113 | b.ToTable("Series");
114 | });
115 |
116 | modelBuilder.Entity("dwCheckApi.Entities.BookCharacter", b =>
117 | {
118 | b.HasOne("dwCheckApi.Entities.Book", "Book")
119 | .WithMany("BookCharacter")
120 | .HasForeignKey("BookId")
121 | .OnDelete(DeleteBehavior.Cascade);
122 |
123 | b.HasOne("dwCheckApi.Entities.Character", "Character")
124 | .WithMany("BookCharacter")
125 | .HasForeignKey("CharacterId")
126 | .OnDelete(DeleteBehavior.Cascade);
127 | });
128 |
129 | modelBuilder.Entity("dwCheckApi.Entities.BookSeries", b =>
130 | {
131 | b.HasOne("dwCheckApi.Entities.Book", "Book")
132 | .WithMany("BookSeries")
133 | .HasForeignKey("BookId")
134 | .OnDelete(DeleteBehavior.Cascade);
135 |
136 | b.HasOne("dwCheckApi.Entities.Series", "Series")
137 | .WithMany("BookSeries")
138 | .HasForeignKey("SeriesId")
139 | .OnDelete(DeleteBehavior.Cascade);
140 | });
141 | #pragma warning restore 612, 618
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/dwCheckApi/Controllers/CharactersController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using System.Linq;
3 | using dwCheckApi.DAL;
4 | using dwCheckApi.DTO.Helpers;
5 | using dwCheckApi.DTO.ViewModels;
6 | using Microsoft.AspNetCore.Http;
7 |
8 | namespace dwCheckApi.Controllers
9 | {
10 | [Route("/[controller]")]
11 | [Produces("application/json")]
12 | public class CharactersController : BaseController
13 | {
14 | private readonly ICharacterService _characterService;
15 |
16 | public CharactersController(ICharacterService characterService)
17 | {
18 | _characterService = characterService;
19 | }
20 |
21 | ///
22 | /// Used to get a Character record by its ID
23 | ///
24 | /// The ID fo the Character record to return
25 | ///
26 | /// If a Character record can be found, then a
27 | /// is returned, which contains a .
28 | /// If no record can be found, then an is returned
29 | ///
30 | [HttpGet("Get/{id}")]
31 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
32 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
33 | public IActionResult GetById(int id)
34 | {
35 | var dbCharacter = _characterService.GetById(id);
36 | if (dbCharacter == null)
37 | {
38 | return NotFoundResponse("Character not found");
39 | }
40 |
41 | return Ok(new SingleResult
42 | {
43 | Success = true,
44 | Result = CharacterViewModelHelpers.ConvertToViewModel(dbCharacter.CharacterName)
45 | });
46 | }
47 |
48 | ///
49 | /// Used to get a Character record by its name
50 | ///
51 | /// The name of the Character record to return
52 | ///
53 | /// If a Character record can be found, then a
54 | /// is returned, which contains a .
55 | /// If no record can be found, then an is returned
56 | ///
57 | [HttpGet("GetByName")]
58 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
59 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
60 | public IActionResult GetByName(string characterName)
61 | {
62 | if (string.IsNullOrWhiteSpace(characterName))
63 | {
64 | return NotFoundResponse("Character name is required");
65 | }
66 |
67 | var character = _characterService.GetByName(characterName);
68 |
69 | if (character == null)
70 | {
71 | return NotFoundResponse("Character not found");
72 | }
73 |
74 | return Ok(new SingleResult
75 | {
76 | Success = true,
77 | Result = CharacterViewModelHelpers.ConvertToViewModel(character.CharacterName)
78 | });
79 | }
80 |
81 | ///
82 | /// Used to search Character records by their name
83 | ///
84 | /// The string to use when searching for Character records
85 | ///
86 | /// If a Character records can be found, then a
87 | /// is returned, which contains a collection of .
88 | /// If no record can be found, then an is returned
89 | ///
90 | [HttpGet("Search")]
91 | [ProducesResponseType(typeof(MultipleResult), StatusCodes.Status200OK)]
92 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
93 | public IActionResult Search(string searchString)
94 | {
95 | var foundCharacters = _characterService
96 | .Search(searchString).ToList();
97 | if (!foundCharacters.Any())
98 | {
99 | return NotFoundResponse("No Characters found");
100 | }
101 |
102 | var flattenedCharacters = foundCharacters
103 | .Select(character => CharacterViewModelHelpers
104 | .ConvertToViewModel(character.Key,
105 | character.ToDictionary(bc => bc.Book.BookOrdinal, bc => bc.Book.BookName)));
106 |
107 | return Ok(new MultipleResult
108 | {
109 | Success = true,
110 | Result = flattenedCharacters.ToList()
111 | });
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/ConfigureHttpPipelineExtension.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using dwCheckApi.Common;
3 | using dwCheckApi.Persistence;
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using OwaspHeaders.Core;
8 | using OwaspHeaders.Core.Enums;
9 | using OwaspHeaders.Core.Extensions;
10 | using OwaspHeaders.Core.Models;
11 |
12 | namespace dwCheckApi
13 | {
14 | ///
15 | /// This class is based on some of the suggestions bty K. Scott Allen in
16 | /// his NDC 2017 talk https://www.youtube.com/watch?v=6Fi5dRVxOvc
17 | ///
18 | public static class ConfigureHttpPipelineExtension
19 | {
20 | private static string CorsPolicyName => new CorsConfiguration().GetCorsPolicyName();
21 |
22 | public static void UseCorsPolicy(this IApplicationBuilder applicationBuilder, string corsPolicyName = null)
23 | {
24 | applicationBuilder.UseCors(corsPolicyName ?? CorsPolicyName);
25 | }
26 |
27 | public static int EnsureDatabaseIsSeeded(this IApplicationBuilder applicationBuilder,
28 | bool autoMigrateDatabase)
29 | {
30 | // seed the database using an extension method
31 | using var serviceScope = applicationBuilder.ApplicationServices.GetRequiredService()
32 | .CreateScope();
33 | var context = serviceScope.ServiceProvider.GetService();
34 | if (autoMigrateDatabase)
35 | {
36 | context.Database.Migrate();
37 | }
38 | return context.EnsureSeedData();
39 | }
40 |
41 | ///
42 | /// Used to tell the to use Swagger and the Swagger UI
43 | ///
44 | ///
45 | /// The which is used in the Http Pipeline
46 | ///
47 | /// The URL for the Swagger endpoint
48 | /// The description for the Swagger endpoint
49 | public static void UseSwagger(this IApplicationBuilder applicationBuilder,
50 | string swaggerUrl, string swaggerDescription)
51 | {
52 | // Enable middleware to serve generated Swagger as a JSON endpoint.
53 | applicationBuilder.UseSwagger();
54 |
55 | // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying
56 | // the Swagger JSON endpoint.
57 | applicationBuilder.UseSwaggerUI(c =>
58 | {
59 | c.SwaggerEndpoint(swaggerUrl, swaggerDescription);
60 | });
61 | }
62 |
63 | ///
64 | /// Used to include the middleware and set up the
65 | /// for it.
66 | ///
67 | ///
68 | /// The which is used in the Http Pipeline
69 | ///
70 | ///
71 | /// OPTIONAL: Used to enable/disable blocking and upgrading odf all requests
72 | /// when not in development
73 | ///
74 | public static void UseSecureHeaders(this IApplicationBuilder applicationBuilder, bool blockAndUpgradeInsecure = true)
75 | {
76 | var config = SecureHeadersMiddlewareBuilder
77 | .CreateBuilder()
78 | .UseHsts()
79 | .UseXFrameOptions()
80 | .UseXSSProtection()
81 | .UseContentTypeOptions()
82 | .UseContentSecurityPolicy(blockAllMixedContent:blockAndUpgradeInsecure, upgradeInsecureRequests: blockAndUpgradeInsecure)
83 | .UsePermittedCrossDomainPolicies()
84 | .UseReferrerPolicy()
85 | .Build();
86 |
87 | config.ContentSecurityPolicyConfiguration.ScriptSrc = new List()
88 | {
89 | new ContentSecurityPolicyElement
90 | {
91 | CommandType = CspCommandType.Directive,
92 | DirectiveOrUri = "self"
93 | },
94 | new ContentSecurityPolicyElement
95 | {
96 | CommandType = CspCommandType.Directive,
97 | DirectiveOrUri = "sha256-gw/4FeYphgTzu5mo/iOEEHUjrRJsQ/F6lgqdtSc23GU="
98 | },
99 | new ContentSecurityPolicyElement
100 | {
101 | CommandType = CspCommandType.Directive,
102 | DirectiveOrUri = "sha256-7I8kfi1IZHgnTNHryKWWH/oZV9dIkctQ77ABbgrpy6w="
103 | },
104 | new ContentSecurityPolicyElement
105 | {
106 | CommandType = CspCommandType.Directive,
107 | DirectiveOrUri = "sha256-3kf2chgLlsbYoTHVrm7JlIF6/529E3h6TGATiBxN4kU="
108 | }
109 | };
110 |
111 | applicationBuilder.UseSecureHeadersMiddleware(config);
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/src/dwCheckApi/Controllers/DatabaseController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 | using dwCheckApi.DAL;
6 | using dwCheckApi.Helpers;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 |
11 | namespace dwCheckApi.Controllers
12 | {
13 | [Route("/[controller]")]
14 | [Produces("application/json")]
15 | public class DatabaseController : BaseController
16 | {
17 | private readonly IConfiguration _configuration;
18 | private readonly IDatabaseService _databaseService;
19 |
20 | public DatabaseController(IConfiguration configuration, IDatabaseService databaseService)
21 | {
22 | _configuration = configuration;
23 | _databaseService = databaseService;
24 | }
25 |
26 | ///
27 | /// Used to Seed the Database (using JSON files which are included with the application)
28 | ///
29 | ///
30 | /// A with either the number of entities which
31 | /// were added to the database, or an exception message.
32 | ///
33 | [HttpGet("SeedData")]
34 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
35 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
36 | public IActionResult SeedData()
37 | {
38 | try
39 | {
40 | var entitiesAdded = _databaseService.SeedDatabase();
41 | return Ok(new SingleResult
42 | {
43 | Success = true,
44 | Result = $"Number of new entities added: {entitiesAdded}"
45 | });
46 | }
47 | catch (Exception ex)
48 | {
49 | return ErrorResponse(StatusCodes.Status500InternalServerError, ex.Message);
50 | }
51 | }
52 |
53 | ///
54 | /// Used to drop all current data from the database and recreate any tables
55 | ///
56 | ///
57 | /// A passphrase like secret to ensure that a Drop Data action should take place
58 | ///
59 | ///
60 | /// A indicating whether we could clear
61 | /// the database or not at this time
62 | ///
63 | [HttpDelete("DropData")]
64 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
65 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
66 | public IActionResult DropData(string secret = null)
67 | {
68 | if (!SecretChecker.CheckUserSuppliedSecretValue(secret,
69 | _configuration["dropDatabaseSecretValue"]))
70 | {
71 | return ErrorResponse(StatusCodes.Status401Unauthorized,"Incorrect secret");
72 | }
73 |
74 | return _databaseService.ClearDatabase()
75 | ? Ok(new SingleResult
76 | {
77 | Success = true,
78 | Result = "Database tabled dropped and recreated"
79 | })
80 | : ErrorResponse(StatusCodes.Status500InternalServerError, "Unable to clear database at this time");
81 | }
82 |
83 | ///
84 | /// Used to prepare and apply all Book cover art (as Base64 strings)
85 | ///
86 | ///
87 | /// A with the number of entities which were altered.
88 | ///
89 | [HttpGet("ApplyBookCoverArt")]
90 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
91 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
92 | public async Task ApplyBookCoverArt()
93 | {
94 | var relevantBooks = _databaseService.BooksWithoutCoverBytes().ToList();
95 |
96 | if (!relevantBooks.Any())
97 | {
98 | return Ok(new SingleResult()
99 | {
100 | Success = true,
101 | Result = "No records to update"
102 | });
103 | }
104 |
105 | try
106 | {
107 | using var client = new HttpClient();
108 | foreach (var book in relevantBooks)
109 | {
110 | var coverData = await client.GetByteArrayAsync(book.BookCoverImageUrl);
111 | book.BookCoverImage = coverData;
112 | }
113 | }
114 | catch (Exception ex)
115 | {
116 | return ErrorResponse(StatusCodes.Status500InternalServerError, ex.Message);
117 | }
118 |
119 | var updatedRecordCount = await _databaseService.SaveAnyChanges();
120 |
121 | return Ok(new SingleResult
122 | {
123 | Success = true,
124 | Result =$"{updatedRecordCount} entities updated"
125 | });
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/Migrations/20170826014619_InitialMigration.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using dwCheckApi.Persistence;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage;
8 | using Microsoft.EntityFrameworkCore.Storage.Internal;
9 | using System;
10 | using System.Diagnostics.CodeAnalysis;
11 |
12 | namespace dwCheckApi.Persistence.Migrations
13 | {
14 | [DbContext(typeof(DwContext))]
15 | [Migration("20170826014619_InitialMigration")]
16 | partial class InitialMigration
17 | {
18 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
19 | {
20 | #pragma warning disable 612, 618
21 | modelBuilder
22 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
23 |
24 | modelBuilder.Entity("dwCheckApi.Entities.Book", b =>
25 | {
26 | b.Property("BookId")
27 | .ValueGeneratedOnAdd();
28 |
29 | b.Property("BookCoverImage");
30 |
31 | b.Property("BookCoverImageUrl");
32 |
33 | b.Property("BookDescription");
34 |
35 | b.Property("BookIsbn10");
36 |
37 | b.Property("BookIsbn13");
38 |
39 | b.Property("BookName");
40 |
41 | b.Property("BookOrdinal");
42 |
43 | b.Property("Created");
44 |
45 | b.Property("Modified");
46 |
47 | b.HasKey("BookId");
48 |
49 | b.ToTable("Books");
50 | });
51 |
52 | modelBuilder.Entity("dwCheckApi.Entities.BookCharacter", b =>
53 | {
54 | b.Property("BookId");
55 |
56 | b.Property("CharacterId");
57 |
58 | b.Property("Created");
59 |
60 | b.Property("Modified");
61 |
62 | b.HasKey("BookId", "CharacterId");
63 |
64 | b.HasIndex("CharacterId");
65 |
66 | b.ToTable("BookCharacters");
67 | });
68 |
69 | modelBuilder.Entity("dwCheckApi.Entities.BookSeries", b =>
70 | {
71 | b.Property("BookId");
72 |
73 | b.Property("SeriesId");
74 |
75 | b.Property("Created");
76 |
77 | b.Property("Modified");
78 |
79 | b.Property("Ordinal");
80 |
81 | b.HasKey("BookId", "SeriesId");
82 |
83 | b.HasIndex("SeriesId");
84 |
85 | b.ToTable("BookSeries");
86 | });
87 |
88 | modelBuilder.Entity("dwCheckApi.Entities.Character", b =>
89 | {
90 | b.Property("CharacterId")
91 | .ValueGeneratedOnAdd();
92 |
93 | b.Property("CharacterName");
94 |
95 | b.Property("Created");
96 |
97 | b.Property("Modified");
98 |
99 | b.HasKey("CharacterId");
100 |
101 | b.ToTable("Characters");
102 | });
103 |
104 | modelBuilder.Entity("dwCheckApi.Entities.Series", b =>
105 | {
106 | b.Property("SeriesId")
107 | .ValueGeneratedOnAdd();
108 |
109 | b.Property("Created");
110 |
111 | b.Property("Modified");
112 |
113 | b.Property("SeriesName");
114 |
115 | b.HasKey("SeriesId");
116 |
117 | b.ToTable("Series");
118 | });
119 |
120 | modelBuilder.Entity("dwCheckApi.Entities.BookCharacter", b =>
121 | {
122 | b.HasOne("dwCheckApi.Entities.Book", "Book")
123 | .WithMany("BookCharacter")
124 | .HasForeignKey("BookId")
125 | .OnDelete(DeleteBehavior.Cascade);
126 |
127 | b.HasOne("dwCheckApi.Entities.Character", "Character")
128 | .WithMany("BookCharacter")
129 | .HasForeignKey("CharacterId")
130 | .OnDelete(DeleteBehavior.Cascade);
131 | });
132 |
133 | modelBuilder.Entity("dwCheckApi.Entities.BookSeries", b =>
134 | {
135 | b.HasOne("dwCheckApi.Entities.Book", "Book")
136 | .WithMany("BookSeries")
137 | .HasForeignKey("BookId")
138 | .OnDelete(DeleteBehavior.Cascade);
139 |
140 | b.HasOne("dwCheckApi.Entities.Series", "Series")
141 | .WithMany("BookSeries")
142 | .HasForeignKey("SeriesId")
143 | .OnDelete(DeleteBehavior.Cascade);
144 | });
145 | #pragma warning restore 612, 618
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 | .vscode/
25 |
26 | # Visual Studio 2015 cache/options directory
27 | .vs/
28 | # Uncomment if you have tasks that create the project's static files in wwwroot
29 | #wwwroot/
30 |
31 | # MSTest test Results
32 | [Tt]est[Rr]esult*/
33 | [Bb]uild[Ll]og.*
34 |
35 | # NUNIT
36 | *.VisualState.xml
37 | TestResult.xml
38 |
39 | # Build Results of an ATL Project
40 | [Dd]ebugPS/
41 | [Rr]eleasePS/
42 | dlldata.c
43 |
44 | # DNX
45 | project.lock.json
46 | artifacts/
47 |
48 | *_i.c
49 | *_p.c
50 | *_i.h
51 | *.ilk
52 | *.meta
53 | *.obj
54 | *.pch
55 | *.pdb
56 | *.pgc
57 | *.pgd
58 | *.rsp
59 | *.sbr
60 | *.tlb
61 | *.tli
62 | *.tlh
63 | *.tmp
64 | *.tmp_proj
65 | *.log
66 | *.vspscc
67 | *.vssscc
68 | .builds
69 | *.pidb
70 | *.svclog
71 | *.scc
72 |
73 | # Chutzpah Test files
74 | _Chutzpah*
75 |
76 | # Visual C++ cache files
77 | ipch/
78 | *.aps
79 | *.ncb
80 | *.opendb
81 | *.opensdf
82 | *.sdf
83 | *.cachefile
84 | *.VC.db
85 | *.VC.VC.opendb
86 |
87 | # Visual Studio profiler
88 | *.psess
89 | *.vsp
90 | *.vspx
91 | *.sap
92 |
93 | # TFS 2012 Local Workspace
94 | $tf/
95 |
96 | # Guidance Automation Toolkit
97 | *.gpState
98 |
99 | # ReSharper is a .NET coding add-in
100 | _ReSharper*/
101 | *.[Rr]e[Ss]harper
102 | *.DotSettings.user
103 |
104 | # JustCode is a .NET coding add-in
105 | .JustCode
106 |
107 | # TeamCity is a build add-in
108 | _TeamCity*
109 |
110 | # DotCover is a Code Coverage Tool
111 | *.dotCover
112 |
113 | # NCrunch
114 | _NCrunch_*
115 | .*crunch*.local.xml
116 | nCrunchTemp_*
117 |
118 | # MightyMoose
119 | *.mm.*
120 | AutoTest.Net/
121 |
122 | # Web workbench (sass)
123 | .sass-cache/
124 |
125 | # Installshield output folder
126 | [Ee]xpress/
127 |
128 | # DocProject is a documentation generator add-in
129 | DocProject/buildhelp/
130 | DocProject/Help/*.HxT
131 | DocProject/Help/*.HxC
132 | DocProject/Help/*.hhc
133 | DocProject/Help/*.hhk
134 | DocProject/Help/*.hhp
135 | DocProject/Help/Html2
136 | DocProject/Help/html
137 |
138 | # Click-Once directory
139 | publish/
140 |
141 | # Publish Web Output
142 | *.[Pp]ublish.xml
143 | *.azurePubxml
144 | # TODO: Comment the next line if you want to checkin your web deploy settings
145 | # but database connection strings (with potential passwords) will be unencrypted
146 | *.pubxml
147 | *.publishproj
148 |
149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
150 | # checkin your Azure Web App publish settings, but sensitive information contained
151 | # in these scripts will be unencrypted
152 | PublishScripts/
153 |
154 | # NuGet Packages
155 | *.nupkg
156 | # The packages folder can be ignored because of Package Restore
157 | **/packages/*
158 | # except build/, which is used as an MSBuild target.
159 | !**/packages/build/
160 | # Uncomment if necessary however generally it will be regenerated when needed
161 | #!**/packages/repositories.config
162 | # NuGet v3's project.json files produces more ignoreable files
163 | *.nuget.props
164 | *.nuget.targets
165 |
166 | # Microsoft Azure Build Output
167 | csx/
168 | *.build.csdef
169 |
170 | # Microsoft Azure Emulator
171 | ecf/
172 | rcf/
173 |
174 | # Windows Store app package directories and files
175 | AppPackages/
176 | BundleArtifacts/
177 | Package.StoreAssociation.xml
178 | _pkginfo.txt
179 |
180 | # Visual Studio cache files
181 | # files ending in .cache can be ignored
182 | *.[Cc]ache
183 | # but keep track of directories ending in .cache
184 | !*.[Cc]ache/
185 |
186 | # Others
187 | ClientBin/
188 | ~$*
189 | *~
190 | *.dbmdl
191 | *.dbproj.schemaview
192 | *.pfx
193 | *.publishsettings
194 | node_modules/
195 | orleans.codegen.cs
196 |
197 | # Since there are multiple workflows, uncomment next line to ignore bower_components
198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
199 | #bower_components/
200 |
201 | # RIA/Silverlight projects
202 | Generated_Code/
203 |
204 | # Backup & report files from converting an old project file
205 | # to a newer Visual Studio version. Backup files are not needed,
206 | # because we have git ;-)
207 | _UpgradeReport_Files/
208 | Backup*/
209 | UpgradeLog*.XML
210 | UpgradeLog*.htm
211 |
212 | # SQL Server files
213 | *.mdf
214 | *.ldf
215 |
216 | # Business Intelligence projects
217 | *.rdl.data
218 | *.bim.layout
219 | *.bim_*.settings
220 |
221 | # Microsoft Fakes
222 | FakesAssemblies/
223 |
224 | # GhostDoc plugin setting file
225 | *.GhostDoc.xml
226 |
227 | # Node.js Tools for Visual Studio
228 | .ntvs_analysis.dat
229 |
230 | # Visual Studio 6 build log
231 | *.plg
232 |
233 | # Visual Studio 6 workspace options file
234 | *.opt
235 |
236 | # Visual Studio LightSwitch build output
237 | **/*.HTMLClient/GeneratedArtifacts
238 | **/*.DesktopClient/GeneratedArtifacts
239 | **/*.DesktopClient/ModelManifest.xml
240 | **/*.Server/GeneratedArtifacts
241 | **/*.Server/ModelManifest.xml
242 | _Pvt_Extensions
243 |
244 | # Paket dependency manager
245 | .paket/paket.exe
246 | paket-files/
247 |
248 | # FAKE - F# Make
249 | .fake/
250 |
251 | # JetBrains Rider
252 | .idea/
253 | *.sln.iml
254 |
255 | # Mac OS chaff
256 | .DS_Store
257 |
258 | # Generated Database
259 | *.db
--------------------------------------------------------------------------------
/dwCheckApi.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dwCheckApi", "src\dwCheckApi\dwCheckApi.csproj", "{AFE4A43C-4747-4DA3-834E-C7AC9BB16DD2}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CF9A7842-48E4-45A1-8573-87B8CF7513E0}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dwCheckApi.Common", "src\dwCheckApi.Common\dwCheckApi.Common.csproj", "{248D7115-B371-46AB-A709-50A266DB842A}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dwCheckApi.DAL", "src\dwCheckApi.DAL\dwCheckApi.DAL.csproj", "{CBE237D4-BF0D-40A1-8F2D-D012D20CD0F3}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dwCheckApi.DTO", "src\dwCheckApi.DTO\dwCheckApi.DTO.csproj", "{68A0C20F-CE67-4303-BFAE-A451175F7AAB}"
15 | EndProject
16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dwCheckApi.Entities", "src\dwCheckApi.Entities\dwCheckApi.Entities.csproj", "{F1C86D9F-4833-4526-8537-0AD6F8937D12}"
17 | EndProject
18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dwCheckApi.Persistence", "src\dwCheckApi.Persistence\dwCheckApi.Persistence.csproj", "{6FC7637C-DD44-4787-8B66-8EF999E1CB81}"
19 | EndProject
20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{09665FBF-87EF-46AE-BF43-49D896423D35}"
21 | EndProject
22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dwCheckApi.Tests", "tests\dwCheckApi.Tests\dwCheckApi.Tests.csproj", "{327EC529-05DA-4881-B98A-9FF0CFF5F3F4}"
23 | EndProject
24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infra", "infra", "{7DBC651F-1DEB-4C5F-ACE5-41FFAE6A94B5}"
25 | ProjectSection(SolutionItems) = preProject
26 | Dockerfile = Dockerfile
27 | EndProjectSection
28 | EndProject
29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{41CF3237-BBA4-4533-8192-FD2AC3EF213F}"
30 | ProjectSection(SolutionItems) = preProject
31 | README.md = README.md
32 | EndProjectSection
33 | EndProject
34 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dwCheckApi.Common.Tests", "tests\dwCheckApi.Common.Tests\dwCheckApi.Common.Tests.csproj", "{6FA3CA86-0B9F-4949-A5F6-8456968BBC4E}"
35 | EndProject
36 | Global
37 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
38 | Debug|Any CPU = Debug|Any CPU
39 | Release|Any CPU = Release|Any CPU
40 | EndGlobalSection
41 | GlobalSection(SolutionProperties) = preSolution
42 | HideSolutionNode = FALSE
43 | EndGlobalSection
44 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
45 | {AFE4A43C-4747-4DA3-834E-C7AC9BB16DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46 | {AFE4A43C-4747-4DA3-834E-C7AC9BB16DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
47 | {AFE4A43C-4747-4DA3-834E-C7AC9BB16DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
48 | {AFE4A43C-4747-4DA3-834E-C7AC9BB16DD2}.Release|Any CPU.Build.0 = Release|Any CPU
49 | {248D7115-B371-46AB-A709-50A266DB842A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
50 | {248D7115-B371-46AB-A709-50A266DB842A}.Debug|Any CPU.Build.0 = Debug|Any CPU
51 | {248D7115-B371-46AB-A709-50A266DB842A}.Release|Any CPU.ActiveCfg = Release|Any CPU
52 | {248D7115-B371-46AB-A709-50A266DB842A}.Release|Any CPU.Build.0 = Release|Any CPU
53 | {CBE237D4-BF0D-40A1-8F2D-D012D20CD0F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
54 | {CBE237D4-BF0D-40A1-8F2D-D012D20CD0F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
55 | {CBE237D4-BF0D-40A1-8F2D-D012D20CD0F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
56 | {CBE237D4-BF0D-40A1-8F2D-D012D20CD0F3}.Release|Any CPU.Build.0 = Release|Any CPU
57 | {68A0C20F-CE67-4303-BFAE-A451175F7AAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58 | {68A0C20F-CE67-4303-BFAE-A451175F7AAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
59 | {68A0C20F-CE67-4303-BFAE-A451175F7AAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
60 | {68A0C20F-CE67-4303-BFAE-A451175F7AAB}.Release|Any CPU.Build.0 = Release|Any CPU
61 | {F1C86D9F-4833-4526-8537-0AD6F8937D12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
62 | {F1C86D9F-4833-4526-8537-0AD6F8937D12}.Debug|Any CPU.Build.0 = Debug|Any CPU
63 | {F1C86D9F-4833-4526-8537-0AD6F8937D12}.Release|Any CPU.ActiveCfg = Release|Any CPU
64 | {F1C86D9F-4833-4526-8537-0AD6F8937D12}.Release|Any CPU.Build.0 = Release|Any CPU
65 | {6FC7637C-DD44-4787-8B66-8EF999E1CB81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
66 | {6FC7637C-DD44-4787-8B66-8EF999E1CB81}.Debug|Any CPU.Build.0 = Debug|Any CPU
67 | {6FC7637C-DD44-4787-8B66-8EF999E1CB81}.Release|Any CPU.ActiveCfg = Release|Any CPU
68 | {6FC7637C-DD44-4787-8B66-8EF999E1CB81}.Release|Any CPU.Build.0 = Release|Any CPU
69 | {327EC529-05DA-4881-B98A-9FF0CFF5F3F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
70 | {327EC529-05DA-4881-B98A-9FF0CFF5F3F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
71 | {327EC529-05DA-4881-B98A-9FF0CFF5F3F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
72 | {327EC529-05DA-4881-B98A-9FF0CFF5F3F4}.Release|Any CPU.Build.0 = Release|Any CPU
73 | {6FA3CA86-0B9F-4949-A5F6-8456968BBC4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
74 | {6FA3CA86-0B9F-4949-A5F6-8456968BBC4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
75 | {6FA3CA86-0B9F-4949-A5F6-8456968BBC4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
76 | {6FA3CA86-0B9F-4949-A5F6-8456968BBC4E}.Release|Any CPU.Build.0 = Release|Any CPU
77 | EndGlobalSection
78 | GlobalSection(NestedProjects) = preSolution
79 | {AFE4A43C-4747-4DA3-834E-C7AC9BB16DD2} = {CF9A7842-48E4-45A1-8573-87B8CF7513E0}
80 | {248D7115-B371-46AB-A709-50A266DB842A} = {CF9A7842-48E4-45A1-8573-87B8CF7513E0}
81 | {CBE237D4-BF0D-40A1-8F2D-D012D20CD0F3} = {CF9A7842-48E4-45A1-8573-87B8CF7513E0}
82 | {68A0C20F-CE67-4303-BFAE-A451175F7AAB} = {CF9A7842-48E4-45A1-8573-87B8CF7513E0}
83 | {F1C86D9F-4833-4526-8537-0AD6F8937D12} = {CF9A7842-48E4-45A1-8573-87B8CF7513E0}
84 | {6FC7637C-DD44-4787-8B66-8EF999E1CB81} = {CF9A7842-48E4-45A1-8573-87B8CF7513E0}
85 | {327EC529-05DA-4881-B98A-9FF0CFF5F3F4} = {09665FBF-87EF-46AE-BF43-49D896423D35}
86 | {6FA3CA86-0B9F-4949-A5F6-8456968BBC4E} = {09665FBF-87EF-46AE-BF43-49D896423D35}
87 | EndGlobalSection
88 | EndGlobal
89 |
--------------------------------------------------------------------------------
/.github/workflows/pr-action.yml:
--------------------------------------------------------------------------------
1 | name: 'PR build action'
2 |
3 | ## Only builds when someone PRs against main
4 | on:
5 | pull_request:
6 | branches:
7 | - main
8 | ## Builds not be run if the following files are the ones which are
9 | ## changed in a PR
10 | ## see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-paths
11 | paths-ignore:
12 | - '**/README.md'
13 | - '**/Dockerfile'
14 | - '**/global.json'
15 |
16 | jobs:
17 |
18 | build:
19 | runs-on: ubuntu-latest
20 | steps:
21 | ## The first thing we need to do is get the latest code out from git, otherwise
22 | ## we can't build anything
23 | - name: Checkout the code
24 | uses: actions/checkout@v4
25 | with:
26 | fetch-depth: 0
27 |
28 | ## Next we need to ensure that we have the .NET tooling installed.
29 | - name: Install the .NET SDK
30 | uses: actions/setup-dotnet@v4
31 | with:
32 | ## Ensure that we install the version of the .NET SDK found in the global.json file
33 | global-json-file: global.json
34 | ## Set a number of environment variables for the .NET tooling (these need
35 | ## to be set on our first step which uses the .NET tooling in order to take
36 | ## effect).
37 | ## We're setting these so that our logs are shorter, easier to read, and
38 | ## so that builds are around 1-2 second faster than normally.
39 | env:
40 | ## removes logo and telemetry message from first run of dotnet cli
41 | DOTNET_NOLOGO: 1
42 | ## opt-out of .NET tooling telemetry being sent to Microsoft
43 | DOTNET_CLI_TELEMETRY_OPTOUT: 1
44 |
45 | ## Now we need to restore any NuGet packages that we rely on in order to build
46 | ## or run the application
47 | - name: Install code level dependencies
48 | run: dotnet restore
49 | working-directory: ${{env.working-directory}}
50 |
51 | ## Building is next. Note the use of both the --configuration and
52 | ## --no-restore flags.
53 | ## The first flag sets the build configuration. We want Release here as it
54 | ## will produce a smaller binary than a Debug (which is the default) build.
55 | ## When running a Release build, the compiler will optimise your code and
56 | ## remove any debugging statements that it adds in order to make debugging
57 | ## easier - note: its still possible to debug Release code.
58 | ## The second flag tells the .NET tooling not to attempt to restore any NuGet
59 | ## packages. This is a time saving operation, as we restored them in the
60 | ## previous step.
61 | - name: Build
62 | run: dotnet build --configuration Release --no-restore
63 | working-directory: ${{env.working-directory}}
64 |
65 | test:
66 | runs-on: ubuntu-latest
67 | needs: ["build"]
68 | steps:
69 |
70 | ## The first thing we need to do is get the latest code out from git, otherwise
71 | ## we can't build anything
72 | - name: Checkout the code
73 | uses: actions/checkout@v4
74 | with:
75 | fetch-depth: 0
76 |
77 | ## Run all of the discovered tests for this repository, telling the dotnet
78 | ## tooling to not waste time building (--no-build), use the Release config
79 | ## (--configuration Release), and only print the normal amount of logs to
80 | ## the screen (--verbosity normal).
81 | ## We also want it to collect cross platform readable code coverage stats
82 | ## (--collect: "XPlat Code Coverage") and store them in a known location
83 | ## (-- results-directory ./coverage)
84 | ## The code coverage stats will show us how much of the code base is covered
85 | ## by our tests. This can be useful to identify which areas are NOT covered
86 | ## by our tests, and it can help us to identify where we should spend our
87 | ## personal and technical bandwidth in shoring up the test coverage.
88 | - name: Run tests
89 | run: dotnet test dwCheckApi.sln --configuration Release --collect:"XPlat Code Coverage" --results-directory ./coverage
90 |
91 | #### FEB 18TH, 2024 :TEMPORARILY COMMENTED OUT THE FOLLOWING THREE STEPS AS THERE
92 | #### ARE MULTIPLE TEST CLASSES, EACH CREATING A SEPARATE coverage.cobertura.xml
93 | #### WILL NEED TO INVESTIGATE A WAY TO COMBINE THEM ALL INTO ONE FILE BEFORE COPYING TO
94 | #### THE OUTPUT DIRECTORY.
95 |
96 | # ## We are about to use a GitHub action called Code Coverage Summary to get a
97 | # ## human readable summary of the code coverage stuff from the previous step.
98 | # ## The Code Coverage Summary action requires the code coverage stuff to be
99 | # ## in a predictable location, so let's copy those files right now.
100 | # - name: Copy Coverage To Known Location
101 | # run: cp coverage/**/coverage.cobertura.xml coverage.cobertura.xml
102 |
103 | # ## Generate a Code Coverage report based on the code coverage we've gotten
104 | # ## in the previous steps.
105 | # ## This will create a file on disk, but not in the repo. So we'll need to
106 | # ## create a PR off the back of this action to add that file to the repo.
107 | # - name: Code Coverage Summary Report
108 | # uses: irongut/CodeCoverageSummary@v1.2.0
109 | # with:
110 | # filename: coverage.cobertura.xml
111 | # badge: true
112 | # fail_below_min: true
113 | # format: markdown
114 | # hide_branch_rate: false
115 | # hide_complexity: true
116 | # indicators: true
117 | # output: both
118 | # thresholds: '0 80'
119 |
120 | # ## Create the PR to add the Code Coverage Summary to the repo
121 | # - name: Add Coverage PR Comment
122 | # uses: marocchino/sticky-pull-request-comment@v2
123 | # if: github.event_name == 'pull_request'
124 | # with:
125 | # recreate: true
126 | # path: code-coverage-results.md
127 |
--------------------------------------------------------------------------------
/tests/dwCheckApi.Tests/ViewModelMappers/BookViewModelMapperTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using dwCheckApi.DTO.Helpers;
5 | using dwCheckApi.DTO.ViewModels;
6 | using dwCheckApi.Entities;
7 | using Xunit;
8 |
9 | namespace dwCheckApi.Tests.ViewModelMappers
10 | {
11 | public class BookViewModelMapperTests
12 | {
13 | [Fact]
14 | public void Given_BookDbModel_Returns_ViewModel()
15 | {
16 | // Arrange
17 | const int idForTest = 1;
18 | var dbBook = GetTestBookById(idForTest);
19 | var testViewModel = GetBookViewModels()
20 | .FirstOrDefault(b => b.BookOrdinal == idForTest);
21 | // Act
22 | var viewModel = BookViewModelHelpers.ConvertToViewModel(dbBook);
23 |
24 | // Assert
25 | Assert.NotNull(testViewModel);
26 | Assert.Equal(testViewModel.BookName, viewModel.BookName);
27 | Assert.Equal(testViewModel.BookDescription, viewModel.BookDescription);
28 | Assert.Equal(testViewModel.BookIsbn10, viewModel.BookIsbn10);
29 | Assert.Equal(testViewModel.BookIsbn13, viewModel.BookIsbn13);
30 | }
31 |
32 | [Fact]
33 | public void Given_BookDbModels_Returns_ViewModels()
34 | {
35 | // Arrange
36 | const int idForTest = 1;
37 | var dbBooks = GetTestBooks();
38 | // Act
39 | var viewModels = BookViewModelHelpers.ConvertToViewModels(dbBooks);
40 |
41 | // Assert
42 | Assert.NotEmpty(viewModels);
43 | Assert.Equal(viewModels.Count, dbBooks.Count);
44 |
45 | for (var i = 0; i < viewModels.Count; i++)
46 | {
47 | Assert.Equal(viewModels[i].BookName, dbBooks[i].BookName);
48 | Assert.Equal(viewModels[i].BookDescription, dbBooks[i].BookDescription);
49 | Assert.Equal(viewModels[i].BookIsbn10, dbBooks[i].BookIsbn10);
50 | Assert.Equal(viewModels[i].BookIsbn13, dbBooks[i].BookIsbn13);
51 | }
52 | }
53 |
54 | [Fact]
55 | public void Given_BookDbModels_Returns_BaseViewModels()
56 | {
57 | // Arrange
58 | const int idForTest = 1;
59 | var dbBooks = GetTestBooks();
60 | // Act
61 | var viewModels = BookViewModelHelpers.ConvertToBaseViewModels(dbBooks);
62 |
63 | // Assert
64 | Assert.NotEmpty(viewModels);
65 | Assert.Equal(viewModels.Count, dbBooks.Count);
66 |
67 | for (var i = 0; i < viewModels.Count; i++)
68 | {
69 | Assert.Equal(viewModels[i].BookId, dbBooks[i].BookId);
70 | Assert.Equal(viewModels[i].BookOrdinal, dbBooks[i].BookOrdinal);
71 | Assert.Equal(viewModels[i].BookName, dbBooks[i].BookName);
72 | Assert.Equal(viewModels[i].BookDescription, dbBooks[i].BookDescription);
73 | }
74 | }
75 |
76 | [Fact]
77 | public void Given_BookDbModel_Returns_BookCoverViewModel()
78 | {
79 | // Arrange
80 | const int idForTest = 1;
81 | var dbBook = GetTestBookById(idForTest);
82 | // Act
83 | var viewModel = BookViewModelHelpers.ConvertToBookCoverViewModel(dbBook);
84 |
85 | // Assert
86 | Assert.NotNull(viewModel);
87 | Assert.Equal(viewModel.bookId, dbBook.BookId);
88 | Assert.Equal(viewModel.BookCoverImage, dbBook.BookCoverImageUrl);
89 | Assert.False(viewModel.BookImageIsBase64String);
90 | }
91 |
92 | private Book GetTestBookById(int id)
93 | {
94 | return GetTestBooks().FirstOrDefault(b => b.BookId == id);
95 | }
96 |
97 | private List GetTestBooks()
98 | {
99 | var testSeries = new Series
100 | {
101 | SeriesName = "A test series",
102 | SeriesId = 2
103 | };
104 |
105 | var testCharacter = new Character
106 | {
107 | CharacterName = Guid.NewGuid().ToString(),
108 | CharacterId = 4
109 | };
110 |
111 | var mockData = new List();
112 | mockData.Add(new Book
113 | {
114 | BookId = 1,
115 | BookName = "Test Book",
116 | BookOrdinal = 1,
117 | BookDescription = "Test entry for unit tests only",
118 | BookIsbn10 = "1234567890",
119 | BookIsbn13 = "1234567890123",
120 | BookCoverImage = new List().ToArray(),
121 | BookSeries = new List
122 | {
123 | new()
124 | {
125 | BookId = 1,
126 | SeriesId = testSeries.SeriesId,
127 | Series = testSeries,
128 | Ordinal = 3
129 | }
130 | },
131 | BookCharacter = new List
132 | {
133 | new()
134 | {
135 | BookId = 1,
136 | CharacterId = testCharacter.CharacterId,
137 | Character = testCharacter
138 | }
139 | }
140 | });
141 |
142 | return mockData;
143 | }
144 |
145 | private List GetBookViewModels()
146 | {
147 | var viewModels = new List();
148 | viewModels.Add(new BookViewModel
149 | {
150 | BookOrdinal = 1,
151 | BookName = "Test Book",
152 | BookDescription = "Test entry for unit tests only",
153 | BookIsbn10 = "1234567890",
154 | BookIsbn13 = "1234567890123"
155 | });
156 |
157 | return viewModels;
158 | }
159 | }
160 | }
--------------------------------------------------------------------------------
/src/dwCheckApi.Persistence/Migrations/20170826014619_InitialMigration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 | using System;
3 | using System.Diagnostics.CodeAnalysis;
4 |
5 | namespace dwCheckApi.Persistence.Migrations
6 | {
7 | [ExcludeFromCodeCoverage]
8 | public partial class InitialMigration : Migration
9 | {
10 | protected override void Up(MigrationBuilder migrationBuilder)
11 | {
12 | migrationBuilder.CreateTable(
13 | name: "Books",
14 | columns: table => new
15 | {
16 | BookId = table.Column(type: "INTEGER", nullable: false)
17 | .Annotation("Sqlite:Autoincrement", true),
18 | BookCoverImage = table.Column(type: "BLOB", nullable: true),
19 | BookCoverImageUrl = table.Column(type: "TEXT", nullable: true),
20 | BookDescription = table.Column(type: "TEXT", nullable: true),
21 | BookIsbn10 = table.Column(type: "TEXT", nullable: true),
22 | BookIsbn13 = table.Column(type: "TEXT", nullable: true),
23 | BookName = table.Column(type: "TEXT", nullable: true),
24 | BookOrdinal = table.Column(type: "INTEGER", nullable: false),
25 | Created = table.Column(type: "TEXT", nullable: false),
26 | Modified = table.Column(type: "TEXT", nullable: false)
27 | },
28 | constraints: table =>
29 | {
30 | table.PrimaryKey("PK_Books", x => x.BookId);
31 | });
32 |
33 | migrationBuilder.CreateTable(
34 | name: "Characters",
35 | columns: table => new
36 | {
37 | CharacterId = table.Column(type: "INTEGER", nullable: false)
38 | .Annotation("Sqlite:Autoincrement", true),
39 | CharacterName = table.Column(type: "TEXT", nullable: true),
40 | Created = table.Column(type: "TEXT", nullable: false),
41 | Modified = table.Column(type: "TEXT", nullable: false)
42 | },
43 | constraints: table =>
44 | {
45 | table.PrimaryKey("PK_Characters", x => x.CharacterId);
46 | });
47 |
48 | migrationBuilder.CreateTable(
49 | name: "Series",
50 | columns: table => new
51 | {
52 | SeriesId = table.Column(type: "INTEGER", nullable: false)
53 | .Annotation("Sqlite:Autoincrement", true),
54 | Created = table.Column(type: "TEXT", nullable: false),
55 | Modified = table.Column(type: "TEXT", nullable: false),
56 | SeriesName = table.Column(type: "TEXT", nullable: true)
57 | },
58 | constraints: table =>
59 | {
60 | table.PrimaryKey("PK_Series", x => x.SeriesId);
61 | });
62 |
63 | migrationBuilder.CreateTable(
64 | name: "BookCharacters",
65 | columns: table => new
66 | {
67 | BookId = table.Column(type: "INTEGER", nullable: false),
68 | CharacterId = table.Column(type: "INTEGER", nullable: false),
69 | Created = table.Column(type: "TEXT", nullable: false),
70 | Modified = table.Column(type: "TEXT", nullable: false)
71 | },
72 | constraints: table =>
73 | {
74 | table.PrimaryKey("PK_BookCharacters", x => new { x.BookId, x.CharacterId });
75 | table.ForeignKey(
76 | name: "FK_BookCharacters_Books_BookId",
77 | column: x => x.BookId,
78 | principalTable: "Books",
79 | principalColumn: "BookId",
80 | onDelete: ReferentialAction.Cascade);
81 | table.ForeignKey(
82 | name: "FK_BookCharacters_Characters_CharacterId",
83 | column: x => x.CharacterId,
84 | principalTable: "Characters",
85 | principalColumn: "CharacterId",
86 | onDelete: ReferentialAction.Cascade);
87 | });
88 |
89 | migrationBuilder.CreateTable(
90 | name: "BookSeries",
91 | columns: table => new
92 | {
93 | BookId = table.Column(type: "INTEGER", nullable: false),
94 | SeriesId = table.Column(type: "INTEGER", nullable: false),
95 | Created = table.Column(type: "TEXT", nullable: false),
96 | Modified = table.Column(type: "TEXT", nullable: false),
97 | Ordinal = table.Column(type: "INTEGER", nullable: false)
98 | },
99 | constraints: table =>
100 | {
101 | table.PrimaryKey("PK_BookSeries", x => new { x.BookId, x.SeriesId });
102 | table.ForeignKey(
103 | name: "FK_BookSeries_Books_BookId",
104 | column: x => x.BookId,
105 | principalTable: "Books",
106 | principalColumn: "BookId",
107 | onDelete: ReferentialAction.Cascade);
108 | table.ForeignKey(
109 | name: "FK_BookSeries_Series_SeriesId",
110 | column: x => x.SeriesId,
111 | principalTable: "Series",
112 | principalColumn: "SeriesId",
113 | onDelete: ReferentialAction.Cascade);
114 | });
115 |
116 | migrationBuilder.CreateIndex(
117 | name: "IX_BookCharacters_CharacterId",
118 | table: "BookCharacters",
119 | column: "CharacterId");
120 |
121 | migrationBuilder.CreateIndex(
122 | name: "IX_BookSeries_SeriesId",
123 | table: "BookSeries",
124 | column: "SeriesId");
125 | }
126 |
127 | protected override void Down(MigrationBuilder migrationBuilder)
128 | {
129 | migrationBuilder.DropTable(
130 | name: "BookCharacters");
131 |
132 | migrationBuilder.DropTable(
133 | name: "BookSeries");
134 |
135 | migrationBuilder.DropTable(
136 | name: "Characters");
137 |
138 | migrationBuilder.DropTable(
139 | name: "Books");
140 |
141 | migrationBuilder.DropTable(
142 | name: "Series");
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/.github/workflows/ci-build-action.yml:
--------------------------------------------------------------------------------
1 | name: 'CI build action'
2 |
3 | ## Only builds when someone pushes to main
4 | on:
5 | push:
6 | branches:
7 | - main
8 | ## Builds not be run if the following files are the ones which are
9 | ## changed in a push to main
10 | ## see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-paths
11 | paths-ignore:
12 | - '**/README.md'
13 | - '**/Dockerfile'
14 | - '**/global.json'
15 |
16 | jobs:
17 |
18 | build:
19 |
20 | runs-on: ubuntu-latest
21 | steps:
22 | ## The first thing we need to do is get the latest code out from git, otherwise
23 | ## we can't build anything
24 | - name: Checkout the code
25 | uses: actions/checkout@v4
26 | with:
27 | fetch-depth: 0
28 |
29 | ## Next we need to ensure that we have the .NET tooling installed.
30 | - name: Install the .NET SDK
31 | uses: actions/setup-dotnet@v4
32 | with:
33 | ## Ensure that we install the version of the .NET SDK found in the global.json file
34 | global-json-file: global.json
35 | ## Set a number of environment variables for the .NET tooling (these need
36 | ## to be set on our first step which uses the .NET tooling in order to take
37 | ## effect).
38 | ## We're setting these so that our logs are shorter, easier to read, and
39 | ## so that builds are around 1-2 second faster than normally.
40 | env:
41 | ## removes logo and telemetry message from first run of dotnet cli
42 | DOTNET_NOLOGO: 1
43 | ## opt-out of .NET tooling telemetry being sent to Microsoft
44 | DOTNET_CLI_TELEMETRY_OPTOUT: 1
45 |
46 | ## Now we need to restore any NuGet packages that we rely on in order to build
47 | ## or run the application
48 | - name: Install code level dependencies
49 | run: dotnet restore
50 | working-directory: ${{env.working-directory}}
51 |
52 | ## Building is next. Note the use of both the --configuration and
53 | ## --no-restore flags.
54 | ## The first flag sets the build configuration. We want Release here as it
55 | ## will produce a smaller binary than a Debug (which is the default) build.
56 | ## When running a Release build, the compiler will optimise your code and
57 | ## remove any debugging statements that it adds in order to make debugging
58 | ## easier - note: its still possible to debug Release code.
59 | ## The second flag tells the .NET tooling not to attempt to restore any NuGet
60 | ## packages. This is a time saving operation, as we restored them in the
61 | ## previous step.
62 | - name: Build
63 | run: dotnet build --configuration Release --no-restore
64 | working-directory: ${{env.working-directory}}
65 |
66 | test:
67 | runs-on: ubuntu-latest
68 | needs: ["build"]
69 | steps:
70 | ## The first thing we need to do is get the latest code out from git, otherwise
71 | ## we can't build anything
72 | - name: Checkout the code
73 | uses: actions/checkout@v4
74 | with:
75 | fetch-depth: 0
76 |
77 | ## Run all of the discovered tests for this repository, telling the dotnet
78 | ## tooling to not waste time building (--no-build), use the Release config
79 | ## (--configuration Release), and only print the normal amount of logs to
80 | ## the screen (--verbosity normal).
81 | ## We also want it to collect cross platform readable code coverage stats
82 | ## (--collect: "XPlat Code Coverage") and store them in a known location
83 | ## (-- results-directory ./coverage)
84 | ## The code coverage stats will show us how much of the code base is covered
85 | ## by our tests. This can be useful to identify which areas are NOT covered
86 | ## by our tests, and it can help us to identify where we should spend our
87 | ## personal and technical bandwidth in shoring up the test coverage.
88 | - name: Run tests
89 | run: dotnet test dwCheckApi.sln --configuration Release --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
90 |
91 | #### FEB 18TH, 2024 :TEMPORARILY COMMENTED OUT THE FOLLOWING THREE STEPS AS THERE
92 | #### ARE MULTIPLE TEST CLASSES, EACH CREATING A SEPARATE coverage.cobertura.xml
93 | #### WILL NEED TO INVESTIGATE A WAY TO COMBINE THEM ALL INTO ONE FILE BEFORE COPYING TO
94 | #### THE OUTPUT DIRECTORY.
95 |
96 | # ## We are about to use a GitHub action called Code Coverage Summary to get a
97 | # ## human readable summary of the code coverage stuff from the previous step.
98 | # ## The Code Coverage Summary action requires the code coverage stuff to be
99 | # ## in a predictable location, so let's copy those files right now.
100 | # - name: Copy Coverage To Known Location
101 | # run: cp coverage/**/coverage.cobertura.xml coverage.cobertura.xml
102 |
103 | # ## Generate a Code Coverage report based on the code coverage we've gotten
104 | # ## in the previous steps.
105 | # ## This will create a file on disk, but not in the repo. So we'll need to
106 | # ## create a PR off the back of this action to add that file to the repo.
107 | # - name: Code Coverage Summary Report
108 | # uses: irongut/CodeCoverageSummary@v1.2.0
109 | # with:
110 | # filename: coverage.cobertura.xml
111 | # badge: true
112 | # fail_below_min: true
113 | # format: markdown
114 | # hide_branch_rate: false
115 | # hide_complexity: true
116 | # indicators: true
117 | # output: both
118 | # thresholds: '0 80'
119 |
120 | # ## Create the PR to add the Code Coverage Summary to the repo
121 | # - name: Add Coverage PR Comment
122 | # uses: marocchino/sticky-pull-request-comment@v2
123 | # if: github.event_name == 'pull_request'
124 | # with:
125 | # recreate: true
126 | # path: code-coverage-results.md
127 |
128 | release:
129 | runs-on: ubuntu-latest
130 | needs: ["test"]
131 |
132 | steps:
133 | ## The first thing we need to do is get the latest code out from git, otherwise
134 | ## we can't build anything
135 | - name: Checkout the code
136 | uses: actions/checkout@v4
137 | with:
138 | fetch-depth: 0
139 |
140 | ## Now that we have the binary built, we want to publish it. A publish
141 | ## action takes the built binary, grabs any runtime required libraries,
142 | ## then copies everything to the output directory.
143 | ## In this command we're:
144 | ## - Copying the output to a directory called "publish" in the root of the
145 | ## source directory (-o publish)
146 | ## - Ensuring that the packaged version of the application is the Release
147 | ## build only (-c Release)
148 | ## - Ensuring that the .NET tooling doesn't waste time restoring any NuGet
149 | ## packages before publishing, as we've already done this
150 | - name: publish
151 | run: dotnet publish dwCheckApi.sln -o ../../../publish -c Release
152 | working-directory: ${{env.working-directory}}
153 |
154 |
--------------------------------------------------------------------------------
/src/dwCheckApi/Controllers/BooksController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using System.Linq;
3 | using dwCheckApi.DAL;
4 | using dwCheckApi.DTO.Helpers;
5 | using dwCheckApi.DTO.ViewModels;
6 | using Microsoft.AspNetCore.Http;
7 |
8 | namespace dwCheckApi.Controllers
9 | {
10 | [Route("/[controller]")]
11 | [Produces("application/json")]
12 | public class BooksController : BaseController
13 | {
14 | private readonly IBookService _bookService;
15 |
16 | public BooksController(IBookService bookService)
17 | {
18 | _bookService = bookService;
19 | }
20 |
21 | ///
22 | /// Used to get a Book record by its ordinal (the order in which it was released)
23 | ///
24 | /// The ordinal of a Book to return
25 | ///
26 | /// If a Book record can be found, then a
27 | /// is returned, which contains a .
28 | /// If no record can be found, then an is returned
29 | ///
30 | ///
31 | /// Sample request:
32 | ///
33 | /// GET /1
34 | ///
35 | ///
36 | [HttpGet("Get/{id}")]
37 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
38 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
39 | public IActionResult GetByOrdinal(int id)
40 | {
41 | var book = _bookService.FindByOrdinal(id);
42 | if (book == null)
43 | {
44 | return NotFoundResponse("Not found");
45 | }
46 |
47 | return Ok(new SingleResult
48 | {
49 | Success = true,
50 | Result = BookViewModelHelpers.ConvertToViewModel(book)
51 | });
52 | }
53 |
54 | ///
55 | /// Used to get a Book by its title
56 | ///
57 | /// The name to use when searching for a book
58 | ///
59 | /// If a Book record can be found, then a
60 | /// is returned, which contains a .
61 | /// If no record can be found, then an is returned
62 | ///
63 | ///
64 | /// Sample request:
65 | ///
66 | /// GET /GetByName?bookName=night%20watch
67 | ///
68 | ///
69 | /// The book object which matches on the supplied title
70 | /// The requested book could not be found
71 | [HttpGet("GetByName")]
72 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
73 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
74 | public IActionResult GetByName(string bookName)
75 | {
76 | if (string.IsNullOrWhiteSpace(bookName))
77 | {
78 | return NotFoundResponse("Book name is required");
79 | }
80 |
81 | var book = _bookService.GetByName(bookName);
82 |
83 | if (book == null)
84 | {
85 | return NotFoundResponse("No book with that name could be found");
86 | }
87 |
88 | return Ok(new SingleResult
89 | {
90 | Success = true,
91 | Result = BookViewModelHelpers.ConvertToViewModel(book)
92 | });
93 | }
94 |
95 | ///
96 | /// Used to search all Book records with a given search string (searches against Book
97 | /// name, description and ISBN numbers)
98 | ///
99 | /// The search string to use
100 | ///
101 | /// If Book records can be found, then a
102 | /// is returned, which contains a collection of .
103 | /// If no records can be found, then an is returned
104 | ///
105 | ///
106 | /// Sample request:
107 | ///
108 | /// GET /Search?searchString=night
109 | ///
110 | ///
111 | [HttpGet("Search")]
112 | [ProducesResponseType(typeof(MultipleResult), StatusCodes.Status200OK)]
113 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
114 | public IActionResult Search(string searchString)
115 | {
116 | var dbBooks = _bookService.Search(searchString).ToList();
117 |
118 | if (!dbBooks.Any())
119 | {
120 | return NotFoundResponse();
121 | }
122 |
123 | return Ok(new MultipleResult
124 | {
125 | Success = true,
126 | Result = BookViewModelHelpers.ConvertToViewModels(dbBooks)
127 | });
128 | }
129 |
130 | ///
131 | /// Used to get all Book records within a Series, by the series ID
132 | ///
133 | /// The ID of the series
134 | ///
135 | /// If Book records can be found, then a
136 | /// is returned, which contains a collection of .
137 | /// If no records can be found, then an is returned
138 | ///
139 | ///
140 | /// Sample request:
141 | ///
142 | /// GET /Series/6
143 | ///
144 | ///
145 | /// All books in the requested series
146 | /// The series with the requested number could not be found
147 | [HttpGet("Series/{seriesId}")]
148 | [ProducesResponseType(typeof(MultipleResult), StatusCodes.Status200OK)]
149 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
150 | public IActionResult GetForSeries(int seriesId)
151 | {
152 | var dbBooks = _bookService.Series(seriesId).ToList();
153 |
154 | if (!dbBooks.Any())
155 | {
156 | return NotFoundResponse();
157 | }
158 |
159 | return Ok(new
160 | {
161 | Success = true,
162 | Result = BookViewModelHelpers.ConvertToViewModels(dbBooks)
163 | });
164 | }
165 |
166 | ///
167 | /// Used to get the Cover Art for a Book record with a given ID
168 | ///
169 | ///
170 | /// The Bookd ID for the relevant book record (this is the identity, not the ordinal)
171 | ///
172 | ///
173 | /// If a Book record can be found, then a
174 | /// is returned, which contains a .
175 | /// If no record can be found, then an is returned
176 | ///
177 | ///
178 | /// Sample request:
179 | ///
180 | /// GET /GetBookCover/29
181 | ///
182 | ///
183 | /// An object representing the requested book's cover art as a Base64 string
184 | /// The requested book could not be found
185 | [HttpGet("GetBookCover/{bookId}")]
186 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status200OK)]
187 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
188 | public IActionResult GetBookCover(int bookId)
189 | {
190 | var dbBook = _bookService.FindById(bookId);
191 | if (dbBook == null)
192 | {
193 | return NotFoundResponse();
194 | }
195 |
196 | return Ok(new
197 | {
198 | Success = true,
199 | Result = BookViewModelHelpers.ConvertToBookCoverViewModel(dbBook)
200 | });
201 | }
202 |
203 | ///
204 | /// Returns an array of all the books in the database
205 | ///
206 | ///
207 | /// If Book records can be found, then a
208 | /// is returned, which contains a collection of .
209 | /// If no records can be found, then an is returned
210 | ///
211 | ///
212 | /// Sample request:
213 | ///
214 | /// GET /All
215 | ///
216 | ///
217 | /// All books in the database
218 | /// No books could be found in the database
219 | [HttpGet("All")]
220 | [ProducesResponseType(typeof(MultipleResult), StatusCodes.Status200OK)]
221 | [ProducesResponseType(typeof(SingleResult), StatusCodes.Status404NotFound)]
222 | public IActionResult GetAll()
223 | {
224 | var books = _bookService.GetAll().ToList();
225 | if (!books.Any())
226 | {
227 | return NotFoundResponse("No books found");
228 | }
229 |
230 | return Ok(BookViewModelHelpers.ConvertToViewModels(books));
231 | }
232 | }
233 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DwCheckApi - .NET Core
2 |
3 | ## Note
4 |
5 | AppVeyor projects have been removed, as this project now uses docker and the chosen docker images are Linux based (for size and speed reasons)
6 |
7 | ## Licence
8 |
9 | [](https://opensource.org/licenses/MIT)
10 |
11 | ## Support This Project
12 |
13 | If you have found this project helpful, either as a library that you use or as a learning tool, please consider buying me a coffee:
14 |
15 |
16 |
17 | ## Code Triage Status
18 |
19 | [](https://www.codetriage.com/gaprogman/dwcheckapi)
20 |
21 | ## Docker Image
22 |
23 | To build and run the docker image, use the following commands:
24 |
25 | 1. `docker build . -t dwcheckapi`
26 | 1. `docker run -p 8080:5000 dwcheckapi`
27 |
28 | This will run the latest build of the docker image and expose the application at [http://localhost:8080/swagger](http://localhost:8080/swagger).
29 |
30 | ## Description
31 |
32 | This project is a .NET core implemented Web API for listing all of the (canon) [Discworld](https://en.wikipedia.org/wiki/Discworld#Novels) novels.
33 |
34 | It uses Entity Framework Core to communicate with a Sqlite database, which contains a record for each of the Discworld novels.
35 |
36 | It has been released, as is, using an MIT licence. For more information on the MIT licence, please see either the `LICENSE` file in the root of the repository or see the tl;dr Legal page for [MIT](https://tldrlegal.com/license/mit-license)
37 |
38 | ## Code of Conduct
39 |
40 | dwCheckApi has a Code of Conduct which all contributors, maintainers and forkers must adhere to. When contributing, maintaining, forking or in any other way changing the code presented in this repository, all users must agree to this Code of Conduct.
41 |
42 | See `Code of Conduct.md` for details.
43 |
44 | ## Pull Requests
45 |
46 | [](http://makeapullrequest.com)
47 |
48 | Pull requests are welcome, but please take a moment to read the Code of Conduct before submitting them or commenting on any work in this repo.
49 |
50 | ## Creating the Database
51 |
52 | This will need to be perfored before running the application for the first time
53 |
54 | 1. Change to the Persistence directory (i.e. `dwCheckApi/dwCheckApi.Persistence`)
55 |
56 | `cd dwCheckApi.Persistence`
57 |
58 | 1. Issue the Entity Framework command to update the database
59 |
60 | `dotnet ef database update`
61 |
62 | This will ensure that all migrations are used to create or alter the local database instance, ready for seeding (see `Seeding the Database`)
63 |
64 | ## Building and Running
65 |
66 | 1. Change to the api directory (i.e. `dwCheckApi/dwCheckApi`)
67 |
68 | `cd dwCheckApi`
69 |
70 | 1. Issue the `dotnet` restore command (this resolves all NuGet packages)
71 |
72 | `dotnet restore`
73 |
74 | 1. Issue the `dotnet` build command
75 |
76 | `dotnet build`
77 |
78 | 1. Issue the `dotnet` run command
79 |
80 | `dotnet run`
81 |
82 | This will start the Kestrel webserver, load the `dwCheckApi` application and tell you, via the terminal, what the url to access `dwCheckApi` will be. Usually this will be `http://localhost:5000`, but it may be different based on your system configuration.
83 |
84 | ## Seeding the Database
85 |
86 | There are a series of API endpoints related to clearing and seeding the database. These can be found at:
87 |
88 | /Database/DropData
89 | /Database/SeedData
90 |
91 | These two commands (used in conjunction with each other) will drop all data from the database, then seed the database (respectively) from a series of JSON files that can be found in the `SeedData` directory.
92 |
93 | `dwCheckApi` has been designed so that the user can add as much data as they like via the JSON files. This means that `dwCheckApi` is not limited to Discworld novels and characters.
94 |
95 | A user of this API could alter the JSON files, drop the data and reseed and have a completely different data set - perhaps Stephen King novels, for example.
96 |
97 | ## Testing
98 |
99 | This repository contains an xUnit.NET test library. To run the tests:
100 |
101 | 1. Change directory to the tests directory
102 |
103 | `cd dwCheckApi.Tests`
104 |
105 | 1. Issue the `dotnet` restore command (this resolves all NuGet packages)
106 |
107 | `dotnet restore`
108 |
109 | 1. Issue the `xunit` command
110 |
111 | `dotnet xunit`
112 |
113 | All tests will be run against a new build of `dwCheckApi` and results will be returned in the open shell/command prompt window.
114 |
115 | ## Polling and Usage of the API
116 |
117 | `dwCheckApi` has the following Controllers:
118 |
119 | 1. Books
120 |
121 | The `Books` controller has two methods:
122 |
123 | 1. Get
124 |
125 | The `Get` action takes an integer Id. This field represents the ordinal for the novel. This ordinal is based on release order, so if the user want data on 'Night Watch', they would set a GET request to:
126 |
127 | /Books/Get/29
128 |
129 | This will return the following JSON data:
130 |
131 | {
132 | "bookOrdinal":29,
133 | "bookName":"Night Watch",
134 | "bookIsbn10":"0552148997",
135 | "bookIsbn13":"9780552148993",
136 | "bookDescription":"This morning, Commander Vimes of the City Watch had it all. He was a Duke. He was rich. He was respected. He had a titanium cigar case. He was about to become a father. This morning he thought longingly about the good old days. Tonight, he's in them.",
137 | "bookCoverImage":null,
138 | "bookCoverImageUrl":"http://wiki.lspace.org/mediawiki/images/4/4f/Cover_Night_Watch.jpg",
139 | "characters" :
140 | [
141 | "Fred Colon",
142 | "Nobby Nobbs",
143 | "Rosie Palm",
144 | "Samuel Vimes",
145 | "The Patrician"
146 | ]
147 | }
148 |
149 | 1. Search
150 |
151 | The `Search` action takes a string parameter called `searchString`. `dwCheckApi` will search the following fields of all Book records and return once which have any matches:
152 |
153 | - BookName
154 | - BookDescription
155 | - BookIsbn10
156 | - BookIsbn13
157 |
158 | If the user wishes to search for the prase "Rincewind", then they should issue the following request:
159 |
160 | /Books/Search?searchString=Rincewind
161 |
162 | This will return the following JSON data:
163 |
164 | [
165 | {
166 | "bookId":23,
167 | "bookOrdinal":2,
168 | "bookName":"The Light Fantastic",
169 | "bookIsbn10":"0861402030",
170 | "bookIsbn13":"9780747530794",
171 | "bookDescription":"As it moves towards a seemingly inevitable collision with a malevolent red star, the Discworld has only one possible saviour. Unfortunately, this happens to be the singularly inept and cowardly wizard called Rincewind, who was last seen falling off the edge of the world ....",
172 | "bookCoverImage":null,
173 | "bookCoverImageUrl":"http://wiki.lspace.org/mediawiki/images/f/f1/Cover_The_Light_Fantastic.jpg",
174 | "characters":
175 | [
176 | "The Lady",
177 | "Rincewind",
178 | "The Partician",
179 | "The Luggage",
180 | "Blind Io",
181 | "Fate",
182 | "Death",
183 | "Twoflower",
184 | "Offler",
185 | "Ridcully"
186 | ]
187 | },
188 | {
189 | "bookId":30,
190 | "bookOrdinal":9,
191 | "bookName":"Eric",
192 | "bookIsbn10":"0575046368",
193 | "bookIsbn13":"9780575046368",
194 | "bookDescription":"Eric is the Discworld's only demonology hacker. Pity he's not very good at it. All he wants is three wishes granted. Nothing fancy - to be immortal, rule the world, have the most beautiful woman in the world fall madly in love with him, the usual stuff. But instead of a tractable demon, he calls up Rincewind, probably the most incompetent wizard in the universe, and the extremely intractable and hostile form of travel accessory known as the Luggage. With them on his side, Eric's in for a ride through space and time that is bound to make him wish (quite fervently) again - this time that he'd never been born.",
195 | "bookCoverImage":null,
196 | "bookCoverImageUrl":"http://wiki.lspace.org/mediawiki/images/2/27/Cover_Eric_%28alt%29.jpg",
197 | "characters" : []
198 | },
199 | {
200 | "bookId":38,
201 | "bookOrdinal":17,
202 | "bookName":"Interesting Times",
203 | "bookIsbn10":"0552142352",
204 | "bookIsbn13":"9780552142359",
205 | "bookDescription":"Mighty Battles! Revolution! Death! War! (and his sons Terror and Panic, and daughter Clancy). The oldest and most inscrutable empire on the Discworld is in turmoil, brought about by the revolutionary treatise What I Did On My Holidays. Workers are uniting, with nothing to lose but their water buffaloes. Warlords are struggling for power. War (and Clancy) are spreading through the ancient cities. And all that stands in the way of terrible doom for everyone is: Rincewind the Wizzard, who can't even spell the word 'wizard' ... Cohen the barbarian hero, five foot tall in his surgical sandals, who has had a lifetime's experience of not dying ...and a very special butterfly.",
206 | "bookCoverImage":null,
207 | "bookCoverImageUrl":"http://wiki.lspace.org/mediawiki/images/9/96/Cover_Interesting_Times.jpg",
208 | "characters" : []
209 | }
210 | ]
211 |
212 |
213 | 1. Characters
214 |
215 | The `Characters` controller has two methods:
216 |
217 | 1. Get
218 |
219 | The `Get` action takes an integer Id. This field represents the id of the character entry in the database. It is not recommended that a consumer of this api uses this controller method, as the id entry relies entirely on the order in which Entity Framework Core persists the entries to the database while creating the dataset, and this is unpredictable. It is included here for completeness, and will probably be removed in a later version.
220 |
221 | This ordinal is based on release order, so if the user want data on 'Night Watch', they would set a GET request to:
222 |
223 | /Characters/Get/4
224 |
225 | This will return JSON data similar to this one (see above for why the specific character entity may not be the same when running on a newly created database):
226 |
227 | {
228 | "characterName":"The Luggage",
229 | "books":
230 | [
231 | "The Colour of Magic"
232 | ]
233 | }
234 |
235 | 1. Search
236 |
237 | The `Search` action takes a string parameter called `searchString`. `dwCheckApi` will search the names of all Character records, and return those which match.
238 |
239 | If the user wishes to search for the prase "ri", then they should issue the following request:
240 |
241 | /Characters/Search?searchString=ri
242 |
243 | This will return the following JSON data:
244 |
245 | [
246 | {
247 | "characterName":"Ridcully",
248 | "books":
249 | [
250 | "The Colour of Magic"
251 | ]
252 | },
253 | {
254 | "characterName":"Rincewind",
255 | "books":
256 | [
257 | "The Colour of Magic"
258 | ]
259 | }
260 | ]
261 |
262 | # Data Source
263 |
264 | The [L-Space wiki](http://wiki.lspace.org/mediawiki/Bibliography#Novels) is currently being used to seed the database.
265 |
266 | All character and book data are copyrighted to Terry Pratchett and/or Transworld Publishers no infringement was intended.
267 |
268 | ## A Note on the JSON files
269 |
270 | In the SeedData directory, there are a collection of JSON files. The data source for these files is a combination of the L-Space Wiki (mentioned above) and y own knowledge of the Discworld series.
271 |
272 | I have not altered any data from the L-Space Wiki in any way when transforming it into the JSON files. As such, the L-Space Wiki license (which is a Creative Commons Attribution ShareAlike 3.0 license) still applies.
273 |
274 | For more information on the license used by the L-Space Wiki, please see the `Data License.md` file.
275 |
--------------------------------------------------------------------------------
/src/dwCheckApi/wwwroot/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
265 |
--------------------------------------------------------------------------------