Porównanie Spring Framework i ASP.NET Core cz.1: Wprowadzenie

Porównanie Spring Framework i ASP.NET Core cz.1: Wprowadzenie

Możesz skomentować ten wpis w serwisach społecznościowych: Linkedin (przejdź) lub Facebook (przejdź).

Na ścieżce rozwoju każdej osoby, która chce się zająć programowaniem, pojawia się w pewnym momencie kwestia wyboru technologii, z której chciałaby korzystać.

Do najbardziej popularnych języków programowania należą te obiektowe. Wśród nich zaś prym wiodą Java i C#.

Po pierwszym kontakcie z w.w. technologiami odniosłem wrażenie: Java i C# są jak Coca-Cola i Pepsi. Tylko koneserzy rozpoznają różnice. 😉

W ramach swojej pracy dyplomowej podjąłem się porównania dwóch popularnych frameworków stworzonych na bazie każdego z tych języków, a mianowicie Spring Framework i ASP.NET Core.

Springa po raz pierwszy opublikowano 1 października 2002 roku (źródło), a obecnie jest już dostępna piąta wersja tego środowiska. Miał być alternatywą dla stosowanej wcześniej technologii Enterprise Java Beans, która dla tworzenia małych aplikacji wymagała podobnego nakładu pracy jak dla aplikacji dużej. Obsługuje zasadę „odwrócenia sterowania” (Inversion of Control) i „wstrzykiwania zależności” (Dependency Injection), które pozwalają na sterowanie aplikacją (np. konstruowanie zapytań HTTP) z poziomu frameworka – programista jedynie dokonuje odpowiedniej konfiguracji. W przypadku wykorzystania Spring Boot także i to nie jest konieczne (istnieje zestaw konfiguracji domyślnych), ale o tym innym razem. Możliwe jest także przekazywanie zależności przy pomocy gotowych instancji obiektów, jako argument konstruktora albo poprzez przypisanie własciwości (pola) danego obiektu w postaci metody setter. Dzięki tej funkcjonalności można przygotować kilka implementacji tego samego interfejsu, a programista korzystający z takiego rozwiązania może wybrać preferowaną implementację lub też napisać własną (wzorzec projektowy „most„).

Spring posiada wiele modułów. Podstawowa część frameworka zgrupowana jest w module Spring Core, Spring Web i Spring Web MVC zapewnia warstwę do obsługi aplikacji sieciowych, Spring Security obejmuje kontrolę dostępu do zasobów i kwestie uwierzytelniania. Są też moduły odpowiedzialne za komunikację z bazami danych, np. Spring DAO, Spring Data, Spring ORM czy Spring Hibernate, jak również Spring Cloud, zapewniający wsparcie dla komunikacji z platformami chmurowymi.
Często stosowanym rozwiązaniem jest Spring Boot, posiadający zestaw gotowych konfiguracji Springa, a także wbudowany serwer do uruchamiania aplikacji.

Pierwsza wersja ASP.NET także została opublikowana w 2002 roku (5 stycznia), (żródło) a jego korzenie sięgają 2000 roku i projektu ASP+. (źródło) Jednak przez wiele lat było to oprogramowanie o zamkniętym kodzie źródłowym, a jego w pełni sprawne wykonywanie możliwe było jedynie w systemach operacyjnych Windows. Powstało wprawdzie Mono – implementacja .NET na systemy Linuxowe, jednak nie posiada ona pełnej zgodności i pojawiają się błędy w działaniu.

Dopiero w 2016 roku opublikowano pierwszą wersję ASP.NET Core, umożliwiającej działanie na różnych systemach operacyjnych, także Linuxowych. Przy okazji jej stworzenia, Microsoft przebudował środowisko, usuwając część bibliotek, które były przestarzałe lub używały funkcji specyficznych dla platformy systemowej Windows. Od czasu powstania wersji Core, kod źródłowy frameworka jest dostępny publicznie na platformie Github, co wg mnie jest bardzo ważną zmianą, pozwalającą na pełniejsze badanie, dlaczego system działa w taki a nie inny sposób. Powstał także .NET Core CLI, umożliwiający obsługę z linii poleceń (konsoli systemowej), m.in. kompilację kodu i uruchamianie aplikacji.
W przeciwieństwie do Springa, jest to rozwiązanie stworzone i polecane przez producenta języka C#, podczas gdy Spring jest rozwiązaniem alternatywnym.

W dalszej części pracy dyplomowej porównywałem kolejne elementy aplikacji sieciowej, m.in. kontrolery, warstwę dostępu do danych, kwestię uwierzytelniania i autoryzacji i inne, sprawdzając jak dane rzeczy robi się w Springu, a jak w ASP.NET, jakie możliwości są dostępne. Czasami poszukiwałem dodatkowych bibliotek wspierających dane funkcjonalności. Zdarzała się także analiza wybranych fragmentów kodu źródłowego samych frameworków.

Najciekawsze elementy będą stopniowo publikowane na niniejszej stronie internetowej.

Możesz skomentować ten wpis w serwisach społecznościowych: Linkedin (przejdź) lub Facebook (przejdź).

Jak użyć ASP.NET Identity i nie zrobić sobie śmietnika w bazie danych?

Możesz skomentować ten wpis w serwisach społecznościowych: Linkedin (przejdź) lub Facebook (przejdź).

Witam,

w tym wpisie chciałem opisać historię swoich „zmagań” z ASP.NET Identity. Myślę że niektórzy programiści mogą z tych zmagań skorzystać i użyć ich efektów we własnych aplikacjach. Jest to także historyjka ciekawa, bo jest to jeden z epizodów – wprawdzie nie najistotniejszy – ale jeden z powodów, dla których moja ścieżka programistyczna potoczyła się tak a nie inaczej.

Wszystko zaczyna się wiosną 2016 r. Moja wiedza o „dużych” bibliotekach czy też frameworkach służących do budowy aplikacji była wówczas bardzo niewielka. Właśnie planowałem sobie aplikację do mojej inżynierki, a jedną z decyzji jakie miałem podjąć, dotyczyła technologii z której skorzystam przy budowie tej aplikacji. W grę wchodziła wówczas Java – a dokładniej zbudowany na jej bazie Spring Framework – oraz środowisko ASP.NET na bazie języka C#.

Obie decyzje były na tamten moment ryzykowne. Ponieważ chciałem, aby moja aplikacja – a dokładniej strona internetowa – nie wymagała przeładowywania strony za każdym razem kiedy użytkownik coś zmienia (tzw. single-page application), potrzebowałem jakiegoś mechanizmu logowania i obsługi kont użytkowników, który zadziała w warunkach REST API (a zatem standardowy formularz logowania nie wchodził w grę). Początkowo próbowałem wstępnie „okiełznać” oba frameworki. O ile się nie mylę, na bardzo wczesnym etapie rozważałem nawet jednoczesne tworzenie API w obydwu technologiach. Inną możliwością był wybór tej biblioteki, którą łatwiej dopasuję do swoich oczekiwań.

W przypadku Springa, przez długi czas barierą nie do pokonania było skonfigurowanie oAuth’a, który miał mi posłużyć do obsługi logowania użytkowników. Z kolei w przypadku ASP.NET problematycznym okazał się moduł ASP.NET Identity.

W wielkim skrócie, ASP.NET Identity – wraz z innymi komponentami ASP.NET – pozwala dość szybko zaprogramować i skonfigurować całą obsługę kont użytkowników, wraz z możliwością wyboru czy chcemy korzystać ze standardowych formularzy logowania, z lokalnego oAuth’a czy też może z zewnętrznego podmiotu uwierzytelniającego takiego jak np. Google czy Facebook. Niemalże automatycznie pozwala też zapisać wszystkie informacje o użytkownikach do bazy danych. Potrzebne do tego jest jedynie skonfigurowanie klasy DbContext, a następnie wykonanie tzw. migracji, która zapisze naszą strukturę danych w rzeczywistej bazie.

Okazało się jednak że tak zbudowana biblioteka wymusza pewną strukturę klasy odzwierciedlającej dane użytkownika systemu. Co gorsza, ta struktura jest zapisywana w bazie danych. Przyczyną tego stanu rzeczy jest fakt, iż domyślny sposób wykorzystania ASP.NET Identity zakłada że klasa reprezentująca użytkownika będzie dziedziczyć po klasie IdentityUser – na co wskazuje nagłówek klasy IdentityDbContext, będącej bazową konfiguracja bazy danych:

public class IdentityDbContext<TUser> : IdentityDbContext<TUser, IdentityRole, string> where TUser : IdentityUser

Źródło: repozytorium ASP.NET Core w serwisie github.com

Z kolei klasa IdentityUser już na samym starcie zawiera całkiem sporo pól. Na każde z nich musi zostać zarezerwowane miejsce w bazie danych.

W efekcie, tabela użytkowników w naszej bazie danych będzie wyglądała mniej więcej tak:

1> SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'AspNetUsers';
2> GO
COLUMN_NAME
--------------------
Id
AccessFailedCount
ConcurrencyStamp
Email
EmailConfirmed
LockoutEnabled
LockoutEnd
NormalizedEmail
NormalizedUserName
PasswordHash
PhoneNumber
PhoneNumberConfirmed
SecurityStamp
TwoFactorEnabled
UserName

(15 rows affected)

Powyższy kod jest zrzutem z konsoli SQL Server w mojej przykładowej bazie danych, która została przygotowana na bazie instancji klasy IdentityUser. Wykonałem tutaj polecenie odczytu nazw wszystkich kolumn w tabeli użytkowników.

I teraz pytanie do Czytelników:

Czy naprawdę każdy z Was w swojej aplikacji potrzebuje pól takich jak: LockoutEnabled, LockoutEnd? A SecurityStamp? ConcurrencyStamp? Są to kwestie dotyczące aktywowania lub dezaktywowania konta użytkowników oraz znaczniki związane z dodawaniem nowego konta użytkownika lub zmianą jego hasła. Jeśli ktoś chce korzystać – droga wolna, ale dla mnie to kompletny bullshit.

AccessFailedCount? Liczba błędnych logowań na konto danego użytkownika. TwoFactorEnabled? Logowanie dwuskładnikowe. Te rzeczy mogą się przydać przy systemach szczególnie wrażliwych na bezpieczeństwo, ale mało prawdopodobne by interesowały twórcę przeciętnej strony internetowej.

Podobnie EmailConfirmed czy też NormalizedEmail i NormalizedUserName.

Nawet pola dotyczące numeru telefonu (PhoneNumber i PhoneNumberConfirmed) nie muszą być konieczne w każdej aplikacji. Nie mam problemu wyobrazić sobie prostej aplikacji, która nie potrzebuje zbierać takich danych. Zwłaszcza po wprowadzeniu RODO mówiło się wiele o tym, by nie zbierać danych których nie musimy używać.

A więc kiedy wspomnianą wiosną 2016 r. zorientowałem się że te wszystkie pola musiałyby siedzieć w bazie danych, zacząłem się zastanawiać, co by tu zrobić żeby się ich pozbyć i korzystać wyłącznie z tych, które są mi rzeczywiście potrzebne.

W tamtym okresie miałem kontakt ze znajomym, który kończył studia i zaczynał się zajmować zawodowo programowaniem w .NET.  Jednak na pytanie co zrobić aby pozbyć się niechcianych pól, stwierdził: co mi te pola przeszkadzają, że nie muszę ich wypełniać danymi, że to nie zajmuje zbyt wiele miejsca w bazie danych, że musiałbym nadpisać kilka komponentów w Identity i w ogóle tak się nie powinno robić. Można powiedzieć iż dał mi do zrozumienia że mi nijak nie pomoże.

Cóż począć… Nie umiałem sobie jeszcze poradzić z tym samodzielnie. Szczególnie że w owym okresie open-source’owy .NET Core był w powijakach. Kojarzę że nawet nie miał jeszcze pierwszej wersji stabilnej, a jedynie testową. Moje ówczesne próby napisania aplikacji do inżynierki dotyczyły jeszcze poprzedniego, Windowsowego środowiska ASP.NET, który nie miał otwartego kodu. Nie mogłem sobie podejrzeć źródła klas, które powinienem nadpisać aby pozbyć się niechcianych pól IdentityUser’a. Mogłem znaleźć jedynie ich nagłówki, a także dokumentację opisującą wprawdzie zastosowanie tych klas, ale bez dokładnej implementacji.

Jakiś czas później udało mi się uporać z problemami które uniemożliwiały mi prawidłowe skonfigurowanie uwierzytelniania w aplikacji Springowej i finalnie aplikacja do pracy inżynierskiej powstała w tym właśnie frameworku – w języku Java. Kwestia „okiełznania” ASP.NET Identity została zaś odłożona ad acta.

Krótko po obronieniu mojej pracy inżynierskiej napisałem nawet dosyć prostą aplikację REST API w środowisku ASP.NET Core. Jednak pomny przykrych doświadczeń z ASP.NET Identity, tamtym razem nie skorzystałem w ogóle z Identity, decydując się w tym względzie na jedno z alternatywnych rozwiązań.

Od czasu nieudanej próby użycia ASP.NET Identity minęło już kilka lat. Oczywiście przez ten czas wiele się zmieniło. Spędziłem wiele czasu, pracując nad swoją pracą magisterską, która dotyczy porównania Spring Framework i ASP.NET Core w zakresie możliwości jakie każdy z nich daje programiście przy tworzeniu aplikacji sieciowej, głównie REST API. Przy tworzeniu tejże pracy niejednokrotnie analizowałem dokumentację, a nawet kod źródłowy każdego z omawianych frameworków. (Na szczęście, dzisiaj ASP.NET w opensource’owym wydaniu Core ma się o wiele lepiej. Nie ma problemu z dotarciem do kodu źródłowego, a i jakość dokumentacji czy literatury jemu poświęconej jest znacznie lepsza – choć moim zdaniem ASP.NET nadal ustępuje w tym względzie Springowi.)

Już od jakiegoś czasu dochodziłem do wniosku że tym razem stać będzie mnie na samodzielne zmierzenie się z wyzwaniem dokonania drobnych modyfikacji dzięki którym będę mógł wykorzystać możliwości ASP.NET Identity, ale moja klasa (a zarazem tabela bazodanowa) użytkownika będzie zawierała te i tylko te pola, które uznam za potrzebne.

Poniżej opiszę, jak te zmagania wyglądały i co należy wykonać aby założony cel osiągnąć.

Przyjrzyjmy się najpierw, jak wygląda proces włączania modułu Identity do naszego projektu. Zajrzyjmy do źródła (https://github.com/aspnet/Identity/):

Jeśli zajrzymy do kodu typowej aplikacji sieciowej wykorzystującej Identity, np. jednego z szablonów generowanych w Visual Studio, w metodzie ConfigureServices klasy Startup pojawia się coś w tym stylu:

services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();

albo takim:

services.AddDefaultIdentity<IdentityUser>().AddDefaultUI(UIFramework.Bootstrap4).AddEntityFrameworkStores<ApplicationDbContext>();

Metodę .AddIdentity<>() możemy odnaleźć w klasie IdentityServiceCollectionExtensions. Nagłówek tej metody wygląda następująco:

public static IdentityBuilder AddIdentity(this IServiceCollection services) where TUser : class where TRole : class => services.AddIdentity(setupAction: null);
[…]
public static IdentityBuilder AddIdentity(this IServiceCollection services, Action setupAction) where TUser : class where TRole : class
{
[…]
}

W tej klasie nie pojawia się odwołanie do klasy IdentityUser, której instancję chcemy usunąć. Klasa użytkownika i jego roli może być dowolną klasą. A zatem tej metody nie musimy ruszać.

Podobnie jest z alternatywną metodą AddDefaultIdentity<>(), której definicję znajdziemy w klasie IdentityServiceCollectionUIExtensions:


public static IdentityBuilder AddDefaultIdentity(this IServiceCollection services, Action configureOptions) where TUser : class
{
[…]
}

Wewnątrz tej metody znajdziemy odwołanie do metody AddIdentityCore<>(), więc pozwolę sobie na pokazanie nagłówka również tej metody (klasa IdentityServiceCollectionExtensions> /znajduje się w innym pakiecie nazw niż pierwsza wymieniona klasa o tej nazwie – to nie jest to samo!/:)


public static IdentityBuilder AddIdentityCore(this IServiceCollection services) where TUser : class => services.AddIdentityCore(o => { });
[...]
public static IdentityBuilder AddIdentityCore(this IServiceCollection services, Action setupAction) where TUser : class
{
[...]
}

Metody AddDefaultTokenProviders() (klasa IdentityBuilderExtensions) oraz AddDefaultUI() (klasa IdentityBuilderUIExtensions) także nie mają nic wspólnego z klasą IdentityUser.

Najwięcej do zrobienia będzie z metodą AddEntityFrameworkStores<>() – klasa IdentityEntityFrameworkBuilderExtensions. Wygląda ona tak:


public static IdentityBuilder AddEntityFrameworkStores(this IdentityBuilder builder)
    where TContext : DbContext
{
    AddStores(builder.Services, builder.UserType, builder.RoleType, typeof(TContext));
    return builder;
}
private static void AddStores(IServiceCollection services, Type userType, Type roleType, Type contextType)
{
    var identityUserType = FindGenericBaseType(userType, typeof(IdentityUser));
    if (identityUserType == null)
    {
        throw new InvalidOperationException(Resources.NotIdentityUser);
    }
    var keyType = identityUserType.GenericTypeArguments[0];
    if (roleType != null)
    {
        var identityRoleType = FindGenericBaseType(roleType, typeof(IdentityRole));
        if (identityRoleType == null)
        {
            throw new InvalidOperationException(Resources.NotIdentityRole);
        }
        Type userStoreType = null;
        Type roleStoreType = null;
        var identityContext = FindGenericBaseType(contextType, typeof(IdentityDbContext));
        if (identityContext == null)
        {
            // If its a custom DbContext, we can only add the default POCOs
            userStoreType = typeof(UserStore).MakeGenericType(userType, roleType, contextType, keyType);
            roleStoreType = typeof(RoleStore).MakeGenericType(roleType, contextType, keyType);
        }
        else
        {
            userStoreType = typeof(UserStore).MakeGenericType(userType, roleType, contextType,
                identityContext.GenericTypeArguments[2],
                identityContext.GenericTypeArguments[3],
                identityContext.GenericTypeArguments[4],
                identityContext.GenericTypeArguments[5],
                identityContext.GenericTypeArguments[7],
                identityContext.GenericTypeArguments[6]);
            roleStoreType = typeof(RoleStore).MakeGenericType(roleType, contextType,
                identityContext.GenericTypeArguments[2],
                identityContext.GenericTypeArguments[4],
                identityContext.GenericTypeArguments[6]);
        }
        services.TryAddScoped(typeof(IUserStore).MakeGenericType(userType), userStoreType);
        services.TryAddScoped(typeof(IRoleStore).MakeGenericType(roleType), roleStoreType);
    }
    else
    {   // No Roles
        Type userStoreType = null;
        var identityContext = FindGenericBaseType(contextType, typeof(IdentityUserContext));
        if (identityContext == null)
        {
            // If its a custom DbContext, we can only add the default POCOs
            userStoreType = typeof(UserOnlyStore).MakeGenericType(userType, contextType, keyType);
        }
        else
        {
            userStoreType = typeof(UserOnlyStore).MakeGenericType(userType, contextType,
                identityContext.GenericTypeArguments[1],
                identityContext.GenericTypeArguments[2],
                identityContext.GenericTypeArguments[3],
                identityContext.GenericTypeArguments[4]);
        }
        services.TryAddScoped(typeof(IUserStore).MakeGenericType(userType), userStoreType);
    }
}

Metoda AddEntityFrameworkStores<>() przekierowuje ruch do kolejnej metody, o nazwie AddStores(). Tam natomiast już na samym początku następuje poszukiwanie typu generycznego na który zapisano klasę typu IdentityUser. Jeśli kompilator nie znajdzie takiej instancji w naszym projekcie, zwróci wyjątek i aplikacja się nie skompiluje.

Zatem, całą tę metodę musimy napisać na nowo, jeśli chcemy pozbyć się konieczności odwołania do klasy IdentityUser.

Co jednak robi ta nieszczęsna metoda AddStores()?

Na początku, jak już pisałem, próbuje ona odszukać typ generyczny przypisany do instancji IdentityUser w naszej aplikacji. Upraszczając, chodzi o typ wpisany w nawiasach trójkątnych obok typu klasy, np. IdentityUser<int>, IdentityUser<string> itp. Jest to typ klucza głównego będącego identyfikatorem danego użytkownika. Może to być wartość liczbowa, albo np. nazwa użytkownika czy też tzw. globally unique identifier, w skrócie GUID.

Następnym etapem wykonywania metody AddStores() jest próba odszukania – w analogiczny sposób – typu identyfikatora w roli użytkownika. Tu również wymusza się na programiście, aby skorzystał z narzuconej przez ASP.NET klasy IdentityRole. Podobnie jak IdentityUser, mamy tutaj kilka nadmiarowych pól, które twórcy frameworka próbują „na siłę” wcisnąć do naszej bazy danych, nie pytając się programisty czy tych danych potrzebuje czy nie. Obok oczywistej informacji, jak nazwa roli użytkownika czy też identyfikatora roli (chociaż niektórzy twórcy aplikacji mogliby przyjąć że nazwa roli wystarczy jako jej identyfikator), wymusza się na programiście zapewnienie miejsca w bazie danych na pola takie jak: nazwa znormalizowana roli czy „concurrency stamp” (tego zwrotu nawet nie udało mi się przetłumaczyć na język polski), według opisu ma to być jakaś losowa wartość zmieniana za każdym razem kiedy umieszcza się nową rolę użytkownika w zbiorze wszystkich ról.

Jednak po kolejnym przyjrzeniu się temu, co dzieje się wewnątrz metody AddStores(), zwróciłem uwagę na zupełnie inny element, który okazał się być KLUCZOWYM.

Zauważmy że próbując odnaleźć te typy generyczne, odwołano się do klas o nazwie UserStore i RoleStore:


userStoreType = typeof(UserStore<...>).MakeGenericType(...)
roleStoreType = typeof(RoleStore<...>).MakeGenericType(...)

Warto także zwrócić uwagę na dodanie serwisów do mechanizmu dependency injection, gdzie ma miejsce odwołanie do interfejsów o nazwach IUserStore<> i IRoleStore<>:

services.TryAddScoped(typeof(IUserStore<>).MakeGenericType(userType), userStoreType);
services.TryAddScoped(typeof(IRoleStore<>).MakeGenericType(roleType), roleStoreType);

Wspomniane klasy UserStore<> i RoleStore<> nadal zależą od nieszczęsnego IdentityUser (i analogicznie od IdentityRole), jednak oglądając ich kod źródłowy znacznie bardziej zbliżamy się do rozwiązania naszego problemu.

Spójrzmy najpierw na nagłówki wspomnianych klas:

public class UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
        UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>,
        IProtectedUserStore<TUser>
        where TUser : IdentityUser<TKey>
        where TRole : IdentityRole<TKey>
        where TContext : DbContext
        where TKey : IEquatable<TKey>
        where TUserClaim : IdentityUserClaim<TKey>, new()
        where TUserRole : IdentityUserRole<TKey>, new()
        where TUserLogin : IdentityUserLogin<TKey>, new()
        where TUserToken : IdentityUserToken<TKey>, new()
        where TRoleClaim : IdentityRoleClaim<TKey>, new()
...

public class RoleStore<TRole, TContext, TKey, TUserRole, TRoleClaim> :
        IQueryableRoleStore<TRole>,
        IRoleClaimStore<TRole>
        where TRole : IdentityRole<TKey>
        where TKey : IEquatable<TKey>
        where TContext : DbContext
        where TUserRole : IdentityUserRole<TKey>, new()
        where TRoleClaim : IdentityRoleClaim<TKey>, new()
...

Dodatkowo, zaprezentuję nagłówek klasy UserStoreBase<>, którą rozszerza wymieniona przed chwilą klasa UserStore<>:

public abstract class UserStoreBase<TUser, TKey, TUserClaim, TUserLogin, TUserToken> :
        IUserLoginStore<TUser>,
        IUserClaimStore<TUser>,
        IUserPasswordStore<TUser>,
        IUserSecurityStampStore<TUser>,
        IUserEmailStore<TUser>,
        IUserLockoutStore<TUser>,
        IUserPhoneNumberStore<TUser>,
        IQueryableUserStore<TUser>,
        IUserTwoFactorStore<TUser>,
        IUserAuthenticationTokenStore<TUser>,
        IUserAuthenticatorKeyStore<TUser>,
        IUserTwoFactorRecoveryCodeStore<TUser>
        where TUser : IdentityUser<TKey>
        where TKey : IEquatable<TKey>
        where TUserClaim : IdentityUserClaim<TKey>, new()
        where TUserLogin : IdentityUserLogin<TKey>, new()
        where TUserToken : IdentityUserToken<TKey>, new()
...

Zwróćmy teraz uwagę na fakt, iż zacytowane klasy: UserStore<>, UserStoreBase<> i RoleStore<> implementują interfejsy odpowiadające za przechowywanie różnych informacji na temat użytkownika lub jego roli w systemie. Wszystkie one dziedziczą odpowiednio po IUserStore<> lub IRoleStore<>.

Przypomnę, iż istotą tych poszukiwań było zdobycie wiedzy o tym, co należy zrobić, aby skonfigurować ASP.NET Identity przy użyciu własnego formatu bazy danych, z użyciem tylko tych pól które są nam rzeczywiście potrzebne.

Rozwiązaniem problemu jest samodzielna implementacja interfejsów IUserStore<>, IRoleStore<> lub, w razie potrzeby innego, bardziej szczegółowego interfejsu dziedziczącego po IUserStore<>, a następnie użycie tej implementacji w konfiguracji naszej aplikacji.

Niestety, rozwiązanie to okazało się być obarczone pewną wadą, która wywodzi się z samej konstrukcji ASP.NET Identity i według mojej wiedzy, nie ma możliwości jej idealnego „ominięcia”.

Wśród metod znajdujących się w najbardziej podstawowych interfejsach IUserStore<> i IRoleStore<> znalazło się odniesienie do tzw. „znormalizowanej nazwy” użytkownika lub roli, w postaci metod GetNormalizedUserNameAsync() i SetNormalizedUserNameAsync() dla IUserStore<> i analogicznych dla IRoleStore<>. Jedynym wyjściem z tej sytuacji – jeżeli nie chcemy zamieścić w bazie osobnego pola dla nazwy znormalizowanej – jest kreatywne „obejście” tej kwestii w kodzie. Możemy tu skorzystać z podpowiedzi zamieszczonej przez jednego z użytkowników portalu Stackoverflow.com – zwracanie w metodzie „Get…” naszej nazwy zapisanej wielkimi literami, oraz ignorowanie operacji „Set…” poprzez pozostawienie tej metody pustej (implementacja nie wykonująca żadnej akcji).

Możesz skomentować ten wpis w serwisach społecznościowych: Linkedin (przejdź) lub Facebook (przejdź).

Java FX

Możesz skomentować ten wpis w serwisach społecznościowych: Linkedin (przejdź) lub Facebook (przejdź).

JavaFX obrazuje może nieco mniej popularne dziś wykorzystanie języka Java. Jest biblioteką służącą do tworzenia aplikacji okienkowych. Ma ona zastąpić biblioteką Swing, której pierwszą wersję opracowano jeszcze w drugiej połowie lat 90. Swing był krytykowany choćby za to, iż środowiska developerskie (IDE) generowały sporą część kodu, niezbędną do działania aplikacji. O ile dobrze pamiętam, np. w Netbeans jego edycja była wręcz zablokowana.

JavaFX jest niewątpliwie nowszą technologią, bazującą na doświadczeniach technologii front-endowych. Pliki widoku są zbudowane w formie przypominającej HTML. Możliwe jest też używanie CSS w celu poprawienia efektów wizualnych, choć nie jest to konieczne.

Postanowiłem wypróbować tę technologię i zobaczyć, „z czym to się je”.

Czytaj dalej