Paweł Łukasiewicz
2020-03-15
Paweł Łukasiewicz
2020-03-15
Udostępnij Udostępnij Kontakt
Wprowadzenie

Dotarliśmy do końca cyklu związanego z wprowadzeniem do Angulara. Im więcej używam tej technologii tym bardziej jestem do niej przekonany. To od Was zależy czy dalej będziemy skupiać się zagadnieniach związanych z tą technologią.

Głównym celem całej tej serii było zbudowanie solidnych podstaw dotyczących Angulara oraz połączenie tej technologii z naszym głównym stackiem technologicznym , tj. .NET Core. Jednym z trendów poprzedniego roku był wzrost popularności .NET Core. Z drugiej strony Angular jest niezwykle częstym wyborem, kiedy przychodzi do tworzenia front-end’u. Wiele głosów wskazuje, iż takie połączenie technologii ma przed sobą świetlaną przyszłość. Zdecydowałem się na taką serię z uwagii na projekty obecnie prowadzone w pracy (separacja back-end oraz front-end) oraz silna potrzebna porównania Angulara z Oracle JET). Ten wpis podzielimy na dwie cześci (opublikowane równolegle) celem rozdzielnia wspomnianych powyżej warstw.

W tej cześci przygotujemy back-end, we wpisie równoległym skupimy się na integracji.

.NET Core API

Z przygotowaniem prostego API nikt nie będzie miał problemów. Jak możecie domyślać się z poprzednich wpisów interesuję się motoryzacją – przygotujemy API pozwalające na obsługę naszego salonu samochodów luksusowych...

Będziemy w stanie wykonywać podstawowe operacje CRUD takie jak dodawnie nowego samochodu do naszej kolekcji, aktualizacja danych pojazdów (nie mówimy tutaj o kręceniu licznika), kasowanie aut już sprzedawanych oraz pobranie danych celem wyświetlenia strony głównej ze wszystkimi ogłoszeniami.

Tworzenie projektu

Projekt przygotujemy w Visual Studio 2019 korzystając z .NET Core 3.1. Wersja ta została udostępniona 3 grudnia 2019 a pobrać ją możecie korzystając z linka Pobierz .NET Core 3.1. Pamiętajcie również o aktualizacji swojego środowiska ponieważ może się okazać, że sama instalacja SDK nie będzie wystarczająca - Visual Studio 2019 może nie dawać wyboru tej wersji przy tworzeniu projektu. Aktualizacja wraz z ponownym uruchomieniem środowiska powinny rozwiązać ten problem.

W pierwszym kroku wybieramy typ naszego projektu: Visual Studio 2019: nowy projekt

Następnie wykonujemy wstępną konfigurację projektu: Visual Studio 2019: nowy projekt

Trzeci krok to wybór wersji fremworka oraz szablon tworzonego projektu: PVisual Studio 2019: nowy projektIS

Dokonajmy jeszcze podstawowej analizy pliku launchSettings.json. Na bazie przygotowanej konfiguracji możemy zauważyć, że po starcie API dojdzie do uruchomienia przeglądarki a adresem startowym będzie https://localhost:44343/weatherforecast - szybkie uruchomienie pozwoli nam sprawdzić czy proces tworzenia projektu przebiegł pomyślnie: ASP.NET Core API: start projektu Domyślna implementacja kontrolera działa a my możemy przejść do kolejnch kroków.

W przypadku tworzenia aplikacji działających w różnych domenach niezbędna jest konfiguracja CORS po stronie serwera. W przypadku ASP.NET Core możemy skorzytać z oprogramowania pośredniczącego. W tym celu otwieramy plik Startup.cs, gdzie dokonujemy modyfikacji dwóch metod: ConfigureServices(…) oraz Configure(…). Spójrzcie poniżej:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Poniższa konfiguracja jest bazowa i niezwykle prosta
    // AllowAnyOrigin() - może zostać zmienione na np. WithOrigings("http://www.naszezrodlo.com") - dostęp tylko z tego miejsca
    // AllowAnyMethod() - możemy zezwolić na dostęp do wybranych metod np. WithMethods("POST", "GET")
    // AllowAnyHeader() - możemy akceptować jedynie wybrane nagłówki, np. WithHeaders("accept", "content-type")
    services.AddCors(options =>
    {
        options.AddPolicy("CorsPolicy",
            builder => builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader()
    });
    services.AddControllers();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    // Nazwa używanej polityki została zdefiniowana w metodzie ConfigureServices(...)
    app.UseCors("CorsPolicy");

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Baza danych

Zanim przejdziemy do etapu tworzenia kontrolera skupimy się na bazie danych. W tym celu posłużymy się artykułem, który napisałem jakiś czas temu: C# - Entity Framework Code First (Wpis potraktujcie jako wprowadzenie)

W pierwszym kroku tworzymy prosty model, który będzie reprezentacją tabeli:

public class CarModel
{
    public int Id { get; set; }

    public string Brand { get; set; }

    public string Model { get; set; }

    public string Engine { get; set; }

    public int Power { get; set; }

    public DateTime Production { get; set; }

    public double Price { get; set; }

    public string ImagePath { get; set; }

    public string Description { get; set; }

    public double Mileage { get; set; }
}

Musimy dodać odpowiednie atrybuty, aby moduł tworzenia bazy danych wykonał swoją pracę zgodnie z naszymi oczekiwaniami:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace CarShowroomAPI.Models
{
    [Table("Cars")]
    public class CarModel
    {
        [Key]
        public int Id { get; set; }

        public string Brand { get; set; }

        public string Model { get; set; }

        public string Engine { get; set; }

        public int Power { get; set; }

        public DateTime Production { get; set; }

        public double Price { get; set; }

        public string ImagePath { get; set; }

        public string Description { get; set; }

        public double Mileage { get; set; }
    }
}

Brakuje nam jeszcze klasy będącej reprezentacją kontekstu bazy danych - krok ten wykonamy nieco inaczej niż w przypadku wspomianego powyżej artykułu. Tym razem bazujemy na technologii .NET Core. Musimy utworzyć nową klasę, która dziedziczy po DbContext:

// Wymagana jest instalacja paczki Microsfot.EntityFrameworkCore
using CarShowroomAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace CarShowroomAPI.DatabaseContext
{
    public class CarContext : DbContext
    {
        public DbSet<CarModel> Cars { get; set; }

        public CarContext(DbContextOptions options) : base(options)
        {

        }
    }
}
Musimy jeszcze zdefiniować połączenie z bazą danych w pliku appsettings.json:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionString": {
    "CarDB": "server=Pawel;database=CarDB;User ID=login;password=haslo;"
  },
  "AllowedHosts": "*"
}
Zmodyfikuj powyższy wpis dopasowując go do swoich potrzeb.

Kontekst, który przygotowaliśmy musi jeszcze zostać zarejestowany w pliku Startup.cs. Spójrzcie na poniższy kod metody ConfigureServices(…):

public void ConfigureServices(IServiceCollection services)
{
    // Wymagana jest instalacja paczki: Microsoft.EntityFrameworkCore.SqlServer
    services.AddDbContext<CarContext>(options => options.UseSqlServer(Configuration["ConnectionString:CarDB"]));

    // Poniższa konfiguracja jest bazowa i niezwykle prosta
    // AllowAnyOrigin() - może zostać zmienione na np. WithOrigings("http://www.naszezrodlo.com") - dostęp tylko z tego miejsca
    // AllowAnyMethod() - możemy zezwolić na dostęp do wybranych metod np. WithMethods("POST", "GET")
    // AllowAnyHeader() - możemy akceptować jedynie wybrane nagłówki, np. WithHeaders("accept", "content-type")
    services.AddCors(options =>
    {
        options.AddPolicy("CorsPolicy",
            builder => builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials());
    });
    services.AddControllers();
}

Aby zapewnić sobie bezproblemowe działanie dodamy jeszcze jeden krok. Krok ten polega na automatyzacji tworzenia bazy danych na bazie naszego modelu przy wykorzystaniu Code-First Migrations.

W tym celu zaintalujemy jeszcze jedną paczkę, tj. Microsoft.EntityFrameworkCore.Tool a następnie wywołamy poniższe polecenie korzystając z konsoli zarządzania pakietami (Package Manager Console):

PM>  Add-Migration CarShowroomAPI.Model.CarContext
Dopasujecie powyższe polecenie do Waszego środowiska.

Efektem wykonania powyższego polecenia jest dodanie klas wspomagających migrację: EF Core: migracja

Pozostał nam ostatni krok – aktualizacja bazy danych na bazie naszego modelu. Możemy tego dokonać wykonując polecenie:

PM> update-database
Efekty możemy zobaczyć uruchamiając Microsoft SQL Server: EF Core: utworzenie nowej bazy danych

W tym momencie nasza baza danych jest kompletnie pusta. Dodamy jeszcze jedną metodę, która pozwoli nam na wrzucenie przykładowych danych w procesie migracji. W tym celu musimy przesłonić metodę OnModelCreating wewnątrz klasy CarContext:

using CarShowroomAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace CarShowroomAPI.Model
{
    public class CarContext : DbContext
    {
        public DbSet<CarModel> Cars { get; set; }

        public CarContext(DbContextOptions options) : base(options)
        {

        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<CarModel>().HasData(
                new CarModel
                {
                    Id = 1,
                    Brand = "Audi",
                    Model = "RS6",
                    Engine = "V10 5.2",
                    Power = 580,
                    Production = new System.DateTime(2012, 1, 2),
                    Price = 120000,
                    ImagePath = "https://apollo-ireland.akamaized.net/v1/files/eyJmbiI6IjV1Z3FkOTBqbzBqNzEtT1RPTU9UT1BMIiwidyI6W3siZm4iOiJ3ZzRnbnFwNnkxZi1PVE9NT1RPUEwiLCJzIjoiMTYiLCJwIjoiMTAsLTEwIiwiYSI6IjAifV19.RWMwGPIAdM3kRNxqXNsJhlDtpAK4npaZ2LUk4TheVJE/image;s=1080x720;cars_;/937787999_;slot=10;filename=eyJmbiI6IjV1Z3FkOTBqbzBqNzEtT1RPTU9UT1BMIiwidyI6W3siZm4iOiJ3ZzRnbnFwNnkxZi1PVE9NT1RPUEwiLCJzIjoiMTYiLCJwIjoiMTAsLTEwIiwiYSI6IjAifV19.RWMwGPIAdM3kRNxqXNsJhlDtpAK4npaZ2LUk4TheVJE_rev001.jpg",
                    Mileage = 130000,
                    Description = "Autko w super stanie! Garażowane! Od fanatyka!"
                },
                new CarModel
                {
                    Id = 2,
                    Brand = "Audi",
                    Model = "R8",
                    Engine = "V10 5.2",
                    Power = 525,
                    Production = new System.DateTime(2010, 3, 24),
                    Price = 320000,
                    ImagePath = "http://blog.ozonee.pl/wp-content/uploads/2018/07/Audi-R8-V10.jpg",
                    Mileage = 54000,
                    Description = "Niestety koszty utrzymania samochodu to fanaberia!"
                });
        }
    }
}

Dostarcznie danych początkowych do bazy danych to proces zwany Data Seeding. Migracje EF Core określą automatycznie jakie operacja należy wykonać podczas tworzenia bazy danych. W celu utworzenia procesu dodawania danych skorzystam z powyższego polecenia zmieniając jedynie nazwę samego procesu:
PM> Add-Migration CarShowroomAPI.Model.CarContextSeed
Po poprawnym wykonaniu powyższego polecenia dokonamy jeszcze aktualizacji bazy danych:
PM> update-database

Jeżeli proces przebiegł bez żadnych problemów powinniśmy zobaczyć rekordy w tabeli naszej bazy danych: SQL Server: nowe rekordy w bazie danych

Jeżeli checie usunąć ostatnią migrację (zdaliście sobie sprawę, że wprowadzone dane nie są poprawne) należy skorzystać z polecenia:

PM> Remove-Migration

Tworzenie repozytorium

Konfiguracja EF przebiegła pomyślnie – potrzebujemy teraz dostępu do bazy danych z interfejsu API. Bezpośredni mechanim dostępu do danych przy użyciu kontekstu jest złą praktyką i powinniśmy tego unikać.

Zaimplementujemy w tym celu proste repozytorium, którę będzie miało dostęp do danych pochodzących z bazy danych. W pierwszym kroku utworzymy nowy folder Repository w którym utworzymy interfejs nazwany ICarsRepository:

using System.Collections.Generic;

namespace CarShowroomAPI.Repository
{
    public interface ICarRepository<TEntity>
    {
        IEnumerable<TEntity> GetAll();
        TEntity Get(int id);
        void Add(TEntity entity);
        void Update(TEntity dbEntity, TEntity entity);
        void Delte(TEntity entity);
    }
}
Nieco później wstrzykniemy ten interfejs do naszego kontrolera – będzie on komunikował się z kontekstem bazy danych za pomocą tego właśnie interfejsu. Zanim jednak przejdziemy do tego kroku utwórzmy klasę, która ten interfejs będzie implementowała. Ja zwykle preferuje podejście w którym klasa jest w tym samym folderze co interfejs. W naszym przypadku wszystko umieścimy w katalogu Repository jako, że nie mamy wewnątrz dodatkowej struktury:
using CarShowroomAPI.Model;
using CarShowroomAPI.Models;
using System.Collections.Generic;
using System.Linq;

namespace CarShowroomAPI.Repository
{
    public class CarRepository : ICarRepository<CarModel>
    {
        private readonly ICarRepository<CarModel> _carRepository;

        public CarController(ICarRepository<CarModel> carRepository)
        {
            _carRepository = carRepository;
        }

        public void Add(CarModel entity)
        {
            _carContext.Cars.Add(entity);
            _carContext.SaveChanges();
        }

        public void Delte(CarModel entity)
        {
            _carContext.Cars.Remove(entity);
            _carContext.SaveChanges();
        }

        public CarModel Get(int id)
        {
            return _carContext.Cars.FirstOrDefault(a => a.Id == id);
        }

        public IEnumerable<CarModel> GetAll()
        ""
            return _carContext.Cars.ToList();
        }

        public void Update(CarModel dbEntity, CarModel entity)
        {
            dbEntity.Brand = entity.Brand;
            dbEntity.Model = entity.Model;
            dbEntity.ImagePath = entity.ImagePath;
            dbEntity.Mileage = entity.Mileage;
            dbEntity.Power = entity.Power;
            dbEntity.Price = entity.Price;
            dbEntity.Production = entity.Production;
            dnEntity.Description = entity.Description;

            _carContext.SaveChanges();

        }
    }
}
Powyższa klasa wraz z jej implementacją obsługuje wszystkie operacje związane z bazą danych. Powodem takiej implementacji jest oddzielenie logiki operacji wykonywanych na danych od naszego kontrolera API.

Wykonajmy jeszcze jeden dodatkowy krok – konfiguracja dependency injection. Operacja ta dokonywana jest w metodzie ConfigureServices wewnątrz klasy Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Wymagana jest instalacja paczki: Microsoft.EntityFrameworkCore.SqlServer
    services.AddDbContext<CarContext>(options => options.UseSqlServer(Configuration["ConnectionString:CarDB"]));

    // Poniższa konfiguracja jest bazowa i niezwykle prosta
    // AllowAnyOrigin() - może zostać zmienione na np. WithOrigings("http://www.naszezrodlo.com") - dostęp tylko z tego miejsca
    // AllowAnyMethod() - możemy zezwolić na dostęp do wybranych metod np. WithMethods("POST", "GET")
    // AllowAnyHeader() - możemy akceptować jedynie wybrane nagłówki, np. WithHeaders("accept", "content-type")
    services.AddCors(options =>
    {
        options.AddPolicy("CorsPolicy",
            builder => builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader());
    });

    // Dependency Injection
    services.AddScoped<ICarRepository<CarModel>, CarRepository>();

    services.AddControllers();
}

Działanie o którym zaraz napiszę nie jest wymagane w naszym API (mamy tylko jedno repozytorium). W przypadku, gdy implementujemy jednak wiele niezależnych modułów (wiele repozytoriów) warto przygotować osobną klasę, żeby nie zaciemniać kodu. Klasa taka mogłby nosić nazwę DependencyInjectionConfig wraz ze statyczną metodą pozwalającą na wykonanie powyższej operacji w odmiennej klasie. Moglibyśmy tego dokonać dodając w metodzie ConfigureServices poniższy kod:

DependencyInjectionConfig.ConfigureServices(services);
Implementacja klasy mogłaby wyglądać tak jak poniżej:
namespace SampleNamespace
{
	class DependencyInjectionsConfig
	{
		/// <summary>
		/// Konfiguracja Dependency Injection 
		/// </summary>
		/// <param name="services">The services.</param>
		public static void ConfigureServices(IServiceCollection services)
		{
            // Dodajemy wymagane interfejsy oraz implementacje
			services.AddScoped<IConnectionFactory, ConnectionFactory>()
				.AddScoped<IXXX_1, XXX_1>()
				.AddScoped<IXXX_2, XXX_2>()
				.AddScoped<IXXX_3, XXX_3>()
				.AddScoped<IXXX_4, XXX_4>()
				.AddScoped<IXXX_5, XXX_5>()
		}
	}
}

To by było na tyle – możemy wreszcie utworzyć API o którym tak dużo pisałem…

API

Repozytorium zostało przygotowane – mamy dostęp do kontekstu utworzonej bazy danych(tabeli). API będzie naszym łącznikiem aplikacji Angular oraz danych przechowywanych w bazie danych. Nie będę skupiał się na szczegółowym opisie dodawania nowego kontrolera ponieważ każdy z nas potrafi przejść przez ten krok bez żadnych problem. W implementacji wykorzystamy nasze repozytorium. Przykładowa implementacja może prezentować się w poniższy sposób:

using System.Collections.Generic;
using CarShowroomAPI.Models;
using CarShowroomAPI.Repository;
using Microsoft.AspNetCore.Mvc;

namespace CarShowroomAPI.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class CarController : ControllerBase
    {
        private readonly ICarRepository<CarModel> _carRepository;

        public CarController(ICarRepository<CarModel> carRepository)
        {
            _carRepository = carRepository;
        }

        // GET: api/Car
        [HttpGet]
        public IActionResult Get()
        {
            IEnumerable<CarModel> cars = _carRepository.GetAll();
            return Ok(cars);
        }

        // GET: api/Car/5
        [HttpGet("{id}", Name = "Get")]
        public IActionResult Get(int id)
        {
            CarModel car = _carRepository.Get(id);

            if (car == null)
            {
                return NotFound("Brak wpisu w bazie danych!");
            }

            return Ok(car);
        }

        // POST: api/Car
        [HttpPost]
        public IActionResult Post([FromBody] CarModel car)
        {
            if (car == null)
            {
                return BadRequest("Brak danych!");
            }

            _carRepository.Add(car);
            return CreatedAtRoute(
                "GET",
                new { Id = car.Id },
                car);
        }

        // PUT: api/Car/5
        [HttpPut("{id}")]
        public IActionResult Put(int id, [FromBody] CarModel car)
        {
            if (car == null)
            {
                return BadRequest("Brak danych!");
            }

            CarModel recordToUpdate = _carRepository.Get(id);
            if(recordToUpdate == null)
            {
                return NotFound("Brak wpisu w bazie danych");
            }

            _carRepository.Update(recordToUpdate, car);
            return NoContent();
        }

        // DELETE: api/ApiWithActions/5
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            CarModel car = _carRepository.Get(id);
            if (car == null)
            {
                return NotFound("Brak wpisu w bazie danych");
            }

            _carRepository.Delete(car);
            return NoContent();
        }
    }
}

Zanim przeskoczymy do kolejnego wpisu warto wykonać testy naszego API. Tutaj mamy sporo możliwości, jedna z nich została opisana w artykule: ASP.NET Core WebAPI: Swagger

Tym razem wykorzystamy jednak inny sposób: Postman.

Testowanie API

Aplikację możemy pobrać z adresu: https://www.getpostman.com/downloads/

Używam jej na co dzień – jestem bardzo zadowolony z dostępnych funkcjonalności i łatwości obsługi.

Zajmiemy się podstawową obsługą – po otwarciu aplikacji klikamy zaznaczony poniżej ‘+’ (pozwala na dodanie nowego requesta: Postman: Get Request

W następnym kroku uruchamiamy API, wprowadzamy adres serwisu do aplikacji oraz wykonujemy najprostszy Get celem sprawdzenia poprawności implementacji: Postman: Get/id Request

Z uwagi na fakt, że nasze API używa certyfikatu z własnym podpisem zobaczycie poniższy błąd: Postman: błąd ssl

Rozwiązanie jest niezwykle proste. Przechodzimy przez File -> Settings -> General i odznaczamy SSL certificate verification: Postman: konfiguracja ssl

Przed przejściem do integracji przeprowadźmy testy kolejnych metod, aby uniknąć (ewentualnych) błędów na późniejszym etapie.

GET/{id}:

POST:

PUT:

DELETE:

Podsumowanie

W tej części przygotowaliśmy kompletny back-end dla naszej aplikacji. Bazę danych przygotowaliśmy korzystając z podejścia Code-First, dodaliśmy repozytorium i kontekst bazy danych, aby przeprowadzać operacje na bazie danych. Przygotowaliśmy również API korzystające z implementacji repozytorium oraz dokonaliśmy podstawowych testów przy użyciu narzędzia jakim jest Postman.

Pora na przeprowadzenie integracji z aplikacją Angular.