├── .dockerignore ├── .github └── workflows │ ├── ci-build-action.yml │ ├── pr-action.yml │ └── weekly-cleanup.yml ├── .gitignore ├── Code of Conduct.md ├── Data License.md ├── Dockerfile ├── LICENSE ├── README.md ├── dwCheckApi.sln ├── dwCheckApi.sln.DotSettings ├── global.json ├── logo.svg ├── src ├── dwCheckApi.Common │ ├── ConfigurationBase.cs │ ├── CorsConfiguration.cs │ ├── DatabaseConfiguration.cs │ ├── ValueNotFoundException.cs │ └── dwCheckApi.Common.csproj ├── dwCheckApi.DAL │ ├── BookService.cs │ ├── CharacterService.cs │ ├── DatabaseService.cs │ ├── IBookService.cs │ ├── ICharacterService.cs │ ├── IDatabaseService.cs │ ├── ISeriesService.cs │ ├── SeriesService.cs │ └── dwCheckApi.DAL.csproj ├── dwCheckApi.DTO │ ├── Helpers │ │ ├── BookViewModelHelper.cs │ │ ├── CharacterViewModelHelper.cs │ │ └── SeriesViewModelHelpers.cs │ ├── ViewModels │ │ ├── BaseViewModel.cs │ │ ├── BookBaseViewModel.cs │ │ ├── BookCoverViewModel.cs │ │ ├── BookViewModel.cs │ │ ├── CharacterViewModel.cs │ │ └── SeriesViewModel.cs │ └── dwCheckApi.DTO.csproj ├── dwCheckApi.Entities │ ├── BaseAuditClass.cs │ ├── Book.cs │ ├── BookCharacter.cs │ ├── BookSeries.cs │ ├── Character.cs │ ├── Series.cs │ └── dwCheckApi.Entities.csproj ├── dwCheckApi.Persistence │ ├── ChangeTrackerExtensions.cs │ ├── DwContextExtensions.cs │ ├── DwContextFactory.cs │ ├── Helpers │ │ ├── BookCharacterSeedData.cs │ │ ├── BookSeriesSeedData.cs │ │ └── DatabaseSeeder.cs │ ├── IDwContext.cs │ ├── Migrations │ │ ├── 20170826014619_InitialMigration.Designer.cs │ │ ├── 20170826014619_InitialMigration.cs │ │ └── DwContextModelSnapshot.cs │ ├── ModelBuilderExtensions.cs │ ├── dwCheckApi.Persistence.csproj │ └── dwContext.cs └── dwCheckApi │ ├── ConfigureContainerExtensions.cs │ ├── ConfigureHttpPipelineExtension.cs │ ├── Controllers │ ├── BaseController.cs │ ├── BooksController.cs │ ├── CharactersController.cs │ ├── DatabaseController.cs │ ├── NotFoundController.cs │ ├── SeriesController.cs │ └── VersionController.cs │ ├── Helpers │ ├── CommonHelpers.cs │ └── SecretChecker.cs │ ├── SeedData │ ├── BookCharacterSeedData.json │ ├── BookSeedData.json │ └── SeriesBookSeedData.json │ ├── appsettings.Production.json │ ├── appsettings.json │ ├── dwCheckApi.csproj │ ├── dwDatabase.db-shm │ ├── dwDatabase.db-wal │ ├── favicon.ico │ ├── program.cs │ ├── startup.cs │ ├── web.config │ └── wwwroot │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── html_code.html │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest └── tests ├── dwCheckApi.Common.Tests ├── ConfigurationBaseTests.cs ├── CorsConfigurationBaseTests.cs ├── DatabaseConfigurationTests.cs ├── TestableConfigurationBase.cs ├── TestableCorsConfiguration.cs ├── Usings.cs ├── appsettings.Tests.json └── dwCheckApi.Common.Tests.csproj └── dwCheckApi.Tests ├── DatabaseSeederTests.cs ├── Helpers ├── CommonHelperTests.cs └── SecretCheckerTests.cs ├── SeedData └── TestBookSeedData.json ├── ViewModelMappers ├── BookViewModelMapperTests.cs ├── CharacterViewModelMapperTests.cs └── SeriesViewModelMapperTests.cs └── dwCheckApi.Tests.csproj /.dockerignore: -------------------------------------------------------------------------------- 1 | **/bin/ 2 | **/obj/ 3 | .idea/ 4 | .vscode/ 5 | *.md 6 | *.yml 7 | *.userprefs -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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/) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | Buy Me A Coffee 16 | 17 | ## Code Triage Status 18 | 19 | [![Code Triagers Badge](https://www.codetriage.com/gaprogman/dwcheckapi/badges/users.svg)](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 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dwCheckApi.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.x" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /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.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.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.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.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.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.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/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.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.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 | } -------------------------------------------------------------------------------- /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.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.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 | } -------------------------------------------------------------------------------- /src/dwCheckApi.DAL/dwCheckApi.DAL.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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.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.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.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.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.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.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.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.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 | } -------------------------------------------------------------------------------- /src/dwCheckApi.DTO/dwCheckApi.DTO.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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.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 | } -------------------------------------------------------------------------------- /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.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.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.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/dwCheckApi.Entities.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | 5 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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.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/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.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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | ] -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /src/dwCheckApi/dwDatabase.db-shm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/dwDatabase.db-shm -------------------------------------------------------------------------------- /src/dwCheckApi/dwDatabase.db-wal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/dwDatabase.db-wal -------------------------------------------------------------------------------- /src/dwCheckApi/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/favicon.ico -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/wwwroot/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/wwwroot/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/wwwroot/apple-touch-icon.png -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/wwwroot/favicon-16x16.png -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/wwwroot/favicon-32x32.png -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/html_code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaProgMan/dwCheckApi/36d29e53dd5b0753d0932ce0a6a3c3334dabb80c/src/dwCheckApi/wwwroot/mstile-150x150.png -------------------------------------------------------------------------------- /src/dwCheckApi/wwwroot/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 153 | 155 | 156 | 157 | 159 | 163 | 165 | 167 | 168 | 216 | 217 | 218 | 221 | 222 | 224 | 226 | 227 | 229 | 230 | 232 | 233 | 237 | 239 | 241 | 242 | 244 | 246 | 248 | 249 | 250 | 251 | 256 | 258 | 259 | 260 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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.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/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 | } -------------------------------------------------------------------------------- /tests/dwCheckApi.Common.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | ] -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------