2425-2ihif-pose-classroom-ex-ex-02-books-ex-ex-02-books-template created by GitHub Classroom
Find a file
github-classroom[bot] 91a3c7b249
add deadline
2025-06-05 12:38:35 +00:00
BooksApp Initial commit 2025-06-03 15:21:16 +00:00
pics Initial commit 2025-06-03 15:21:16 +00:00
server Initial commit 2025-06-03 15:21:16 +00:00
readme.adoc add deadline 2025-06-05 12:38:35 +00:00

[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/mMz_WsqS)
: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.