Użycie NHibernate w aplikacji ASP.NET Core

Narzędzia ORM są szeroko stosowane do wykonywania operacji na bazie danych. W tym artykule opiszę jak w prosty sposób użyć narzędzia NHibernate w projekcie ASP.NET Core. Przykłady zostały stworzone w .NET 5, aczkolwiek wszystko co tutaj opisuję ma zastosowanie do starszych wersji .NET, a także do najnowszej .NET 6. Zakładam, że znasz NHibernate. Jeśli nie, to tutaj znajdziesz podstawowe informacje.

Wstęp

Na początku było ADO.NET, czyli warstwa dostępu do bazy danych wykorzystująca bezpośrednie połączenie oraz zapytania SQL. Korzystanie z tego typu podejścia w komercyjnych aplikacjach nie było idealnym rozwiązaniem, gdyż zapytania były pisane w dialekcie SQL zgodnym z konkretnym silnikiem bazy danych. Przejście na inną bazę wymagałoby przepisania większości zapytań. Dodatkowo programista za każdym razem musiał mapować dane zwracane przez zapytania na klasy. Aby rozwiązać te i wiele innych problemów, świat stworzył narzędzia ORM (Object–relational mapping).

Na platformie .NET rekomendowanym ORM jest Entity Framework, jednak ja od wielu lat korzystam z bardziej dojrzałego narzędzia NHibernate. Oryginalnie powstał on w Javie i w międzyczasie został przeniesiony także do .NET. Posiada bardzo wiele użytecznych ficzerów i moim skromnym zdaniem nadal jest lepszy od Entity Framework. Dlatego, rozpoczynając nowy projekt, zawsze opieram go o NHibernate i za każdym razem widzę, jak wiele rzeczy mi dostarcza.

Tworzymy nowy projekt

Będzie to aplikacja ASP.NET Core wraz z aplikacją SPA napisaną w Blazor. W tym artykule skupimy się przede wszystkim na ASP.NET Core, gdyż tylko tam jest wykorzystywany NHibernate. Jako IDE korzystam z Visual Studio 2019. Wersja Community jest bezpłatna dla małych zespołów, więc idealnie pasuje do SoloProgramisty.

Źródła znajdziesz tutaj:

GitHub: https://github.com/robocik/AspNetCoreWithNHibernate

Zaczynamy zatem:

  1. Stwórz w VS 2019 nowy projekt Blazor WebAssembly App
Ustawienia projektu Blazor WebAssembly App

Zwróć uwagę na wybrane opcje:

  • ASP.NET Core hosted – spowoduje dodanie do solucji projektu aplikacji ASP.NET Core.
  • Authentication Type – Individual Accounts, dzięki czemu od razu dostajemy bazowy kod logowania i tworzenia użytkowników. VS korzysta z Entity Frameworka do operacji bazodanowych, a my chcemy NHibernate, dlatego część kodu usuniemy w dalszych krokach
  1. Nasz projekt wygląda tak:
Projekt po utworzeniu w oknie SolutionExplorer
Projekt Blazor WebAssembly App bez naszych zmian

Widzmy 3 projekty:

  • NoteBookApp.Client – aplikacja Blazor
  • NoteBookApp.Server – aplikacja ASP.NET Core, która obecnie pełni rolę WebApi dla aplikacji klienckiej
  • NoteBookApp.Shared – biblioteka zawierająca wspólne elementy pomiędzy serwerem, a klientem (np definicję obiektów DTO)
  1. Na początek dodajmy nowy projekt NoteBookApp.Logic (Class Library), który będzie zawierał logikę biznesową (Opcjonalnie).
Nasze projekty w SolutionExplorer

Jeśli chcesz wiedzieć dlaczego warto wydzielać logikę biznesową, kliknij tutaj.

Usunięcie EntityFramework

Stworzony projekt domyślnie używa EntityFramework (EF), więc następnym krokiem jest usunięcie wszystkich komponentów z nim związanych.

  1. Usuwamy katalog Data
Katalog Data - do usunięcia

W tym folderze znajdują się klasy związane z tworzeniem bazy danych oraz dostępem do niej z EF. NHibernate z nich nie korzysta.

  1. Usuwamy pakiety EntityFrameworka. Najprościej zaznaczyć w Solution Explorer projekt NoteBookApp.Server i usunąć wszystkie pozycje z EntityFrameworkCore
Referencje EntityFramework do usunięcia

W moim przypadku są to:

  • Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Gdy już usuniemy powyższe wpisy, nasz projekt powinien wyglądać mniej więcej tak:

Projekt po usunięciu referencji do EntityFramework

Możesz też usunąć te referencje klikając prawym myszy na projekcie NoteBookApp.Server i wybierając Manage NuGet Packages…

Zarządzanie pakietami NuGet
  1. Na koniec usuwamy pozostałości po EF z klasy Startup
Pozostałości po EntityFramework w klasie Startup

Wszystkie instrukcje, które na powyższym zrzucie ekranu są na czerwono, usuwamy (pamietaj też o usunięciu wszystkich instrukcji using związanych z EF na początku pliku). Ostatecznie nasza klasa Startup powinna wyglądać mniej więcej tak:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NoteBookApp.Server.Models;
using System.Linq;

namespace NoteBookApp.Server
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication()
                .AddIdentityServerJwt();

            services.AddControllersWithViews();
            services.AddRazorPages();
        }

        // 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.UseWebAssemblyDebugging();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseBlazorFrameworkFiles();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseIdentityServer();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllers();
                endpoints.MapFallbackToFile("index.html");
            });
        }
    }
}

  1. Jeśli powyższe kroki wykonałeś prawidłowo, projekt powienien się skompilować. Wciśnij Ctrl+Shift+B, aby to sprawdzić (lub wybierz Build solution z menu Build)
Po usunięciu EntityFramework, projekt się kompiluje

Dodanie NHibernate do projektu

W tym momencie możemy już dodać NHibernate do naszego projektu.

  1. Dodaj do projektu NoteBookApp.Server następujące pakiety:
  • NHibernate – właściwy pakiet naszego ORM
  • NHibernate.AspNetCore.Identity – implementacja ASP.NET Identity za pomocą NHibernate
  • NHibernate.NetCore – zawiera metody ułatwiające konfigurację NHibernate
  • Microsoft.Data.SqlClient – pakiet umożliwiający łączenie się z Sql Server

Natomiast do projektu NoteBookApp.Logic dodaj

  • NHibernate
  • NHibernate.AspNetCore.Identity

Aby to zrobić, kliknij prawym na solucji w oknie Solution Explorer i wybierz Manage NuGet Packages for Solution…

Zarządzanie pakietami NuGet dla Solucji

W oknie NuGet – Solution znajdujemy wymagane pakiety i instalujemy w odpowiednich projektach

NuGet - Solution
  1. (Opcjonalnie) Przenieś katalog Model z NoteBookApp.Server do projektu NoteBookApp.Logic wraz ze zmianą przestrzeni nazw.

W naszej testowej aplikacji klasy modelowe i mappingi będę umieszczać w tym projekcie, jednak nic nie stoi na przeszkodzie, aby pozostawić te rzeczy w NoteBookApp.Server.

  1. Domyślnie klasa ApplicationUser dziedziczy z Microsoft.AspNetCore.Identity.IdentityUser, która jest pozostałością po implementacji EntityFrameworka. W przypadku NHibernate, musimy zastąpić ją przez NHibernate.AspNetCore.Identity.IdentityUser.
using NHibernate.AspNetCore.Identity;

namespace NoteBookApp.Logic.Domain
{
    public class ApplicationUser : IdentityUser
    {
    }
}

  1. Dodajemy mapping dla klasy ApplicationUser
using NHibernate.Mapping.ByCode.Conformist;
using NoteBookApp.Logic.Models;

namespace NoteBookApp.Logic.Mappings
{
    public class ApplicationUserMapping : JoinedSubclassMapping<ApplicationUser>
    {
        public ApplicationUserMapping()
        {

        }
    }
}

Obecnie nasza klasa ApplicationUser nie ma żadnych właściwości (prócz tych, które dziedziczy z IdentityUser).

  1. Teraz czas na konfigurację NHibernate. W tym celu w projekcie NoteBookApp.Server tworzymy katalog Infrastructure, a w nim klasę NHibernateSqlServerInstaller:
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using NHibernate.AspNetCore.Identity;
using NHibernate.Cfg;
using NHibernate.Connection;
using NHibernate.Dialect;
using NHibernate.Driver;
using NHibernate.Mapping.ByCode;
using NHibernate.NetCore;
using NoteBookApp.Logic.Domain;
using NoteBookApp.Logic.Mappings;

namespace NoteBookApp.Server.Infrastructure
{
    public static class NHibernateSqlServerInstaller
    {
        public static IServiceCollection AddNHibernateSqlServer(this IServiceCollection services, string cnString)
        {
            var cfg = new Configuration();

            cfg.DataBaseIntegration(db =>
            {
                db.Dialect<MsSql2012Dialect>();
                db.Driver<MicrosoftDataSqlClientDriver>();
                db.ConnectionProvider<DriverConnectionProvider>();
                db.LogSqlInConsole = true;
                db.ConnectionString = cnString;
                db.Timeout = 30;/*seconds*/
                db.SchemaAction = SchemaAutoAction.Validate;
            });

            services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
                .AddDefaultTokenProviders()
                .AddHibernateStores();
            cfg.Cache(c => c.UseQueryCache = false);

            var mapping = new ModelMapper();
            mapping.AddMappings(typeof(ApplicationUserMapping).Assembly.GetTypes());
            mapping.AddMapping(typeof(NHibernate.AspNetCore.Identity.Mappings.IdentityUserMappingMsSql));
            mapping.AddMapping(typeof(NHibernate.AspNetCore.Identity.Mappings.IdentityUserLoginMappingMsSql));
            mapping.AddMapping(typeof(NHibernate.AspNetCore.Identity.Mappings.IdentityUserTokenMappingMsSql));
            var mappingDocument = mapping.CompileMappingForAllExplicitlyAddedEntities();
            cfg.AddMapping(mappingDocument);
            services.AddHibernate(cfg);
#if DEBUG
            cfg.BuildSessionFactory();
#endif

            return services;
        }
    }
}

W tej klasie znajduje się cała konfiguracja NHibernate. Jeśli nie wiesz jak korzystać z NHibernate, to tutaj znajdziesz tutorial opisujący podstawy. Poniżej, klika ciekawszych fragmentów:

Konfiguracja ASP.NET Identity

Korzysta z pakietu NHibernate.AspNetCore.Identity.

services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
                .AddDefaultTokenProviders()
                .AddHibernateStores();

Usprawnione debugowanie

#if DEBUG
    cfg.BuildSessionFactory();
#endif

Powyższy kod jest odpalany tylko w trybie DEBUG, gdyż ma nam ułatwić wyłapywanie błędów (np. brakujących kolumn w bazie danych itp). Standardowo cfg.BuildSessionFactory jest wywoływana później i z tego względu, gdy ta metoda rzuci wyjątek, to mamy utrudnione zadanie, aby go złapać i zobaczyć co jest nie tak (w przypadku braków w bazie danych, obiekt wyjątku pokazuje czego brakuje).

Gdy metodę odpalimy ręcznie, to nasze IDE bez problemu pokaże nam ewentualne wyjątki.

Dodawanie mappingów dla ASP.NET Identity

mapping.AddMapping(typeof(NHibernate.AspNetCore.Identity.Mappings.IdentityUserMappingMsSql));
            mapping.AddMapping(typeof(NHibernate.AspNetCore.Identity.Mappings.IdentityUserLoginMappingMsSql));
            mapping.AddMapping(typeof(NHibernate.AspNetCore.Identity.Mappings.IdentityUserTokenMappingMsSql));

W tym miejscu konfigurujemy obiekty ASP.NET Identity (pochodzące z NHibernate.AspNetCore.Identity) w NHibernacie.

  1. Ostatnim krokiem jest wywołanie naszej konfiguracji w klasie Startup. Poniższy kod dodaj na początek metody ConfigureServices:
var connectionString = Configuration.GetValue<string>("DefaultConnection");
services.AddNHibernateSqlServer(connectionString);

Connection String do naszej bazy znajduje się w plik appsettings.json

{
  "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=NotesDb;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "IdentityServer": {
    "Clients": {
      "NoteBookApp.Client": {
        "Profile": "IdentityServerSPA"
      }
    }
  },
  "AllowedHosts": "*"
}

W tym momencie masz prawidłowo skonfigurowanego NHibernate w aplikacji ASP.NET Core.

Konfiguracja IdentityServer (opcjonalnie)

Jeśli Twoja aplikacja ASP.NET Core współpracuje z aplikacją SPA napisaną w Blazor (jak w naszym przykładzie), pozostaje nam jeszcze skonfigurować bibliotekę IdentityServer, która odpowiada za proces uwierzytelniania użytkowników.

Dodaj poniższy kod do metody ConfigureServices w klasie Startup

services.AddIdentityServer()
   .AddAspNetIdentity<ApplicationUser>()
   .AddIdentityResources()
   .AddApiResources()
   .AddClients()
   .AddDeveloperSigningCredential();

W tym kodzie prócz standardowych ustawień, wskazujemy naszą nową klasę ApplicationUser. Dzięki temu, IdentityServer będzie z niej korzystał (i NHibernate) podczas logowania.

Tworzenie bazy danych

Mamy już napisany kod, ale nie mamy przygotowanej bazy danych (z tabelami dla ASP.NET Identity). Czas to zmienić.

  1. Stwórz pustą bazę w Sql Server. Możesz również skorzystać z innego silnika bazodanowego, np. MySql, Postgres, SqLite)
  2. Aby dodać tabelę dla ASP.NET Identity, możesz przejść na stronę projektu NHibernate.AspNetCore.Identity:

Znajdziesz tam skrypty tworzące odpowiednie tabele dla wybranych silników bazodanowych. Uruchom skrypt 00_aspnet_core_identity.sql na swojej bazie danych.

  1. Następnie musisz stworzyć tabelę dla ApplicationUser. Tutaj jest przykładowy skrypt:
CREATE TABLE ApplicationUser
(
	ApplicationUser_Key  nvarchar(32) NOT NULL Primary key,
	constraint FK_ApplicationUser_AspNetUsers foreign key (ApplicationUser_Key) references AspNetUsers,
) 
  1. Po tej operacji baza powinna wyglądać podobnie do mojej
Struktura bazy po uruchomieniu skryptów

Teraz powinieneś być w stanie uruchomić testową aplikację, stworzyć nowe konto i zalogować się.

Podsumowanie

Dodanie NHibernate do projektu ASP.NET Core jest dość prostą operacją, jednak wymaga trochę pracy. W kolejnych artykułach powiem Ci, dlaczego lubię NHibernate i co jeszcze można z nim ciekawego zrobić.

Napisz proszę z jakiego ORM korzystasz w swoich projektach i jakie to narzędzie ma plusy.

Share