{"id":41,"date":"2022-10-18T14:19:00","date_gmt":"2022-10-18T12:19:00","guid":{"rendered":"http:\/\/217.182.75.69\/blog-content\/?p=41"},"modified":"2022-10-19T15:54:02","modified_gmt":"2022-10-19T13:54:02","slug":"jak-uzyc-asp-net-identity-i-nie-zrobic-sobie-smietnika-w-bazie-danych","status":"publish","type":"post","link":"https:\/\/blog.michalch.pl\/index.php\/2022\/10\/18\/jak-uzyc-asp-net-identity-i-nie-zrobic-sobie-smietnika-w-bazie-danych\/","title":{"rendered":"Jak u\u017cy\u0107 ASP.NET Identity i nie zrobi\u0107 sobie \u015bmietnika w bazie danych?"},"content":{"rendered":"<p><em>Mo\u017cesz skomentowa\u0107 ten wpis w serwisach spo\u0142eczno\u015bciowych: Linkedin (<a href=\"https:\/\/www.linkedin.com\/posts\/michalch-pl_jak-u%C5%BCy%C4%87-aspnet-identity-i-nie-zrobi%C4%87-sobie-activity-6988497008026116096-rtaJ\">przejd\u017a<\/a>) lub Facebook (<a href=\"https:\/\/www.facebook.com\/michalchpl\/posts\/pfbid02WQZticLtFYK1YNFiz9yGLRv7rsBKFojvvhTaQsiq4t6CRAZSHzioRFcmhoTuPBT7l\">przejd\u017a<\/a>).<\/em><\/p>\n<p>Witam,<\/p>\n<p>w tym wpisie chcia\u0142em opisa\u0107 histori\u0119 swoich &#8222;zmaga\u0144&#8221; z ASP.NET Identity. My\u015bl\u0119 \u017ce niekt\u00f3rzy programi\u015bci mog\u0105 z tych zmaga\u0144 skorzysta\u0107 i u\u017cy\u0107 ich efekt\u00f3w we w\u0142asnych aplikacjach. Jest to tak\u017ce historyjka ciekawa, bo jest to jeden z epizod\u00f3w &#8211; wprawdzie nie najistotniejszy &#8211; ale jeden z powod\u00f3w, dla kt\u00f3rych moja \u015bcie\u017cka programistyczna potoczy\u0142a si\u0119 tak a nie inaczej.<\/p>\n<p>Wszystko zaczyna si\u0119 wiosn\u0105 2016 r. Moja wiedza o &#8222;du\u017cych&#8221; bibliotekach czy te\u017c frameworkach s\u0142u\u017c\u0105cych do budowy aplikacji by\u0142a w\u00f3wczas bardzo niewielka. W\u0142a\u015bnie planowa\u0142em sobie aplikacj\u0119 do mojej in\u017cynierki, a jedn\u0105 z decyzji jakie mia\u0142em podj\u0105\u0107, dotyczy\u0142a technologii z kt\u00f3rej skorzystam przy budowie tej aplikacji. W gr\u0119 wchodzi\u0142a w\u00f3wczas Java &#8211; a dok\u0142adniej zbudowany na jej bazie Spring Framework &#8211; oraz \u015brodowisko ASP.NET na bazie j\u0119zyka C#.<\/p>\n<p>Obie decyzje by\u0142y na tamten moment ryzykowne. Poniewa\u017c chcia\u0142em, aby moja aplikacja &#8211; a dok\u0142adniej strona internetowa &#8211; nie wymaga\u0142a prze\u0142adowywania strony za ka\u017cdym razem kiedy u\u017cytkownik co\u015b zmienia (tzw.\u00a0<em>single-page application<\/em>), potrzebowa\u0142em jakiego\u015b mechanizmu logowania i obs\u0142ugi kont u\u017cytkownik\u00f3w, kt\u00f3ry zadzia\u0142a w warunkach REST API (a zatem standardowy formularz logowania nie wchodzi\u0142 w gr\u0119).\u00a0Pocz\u0105tkowo pr\u00f3bowa\u0142em wst\u0119pnie &#8222;okie\u0142zna\u0107&#8221; oba frameworki. O ile si\u0119 nie myl\u0119, na bardzo wczesnym etapie rozwa\u017ca\u0142em nawet jednoczesne tworzenie API w obydwu technologiach. Inn\u0105 mo\u017cliwo\u015bci\u0105 by\u0142 wyb\u00f3r tej biblioteki, kt\u00f3r\u0105 \u0142atwiej dopasuj\u0119 do swoich oczekiwa\u0144.<\/p>\n<p>W przypadku Springa, przez d\u0142ugi czas barier\u0105 nie do pokonania by\u0142o skonfigurowanie oAuth&#8217;a, kt\u00f3ry mia\u0142 mi pos\u0142u\u017cy\u0107 do obs\u0142ugi logowania u\u017cytkownik\u00f3w. Z kolei w przypadku ASP.NET problematycznym okaza\u0142 si\u0119 modu\u0142 ASP.NET Identity.<\/p>\n<p>W wielkim skr\u00f3cie, ASP.NET Identity &#8211; wraz z innymi komponentami ASP.NET &#8211; pozwala do\u015b\u0107 szybko zaprogramowa\u0107 i skonfigurowa\u0107 ca\u0142\u0105 obs\u0142ug\u0119 kont u\u017cytkownik\u00f3w, wraz z mo\u017cliwo\u015bci\u0105 wyboru czy chcemy korzysta\u0107 ze standardowych formularzy logowania, z lokalnego oAuth&#8217;a czy te\u017c mo\u017ce z zewn\u0119trznego podmiotu uwierzytelniaj\u0105cego takiego jak np. Google czy Facebook. Niemal\u017ce automatycznie pozwala te\u017c zapisa\u0107 wszystkie informacje o u\u017cytkownikach do bazy danych. Potrzebne do tego jest jedynie skonfigurowanie klasy <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.entityframeworkcore.dbcontext\">DbContext<\/a>, a nast\u0119pnie wykonanie tzw. migracji, kt\u00f3ra zapisze nasz\u0105 struktur\u0119 danych w rzeczywistej bazie.<\/p>\n<p>Okaza\u0142o si\u0119 jednak \u017ce tak zbudowana biblioteka wymusza pewn\u0105 struktur\u0119 klasy odzwierciedlaj\u0105cej dane u\u017cytkownika systemu. Co gorsza, ta struktura jest zapisywana w bazie danych. Przyczyn\u0105 tego stanu rzeczy jest fakt, i\u017c domy\u015blny spos\u00f3b wykorzystania ASP.NET Identity zak\u0142ada \u017ce klasa reprezentuj\u0105ca u\u017cytkownika b\u0119dzie dziedziczy\u0107 po klasie <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.aspnetcore.identity.identityuser\">IdentityUser<\/a>\u00a0&#8211;\u00a0na co wskazuje nag\u0142\u00f3wek klasy <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.aspnetcore.identity.entityframeworkcore.identitydbcontext\">IdentityDbContext<\/a>, b\u0119d\u0105cej bazow\u0105 konfiguracja bazy danych:<\/p>\n<pre><span class=\"pl-k\">public<\/span> <span class=\"pl-k\">class<\/span> <span class=\"pl-en\">IdentityDbContext<\/span>&lt;<span class=\"pl-en\">TUser<\/span>&gt; : <span class=\"pl-en\">IdentityDbContext<\/span>&lt;<span class=\"pl-en\">TUser<\/span>, <span class=\"pl-en\">IdentityRole<\/span>, <span class=\"pl-k\">string<\/span>&gt; <span class=\"pl-k\">where<\/span> <span class=\"pl-en\">TUser<\/span> : <span class=\"pl-en\">IdentityUser<\/span><\/pre>\n<p><small><small>\u0179r\u00f3d\u0142o: <a href=\"https:\/\/github.com\/aspnet\/AspNetCore\/blob\/6e35229b015f0164c0876e2f6dc051409b1d1390\/src\/Identity\/EntityFrameworkCore\/src\/IdentityDbContext.cs\">repozytorium ASP.NET Core w serwisie github.com<\/a><\/small><\/small><\/p>\n<p>Z kolei klasa IdentityUser ju\u017c na samym starcie zawiera ca\u0142kiem sporo p\u00f3l. Na ka\u017cde z nich musi zosta\u0107 zarezerwowane miejsce w bazie danych.<\/p>\n<p>W efekcie, tabela u\u017cytkownik\u00f3w w naszej bazie danych b\u0119dzie wygl\u0105da\u0142a mniej wi\u0119cej tak:<\/p>\n<pre>1&gt; SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'AspNetUsers';\n2&gt; GO\nCOLUMN_NAME\n--------------------\nId\nAccessFailedCount\nConcurrencyStamp\nEmail\nEmailConfirmed\nLockoutEnabled\nLockoutEnd\nNormalizedEmail\nNormalizedUserName\nPasswordHash\nPhoneNumber\nPhoneNumberConfirmed\nSecurityStamp\nTwoFactorEnabled\nUserName\n\n(15 rows affected)<\/pre>\n<p>Powy\u017cszy kod jest zrzutem z konsoli SQL Server w mojej przyk\u0142adowej bazie danych, kt\u00f3ra zosta\u0142a przygotowana na bazie instancji klasy IdentityUser. Wykona\u0142em tutaj polecenie odczytu nazw wszystkich kolumn w tabeli u\u017cytkownik\u00f3w.<\/p>\n<p>I teraz pytanie do Czytelnik\u00f3w:<\/p>\n<p>Czy naprawd\u0119 ka\u017cdy z Was w swojej aplikacji potrzebuje p\u00f3l takich jak: LockoutEnabled, LockoutEnd? A SecurityStamp?\u00a0ConcurrencyStamp? S\u0105 to kwestie dotycz\u0105ce aktywowania lub dezaktywowania konta u\u017cytkownik\u00f3w oraz znaczniki zwi\u0105zane z dodawaniem nowego konta u\u017cytkownika lub zmian\u0105 jego has\u0142a. Je\u015bli kto\u015b chce korzysta\u0107 &#8211; droga wolna, ale dla mnie to kompletny bullshit.<\/p>\n<p>AccessFailedCount? Liczba b\u0142\u0119dnych logowa\u0144 na konto danego u\u017cytkownika. TwoFactorEnabled? Logowanie dwusk\u0142adnikowe. Te rzeczy mog\u0105 si\u0119 przyda\u0107 przy systemach szczeg\u00f3lnie wra\u017cliwych na bezpiecze\u0144stwo, ale ma\u0142o prawdopodobne by interesowa\u0142y tw\u00f3rc\u0119 przeci\u0119tnej strony internetowej.<\/p>\n<p>Podobnie EmailConfirmed czy te\u017c\u00a0NormalizedEmail i\u00a0NormalizedUserName.<\/p>\n<p>Nawet pola dotycz\u0105ce numeru telefonu (PhoneNumber i\u00a0PhoneNumberConfirmed) nie musz\u0105 by\u0107 konieczne w ka\u017cdej aplikacji. Nie mam problemu wyobrazi\u0107 sobie prostej aplikacji, kt\u00f3ra nie potrzebuje zbiera\u0107 takich danych. Zw\u0142aszcza po wprowadzeniu RODO m\u00f3wi\u0142o si\u0119 wiele o tym, by nie zbiera\u0107 danych kt\u00f3rych nie musimy u\u017cywa\u0107.<\/p>\n<p>A wi\u0119c kiedy wspomnian\u0105 wiosn\u0105 2016 r. zorientowa\u0142em si\u0119 \u017ce te wszystkie pola musia\u0142yby siedzie\u0107 w bazie danych, zacz\u0105\u0142em si\u0119 zastanawia\u0107, co by tu zrobi\u0107 \u017ceby si\u0119 ich pozby\u0107 i korzysta\u0107 wy\u0142\u0105cznie z tych, kt\u00f3re s\u0105 mi rzeczywi\u015bcie potrzebne.<\/p>\n<p>W tamtym okresie mia\u0142em kontakt ze znajomym, kt\u00f3ry ko\u0144czy\u0142 studia i zaczyna\u0142 si\u0119 zajmowa\u0107 zawodowo programowaniem w .NET.\u00a0 Jednak na pytanie co zrobi\u0107 aby pozby\u0107 si\u0119 niechcianych p\u00f3l, stwierdzi\u0142: co mi te pola przeszkadzaj\u0105, \u017ce nie musz\u0119 ich wype\u0142nia\u0107 danymi, \u017ce to nie zajmuje zbyt wiele miejsca w bazie danych, \u017ce musia\u0142bym nadpisa\u0107 kilka komponent\u00f3w w Identity i w og\u00f3le tak si\u0119 nie powinno robi\u0107. Mo\u017cna powiedzie\u0107 i\u017c da\u0142 mi do zrozumienia \u017ce mi nijak nie pomo\u017ce.<\/p>\n<p>C\u00f3\u017c pocz\u0105\u0107&#8230; Nie umia\u0142em sobie jeszcze poradzi\u0107 z tym samodzielnie. Szczeg\u00f3lnie \u017ce w owym okresie open-source&#8217;owy .NET Core by\u0142 w powijakach. Kojarz\u0119 \u017ce nawet nie mia\u0142 jeszcze pierwszej wersji stabilnej, a jedynie testow\u0105. Moje \u00f3wczesne pr\u00f3by napisania aplikacji do in\u017cynierki dotyczy\u0142y jeszcze poprzedniego, Windowsowego \u015brodowiska ASP.NET, kt\u00f3ry nie mia\u0142 otwartego kodu. Nie mog\u0142em sobie podejrze\u0107 \u017ar\u00f3d\u0142a klas, kt\u00f3re powinienem nadpisa\u0107 aby pozby\u0107 si\u0119 niechcianych p\u00f3l IdentityUser&#8217;a. Mog\u0142em znale\u017a\u0107 jedynie ich nag\u0142\u00f3wki, a tak\u017ce dokumentacj\u0119 opisuj\u0105c\u0105 wprawdzie zastosowanie tych klas, ale bez dok\u0142adnej implementacji.<\/p>\n<p>Jaki\u015b czas p\u00f3\u017aniej uda\u0142o mi si\u0119 upora\u0107 z problemami kt\u00f3re uniemo\u017cliwia\u0142y mi prawid\u0142owe skonfigurowanie uwierzytelniania w aplikacji Springowej i finalnie aplikacja do pracy in\u017cynierskiej powsta\u0142a w tym w\u0142a\u015bnie frameworku &#8211; w j\u0119zyku Java. Kwestia &#8222;okie\u0142znania&#8221; ASP.NET Identity zosta\u0142a za\u015b od\u0142o\u017cona\u00a0<em>ad acta<\/em>.<\/p>\n<p>Kr\u00f3tko po obronieniu mojej pracy in\u017cynierskiej napisa\u0142em nawet dosy\u0107 prost\u0105 aplikacj\u0119 REST API w \u015brodowisku ASP.NET Core. Jednak pomny przykrych do\u015bwiadcze\u0144 z ASP.NET Identity, tamtym razem nie skorzysta\u0142em w og\u00f3le z Identity, decyduj\u0105c si\u0119 w tym wzgl\u0119dzie na jedno z alternatywnych rozwi\u0105za\u0144.<\/p>\n<p>&#8230;<\/p>\n<p>Od czasu nieudanej pr\u00f3by u\u017cycia ASP.NET Identity min\u0119\u0142o ju\u017c kilka lat. Oczywi\u015bcie przez ten czas wiele si\u0119 zmieni\u0142o. Sp\u0119dzi\u0142em wiele czasu, pracuj\u0105c nad swoj\u0105 prac\u0105 magistersk\u0105, kt\u00f3ra dotyczy por\u00f3wnania Spring Framework i ASP.NET Core w zakresie mo\u017cliwo\u015bci jakie ka\u017cdy z nich daje programi\u015bcie przy tworzeniu aplikacji sieciowej, g\u0142\u00f3wnie REST API. Przy tworzeniu tej\u017ce pracy niejednokrotnie analizowa\u0142em dokumentacj\u0119, a nawet kod \u017ar\u00f3d\u0142owy ka\u017cdego z omawianych framework\u00f3w. (Na szcz\u0119\u015bcie, dzisiaj ASP.NET w opensource&#8217;owym wydaniu Core ma si\u0119 o wiele lepiej. Nie ma problemu z dotarciem do kodu \u017ar\u00f3d\u0142owego, a i jako\u015b\u0107 dokumentacji czy literatury jemu po\u015bwi\u0119conej jest znacznie lepsza &#8211; cho\u0107 moim zdaniem ASP.NET nadal ust\u0119puje w tym wzgl\u0119dzie Springowi.)<\/p>\n<p>Ju\u017c od jakiego\u015b czasu dochodzi\u0142em do wniosku \u017ce tym razem sta\u0107 b\u0119dzie mnie na samodzielne zmierzenie si\u0119 z wyzwaniem dokonania drobnych modyfikacji dzi\u0119ki kt\u00f3rym b\u0119d\u0119 m\u00f3g\u0142 wykorzysta\u0107 mo\u017cliwo\u015bci ASP.NET Identity, ale moja klasa (a zarazem tabela bazodanowa) u\u017cytkownika b\u0119dzie zawiera\u0142a te i tylko te pola, kt\u00f3re uznam za potrzebne.<\/p>\n<p>Poni\u017cej opisz\u0119, jak te zmagania wygl\u0105da\u0142y i co nale\u017cy wykona\u0107 aby za\u0142o\u017cony cel osi\u0105gn\u0105\u0107.<\/p>\n<p>Przyjrzyjmy si\u0119 najpierw, jak wygl\u0105da proces w\u0142\u0105czania modu\u0142u Identity do naszego projektu. Zajrzyjmy do \u017ar\u00f3d\u0142a (https:\/\/github.com\/aspnet\/Identity\/):<\/p>\n<p>Je\u015bli zajrzymy do kodu typowej aplikacji sieciowej wykorzystuj\u0105cej Identity, np. jednego z szablon\u00f3w generowanych w Visual Studio, w metodzie ConfigureServices klasy Startup pojawia si\u0119 co\u015b w tym stylu:<\/p>\n<pre>services.AddIdentity&lt;ApplicationUser, IdentityRole&gt;().AddEntityFrameworkStores&lt;ApplicationDbContext&gt;().AddDefaultTokenProviders();\n<\/pre>\n<p>albo takim:<\/p>\n<pre>services.AddDefaultIdentity&lt;IdentityUser&gt;().AddDefaultUI(UIFramework.Bootstrap4).AddEntityFrameworkStores&lt;ApplicationDbContext&gt;();\n<\/pre>\n<p>Metod\u0119 .AddIdentity&lt;&gt;() mo\u017cemy odnale\u017a\u0107 w klasie <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/master\/src\/Identity\/Core\/src\/IdentityServiceCollectionExtensions.cs\">IdentityServiceCollectionExtensions<\/a>. Nag\u0142\u00f3wek tej metody wygl\u0105da nast\u0119puj\u0105co:<\/p>\n<pre>public static IdentityBuilder AddIdentity(this IServiceCollection services) where TUser : class where TRole : class =&gt; services.AddIdentity(setupAction: null);\n[\u2026]\npublic static IdentityBuilder AddIdentity(this IServiceCollection services, Action setupAction) where TUser : class where TRole : class\n{\n[\u2026]\n}\n<\/pre>\n<p>W tej klasie nie pojawia si\u0119 odwo\u0142anie do klasy IdentityUser, kt\u00f3rej instancj\u0119 chcemy usun\u0105\u0107. Klasa u\u017cytkownika i jego roli mo\u017ce by\u0107 dowoln\u0105 klas\u0105. A zatem tej metody nie musimy rusza\u0107.<\/p>\n<p>Podobnie jest z alternatywn\u0105 metod\u0105 <em>AddDefaultIdentity&lt;&gt;()<\/em>, kt\u00f3rej definicj\u0119 znajdziemy w klasie <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/master\/src\/Identity\/UI\/src\/IdentityServiceCollectionUIExtensions.cs\">IdentityServiceCollectionUIExtensions<\/a>:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>\npublic static IdentityBuilder AddDefaultIdentity(this IServiceCollection services, Action configureOptions) where TUser : class\n{\n&#91;\u2026]\n}\n<\/code><\/pre>\n\n\n<p>Wewn\u0105trz tej metody znajdziemy odwo\u0142anie do metody <em>AddIdentityCore&lt;&gt;()<\/em>, wi\u0119c pozwol\u0119 sobie na pokazanie nag\u0142\u00f3wka r\u00f3wnie\u017c tej metody (klasa <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/master\/src\/Identity\/Extensions.Core\/src\/IdentityServiceCollectionExtensions.cs\">IdentityServiceCollectionExtensions&gt;<\/a> \/znajduje si\u0119 w innym pakiecie nazw ni\u017c pierwsza wymieniona klasa o tej nazwie &#8211; to nie jest to samo!\/:)<\/p>\n\n\n<pre class=\"wp-block-code\"><code>\npublic static IdentityBuilder AddIdentityCore(this IServiceCollection services) where TUser : class =&gt; services.AddIdentityCore(o =&gt; { });\n&#91;...]\npublic static IdentityBuilder AddIdentityCore(this IServiceCollection services, Action setupAction) where TUser : class\n{\n&#91;...]\n}\n<\/code><\/pre>\n\n\n<p>Metody AddDefaultTokenProviders() (klasa <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/master\/src\/Identity\/Core\/src\/IdentityBuilderExtensions.cs\">IdentityBuilderExtensions<\/a>) oraz AddDefaultUI() (klasa <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/master\/src\/Identity\/UI\/src\/IdentityBuilderUIExtensions.cs\">IdentityBuilderUIExtensions<\/a>) tak\u017ce nie maj\u0105 nic wsp\u00f3lnego z klas\u0105 IdentityUser.<\/p>\n<p>Najwi\u0119cej do zrobienia b\u0119dzie z metod\u0105 <em>AddEntityFrameworkStores&lt;&gt;()<\/em> &#8211; klasa <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/master\/src\/Identity\/EntityFrameworkCore\/src\/IdentityEntityFrameworkBuilderExtensions.cs\">IdentityEntityFrameworkBuilderExtensions<\/a>. Wygl\u0105da ona tak:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>\npublic static IdentityBuilder AddEntityFrameworkStores<TContext>(this IdentityBuilder builder)\n    where TContext : DbContext\n{\n    AddStores(builder.Services, builder.UserType, builder.RoleType, typeof(TContext));\n    return builder;\n}\nprivate static void AddStores(IServiceCollection services, Type userType, Type roleType, Type contextType)\n{\n    var identityUserType = FindGenericBaseType(userType, typeof(IdentityUser<>));\n    if (identityUserType == null)\n    {\n        throw new InvalidOperationException(Resources.NotIdentityUser);\n    }\n    var keyType = identityUserType.GenericTypeArguments[0];\n    if (roleType != null)\n    {\n        var identityRoleType = FindGenericBaseType(roleType, typeof(IdentityRole<>));\n        if (identityRoleType == null)\n        {\n            throw new InvalidOperationException(Resources.NotIdentityRole);\n        }\n        Type userStoreType = null;\n        Type roleStoreType = null;\n        var identityContext = FindGenericBaseType(contextType, typeof(IdentityDbContext<,,,,,,,>));\n        if (identityContext == null)\n        {\n            \/\/ If its a custom DbContext, we can only add the default POCOs\n            userStoreType = typeof(UserStore<,,,>).MakeGenericType(userType, roleType, contextType, keyType);\n            roleStoreType = typeof(RoleStore<,,>).MakeGenericType(roleType, contextType, keyType);\n        }\n        else\n        {\n            userStoreType = typeof(UserStore<,,,,,,,,>).MakeGenericType(userType, roleType, contextType,\n                identityContext.GenericTypeArguments[2],\n                identityContext.GenericTypeArguments[3],\n                identityContext.GenericTypeArguments[4],\n                identityContext.GenericTypeArguments[5],\n                identityContext.GenericTypeArguments[7],\n                identityContext.GenericTypeArguments[6]);\n            roleStoreType = typeof(RoleStore<,,,,>).MakeGenericType(roleType, contextType,\n                identityContext.GenericTypeArguments[2],\n                identityContext.GenericTypeArguments[4],\n                identityContext.GenericTypeArguments[6]);\n        }\n        services.TryAddScoped(typeof(IUserStore<>).MakeGenericType(userType), userStoreType);\n        services.TryAddScoped(typeof(IRoleStore<>).MakeGenericType(roleType), roleStoreType);\n    }\n    else\n    {   \/\/ No Roles\n        Type userStoreType = null;\n        var identityContext = FindGenericBaseType(contextType, typeof(IdentityUserContext<,,,,>));\n        if (identityContext == null)\n        {\n            \/\/ If its a custom DbContext, we can only add the default POCOs\n            userStoreType = typeof(UserOnlyStore<,,>).MakeGenericType(userType, contextType, keyType);\n        }\n        else\n        {\n            userStoreType = typeof(UserOnlyStore<,,,,,>).MakeGenericType(userType, contextType,\n                identityContext.GenericTypeArguments[1],\n                identityContext.GenericTypeArguments[2],\n                identityContext.GenericTypeArguments[3],\n                identityContext.GenericTypeArguments[4]);\n        }\n        services.TryAddScoped(typeof(IUserStore<>).MakeGenericType(userType), userStoreType);\n    }\n}\n<\/code><\/pre>\n\n\n<p>Metoda <em>AddEntityFrameworkStores&lt;&gt;()<\/em> przekierowuje ruch do kolejnej metody, o nazwie <em>AddStores()<\/em>. Tam natomiast ju\u017c na samym pocz\u0105tku nast\u0119puje poszukiwanie typu generycznego na kt\u00f3ry zapisano klas\u0119 typu IdentityUser. Je\u015bli kompilator nie znajdzie takiej instancji w naszym projekcie, zwr\u00f3ci wyj\u0105tek i aplikacja si\u0119 nie skompiluje.<\/p>\n<p>Zatem, ca\u0142\u0105 t\u0119 metod\u0119 musimy napisa\u0107 na nowo, je\u015bli chcemy pozby\u0107 si\u0119 konieczno\u015bci odwo\u0142ania do klasy IdentityUser.<\/p>\n<p>Co jednak robi ta nieszcz\u0119sna metoda <em>AddStores()<\/em>?<\/p>\n<p>Na pocz\u0105tku, jak ju\u017c pisa\u0142em, pr\u00f3buje ona odszuka\u0107 typ generyczny przypisany do instancji IdentityUser w naszej aplikacji. Upraszczaj\u0105c, chodzi o typ wpisany w nawiasach tr\u00f3jk\u0105tnych obok typu klasy, np. IdentityUser&lt;int&gt;, IdentityUser&lt;string&gt; itp. Jest to typ klucza g\u0142\u00f3wnego b\u0119d\u0105cego identyfikatorem danego u\u017cytkownika. Mo\u017ce to by\u0107 warto\u015b\u0107 liczbowa, albo np. nazwa u\u017cytkownika czy te\u017c <a href=\"https:\/\/pl.wikipedia.org\/wiki\/Globally_Unique_Identifier\">tzw. globally unique identifier, w skr\u00f3cie GUID.<\/a><\/p>\n<p>Nast\u0119pnym etapem wykonywania metody&nbsp;<em>AddStores()<\/em> jest pr\u00f3ba odszukania &#8211; w analogiczny spos\u00f3b &#8211; typu identyfikatora w roli u\u017cytkownika. Tu r\u00f3wnie\u017c wymusza si\u0119 na programi\u015bcie, aby skorzysta\u0142 z narzuconej przez ASP.NET klasy <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/master\/src\/Identity\/Extensions.Stores\/src\/IdentityRole.cs\">IdentityRole<\/a>. Podobnie jak IdentityUser, mamy tutaj kilka nadmiarowych p\u00f3l, kt\u00f3re tw\u00f3rcy frameworka pr\u00f3buj\u0105 &#8222;na si\u0142\u0119&#8221; wcisn\u0105\u0107 do naszej bazy danych, nie pytaj\u0105c si\u0119 programisty czy tych danych potrzebuje czy nie. Obok oczywistej informacji, jak nazwa roli u\u017cytkownika czy te\u017c identyfikatora roli (chocia\u017c niekt\u00f3rzy tw\u00f3rcy aplikacji mogliby przyj\u0105\u0107 \u017ce nazwa roli wystarczy jako jej identyfikator), wymusza si\u0119 na programi\u015bcie zapewnienie miejsca w bazie danych na pola takie jak: nazwa znormalizowana roli czy &#8222;concurrency stamp&#8221; (tego zwrotu nawet nie uda\u0142o mi si\u0119 przet\u0142umaczy\u0107 na j\u0119zyk polski), wed\u0142ug opisu ma to by\u0107 jaka\u015b losowa warto\u015b\u0107 zmieniana za ka\u017cdym razem kiedy umieszcza si\u0119 now\u0105 rol\u0119 u\u017cytkownika w zbiorze wszystkich r\u00f3l.<\/p>\n<p><\/p>\n<p>Jednak <strong>po kolejnym przyjrzeniu si\u0119<\/strong> temu, co dzieje si\u0119 wewn\u0105trz metody&nbsp;<em>AddStores(), <\/em>zwr\u00f3ci\u0142em uwag\u0119 na <strong>zupe\u0142nie inny element<\/strong>, kt\u00f3ry&nbsp;<strong>okaza\u0142 si\u0119 by\u0107 KLUCZOWYM<\/strong>.<\/p>\n<p>Zauwa\u017cmy \u017ce pr\u00f3buj\u0105c odnale\u017a\u0107 te typy generyczne, odwo\u0142ano si\u0119 do klas o nazwie <em>UserStore<\/em> i <em>RoleStore<\/em>:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>\nuserStoreType = typeof(UserStore&lt;...&gt;).MakeGenericType(...)\nroleStoreType = typeof(RoleStore&lt;...&gt;).MakeGenericType(...)\n<\/code><\/pre>\n\n\n\n<p>Warto tak\u017ce zwr\u00f3ci\u0107 uwag\u0119 na dodanie serwis\u00f3w do mechanizmu <em>dependency injection<\/em>, gdzie ma miejsce  odwo\u0142anie do interfejs\u00f3w o nazwach <em>IUserStore&lt;&gt;<\/em> i <em>IRoleStore&lt;&gt;<\/em>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services.TryAddScoped(typeof(IUserStore&lt;&gt;).MakeGenericType(userType), userStoreType);\nservices.TryAddScoped(typeof(IRoleStore&lt;&gt;).MakeGenericType(roleType), roleStoreType);<\/code><\/pre>\n\n\n\n<p>Wspomniane klasy <em><a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/main\/src\/Identity\/EntityFrameworkCore\/src\/UserStore.cs\" data-type=\"URL\" data-id=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/main\/src\/Identity\/EntityFrameworkCore\/src\/UserStore.cs\">UserStore<\/a>&lt;&gt;<\/em> i <em><a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/main\/src\/Identity\/EntityFrameworkCore\/src\/RoleStore.cs\" data-type=\"URL\" data-id=\"https:\/\/github.com\/dotnet\/aspnetcore\/blob\/main\/src\/Identity\/EntityFrameworkCore\/src\/RoleStore.cs\">RoleStore<\/a>&lt;&gt;<\/em> nadal zale\u017c\u0105 od nieszcz\u0119snego <em>IdentityUser<\/em> (i analogicznie od <em>IdentityRole<\/em>), jednak ogl\u0105daj\u0105c ich kod \u017ar\u00f3d\u0142owy znacznie bardziej zbli\u017camy si\u0119 do rozwi\u0105zania naszego problemu.<\/p>\n\n\n\n<p>Sp\u00f3jrzmy najpierw na nag\u0142\u00f3wki wspomnianych klas:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class UserStore&lt;TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim&gt; :\n        UserStoreBase&lt;TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim&gt;,\n        IProtectedUserStore&lt;TUser&gt;\n        where TUser : IdentityUser&lt;TKey&gt;\n        where TRole : IdentityRole&lt;TKey&gt;\n        where TContext : DbContext\n        where TKey : IEquatable&lt;TKey&gt;\n        where TUserClaim : IdentityUserClaim&lt;TKey&gt;, new()\n        where TUserRole : IdentityUserRole&lt;TKey&gt;, new()\n        where TUserLogin : IdentityUserLogin&lt;TKey&gt;, new()\n        where TUserToken : IdentityUserToken&lt;TKey&gt;, new()\n        where TRoleClaim : IdentityRoleClaim&lt;TKey&gt;, new()\n...\n\npublic class RoleStore&lt;TRole, TContext, TKey, TUserRole, TRoleClaim&gt; :\n        IQueryableRoleStore&lt;TRole&gt;,\n        IRoleClaimStore&lt;TRole&gt;\n        where TRole : IdentityRole&lt;TKey&gt;\n        where TKey : IEquatable&lt;TKey&gt;\n        where TContext : DbContext\n        where TUserRole : IdentityUserRole&lt;TKey&gt;, new()\n        where TRoleClaim : IdentityRoleClaim&lt;TKey&gt;, new()\n...<\/code><\/pre>\n\n\n\n<p>Dodatkowo, zaprezentuj\u0119 nag\u0142\u00f3wek klasy <em>UserStoreBase&lt;&gt;<\/em>, kt\u00f3r\u0105 rozszerza wymieniona przed chwil\u0105 klasa <em>UserStore&lt;&gt;<\/em>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public abstract class UserStoreBase&lt;TUser, TKey, TUserClaim, TUserLogin, TUserToken&gt; :\n        IUserLoginStore&lt;TUser&gt;,\n        IUserClaimStore&lt;TUser&gt;,\n        IUserPasswordStore&lt;TUser&gt;,\n        IUserSecurityStampStore&lt;TUser&gt;,\n        IUserEmailStore&lt;TUser&gt;,\n        IUserLockoutStore&lt;TUser&gt;,\n        IUserPhoneNumberStore&lt;TUser&gt;,\n        IQueryableUserStore&lt;TUser&gt;,\n        IUserTwoFactorStore&lt;TUser&gt;,\n        IUserAuthenticationTokenStore&lt;TUser&gt;,\n        IUserAuthenticatorKeyStore&lt;TUser&gt;,\n        IUserTwoFactorRecoveryCodeStore&lt;TUser&gt;\n        where TUser : IdentityUser&lt;TKey&gt;\n        where TKey : IEquatable&lt;TKey&gt;\n        where TUserClaim : IdentityUserClaim&lt;TKey&gt;, new()\n        where TUserLogin : IdentityUserLogin&lt;TKey&gt;, new()\n        where TUserToken : IdentityUserToken&lt;TKey&gt;, new()\n...<\/code><\/pre>\n\n\n\n<p>Zwr\u00f3\u0107my teraz uwag\u0119 na fakt, i\u017c zacytowane klasy: <em>UserStore&lt;&gt;<\/em>, <em>UserStoreBase&lt;&gt;<\/em> i <em>RoleStore&lt;&gt;<\/em> implementuj\u0105 interfejsy odpowiadaj\u0105ce za przechowywanie r\u00f3\u017cnych informacji na temat u\u017cytkownika lub jego roli w systemie. Wszystkie one dziedzicz\u0105 odpowiednio po <em>IUserStore&lt;&gt;<\/em> lub <em>IRoleStore&lt;&gt;<\/em>.<\/p>\n\n\n\n<p>Przypomn\u0119, i\u017c istot\u0105 tych poszukiwa\u0144 by\u0142o zdobycie wiedzy o tym, co nale\u017cy zrobi\u0107, aby skonfigurowa\u0107 ASP.NET Identity przy u\u017cyciu w\u0142asnego formatu bazy danych, z u\u017cyciem tylko tych p\u00f3l kt\u00f3re s\u0105 nam rzeczywi\u015bcie potrzebne.<\/p>\n\n\n\n<p><strong>Rozwi\u0105zaniem<\/strong> problemu jest <strong>samodzielna implementacja<\/strong> interfejs\u00f3w <em>IUserStore&lt;&gt;<\/em>, <em>IRoleStore&lt;&gt;<\/em> lub, w razie potrzeby innego, bardziej szczeg\u00f3\u0142owego interfejsu dziedzicz\u0105cego po <em>IUserStore&lt;&gt;<\/em>, a nast\u0119pnie  u\u017cycie tej implementacji w konfiguracji naszej aplikacji.<\/p>\n\n\n\n<p>Niestety, rozwi\u0105zanie to okaza\u0142o si\u0119 by\u0107 obarczone pewn\u0105 wad\u0105, kt\u00f3ra wywodzi si\u0119 z samej konstrukcji ASP.NET Identity i wed\u0142ug mojej wiedzy, nie ma mo\u017cliwo\u015bci jej idealnego &#8222;omini\u0119cia&#8221;.<\/p>\n\n\n\n<p>W\u015br\u00f3d metod znajduj\u0105cych si\u0119 w najbardziej podstawowych interfejsach IUserStore&lt;&gt; i IRoleStore&lt;&gt; znalaz\u0142o si\u0119 odniesienie do tzw. &#8222;znormalizowanej nazwy&#8221; u\u017cytkownika lub roli, w postaci metod <em>GetNormalizedUserNameAsync<\/em>() i <em>SetNormalizedUserNameAsync()<\/em> dla <em>IUserStore&lt;&gt;<\/em> i analogicznych dla <em>IRoleStore&lt;&gt;<\/em>. Jedynym wyj\u015bciem z tej sytuacji &#8211; je\u017celi nie chcemy zamie\u015bci\u0107 w bazie osobnego pola dla nazwy znormalizowanej &#8211; jest kreatywne &#8222;obej\u015bcie&#8221; tej kwestii w kodzie. Mo\u017cemy tu skorzysta\u0107 z <a href=\"https:\/\/stackoverflow.com\/a\/39727911\" data-type=\"URL\" data-id=\"https:\/\/stackoverflow.com\/a\/39727911\">podpowiedzi zamieszczonej przez jednego z u\u017cytkownik\u00f3w portalu Stackoverflow.com<\/a> &#8211; zwracanie w metodzie &#8222;Get&#8230;&#8221; naszej nazwy zapisanej wielkimi literami, oraz ignorowanie operacji &#8222;Set&#8230;&#8221; poprzez pozostawienie tej metody pustej (implementacja nie wykonuj\u0105ca \u017cadnej akcji).<\/p>\n\n\n\n<p><em>Mo\u017cesz skomentowa\u0107 ten wpis w serwisach spo\u0142eczno\u015bciowych: Linkedin (<a href=\"https:\/\/www.linkedin.com\/posts\/michalch-pl_jak-u%C5%BCy%C4%87-aspnet-identity-i-nie-zrobi%C4%87-sobie-activity-6988497008026116096-rtaJ\">przejd\u017a<\/a>) lub Facebook (<a href=\"https:\/\/www.facebook.com\/michalchpl\/posts\/pfbid02WQZticLtFYK1YNFiz9yGLRv7rsBKFojvvhTaQsiq4t6CRAZSHzioRFcmhoTuPBT7l\">przejd\u017a<\/a>).<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Mo\u017cesz skomentowa\u0107 ten wpis w serwisach spo\u0142eczno\u015bciowych: Linkedin (przejd\u017a) lub Facebook (przejd\u017a). Witam, w tym wpisie chcia\u0142em opisa\u0107 histori\u0119 swoich &#8222;zmaga\u0144&#8221; z ASP.NET Identity. My\u015bl\u0119 \u017ce niekt\u00f3rzy programi\u015bci mog\u0105 z tych zmaga\u0144 skorzysta\u0107 i u\u017cy\u0107 ich efekt\u00f3w we w\u0142asnych aplikacjach. Jest to tak\u017ce historyjka ciekawa, bo jest to jeden z epizod\u00f3w &#8211; wprawdzie nie najistotniejszy [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[4],"tags":[],"_links":{"self":[{"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/posts\/41"}],"collection":[{"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/comments?post=41"}],"version-history":[{"count":41,"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/posts\/41\/revisions"}],"predecessor-version":[{"id":134,"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/posts\/41\/revisions\/134"}],"wp:attachment":[{"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/media?parent=41"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/categories?post=41"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.michalch.pl\/index.php\/wp-json\/wp\/v2\/tags?post=41"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}