2425-2ihif-pose-classroom-ex-ex-02-books-ex-ex-02-books-template created by GitHub Classroom
| BooksApp | ||
| pics | ||
| server | ||
| readme.adoc | ||
[](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.