# 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<Genre, GenreVM>();
    ```
- 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<GenreVM>(genre);

  // ...or a collection of viewmodels
  var genreVms = _mapper.Map<IEnumerable<GenreVM>>(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<Audio, SongVM>();
  ```

  ```C#
  // In SongController, GET Index()
  var songs = _context.Audios
    .Include(x => x.Genre)
    .Include(x => x.Artist);

  var songVms = _mapper.Map<IEnumerable<SongVM>>(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<Audio, SongVM>()
    .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<IDiagnostics, Diagnostics>();
  ```

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<int> 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 `<IAudioRepository, AudioRepository>` kao servis u DI kontejneru

5. Kopirajte Linq zahtjev na bazu podataka iz `SongController.Index()` to `AudioRepository.GetAll()`

    ```
    public IEnumerable<Audio> GetAll()
    {
      var songs = _context.Audios
          .Include(x => x.Genre)
          .Include(x => x.Artist)
          .Include(x => x.AudioTags);

      return songs;
    }
    ```

6. U `SongController.Index()` zamijenite Linq zahtjev na bazu podataka pozivom servisne metode.

    ```
    var songs = _audioRepo.GetAll();
    var songVms = _mapper.Map<IEnumerable<SongVM>>(songs);
    ```

Učinite korake 5+6 i za druge CRUD operacije. _Obratite pažnju da za kreiranje, ažuriranje i brisanje trebate implementirati POST akcije._

### 13.10 Implementacija repozitorija: POST primjer

  ```C#
  // In AudioRepository
  public Audio Add(string title, int? year, int genreId, int artistId, int duration, string url)
  {
    var audio = new Audio
    {
        Title = title,
        Year = year,
        GenreId = genreId,
        ArtistId = artistId,
        Duration = duration,
        Url = url
    };

    _context.Audios.Add(audio);

    _context.SaveChanges();

    return audio;
  }
  ```

  ```C#
  // In SongController.Create
  //...
  _audioRepo.Add(
      song.Title,
      song.Year,
      song.GenreId,
      song.ArtistId,
      song.Duration,
      song.Url);
  //...
  ```

### 13.11 Implementacija repozitorija: PUT primjer

  ```C#
  // In AudioRepository
  public Audio Modify(int id, string title, int? year, int genreId, int artistId, int duration, string url, IEnumerable<int> tagIds)
  {
      var audio = _context.Audios.Include(x => x.AudioTags).FirstOrDefault(x => x.Id == id);
      audio.Title = title;
      audio.Year = year;
      audio.GenreId = genreId;
      audio.ArtistId = artistId;
      audio.Duration = duration;
      audio.Url = url;

      _context.RemoveRange(audio.AudioTags);
      var audioTags = tagIds.Select(x => new AudioTag { AudioId = id, TagId = x });
      foreach (var tag in audioTags)
      {
          audio.AudioTags.Add(tag);
      }

      _context.SaveChanges();

      return audio;
  }
  ```

  ```C#
  // In SongController.Edit
  //...
  _audioRepo.Modify(
      id,
      song.Title,
      song.Year,
      song.GenreId,
      song.ArtistId,
      song.Duration,
      song.Url,
      song.TagIds);
  //...
  ```

### 13.12 Implementacija repozitorija: DELETE primjer

  ```C#
  // In AudioRepository
  public Audio Remove(int id)
  {
      var audio = _context.Audios
          .Include(x => x.AudioTags)
          .FirstOrDefault(x => x.Id == id);

      _context.RemoveRange(audio.AudioTags);
      _context.Audios.Remove(audio);

      _context.SaveChanges();

      return audio;
  }
  ```

  ```C#
  // In SongController.Edit
  //...
  _audioRepo.Remove(id);
  //...
  ```

### 13.13 Vježba: Implementirajte dodatne dijagnostičke podatke

Implementirati dodatne dijagnostičke podatke i prikažite ih:
  - int CountGenres()
  - int CountArtists()

### 13.14 Vježba: Stvorite prilagođenu uslugu za predmemoriranje

Premjestite metode `GenreListItems()` i `GetArtistListItems()` u prilagođeni servis predmemoriranja (engl. caching) i upotrijebite ga. Učinite to i za `GetTagListItems()` i tamo implementirajte predmemoriju.

> Savjet: Za dobivanje `HttpContext` u usluzi koristite `IHttpContextAccessor`; _vidi: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-8.0#access-httpcontext-from-custom-components_

### 13.15 Vježba: Korištenje AutoMappera za podršku mapiranja DTO objekata Web API-ja

U Web API projektu također možete reducirati količinu koda korištenjem AutoMappera. Recimo, svi DTO objekti mapirani su ručno. Promijenite to tako da koristite AutoMapper.