Czy sposób w jaki to zrobiłem jest odpowiedni, czy da się coś poprawić?
Nie ma czegoś co da się nazwać kodem idealnym więc poprawiać można w nieskończoność, należy sobie zadać pytanie czy się to opłaca. Tutaj chyba jeszcze tak.
Metoda Index kontrolera ma zahardkowaną tabelę C. Dlaczego nie przekażesz tego z frontu? Poza tym, z punktu widzenia użytkownika to nie wiem czy ma to wielki sens. Gdybym to ja odpalał aplikację to chciałbym zobaczyć pewnie całą listę lub jakąś konkretną walutę albo ich zestaw, Więc na twoim miejscu pobrałbym wszystkie a potem przedstawiał użytkownikom w określonym formacie.
Co do samego pobierania i zapisu - CurrencySeeder pobiera kursy tylko wtedy kiedy nie ma niczego w bazie. To znaczy, że po godzinie, na drugi dzień czy za miesiąc aplikacja będzie przechowywać stare, już dawno nieaktualne kursy. Co ciekawe, kontroler pobiera kursy uzywając implementacji ICurrencyService z całkowitym pominięciem bazy. Może lepiej wykonać zapis i przechowywać historię kursów? A skoro masz bazę - dlaczego nie sprawdzić czy to co zostało już pobrane i jest w bazie danych jest jeszcze aktualne i jeśli tak to zwrócić najnowszy zestaw z bazy (a jeśli nie to strzelić do API NBP, pobrać nowe, zapisać i zwrócić?).
Co do samego trybu pobierania - jeśli ta apka ma działać bez przerw to może zamiast uruchamiać zadanie pobierania kursów w czasie jej uruchomienia to byłoby lepiej zaimplementować BackgroundService który będzie robić to cyklicznie (https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-6.0&tabs=visual-studio)?
ICurrencyService - po co ci ten interfejs? Z tego co widzę to nie jest on ani trochę potrzebny, przynajmniej w stanie obecnym.
Brak testów - testować możesz zarówno backend jak i logikę w js (tego akurat nie używasz), może też jakieś testy UI?
Katalog Models - dlaczego trzymasz modele bazodanowe z view modelem reprezentującym błąd? Dlaczego kontroler zwraca typ bazodanowy zamiast wyłącznie tego co jest potrzebne na widoku?
Poza tym pomyśleć o rozbiciu klas na mniejsze, tak aby miały wyłącznie jedną odpowiedzialność. Na przykład CurrencyService, tutaj mamy pobieranie konfiguracji (o tym w ogóle za chwilę), tworzenie urla, pobranie odpowiedzi i jej deserializacja. Konfigurację można przekazać w dużo lepszy sposób, wstrzyknąć jako konkretny typ utworzony z IConfiguration albo użyć patternu IOptions (https://docs.microsoft.com/en-us/dotnet/core/extensions/options). Sama obsługa sprawdzenia poprawności odpowiedzi (czego u ciebie nie ma) i jej deserializacja może być zaimplementowana generycznie. Pobranie danych z api nbp również. W ten sposób CurrencyService byłby okrojony wyłącznie do jednej, jedynej odpowiedzialności a mianowicie definiowania procesu. Sam w sobie nie robiłby nic innego jak definiował sekwencję wywołania metod różnych klas tak aby ostatecznie zwrócić to co ma zostać zwrócone. A te mniejsze klasy zajmowałyby się właśnie pobieraniem danych, obsługą i deserializacją odpowiedzi, generowanie odpowiedniego urla też raczej warto wyciągnąć do dedykowanego typu.
CurrencySeeder - nie wiem czy warto wykonywać metodę _context.Database.CanConnectAsync() bo można po prostu spróbować wykonać odczyt. Tu ponownie wraca problem braku obsługi błędów.
Zastanawiałeś się nad dodaniem logów?
Generalnie można tu zrobić jeszcze całkiem sporo, jeśli to apka na której się uczysz i nie masz parcia na pisanie czegoś nowego to warto w tą zainwestować trochę czasu.