276 lines
No EOL
9.5 KiB
Text
276 lines
No EOL
9.5 KiB
Text
: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. |