Initial commit

This commit is contained in:
github-classroom[bot] 2025-06-03 15:21:16 +00:00 committed by GitHub
commit 49d2d70965
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 4912 additions and 0 deletions

3551
BooksApp/.editorconfig Normal file

File diff suppressed because it is too large Load diff

583
BooksApp/.gitignore vendored Normal file
View file

@ -0,0 +1,583 @@
# Created by https://www.toptal.com/developers/gitignore/api/csharp,visualstudio
# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,visualstudio
### Csharp ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### VisualStudio ###
# User-specific files
# User-specific files (MonoDevelop/Xamarin Studio)
# Mono auto generated files
# Build results
# Visual Studio 2015/2017 cache/options directory
# Uncomment if you have tasks that create the project's static files in wwwroot
# Visual Studio 2017 auto generated files
# MSTest test Results
# NUnit
# Build Results of an ATL Project
# Benchmark Results
# .NET Core
# ASP.NET Scaffolding
# StyleCop
# Files built by Visual Studio
# Chutzpah Test files
# Visual C++ cache files
# Visual Studio profiler
# Visual Studio Trace Files
# TFS 2012 Local Workspace
# Guidance Automation Toolkit
# ReSharper is a .NET coding add-in
# TeamCity is a build add-in
# DotCover is a Code Coverage Tool
# AxoCover is a Code Coverage Tool
# Coverlet is a free, cross platform Code Coverage Tool
# Visual Studio code coverage results
# NCrunch
# MightyMoose
# Web workbench (sass)
# Installshield output folder
# DocProject is a documentation generator add-in
# Click-Once directory
# Publish Web Output
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
# NuGet Packages
# NuGet Symbol Packages
# The packages folder can be ignored because of Package Restore
# except build/, which is used as an MSBuild target.
# Uncomment if necessary however generally it will be regenerated when needed
# NuGet v3's project.json files produces more ignorable files
# Microsoft Azure Build Output
# Microsoft Azure Emulator
# Windows Store app package directories and files
# Visual Studio cache files
# files ending in .cache can be ignored
# but keep track of directories ending in .cache
# Others
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
# RIA/Silverlight projects
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
# SQL Server files
# Business Intelligence projects
# Microsoft Fakes
# GhostDoc plugin setting file
# Node.js Tools for Visual Studio
# Visual Studio 6 build log
# Visual Studio 6 workspace options file
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
# Paket dependency manager
# FAKE - F# Make
# CodeRush personal settings
# Python Tools for Visual Studio (PTVS)
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
# Telerik's JustMock configuration file
# BizTalk build output
# OpenCover UI analysis results
# Azure Stream Analytics local run output
# MSBuild Binary and Structured Log
# NVidia Nsight GPU debugger configuration file
# MFractors (Xamarin productivity tool) working folder
# Local History for Visual Studio
# Visual Studio History (VSHistory) files
# BeatPulse healthcheck temp database
# Backup folder for Package Reference Convert tool in Visual Studio 2017
# Ionide (cross platform F# VS Code tools) working folder
# Fody - auto-generated XML schema
# VS Code files for those working on multiple tools
# Local History for Visual Studio Code
# Windows Installer files from build outputs
# JetBrains Rider
### VisualStudio Patch ###
# Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/csharp,visualstudio

View file

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Using Include="FluentAssertions" />
<Using Include="Xunit" />
<Using Include="NSubstitute" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="8.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Books\Books.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\Data\*.csv" Link="Data\%(RecursiveDir)%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

31
BooksApp/Books.sln Normal file
View file

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33103.184
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Books", "Books\Books.csproj", "{7F30E637-BAFE-42FA-A173-F42B3902ED3B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Books.Test", "Books.Test\Books.Test.csproj", "{161D9B2A-4E8B-43B6-A77E-40BED559521F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7F30E637-BAFE-42FA-A173-F42B3902ED3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F30E637-BAFE-42FA-A173-F42B3902ED3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F30E637-BAFE-42FA-A173-F42B3902ED3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F30E637-BAFE-42FA-A173-F42B3902ED3B}.Release|Any CPU.Build.0 = Release|Any CPU
{161D9B2A-4E8B-43B6-A77E-40BED559521F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{161D9B2A-4E8B-43B6-A77E-40BED559521F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{161D9B2A-4E8B-43B6-A77E-40BED559521F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{161D9B2A-4E8B-43B6-A77E-40BED559521F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D322E690-379C-4802-8F7E-1AC6AD090D59}
EndGlobalSection
EndGlobal

29
BooksApp/Books/Book.cs Normal file
View file

@ -0,0 +1,29 @@
namespace Books;
public sealed record Book(string Title, string Author, string Publisher, int Year, string ISBN)
{
public bool CheckISBNValid()
{
try
{
CheckISBNValid(ISBN);
return true;
}
catch (ISBNValidationException ex)
{
Console.WriteLine(ex.Message);
return false;
}
}
public static void CheckISBNValid(string isbn)
{
const int Length = 13;
throw new NotImplementedException();
}
}
// TODO ISBNValidationException

View file

@ -0,0 +1,74 @@
using Books.DataLoading;
namespace Books;
public sealed class BookStore(LoaderType loaderType, bool skipInvalids)
{
private const string BooksCsvFilePath = "Data/books.csv";
private const string BooksWebEndpoint = "http://localhost:5000/books";
private readonly ISet<Book> _allBooks = new HashSet<Book>();
private readonly IDictionary<string, ISet<Book>> _booksByAuthor = new Dictionary<string, ISet<Book>>();
private readonly IDictionary<string, ISet<Book>> _booksByPublisher = new Dictionary<string, ISet<Book>>();
private readonly IDictionary<int, ISet<Book>> _booksByYear = new Dictionary<int, ISet<Book>>();
private bool _dataLoaded = false;
public async ValueTask<IReadOnlyCollection<Book>> GetAllBooksAsync(bool orderByYearDescending = false)
{
throw new NotImplementedException();
}
public async ValueTask<IReadOnlyCollection<Book>> GetBooksByAuthorAsync(
string author, bool orderByYearDescending = false)
{
throw new NotImplementedException();
}
public async ValueTask<IReadOnlyCollection<Book>> GetBooksByPublisherAsync(
string publisher, bool orderByYearDescending = false)
{
throw new NotImplementedException();
}
public async ValueTask<IReadOnlyCollection<Book>> GetBooksByYearAsync(
int year)
{
throw new NotImplementedException();
}
private static IReadOnlyCollection<Book> GetSortedList(IEnumerable<Book> unordered, bool orderByYearDescending)
{
throw new NotImplementedException();
}
private ValueTask EnsureDataLoaded() => _dataLoaded ? ValueTask.CompletedTask : LoadDataAsync();
private async ValueTask LoadDataAsync()
{
throw new NotImplementedException();
}
private void PopulateBookCollections(IBookLoader loader)
{
throw new NotImplementedException();
}
}
public enum LoaderType
{
Web,
Csv
}
public sealed class BookByYearComparer(bool descending) : IComparer<Book>
{
public int Compare(Book? x, Book? y) =>
descending
? CompareAsc(x, y) * -1
: CompareAsc(x, y);
private static int CompareAsc(Book? x, Book? y)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Content Include="..\Data\*.csv" Link="Data\%(RecursiveDir)%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="HTLLeonding.Utility.LeoAnalyzers" Version="1.0.2" />
<PackageReference Include="Spectre.Console" Version="0.50.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,28 @@
namespace Books.DataLoading;
public sealed class CsvDataLoader : LoaderBase
{
private FileStream? _fileStream;
public CsvDataLoader(string filePath)
{
throw new NotImplementedException();
}
public override ValueTask DisposeAsync() => throw new NotImplementedException();
public override async ValueTask LoadAsync()
{
throw new NotImplementedException();
}
private async ValueTask<IReadOnlyCollection<Book>> LoadBooksAsync()
{
const char Separator = ';';
const int ExpectedNumberOfValues = 5;
throw new NotImplementedException();
}
}
// TODO CsvProcessingException

View file

@ -0,0 +1,9 @@
namespace Books.DataLoading;
public interface IBookLoader : IAsyncDisposable, IEnumerable<Book>
{
public ValueTask LoadAsync();
public bool LoadingDone { get; }
}
// TODO NotLoadedException

View file

@ -0,0 +1,20 @@
using System.Collections;
namespace Books.DataLoading;
public abstract class LoaderBase : IBookLoader
{
protected IReadOnlyCollection<Book>? Books;
public IEnumerator<Book> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public abstract ValueTask DisposeAsync();
public abstract ValueTask LoadAsync();
public bool LoadingDone { get; protected set; }
}

View file

@ -0,0 +1,30 @@
using System.Net.Http.Json;
namespace Books.DataLoading;
public sealed class WebDataLoader(string endpointUrl, IHttpClientFactory httpClientFactory) : LoaderBase
{
private readonly HttpClient? _httpClient = httpClientFactory.CreateClient();
public override ValueTask DisposeAsync()
{
throw new NotImplementedException();
}
public override async ValueTask LoadAsync()
{
throw new NotImplementedException();
}
}
// TODO WebProcessingException
public interface IHttpClientFactory
{
public HttpClient CreateClient();
}
public sealed class HttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient() => new();
}

70
BooksApp/Books/Program.cs Normal file
View file

@ -0,0 +1,70 @@
using System.Text;
using Books;
using Spectre.Console;
Console.OutputEncoding = Encoding.UTF8;
const string CsvChoice = "csv";
const string WebChoice = "web";
const string AuthorChoice = "author";
const string PublisherChoice = "publisher";
const string YearChoice = "year";
const string AscChoice = "asc";
const string DescChoice = "desc";
AnsiConsole.Write(new FigletText("BOOKS")
.Centered()
.Color(Color.Red));
string source = AnsiConsole.Prompt(new SelectionPrompt<string>()
.Title("Please choose the source of books")
.AddChoices(CsvChoice, WebChoice));
string filter = AnsiConsole.Prompt(new SelectionPrompt<string>()
.Title("Please choose the filter type")
.AddChoices(AuthorChoice, PublisherChoice, YearChoice));
string? filterValue = null;
int filterYear = -1;
string order = AscChoice;
if (filter == YearChoice)
{
do
{
filterYear = AnsiConsole.Ask<int>("Please enter the year (4 digits, 0-2100):");
} while (filterYear is < 0 or > 2100);
}
else
{
filterValue = AnsiConsole.Ask<string>($"Please enter the {filter}:");
order = AnsiConsole.Prompt(new SelectionPrompt<string>()
.Title("Please choose the order")
.AddChoices(AscChoice, DescChoice));
}
var bookStore = new BookStore(source == CsvChoice ? LoaderType.Csv : LoaderType.Web, true);
bool descending = order == DescChoice;
IReadOnlyCollection<Book> results = filter switch
{
AuthorChoice when filterValue is not null => await bookStore
.GetBooksByAuthorAsync(filterValue, descending),
PublisherChoice when filterValue is not null => await bookStore
.GetBooksByPublisherAsync(filterValue, descending),
YearChoice => await bookStore.GetBooksByYearAsync(filterYear),
_ => throw new ArgumentOutOfRangeException(nameof(filter))
};
var table = new Table()
.AddColumn("Title")
.AddColumn("Author")
.AddColumn("Publisher")
.AddColumn("Year")
.AddColumn("ISBN");
foreach (var book in results)
{
table.AddRow(book.Title, book.Author, book.Publisher, book.Year.ToString(), book.ISBN);
}
AnsiConsole.Write(table);
Console.ReadKey();

31
BooksApp/Data/books.csv Normal file
View file

@ -0,0 +1,31 @@
Title;Author;Publisher;Year;ISBN
To Kill a Mockingbird;Harper Lee;J. B. Lippincott & Co.;1960;9780965773607
1984;George Orwell;Secker & Warburg;1949;9780436410550
The Great Gatsby;F. Scott Fitzgerald;Charles Scribner's Sons;1925;9780684717609
The Catcher in the Rye;J.D. Salinger;Little, Brown and Company;1951;9780316769488
The Hobbit;J.R.R. Tolkien;Allen & Unwin;1937;9780048231543
The Lord of the Rings;J.R.R. Tolkien;Allen & Unwin;1954;9780048230454
Brave New World;Aldous Huxley;Chatto & Windus;1932;9780701107918
Moby-Dick;Herman Melville;Harper & Brothers;1851;9781122714389
War and Peace;Leo Tolstoy;The Russian Messenger;1869;9781535299534
Crime and Punishment;Fyodor Dostoevsky;The Russian Messenger;1866;9789352763160
Pride and Prejudice;Jane Austen;T. Egerton, Whitehall;1813;9781532995842
The Odyssey;Homer;Penguin Classics;-800;9780140449112
One Hundred Years of Solitude;Gabriel García Márquez;Harper & Row;1967;9780060114183
Ulysses;James Joyce;Sylvia Beach;1922;9783518472279
Don Quixote;Miguel de Cervantes;Francisco de Robles;1605;9798595886857
Madame Bovary;Gustave Flaubert;Michel Lévy Frères;1857;9781543023039
Wuthering Heights;Emily Brontë;Thomas Cautley Newby;1847;9781549501296
The Brothers Karamazov;Fyodor Dostoevsky;The Russian Messenger;1880;9780553212167
Anna Karenina;Leo Tolstoy;The Russian Messenger;1877;9781535299008
The Adventures of Huckleberry Finn;Mark Twain;Charles L. Webster and Company;1884;9780486443225
The Iliad;Homer;Penguin Classics;-762;9780140445923
Hamlet;William Shakespeare;Folger;1603;9781451669411
The Divine Comedy;Dante Alighieri;Benedetto Cairoli;1320;9780451208637
Middlemarch;George Eliot;William Blackwood and Sons;1871;9781362391845
Remembrance of Things Past;Marcel Proust;Grasset and Gallimard;1913;9782070754922
Great Expectations;Charles Dickens;Chapman & Hall;1861;9781503275188
Gulliver's Travels;Jonathan Swift;Benjamin Motte;1726;9783150108215
Bleak House;Charles Dickens;Bradbury & Evans;1853;9780141439723
Les Misérables;Victor Hugo;A. Lacroix, Verboeckhoven & Ce;1862;9781271955978
The Sound and the Fury;William Faulkner;Jonathan Cape and Harrison Smith;1929;9789123412167
1 Title Author Publisher Year ISBN
2 To Kill a Mockingbird Harper Lee J. B. Lippincott & Co. 1960 9780965773607
3 1984 George Orwell Secker & Warburg 1949 9780436410550
4 The Great Gatsby F. Scott Fitzgerald Charles Scribner's Sons 1925 9780684717609
5 The Catcher in the Rye J.D. Salinger Little, Brown and Company 1951 9780316769488
6 The Hobbit J.R.R. Tolkien Allen & Unwin 1937 9780048231543
7 The Lord of the Rings J.R.R. Tolkien Allen & Unwin 1954 9780048230454
8 Brave New World Aldous Huxley Chatto & Windus 1932 9780701107918
9 Moby-Dick Herman Melville Harper & Brothers 1851 9781122714389
10 War and Peace Leo Tolstoy The Russian Messenger 1869 9781535299534
11 Crime and Punishment Fyodor Dostoevsky The Russian Messenger 1866 9789352763160
12 Pride and Prejudice Jane Austen T. Egerton, Whitehall 1813 9781532995842
13 The Odyssey Homer Penguin Classics -800 9780140449112
14 One Hundred Years of Solitude Gabriel García Márquez Harper & Row 1967 9780060114183
15 Ulysses James Joyce Sylvia Beach 1922 9783518472279
16 Don Quixote Miguel de Cervantes Francisco de Robles 1605 9798595886857
17 Madame Bovary Gustave Flaubert Michel Lévy Frères 1857 9781543023039
18 Wuthering Heights Emily Brontë Thomas Cautley Newby 1847 9781549501296
19 The Brothers Karamazov Fyodor Dostoevsky The Russian Messenger 1880 9780553212167
20 Anna Karenina Leo Tolstoy The Russian Messenger 1877 9781535299008
21 The Adventures of Huckleberry Finn Mark Twain Charles L. Webster and Company 1884 9780486443225
22 The Iliad Homer Penguin Classics -762 9780140445923
23 Hamlet William Shakespeare Folger 1603 9781451669411
24 The Divine Comedy Dante Alighieri Benedetto Cairoli 1320 9780451208637
25 Middlemarch George Eliot William Blackwood and Sons 1871 9781362391845
26 Remembrance of Things Past Marcel Proust Grasset and Gallimard 1913 9782070754922
27 Great Expectations Charles Dickens Chapman & Hall 1861 9781503275188
28 Gulliver's Travels Jonathan Swift Benjamin Motte 1726 9783150108215
29 Bleak House Charles Dickens Bradbury & Evans 1853 9780141439723
30 Les Misérables Victor Hugo A. Lacroix, Verboeckhoven & Ce 1862 9781271955978
31 The Sound and the Fury William Faulkner Jonathan Cape and Harrison Smith 1929 9789123412167

BIN
pics/sample_run.mp4 Normal file

Binary file not shown.

276
readme.adoc Normal file
View file

@ -0,0 +1,276 @@
:sectnums:
:nofooter:
:toc: left
:icons: font
:data-uri:
:source-highlighter: highlightjs
= Exc.02 -- Books
Exceptions are such an easy topic that you are getting some more leeway with this exercise already.
Additionally, we'll hone some of your other skills which might have gotten a bit rusty over the last weeks.
And to top if off -- after working with it in WMC already -- you'll finally deal with `async/await` and `ValueTask` in this assignment.
You are going to process book data, which is retrieved either from a file or a webservice.
To make it easier for you both the CSV file and the web service are already done and have been provided.
== Class Diagram
Split into three parts to improve readability.
.Loading
[plantuml]
----
@startuml
hide empty members
interface IAsyncDisposable {}
interface IEnumerable<Book> {}
interface IBookLoader {
+ValueTask LoadAsync()
+bool LoadingDone [readonly]
}
abstract class LoaderBase {
#IReadOnlyCollection<Book>? Books
+bool LoadingDone [protected set]
}
class CsvDataLoader <<sealed>> {
-FileStream? _fileStream
+CsvDataLoader(string)
-ValueTask<IReadOnlyCollection<Book>> LoadBooksAsync() [async]
}
class WebDataLoader <<sealed>> {
-HttpClient? _httpClient [readonly]
-string _endpointUrl [readonly]
+WebDataLoader(string, IHttpClientFactory)
}
interface IHttpClientFactory {
+HttpClient CreateClient()
}
class HttpClientFactory <<sealed>> {}
IAsyncDisposable <|.. IBookLoader
IEnumerable <|.. IBookLoader
IBookLoader <|.. LoaderBase
IHttpClientFactory <|.. HttpClientFactory
LoaderBase <|-- CsvDataLoader
LoaderBase <|-- WebDataLoader
@enduml
----
.Exceptions
[plantuml]
----
@startuml
hide empty members
class NotLoadedException <<sealed>> {
+NotLoadedException()
}
class CsvProcessingException <<sealed>> {
+CsvProcessingException(string)
}
class WebProcessingException <<sealed>> {
+WebProcessingException(string, Exception? = null)
}
class ISBNValidationException <<sealed>> {
+ISBNValidationException(string)
}
@enduml
----
.Book Store
[plantuml]
----
@startuml
hide empty members
class Book <<record,sealed>> {
+string Title [readonly]
+string Author [readonly]
+string Publisher [readonly]
+int Year [readonly]
+string ISBN [readonly]
+bool CheckISBNValid()
{static} +void CheckISBNValid(string)
}
class BookStore <<sealed>> {
-string BooksCsvFilePath [const]
-string BooksWebEndpoint [const]
-ISet<Book> _allBooks [readonly]
-IDictionary<string, ISet<Book>> _booksByAuthor [readonly]
-IDictionary<string, ISet<Book>> _booksByPublisher [readonly]
-IDictionary<int, ISet<Book>> _booksByYear [readonly]
-LoaderType _loaderType [readonly]
-bool _skipInvalids [readonly]
-bool _dataLoaded
+BookStore(LoaderType, bool)
+ValueTask<IReadOnlyCollection<Book>> GetAllBooksAsync(bool = false) [async]
+ValueTask<IReadOnlyCollection<Book>> GetBooksByAuthorAsync(string, bool = false) [async]
+ValueTask<IReadOnlyCollection<Book>> GetBooksByPublisherAsync(string, bool = false) [async]
+ValueTask<IReadOnlyCollection<Book>> GetBooksByYearAsync(int) [async]
{static} -IReadOnlyCollection<Book> GetSortedList(IEnumerable<Book>, bool)
-ValueTask EnsureDataLoaded()
-ValueTask LoadDataAsync() [async]
-void PopulateBookCollections(IBookLoader)
}
enum LoaderType
{
Web,
Csv
}
class BookByYearComparer <<sealed>> {
-bool _descending [readonly]
+BookByYearComparer(bool)
{static} -int CompareAsc(Book?, Book?)
}
interface IComparer<Book> {}
IComparer <|.. BookByYearComparer
BookStore *-- LoaderType
BookStore o-- Book
@enduml
----
== Tasks
=== Learn about `async/await` and `ValueTask`
In WMC you are already proficient with `async/await` and `Promise`.
Now it is high time to learn how to use this concept in C# as well.
* First, read through the following https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/[article describing the concept]
** That's quite a long article, but rest assured that I _will_ check if you actually read it 😎
* Then https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/[learn why `ValueTask` is preferable over `Task`]
** This is a very 'techy' article -- if you struggle it is sufficient if you remember to use `ValueTask` instead of `Task` everywhere except where an existing API requires `Task`
TIP: Luckily `async/await` is practically the same and if you treat a `ValueTask` like a `Promise` you'll be fine.
=== Start the webservice
You've been provided with a (already completed) webservice that will provide book data.
However, to be able to use it you need to start it first -- just as you did in WMC with `json-server`.
To do that use the following command:
.Starting the server
[source,bash]
----
# make sure you are within the 'server' directory
dotnet run # alternatively run the run.ps1 script
# don't close the terminal window!
----
You can verify that the server is running correctly by opening the http://localhost:5000/books[Books Endpoint].
=== Implement the Application
You have to understand resource management (`try/finally` and/or `using`) -- if that is not the case, please consult the slides or workshop material again.
`ValueTask.CompletedTask` will also be useful.
NOTE: Since you are going to write tests & documentation yourself anyway you are _allowed_ this time to change method signatures, remove or add methods, etc. -- as long as the functionality stays the same and the code is clean, readable, follows best practices and _works_.
==== Testing
* No unit tests have been provided for this assignment
* => You have to write your own tests!
** Remember to do TDD and commit after every step
* Think about which parts can be tested, which parts can't and where a clever mock can help you out
* A unit test project has already been created for you and sample data is linked
==== Book
* Represents a simple book with some common properties and an https://en.wikipedia.org/wiki/ISBN[ISBN]
* It has a _property_ which allows to validate the ISBN of the instance
* And it has a _static_ method which allows to validate any ISBN
** You _only_ have to assume ISBN-13 format
** The validation algorithm is described https://en.wikipedia.org/wiki/ISBN#ISBN-13_check_digit_calculation[here]
===== Exceptions
You have to throw the following `ISBNValidationException`:
* ISBN is empty
* ISBN has a length other than 13
* ISBN contains non-numeric characters
* ISBN has an invalid check digit
==== Loader Base
* Throws a `NotLoadedException` if the enumerator is retrieved without `LoadAsync` being called (and successfully executed) first
==== CSV Loader
* _Not_ the same as all those CSV readers we've written before!
* This time you _have_ to use a https://learn.microsoft.com/en-us/dotnet/api/system.io.filestream?view=net-9.0[`FileStream`] and close it properly in the `DisposeAsync` method
** And it has to be readonly
* Hints:
** You will need both `FileMode.Open` & `FileAccess.Read`
** You are allowed to use a https://learn.microsoft.com/en-us/dotnet/api/system.io.streamreader?view=net-9.0[`StreamReader`] with the `FileStream` which will make consuming the stream much easier
*** `using var reader = ...`
** Use the 'Async' versions of the methods and don't forget to `await` them
===== Exceptions
You have to throw the following exceptions:
* `FileNotFoundException` if the file does not exist
** Make sure to use the correct constructor (message and filename)
* `InvalidOperationException` if loading is attempted a second time
* `CsvProcessingException` if
** The file stream is null
** A null line is read from the stream before reaching EOF
** A line contains the wrong number of fields (columns)
** A value (in a column) is null or empty
** The year value is not a valid integer
==== Web Loader
* Uses a `HttpClient` to _get_ data from the webservice
** Retrieved from the `IHttpClientFactory` during construction
** Properly disposed in the `DisposeAsync` method
* Use the `GetFromJsonAsync<Book[]>` method to retrieve the data
===== Exceptions
You have to throw the following exceptions:
* `InvalidOperationException` if loading is attempted a second time
* `WebProcessingException` if
** The `HttpClient` is null when loading data
** Any exception occurs during the `GetFromJsonAsync` call (wrap as inner exception)
==== Book Store
* The book store(age) uses _either_ a `CsvLoader` or a `WebLoader` to load the data
* Data is only loaded on first request (and then cached)
** We _avoid_ long-running operations in the constructor, which a file read or even web request would be
* Upon loading the data is put into quick access collections
** By author
** By publisher
** By year
* The user can access the books in the store by these paths (or get all)
* The store will check for every book if its ISBN is valid -- the flag `skipInvalids` determines if invalid books are skipped (= not included in the catalogue) or not (kept, despite being invalid)
* You _will_ need to do `await using IBookLoader loader = ...`
** Make sure you understand why we have to do `await using` instead of `using` here
===== Comparer
One more time: create a comparer for books which can be used to sort them by year.
Mind that, as usual, the user can specify ascending or descending order.
=== Write the documentation
Pretty straight forward: Write the complete XMLDoc for the application (all non-private members).
=== Sample Run
`Program` is already complete.
Here is a simple run of the application:
video::pics/sample_run.mp4[Sample Run,width=800]
WARNING: Application won't compile initially, you have to create some types and assign some fields first.

10
server/BooksServer.csproj Normal file
View file

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>

62
server/Program.cs Normal file
View file

@ -0,0 +1,62 @@
const string CorsPolicy = "_corsAllowAll";
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(CorsPolicy,
b =>
{
b.AllowAnyOrigin();
b.AllowAnyMethod();
b.AllowAnyHeader();
});
});
var app = builder.Build();
app.UseCors(CorsPolicy);
Book[] books =
[
new ("To Kill a Mockingbird", "Harper Lee", "J. B. Lippincott & Co.", 1960, "9780965773607"),
new ("1984", "George Orwell", "Secker & Warburg", 1949, "9780436410550"),
new ("The Great Gatsby", "F. Scott Fitzgerald", "Charles Scribner's Sons", 1925, "9780684717609"),
new ("The Catcher in the Rye", "J.D. Salinger", "Little, Brown and Company", 1951, "9780316769488"),
new ("The Hobbit", "J.R.R. Tolkien", "Allen & Unwin", 1937, "9780048231543"),
new ("The Lord of the Rings", "J.R.R. Tolkien", "Allen & Unwin", 1954, "9780048230454"),
new ("Brave New World", "Aldous Huxley", "Chatto & Windus", 1932, "9780701107918"),
new ("Moby-Dick", "Herman Melville", "Harper & Brothers", 1851, "9781122714389"),
new ("War and Peace", "Leo Tolstoy", "The Russian Messenger", 1869, "9781535299534"),
new ("Crime and Punishment", "Fyodor Dostoevsky", "The Russian Messenger", 1866, "9789352763160"),
new ("Pride and Prejudice", "Jane Austen", "T. Egerton, Whitehall", 1813, "9781532995842"),
new ("The Odyssey", "Homer", "Penguin Classics", -800, "9780140449112"),
new ("One Hundred Years of Solitude", "Gabriel García Márquez", "Harper & Row", 1967, "9780060114183"),
new ("Ulysses", "James Joyce", "Sylvia Beach", 1922, "9783518472279"),
new ("Don Quixote", "Miguel de Cervantes", "Francisco de Robles", 1605, "9798595886857"),
new ("Madame Bovary", "Gustave Flaubert", "Michel Lévy Frères", 1857, "9781543023039"),
new ("Wuthering Heights", "Emily Brontë", "Thomas Cautley Newby", 1847, "9781549501296"),
new ("The Brothers Karamazov", "Fyodor Dostoevsky", "The Russian Messenger", 1880, "9780553212167"),
new ("Anna Karenina", "Leo Tolstoy", "The Russian Messenger", 1877, "9781535299008"),
new ("The Adventures of Huckleberry Finn", "Mark Twain", "Charles L. Webster and Company", 1884, "9780486443225"),
new ("The Iliad", "Homer", "Penguin Classics", -762, "9780140445923"),
new ("Hamlet", "William Shakespeare", "Folger", 1603, "9781451669411"),
new ("The Divine Comedy", "Dante Alighieri", "Benedetto Cairoli", 1320, "9780451208637"),
new ("Middlemarch", "George Eliot", "William Blackwood and Sons", 1871, "9781362391845"),
new ("Remembrance of Things Past", "Marcel Proust", "Grasset and Gallimard", 1913, "9782070754922"),
new ("Great Expectations", "Charles Dickens", "Chapman & Hall", 1861, "9781503275188"),
new ("Gulliver's Travels", "Jonathan Swift", "Benjamin Motte", 1726, "9783150108215"),
new ("Bleak House", "Charles Dickens", "Bradbury & Evans", 1853, "9780141439723"),
new ("Les Misérables", "Victor Hugo", "A. Lacroix, Verboeckhoven & Ce", 1862, "9781271955978"),
new ("The Sound and the Fury", "William Faulkner", "Jonathan Cape and Harrison Smith", 1929, "9789123412167"),
new ("Frankenstein", "Mary Shelley", "Lackington, Hughes, Harding, Mavor & Jones", 1818, "9780441439472"),
new ("The Picture of Dorian Gray", "Oscar Wilde", "Ward, Lock and Company", 1890, "9780241439571")
];
app.MapGet("/books", () =>
{
return books.OrderBy(b => b.Year);
});
await app.RunAsync();
public sealed record Book(string Title, string Author, string Publisher, int Year, string ISBN);

4
server/books.http Normal file
View file

@ -0,0 +1,4 @@
@Host = http://localhost:5000
### Get all books
GET {{Host}}/books

40
server/run.ps1 Normal file
View file

@ -0,0 +1,40 @@
$dotnetVersionStr = (& dotnet --version).Trim()
try {
$dotnetVersion = [version]$dotnetVersionStr
} catch {
Write-Error "Unable to parse dotnet version: $dotnetVersionStr"
exit 1
}
$csprojFile = Get-ChildItem -Path . -Filter *.csproj | Select-Object -First 1
if (-not $csprojFile) {
Write-Host "No csproj file found in the current directory."
exit 1
}
[xml]$csprojXml = Get-Content $csprojFile.FullName
$targetFramework = $csprojXml.Project.PropertyGroup.TargetFramework
if (-not $targetFramework) {
Write-Host "TargetFramework element not found in $($csprojFile.Name)."
exit 1
}
if ($targetFramework -match 'net(\d+(\.\d+)?)') {
$requiredVersionStr = $matches[1]
try {
$requiredVersion = [version]$requiredVersionStr
} catch {
Write-Host "Unable to parse required version: $requiredVersionStr"
exit 1
}
} else {
Write-Host "Unrecognized TargetFramework format: $targetFramework"
exit 1
}
if ($dotnetVersion -lt $requiredVersion) {
Write-Warning "Your dotnet version ($dotnetVersion) is outdated. Go to https://get.dot.net and download the latest SDK"
} else {
Write-Host "Your dotnet version ($dotnetVersion) is up to date"
Write-Host "Starting the server - keep the console window open!"
dotnet run
}