# MVC arhitektura Ovaj je materijal dio ishoda učenja 4 (željeno). ## 13 Pregled - Višeslojna arhitektura - AutoMapper - Servisi i repozitoriji ### 13.1 Postavke vježbe **Postavljanje SQL poslužitelja** U SQL Server Management Studiju učinite sljedeće: - preuzmite skriptu: https://pastebin.com/jtJfak9E - u skripti **promijenite naziv baze podataka u Exercise13 i koristite tu bazu** - izvršite je da biste stvorili bazu podataka, njezinu strukturu i neke testne podatke - execute additional script: https://pastebin.com/SeHBs1BA **Starter projekt** > Sljedeće je već dovršeno kao starter projekt: > > - Postavljeni modeli i repozitorij > - Podešen "Launch settings" > - Stvoreni osnovni CRUD prikazi i funkcionalnost (Genre, Artist, Song) > - Implementirana validacija i označavanje korištenjem viewmodela > - Autentifikacija i autorizacija > > Za detalje pogledajte prethodne vježbe. Raspakirajte starter arhivu i otvorite rješenje u Visual Studiju. Postavite connection string i pokrenite aplikaciju. Provjerite radi li aplikacija (npr. navigacija, popis pjesama, dodavanje nove pjesme). > U slučaju da ne radi, provjerite jeste li ispravno slijedili upute. ### 13.2 Postavljanje višeslojne arhitekture U višeslojnoj arhitekturi, jedan sloj zavisi o drugom. ASP.NET rješenje implementira ovo ponašanje u obliku projekata - zavisnosti su projekti. - Stvorite projekt `Class Library` u svom rješenju pod nazivom `ex13.BL`. Koristite istu verziju programskog okvira (engl. framework). - Ovaj će projekt biti onaj o kojem ovisi vaš glavni projekt, pa ga dodajte zavisnostima o glavnom projektu. - Taj će novi projekt biti onaj koji izravno koristi bazu podataka, stoga instalirajte podršku za bazu podataka za taj projekt. ``` dotnet add package Microsoft.EntityFrameworkCore --version 7 dotnet add package Microsoft.EntityFrameworkCore.Design --version 7 dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7 ``` - EF connection string i dalje je konfiguriran u `appsettings.json` glavnog projekta, tako da ga ne morate ponovno konfigurirati - Također, `Program.cs` glavnog projekta već sadrži postavke servisa, tako da ne morate to raditi ponovno - Morate ponovno pokrenuti reverse engineering konteksta baze podataka i modela u vašem novom projektu. **Obratite pozornost na to da promjenite direktorij na taj projekt, a ne na glavni.** ``` dotnet ef dbcontext scaffold "{your-full-connection-string}" Microsoft.EntityFrameworkCore.SqlServer -o Models --force ``` > Napomena: name-referenca ovdje ne bi funkcionirala jer alat reverse engineering ne zna za konfiguraciju u glavnom projektu. Upotrijebite cijeli connection string i zamijenite ga name-referencom u db kontekstu nakon toga. - Uklonite mapu `Models` s njezinim sadržajem iz glavnog projekta - Sada ćete morati zamijeniti direktive `using` za `exercise_13.Models` s `ex13.BL.Models`, budući da su vaši modeli baze podataka u potpuno drugom imenskom prostoru koji je u novom projektu - Također, riješite se `ErrorViewModel` modela i njegovog prikaza Testirajte i potvrdite da vaša aplikacija i dalje radi. Sada ona koristi zasebni sloj za pristup bazi podataka. ### 13.3 Korištenje višeslojne arhitekture za podršku Web API i MVC projektu Jedna od prednosti korištenja višeslojne arhitekture je mogućnost ponovne upotrebe sloja. Na primjer: - ASP.NET MVC projekt koji koristi BL sloj - ASP.NET Web API projekt koji koristi BL sloj Kreirajmo ASP.NET projekt koji koristi isti BL sloj. Bit će to ASP.NET Web API projekt. Konfiguracija koju trebate imati: - `ex13.Web` - MVC projekt, ovisi o ex13.BL - `ex13.Api` - Web API projekt, ovisi o ex13.BL - `ex13.BL` - poslovni sloj Znači: - preimenujte projekt `exercise-13` u `ex13.Web` - dodajte Web API projekt s nazivom `ex13.Api` - dodajte odgovarajuću zavisnost novom projektu Web API-ja - sada i `ex13.Web` i `ex13.Api` zavise o `ex13.BL` Za projekt `ex13.Api`: - instalirajte pakete za pristup bazi podataka - postavite connection string - u uslugama dodajte db kontekst (u `Program.cs` koristite name-referencu) - dodajte `GenreController` **Web API** kontroler u projekt - podržite db kontekst u `GenreController` - vratite žanrove iz akcije `GET Get()` Koristite Swagger kako biste bili sigurni da možete dohvatiti žanrove baze podataka. > Da biste pokrenuli novi projekt, trebate ga ili postaviti kao početni projekt ili u rješenju postaviti neka pokreće trenutno odabrani projekt (current selection). Također možete za pokretanje postaviti i više projekata i pokrenuti i Web API i MVC projekte zajedno. Koristite MVC aplikaciju za promjenu naziva žanra. Upotrijebite Web API aplikaciju da biste provjerili je li ime promijenjeno. ### 13.4 AutoMapper Ovakav kod koristi se previše puta u aplikaciji: ```C# var genreVms = _context.Genres.Select(x => new GenreVM { Id = x.Id, Name = x.Name, }).ToList(); ``` Ovdje se model baze podataka mapira u viewmodel samo da bi se proslijedio u prikaz. Postoji elegantnije rješenje - AutoMapper. Ovdje radimo sljedeće korake kako bismo omogućili AutoMapper i pojednostavili mapiranje: - instalirajte paket AutoMapper u projekt - `cd` u Vaš web projekt (još uvijek je u mapi `vježba-13`) ``` dotnet add package AutoMapper ``` > Od verzije 13 AutoMappera ne trebate zasebnu instalaciju DI paketa za AutoMapper - stvorite AutoMapper profil za mapiranje - dodajte mapu AutoMapper svom projektu - u tu mapu dodajte klasu nrp. `MappingProfile` koja nasljeđuje klasu AutoMapper.Profile ```C# using AutoMapper; namespace exercise_13.AutoMapper { public class MappingProfile : Profile { } } ``` - stvorite konstruktor klase - unutar konstruktora, stvorite zadano mapiranje iz `Genre` u `GenreVM` ```C# CreateMap(); ``` - dodajte konfiguraciju AutoMappera u `Startup.cs` ``` builder.Services.AddAutoMapper(typeof(MappingProfile)); ``` Sada se mapiranje može jednostavno napraviti na ovaj način: - proslijedite `IMapper` konstruktoru preko DI ```C# // ... private readonly IMapper _mapper; public GenreController(..., IMapper mapper) { // ... _mapper = mapper; } ``` - koristite instancu `IMapper` za izvođenje mapiranja ```C# // A single viewmodel var genreVm = _mapper.Map(genre); // ...or a collection of viewmodels var genreVms = _mapper.Map>(genres); ``` Koristite AutoMapper za podršku mapiranju kroz cijeli `GenreController`. ### 13.5 AutoMapper i konvencije imenovanja Koristite AutoMapper za podršku mapiranja iz `Audio` u `SongVM`. ```C# // In MappingProfile CreateMap(); ``` ```C# // In SongController, GET Index() var songs = _context.Audios .Include(x => x.Genre) .Include(x => x.Artist); var songVms = _mapper.Map>(songs); ``` Primijetite da se `ArtistName` i `GenreName` automatski mapiraju. To je dio konvencije, na primjer `Artist.Name` iz izvora se preslikava na `ArtistName` na odredištu. > Ta je funkcionalnost također poznata kao "ravnanje" (engl. "flattening"), što znači da se složenija struktura može transformirati u manje složenu i koristiti kao takva. ### 13.6 AutoMapper i prilagođeno mapiranje članova modela Možete primijetiti da audio oznake na izvoru mapiranja (Audio.AudioTags) nisu automatski mapirane na ID-eve odredišnih oznaka mapiranja (SongVM.TagIds). Na primjer, stranica za uređivanje pjesama omogućuje korisniku odabir više oznaka i njihovo spremanje. Ako koristimo zadano mapiranje, cijeli model neće biti mapiran i izgubit ćemo neke podatke. Rješenje je korištenje AutoMapper-ove opcije za prilagodbu `.ForMember()`. ``` CreateMap() .ForMember(dst => dst.TagIds, opt => opt.MapFrom(src => src.AudioTags.Select(x => x.TagId))); ``` > Postoji još više prilagodbi AutoMappera: > - prilagođeno mapiranje nakon mapiranja > - obrnuto mapiranje > - svojstva označena kao zanemarena > - mogućnost implementacije prilagođenog "value resolvera" > > ...i puno više: https://docs.automapper.org/en/stable/index.html ### 13.7 Servisi i repozitoriji Kao što već znate, trebali biste izbjegavati pisanje poslovne logike unutar akcija. Servisi i repozitoriji jedna su od mogućih enkapsulacija logike koda koja bi inače završila unutar akcija. Drugim riječima, u tu svrhu trebate koristiti servise i repozitorije. Servis je opća enkapsulacija _poslovne logike_. Repozitorij je enkapsulacija _operacijske logike koja se izvodi na podacima_. Sa stajališta ASP.NET Core podržane su samo usluge, a repozitoriji su onda samo usluge koje npr. izvršavaju CRUD na podacima. Implementacija i korištenje prilagođene usluge obično uključuje sljedeće korake: - stvaranje sučelja (engl. interface) - stvaranje klase koja implementira sučelje - registriranje servisa (sučelje + klasa) u DI spremniku - dopuštanje DI spremniku da proslijedi implementaciju sučelja u kontroler putem konstruktorskog ubacivanja (engl. constructor injection) - korištenje implementacije kontrolera u akciji gdje Vam je to potrebno ### 13.8 Implementacija servisa Ovdje pokazujemo "jednostavan" primjer kako koristiti servis u ASP.NET Core MVC: - u projektu `ex13.BL` kreirajte mapu `Services` - kreirajte sučelje `IDiagnostics` sa sljedećim članovima: - int CountSongs() - float CountTempPathFiles() - implementirajte sučelje ``` public class Diagnostics : IDiagnostics { private readonly Exercise13Context _context; public Diagnostics(Exercise13Context context) { _context = context; } public int CountSongs() { return _context.Audios.Count(); } public float CountTempPathFiles() { var tempPath = Path.GetTempPath(); return Directory.GetFiles(tempPath).Length; } } ``` - stvorite MVC `DiagnosticsController` - dohvatite parametar `IDiagnostics` iz DI spremnika u konstruktor ``` public readonly IDiagnostics _diagnostics; public DiagnosticsController(IDiagnostics diagnostics) { _diagnostics = diagnostics; } ``` - kreirajte model `DiagnosticsVM` i upotrijebite ga u akciji Index tog kontrolera za popunjavanje podataka - int SongCount - int TempPathFileCount ``` public IActionResult Index() { var diagVm = new DiagnosticsVM { SongCount = _diagnostics.CountSongs(), TempPathFileCount = _diagnostics.CountTempPathFiles() }; return View(diagVm); } ``` - automatski generirajte prikaz (koristite predložak `Details`) - dodajte vezu `Diagnostics` u layout prikaz Kada pokušate kliknuti novu navigacijsku stavku "Diagnostics", trebali biste dobiti sljedeću grešku: > Unable to resolve service for type 'ex13.BL.Services.IDiagnostics' while attempting to activate 'exercise_13.Controllers.DiagnosticsController'. To je zbog neregistriranja servisa u DI spremniku. Rješenje: ``` builder.Services.AddScoped(); ``` Pokušajte sada kliknuti vezu - trebala bi pravilno prikazati dijagnostičke podatke. > Postoje tri opcije za dodavanje usluga: > - Singleton > - Scoped > - Transient > > Vrlo često zapravo ne razmišljamo o tome koliko dugo će usluga biti potrebna, impliciramo da je njen životni vijek učinkovit samo tijekom jednog HTTP zahtjeva. To znači da koristimo Scoped uslugu. > Ako to ne uspije, pokušavamo s uslugom Singleton (dulje trajanje) ili Transient (živi samo za jednu upotrebu). > > _Za detalje vidi: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0#service-registration-methods_ ### 13.9 Implementacija repozitorija Ovo je primjer implementacije repozitorija na temelju postojećeg koda. 1. Napravite klasu `AudioRepository` koji implementira sučelje `IAudioRepository`. Automatski generirajte implementaciju. ``` public interface IAudioRepository { public Audio GetAll(); public Audio Get(int id); public Audio Add(string title, int? year, int genreId, int artistId, int duration, string url); public Audio Modify(int id, string title, int? year, int genreId, int artistId, int duration, string url, IEnumerable tagIds); public Audio Remove(int id); } public class AudioRepository : IAudioRepository { public Audio Add(string title, int? year, int genreId, int artistId, int duration, string url) { throw new NotImplementedException(); } // ... public Audio Remove(int id) { throw new NotImplementedException(); } } ``` 2. Ubacite db kontekst u implementaciju `SongController` 3. Ubacite `IAudioRepository` u `SongController` 4. Registrirajte par `` kao servis u DI kontejneru 5. Kopirajte Linq zahtjev na bazu podataka iz `SongController.Index()` to `AudioRepository.GetAll()` ``` public IEnumerable