Author Archives: Robert Haken

avatar Neznámé

About Robert Haken

Software Architect, Founder at HAVIT, Microsoft MVP - ASP.NET/IIS

Zkušenosti s přechodu na TFS a agilní techniky – záznam a slides [WUG Praha 1/2015]

Slides z přednášky 15.1.2015 pro Windows User Group Praha (WUG), kde jsme s kolegou Jirkou Kandou povídali i našich zkušenostech z přechodu HAVITu na TFS a agilní techniky (v období 08/2012 až 12/2014):

Z přednášky jsem pořizoval záznam, který najdete na našem HAVIT YouTube Channelu:

Omluvte sníženou kvalitu zvuku záznamu, nahrávali jsme tentokrát na dva různé mikrofony a výsledný záznam je sestřihán ze dvou různých zvukových stop (jedna z mikrofonů navíc nebyl zřejmě úplně ideálně umístěn a chytal nežádoucí ruchy).

iTunes (+Match) – nelze změnit Media Kind (např. na Audiobook)

Pro hudbu a audioknihy používám iTunes. Protože mám iTunes Match, cpu do toho všechny CD, co se nám doma objeví. iTunes si je grabnou do M4A a uloží do Library. Ještě do „nedávna“ šlo měnit po nacucnutí CD pomocí Get Info volbu Media Kind (sekce Options), přičemž po přepnutí na Audiobook se album přesunulo ze sekce Music do sekce Audiobooks.

Dneska jsem zjistil, že volba Media Kind mi nabízí jedinou možnost: Music. Naimportované CD s pohádkami se mi nedaří dostat mezi Audiobooks.

Nakonec se ukazuje, že to je jakési nové chování iTunes, které se projevuje ve spojení s aktivním iTunes Match, kdy nelze Media Kind změnit.

Workaround: Stačí na chvilku vypnout iTunes Match (menu: Store / Turn Off iTunes Match), potom lze Media Kind změnit. Následně iTunes Match reaktivovat. To mi funguje.

Někdo další radí založit druhou Library bez napojení na iTunes Match, do ní si grabovat, nastavit Media Kind a další tagy a následně odtud hrnout do hlavní Library.

Jak vypnout automatické stmívání LCD panelu notebooku při tmavých obrazech

Už pěknou dobu mě štve, že můj notebook při tmavých obrazech (například Visual Studio bez otevřeného souboru) automaticky stahuje intenzitu podsvícení, což zhoršuje čitelnost a po změně obrazu na světlý mu několik vteřin trvá, než se zadaptuje zpět.

Je mi jasné, že to je hlavně kvůli úspoře baterie, ale proč to dělá i na AC adaptéru, to nevím. Každopádně mě to štve natolik, že jsem se to snažil od počátku nějak vypnout. Celkem rychle jsem se (chybně) dogooglil k nastavení Intel HD Graphics, které by to mělo být:

image

Po opakovaných neúspěšných pokusech s tímto nastavením jsem si to pro sebe uzavřel s tím, že mám holt smůlu a vypnout se mi to nepodaří. (Nakonec se ukázalo, že musí být vypnuté i toto, resp. alespoň v poloze „max. jeden dílek od Maximální kvality“).

Dnes mě to po delší době zase rozběsnilo, tak jsem pátral znovu. Nakonec jsem se dopátral k volbě, která zabírá, a kterou se to dá opravdu vypnout:

image

Nakonec se ukazuje, že vypnout je potřeba obojí. Volba přes Windows Power Options je výrazně agresivnější, nicméně i s jejím vypnutím se stále trochu obraz stmívá (není už to poznat ve Visual Studiu, ale třeba SnagIt, který je celý černý, tak nakonec intenzitu stáhne). Teprve po vypnutí na obou místech přestane stmívání úplně. Různou kombinací voleb lze určitě dosáhnout i přijatelného kompromisu mezi spotřebou a koukatelností.

.NET Memory Internals 2/2 – Heap, Garbage Collector – záznam, slides a dema [MS Fest Praha 2014]

Slides z mé přednášky 30.11.2014 pro konferenci MS Fest Praha:

Z přednášky jsem pořizoval záznam, který najdete na našem HAVIT YouTube Channelu:

.NET Memory Internals 1/2 – záznam, slides a dema [MS Fest Praha 2014]

Slides z mé přednášky 29.11.2014 pro konferenci MS Fest Praha:

Z přednášky jsem pořizoval záznam, který najdete na našem HAVIT YouTube Channelu:

Jak dělám záznamy svých přednášek pro YouTube

Od určité doby nahrávám veškeré své přednášky pro HAVIT YouTube Channel. Nenahrávám stage, ale jen obrazovku a audio.

Základní setup je jednoduchý…

Short Story

Záznam obrazovky pomocí Camtasie, audio nahrávám pomocí klopového mikrofonu do iPhonu, v Camtasii to spojím v bodě tlesknutí a vyexportuju do MP4 pro YouTube.

TechSmith Camtasia pro záznam obrazovky

image

Konkrétně Camtasia Recorder:

          • v režimu Full Screen
  • se záznamem z vestavěného mikrofonu v notebooku (je dobré si dopředu vyzkoušet) – pro některé „nepohyblivé“ přednášky stačí, jinak mi poskytuje synchronizační klapku se samostatným audiozáznamem (viz dále)
  • jediná záludnost – nesmíte pustit Camtasia Recorder, dokud se nepřipojíte na projektor a neusadíte si rozlišení. Jinak se můžete dočkat nepříjemností, jako já letos na brněnském MS Festu. Jak jsem dodatečně analyzoval, zapnutý Recorder blokoval změny rozlišení, a mě se zoufale v časovém presu nedařilo s projektorem uspokojivě skamarádit.
  • licenci Camtasie mám jako MVP od TechSmith bezplatně, alternativně lze použít Windows Media Encoder (ale sám jsem nikdy nezkoušel).

Rode smartLav mikrofon + iPhone pro záznam zvuku

image

Velmi kvalitní „klopový“ mikrofon:

  • v zapojení do iPhone telefonu (pro přednášku vždy přepínám do Flight modu),
  • se záznamem do aplikace Rode Rec (ale úplně v pohodě to fungovalo i do předinstalované aplikace Diktafon)
  • nahrávám v nastavení 11025 Hz, Mono (vychází to kolem 20 MB/hod),
  • mikrofon nedávat ke krku, ale spíš někam na prsa/solar, citlivý je dost,
  • důležité je před záznamem nastavit/zkontroloval leveling záznamu, obvykle je citlivost vytočena na maximum, a pokud by se to nechalo, záznam bude téměř nepoužitelný (Jednou jsem musel použití i původní obyčejný záznam z vestavěného mikrofonu notebooku, když jsem na toto zapomněl). Je potřeba stáhnout citlivost tak, aby se při mluvení nedostával ukazatel do červené zóny.
  • výstup si přes File Sharing do formátu AAC (.m4a), stahuji z iPhone přes iTunes (starší verze Camtasie M4A neuměly, bylo potřeba to prohnat přes iTunes do MP3, nyní už to jde přímo),
  • stačil by zřejmě i mnohem obyčejnější mikrofon a jakékoliv jiné záznamové zařízení k němu, které vám umožní volný pohyb. Michal Altair Valášek třeba používá stejným způsobem diktafon SONY, který položí někam zhruba do první řady.

Sestříhání v Camtasia Studiu

Celý trik následného spojení v Camtasia Studiu spočívá v použití synchronizační klapky (stejný princip, jako u filmu), v amatérských podmínkách tedy tlesknutí/lusknutí, nebo podobném zvuku na začátku záznamu, který zobrazí na audio-křivce špičku.

image

Následně je potřeba zahodit méně kvalitní audio ze záznamu obrazovky, tj. rozpojit obraz od audia (pravé tlačítko myši na stopě + Separate video and audio), stejně tak zahodit track System Audio (pokud tam speciálně něco nemáte).

Obvykle dávám ještě:

  • Enable noise removal (Adjust sensitivity = 40-50)
  • někdy pomáhá Enable volume leveling, pokud nebyl dobře nalevelovaný mikrofon při záznamu.

Produkce MP4 pro YouTube

Dle instrukcí, které jsem našel přímo na webu na YouTube a zkombinoval s vlastními požadavky, používám obvykle nastavení produkce (ve finále koukám jiné, než YouTube doporučuje :-)):

  • Editing Dimensions (= Recording Dimensions)
  • Format = MP4
  • bez Controlleru
  • ostatní nechávám default (Frame Rate = automatic, …)

Dostanu MP4 soubor (obvykle cca 100-200MB), který si archivuji a nahraju na YouTube. Zdrojové soubory nakonec zahazuji.

Na YouTube ještě většinou nahrávám vlastní „miniaturu“. První slide uložím z PowerPointu jako PNG (File / Save As…) a doplním ho v Paint.NET na velikost 1280×720 (rozměr dle doporučení YouTube).

Na Azure mi nešla založit DB ve WestEurope, SQL server ano, jiné regiony ano (Azure Free Trial?)

Krátká empirická zkušenost.

Symptoms

Nová subscription, která vznikla tak nějak mimoděk, jako první subscription k danému organizational accountu a při zakládání této první subscription se to na nic neptalo a založilo to Trial Subscription s kreditem 150 EUR (pěkně děkuji, viz níže).

No nic, víc jsem to nezkoumal a prostě jen vypnul spending limit s tím, že se z toho stane běžná Pay-As-You-Go (

Zakládáme do subscription nové služby. WebSites v pohodě, WebJobs v pohodě, SendGrid v pohodě, při zakládání nové databáze „starý“ portal (manage.windowsazure.com) založí databázový server, ale k tomu řekne Could not create database ‚XY‘:

clip_image002

V detailech zprávy je výstižné upřesnění: Could not create database ‚XY‘.

Nejpozoruhodnější na tom je, že úplně stejná databáze v regionu East Asia se bez problémů vytvoří. Ostatní služby WestEurope OK.

Při použití nového portálu (portal.azure.com) je chování stejné, jen v detailech problému je cosi o „not supported for your subscription“ (WTF!). Na homepage všechny uzly Azure zelené, žádný výpadek není hlášen.

?Resolution?

Běžné Googlení „Could not create database“ dává diskuzní příspěvky o tom, že se něco v Azure nezpropagovalo a nezbývá než kontaktovat Azure Support. Pay-As-You-Go však má support omezený a nechce se mi čekat, než se to někam pohne. Zakládám tedy raději novou druhou subscription (tentokrát se to již ptá na subscription type a volím explicitně Pay-As-You-Go) a po chvilce usazování (napoprvé to opět nešlo, ale pro změnu nešel založit ani SQL server) vše úspěšně funguje.

?Cause?

Pokud i nyní zkusím na té prvně vytvořené subscription (která se pořád na různých místech identifikuje jako Trial a zřejmě tedy odstraněním spending limitu nedojde k přepnutí na produkční) založit databázi do WestEurope, dojde pouze k založení DB serveru, ale založení DB je zamítnuto.

Celé to zavání nějakým nedostatkem instancí v datovém centru WestEurope, což by vysvětlovalo i skutečnost, že subscription, která se označuje jako Trial, je na tom hůře, než následně založená Production. Divné jenom je, že na té Trial subscription to založí server a „jenom“ na něm nedovolí založit DB (i když i to nějakou logiku má).

Závěr

Přesnou příčinu neznám a více času s tím ztrácet nechci. Celé to do KnowledgeBase zapisuji proto, že nemusím být sám, kdo bude takovým „Free Trial“ obšťastněn.

Někdo máte podobnou/jinou zkušenost? Někdo máte lepší vysvětlení?

ASP.NET Identity 2.1 do MVC projektu s vlastní implementací UserStore (přes repositories)

Tento post popisuje jeden z mých prvních pokusů s ASP.NET Identity, které jsem chtěl zapojit do N-tier projektu s odděleným doménovým modelem, datovou vrstvou nad EntityFrameworkem, repositories, unit-of-work, atp. Webový projekt zde je abstrahován od Entity Frameworku a nemá přístup k DbContextu (nemá referenci na EntityFramework.dll).

Rychle se tedy ukázalo, že se musím vydat cestou vlastní implementace UserStore (resp. custom storage provider), který by na rozdíl od Microsoftí implementace (z balíčku Microsoft.AspNet.Identity.EntityFramework) neměl dependency na DbContext, ale na Repositories.

Relevantní články

Před zahájením implementace doporučuji zorientovat se trochu v problematice:

Pozor také, že ASP.NET Identity existuje v několika verzích a starší články se odkazují na starší verze. Verze 1.0 byla pouze základem, např, s omezením na string-klíče na IUser a IRole. Verze 2.x je aktuální podoba pro ASP.NET 4.x a mimo IUser<TKey>, IRole<TKey> interface s volbou typu klíče doplňuje i podporu zamykání účtů a spoustu dalších drobností. ASP.NET Identity 3.x je ve vývoji pro ASP.NET 5. Svět ASP.NET je nyní hodně dynamický a „kvalita“ dokumentace tomu bohužel odpovídá.

Výchozí struktura projektů

DomainClasses

  • POCO entity
  • nereferencuje skoro nic (rozhodně ne EntityFramework)
  • sem budeme umisťovat naše entity User, Role, …

DataLayer

  • persistence doménových tříd pomocí EntityFrameworku
  • mapování entit na DB, DbContext
  • referencuje DomainClasses, Entity Framework
  • sem musíme našim entitám User, Role, … nastavit mapování na DB

Repositories

  • prostě repositories :-) a související (UnitOfWork, atp.
  • referencuje DomainClasses, DataLayer
  • sem doplníme UserRepository, RoleRepository, …

Services

  • aplikační logika, WCF služby, fasády pro UI, …
  • referencuje DomainClasses, Repositories
  • sem budeme umisťovat naši implementaci UserStore s napojením na Repositories

Web

  • klasický ASP.NET MVC projekt
  • referencuje DomainClassses, Services, Repositories, nikoliv však EntityFramework
  • …a tady „to“ celé pomocí UserManageru použijeme
  • a propojíme s OWIN authentizací (cookies-based)

1. Instalace NuGet balíčků

Nejprve musíme do naší solution nainstalovat příslušné NuGet balíčky.

Microsoft.AspNet.Identity.Core

Tento NuGet balíček je základem celého ASP.NET Identity a musíme ho dostat skoro všude (bohužel!)

  • do DomainClasses – budeme potřebovat interfaces IUser, IRole. Dalo by se obejít bez těchto interfaces, ale museli bychom potom buď dodefinovat interfaces na nějakém potomkovi vytvořeném v Services pro účely propojení s ASP.NET Identity, nebo bychom museli pro ASP.NET Identity vytvořit třídy nové a na doménové třídy (entity) je mapovat (např. pomocí AutoMapperu)
  • dp DataLayer, Repositories – pořád kvůli interfaces IUser, IRole
  • do Services – budeme potřebovat pro IUserStore, IRoleStore, IUserRoleStore, …
  • do Web – sem hlavně ;-)

Balíček Microsoft.AspNet.Identity.Core naštěstí nemá žádné další závislosti, takže nám do DomainClasses, DAL, Repositories a Services nedotáhne žádný další „bordel“.

Microsoft.AspNet.Identity.Owin

OWIN implementace ASP.NET Identity do projektu Web.

Přitáhne sebou spoustu dependencies jako OWIN a další související balíčky Microsoft.Owin.Security.

Microsoft.Owin.Host.SystemWeb

Aby nám OWIN fungoval na „legacy“ System.Web. :-))´

2. Vytvoření entit User, Role + persistence do DB v DAL + Repositories

Do projektu DomainClasses vytvoříme entity User a Role.

User implementuje rozhraní IUser<TKey>, které naštěstí předepisuje jen TKey Id a string UserName. Další properties jsou na nás, později sem přidáme třeba něco na uložení hashovaného hesla, atp.

public interface IUser<out TKey>
{
    TKey Id { get; }
    string UserName { get; set; }
}

User pak může vypadat třeba takto:

public class User : IUser<int>
{
  [Required]
  public int Id { get; set; }

  [Required]
  public string UserName { get; set; }

  public ICollection<Role> Roles { get; set; }
  
  public string PasswordHash { get; set; }
}

Role implementuje obdobné rozhraní IRole<TKey>, které předpisuje properties TKey Id a string Name.

public interface IRole<out TKey>
{
    TKey Id { get; }
    string Name { get; set; }
}

Role pak může vypadat takto:

public class Role : IRole<int>
{
  [Required]
  public int Id { get; set; }

  [Required]
  public string Name { get; set; }
}

Dle vlastního způsobu implementace přidáme do projektu DataLayer persistenci nových entit (mapování EntityTypeConfiguration<>, IDbSet<> do DbContext, atp.). Stejně tak svým způsobem vytvoříme příslušné Repositories. Ani jedno není předmětem tohoto článku – pěkně v kostce viz třeba PluralSight: Entity Framework in the Enterprise (Julie Lerman).

3. Implementace UserStore, popř. RoleStore

UserStore opět není nic jiného, než naše třída, která implementuje jedno nebo více rozhraní, podle toho, co všechno od ASP.NET Identity očekáváme. Jedná se víceméně o CRUD operace, popř. několik dalších „storage“ operací, jako např. vyhledávání dle UserName, atp., vše pěkně připraveno pro async:

public interface IUserStore<TUser, in TKey> : IDisposable where TUser: class, IUser<TKey>
{
    Task CreateAsync(TUser user);
    Task DeleteAsync(TUser user);
    Task<TUser> FindByIdAsync(TKey userId);
    Task<TUser> FindByNameAsync(string userName);
    Task UpdateAsync(TUser user);
}

UserStore následně předhazujeme jako dependency do UserManageru, což je středobod ASP.NET Identity. UserManager obsahuje většinu aplikační logiky a kdo zná starší MembershipProvidery, tak si jej může představit jako novou podobu třídy Membership.

Nemá smysl, abych zde vypisoval celý UserStore, stačí malá ochutnávka, ostatní metody jsou implementovány stejně, zpravidla jako prosté volání metody příslušné Repository, nebo něco málo nad tím. Pro ukázku uvádím podobu volání vůči Repository, která nemá async podporu. Pokud máte async Repository, potom si jistě dokážete představit, že by kód byl ještě jednodušší.

public class UserStore : IUserStore<User, int>
{
  private readonly IUserRepository userRepository;
  private readonly IRoleRepository roleRepository;

  public UserStore(IUserRepository userRepository, IRoleRepository roleRepository)
  {
    this.userRepository = userRepository;
    this.roleRepository = roleRepository;
  }

  /// <summary>
  /// Insert a new user
  /// </summary>
  public Task CreateAsync(User user)
  {
    Contract.Requires<ArgumentNullException>(user != null);
    
    userRepository.AddNew(user);

    return Task.FromResult<object>(null);
  }

  

  /// <summary>
  /// Finds a user
  /// </summary>
  public Task<User> FindByIdAsync(int userId)
  {
    return Task.FromResult(userRepository.GetByID(userId));
  }

  /// <summary>
  /// Find a user by name
  /// </summary>
  public Task<User> FindByNameAsync(string userName)
  {
    Contract.Requires<ArgumentException>(!String.IsNullOrWhiteSpace(userName));

    return Task.FromResult(userRepository.GetByUserName(userName));
  }
  
  ...

}

Pro úplnost uvádím, že UserStore zde počítá s dependency-injection repositories přes constructor. Custom implementace zde může být jiná.

4. Nastavení OWIN autentizace ve Startup.cs

Dostáváme se pomalu k projektu Web a použití ASP.NET Identity v něm. Samotné ASP.,NET Identity authentizaci neimplementuje, stejně tak jako nebyla implementována v MembershipProviderech. ASP.NET Identity poskytuje pouze určité služby pro obsluhu uživatelů, jejich členství v rolích, ukládání hesel, vazeb na externí authentizační služby ála Google/Facebook/Twitter, atp.

Samotný authentizační mechanizmus použijeme z OWIN, konkrétně Microsoft.Owin.Security.Cookies, což je něco jako nástupce FormsAuthentication a jeho FormAuthenticationTicketu v cookie.

Do webového projektu tedy přidáme novou položku „OWIN Startup class“ (pokud ji tam už nemáme), je to klasická item-template ve Visual Studiu 2013 (tuším od Update 2 nebo 3).

[assembly: OwinStartup(typeof(Havit.HealthGuard.Web.Startup))]

namespace Havit.HealthGuard.Web
{
	public class Startup
	{
		public void Configuration(IAppBuilder app)
		{
			app.UseCookieAuthentication(new CookieAuthenticationOptions()
				{
					AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
					LoginPath = new PathString("/authentication/login")
				});
		}
	}
}

5. Nastavení autorizace stránek

Aby celé naše snažení mělo nějaký smysl, musíme samozřejmě nejenom zajistit autentizaci (ověření identity), ale i autorizaci (nastavení přístupových práv). Jelikož zde předpokládám MVC projekt (WebForms viz jeden z článků odkazovaných výše), použijeme atribut [Authorize], pro začátek třeba v podobě globálního filtru (z Global.asax):

protected void Application_Start(object sender, EventArgs e)
{
	...
	FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
	...
}

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
	...
	filters.Add(new AuthorizeAttribute());
	...
}		 

…čímž omezíme přístup na všechny controllery pouze na přihlášené uživatele

6. Přihlašovací stránka

Dostáváme se k jádru věci – přihlašovací stránce. Nebudu zde popisovat zjevnou podobu View, ani jeho ViewModel, příslušná Action vypadá takto (userManager je zde přes dependency-injection z constructoru controlleru, ale můžete si samozřejmě jeho instanci postavit prostým constructorem):

[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(string returnUrl, AuthenticationLoginViewModel loginModel)
{
	if (ModelState.IsValid)
	{

		User user = userManager.Find(loginModel.UserName, loginModel.Password);
		if (user != null)
		{
			IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
			authenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);

			ClaimsIdentity identity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
			AuthenticationProperties props = new AuthenticationProperties();
			props.IsPersistent = loginModel.IsPersistent;
			authenticationManager.SignIn(props, identity);
					
			if (Url.IsLocalUrl(returnUrl))
			{
				return Redirect(returnUrl);
			}
			return this.RedirectToAction<HomeController>(c => c.Index());
		}
		ModelState.AddModelError("", "Invalid username or password.");
	}

	return View(loginModel);
}

Klíčovým řádkem je zde volání userManager.Find(string userName, string password), kterážto metoda vrací naší třídu User, resp. null, pokud přihlášení není úspěšné. Zbylé řádky jsou již jen tanečky kolem OWIN AuthenticationManageru, které nejsou předmětem tohoto článku.

Pokud nyní aplikaci pustíme a přihlašování zkusíme, pak budeme dostávat hlášku „Invalid username or password.“, pokud jsme do tabulky User ještě žádného uživatele nepřidali. Pokud ho tam přidáme, pak při vyplnění správného UserName dostaneme:

System.NotSupportedException: Store does not implement IUserPasswordStore<TUser>.

7. Rozšiřování UserStore

Dostali jsme se přesně do bodu, kde se ukazuje, jak je zamýšlen způsob implementace vlastních „storage-providerů“. ASP.NET Identity přichází se smečkou rozhraní a jednotlivé funkce UserManageru fungují/nefungují podle toho, kolik z těchto rozhraní UserStore implementuje.

Chceme-li použít lokální účty s lokálními hesly, musíme na náš UserStore přidat a implementovat rozhraní IUserPasswordStore.

public interface IUserPasswordStore<TUser, in TKey> : IUserStore<TUser, TKey>, IDisposable where TUser: class, IUser<TKey>
{
    Task<string> GetPasswordHashAsync(TUser user);
    Task<bool> HasPasswordAsync(TUser user);
    Task SetPasswordHashAsync(TUser user, string passwordHash);
}

…k rozhraní IUserStore<>, které již nemusíme explicitně uvádět, přidává několik metod, které pro práci s hesly slouží. Odbočkou se dostáváme k tomu, že UserStore je zde opravdu odpovědný pouze za perzistenci hashe, nikoliv jeho samotný výpočet. Ten svojí aplikační logiku zajišťuje UserManager, a pokud ho chceme ovlivnit, můžeme vyměnit UserManager.PasswordHasher za jinou/vlastní implementaci IPasswordHasher.

Pro UserStore se nabízejí následující interfaces:

V našem případě budeme chtít implementovat IUserPasswordStore pro ukládání hesel a IUserRoleStore pro přiřazování rolí uživatelům:

public class UserStore : IUserStore<User, int>, IUserPasswordStore<User, int>, IUserRoleStore<User, int>
{

  ...

  /// <summary>
  /// Set the user password hash
  /// </summary>
  public Task SetPasswordHashAsync(User user, string passwordHash)
  {
    Contract.Requires<ArgumentNullException>(user != null);

    user.PasswordHash = passwordHash;
  
    return UpdateAsync(user);
  }

  /// <summary>
  /// Get the user password hash
  /// </summary>
  public Task<string> GetPasswordHashAsync(User user)
  {
    Contract.Requires<ArgumentNullException>(user != null);

    return Task.FromResult(user.PasswordHash);
  }

  /// <summary>
  /// Returns true if a user has a password set
  /// </summary>
  public Task<bool> HasPasswordAsync(User user)
  {
    Contract.Requires<ArgumentNullException>(user != null);
    
    return Task.FromResult(!String.IsNullOrWhiteSpace(user.PasswordHash));
  }

  /// <summary>
  /// Adds a user to a role
  /// </summary>
  public Task AddToRoleAsync(User user, string roleName)
  {
    Contract.Requires<ArgumentNullException>(user != null);
    Contract.Requires<ArgumentException>(!String.IsNullOrWhiteSpace(roleName));

    Role role = roleRepository.GetByName(roleName);
    if (role == null)
    {
      throw new InvalidOperationException(String.Format("Unrecognized role name: {0}", roleName));
    }

    if (!user.Roles.Contains(role))
    {
      user.Roles.Add(role);
    }

    return UpdateAsync(user);
  }

  /// <summary>
  /// Removes the role for the user
  /// </summary>
  public Task RemoveFromRoleAsync(User user, string roleName)
  {
    Contract.Requires<ArgumentNullException>(user != null);
    Contract.Requires<ArgumentException>(!String.IsNullOrWhiteSpace(roleName));

    user.Roles.Where(r => (r.Name == roleName)).ForEach(r => user.Roles.Remove(r));

    return UpdateAsync(user);
  }

  /// <summary>
  /// Returns the roles for this user
  /// </summary>
  public Task<IList<string>> GetRolesAsync(User user)
  {
    Contract.Requires<ArgumentNullException>(user != null);

    var roles = user.Roles.Select(r => r.Name).ToArray();

    return Task.FromResult<IList<string>>(roles);
  }

  /// <summary>
  /// Returns true if a user is in the role
  /// </summary>
  public Task<bool> IsInRoleAsync(User user, string roleName)
  {
    Contract.Requires<ArgumentNullException>(user != null);
    Contract.Requires<ArgumentException>(!String.IsNullOrWhiteSpace(roleName));

    bool result = user.Roles.Any(r => (r.Name == roleName));

    return Task.FromResult(result);
  }
}

RoleStore, RoleManager

Nakonec nutno dodat, že vedle IUserStore existuje ještě IRoleStore pro podporu perzistence rolí, tedy jejich samotných definicí, nikoliv jejich přiřazení uživatelům (což řeší výše uvedený IUserRolesStore).

K RoleStore samozřejmě existuje i RoleManager, který obsahuje aplikační logiku kolem rolí.

Závěr

Postavit vlastní UserStore pro ASP.NET Identity, který by používal Repositories, je jednoduché a velmi přímočaré. Jedinou vadou na kráse je absolutní nedostatek seriózní MSFT-dokumentace, na což si bohužel budeme muset zvykat.

Indexy (vč. unikátních) v Entity Frameworku (EF6.1)

Při studiu zdrojových kódu knihovny Microsoft.AspNet.Identity.EntityFramework.dll jsem narazil na zápis, který mi připomněl novinku v Entity Frameworku 6.1 – možnost definovat entitám indexy, včetně těch unikátních.

Principiálně jde o použití data-annotation atributu [Index(„<index name>“, [isUnique: true|false])] na příslušné property, přičemž pokud je atribut se stejným názvem použit na více properties, vznikne index přes více sloupců (vlastností Order se řídí pořadí sloupců v definici indexu).

Ve FluentAPI lze takový efekt zapsat nepřímo, pomocí doplnění anotace:

this.Property(t =&gt; t.Identifier)
	.HasColumnAnnotation(
		IndexAnnotation.AnnotationName,
			new IndexAnnotation(
				new IndexAttribute("UIDX_MonitoredApplication_Identifier") { IsUnique = true }));

UPDATE: Pokud chceme index přes více sloupců, nebo jeden sloupec zahrnout do více indexů, může kód vypadat takto:

Property(loginAccount =&gt; loginAccount.Email)
    .HasColumnAnnotation(
        IndexAnnotation.AnnotationName,
        new IndexAnnotation(
            new IndexAttribute("UIDX_LoginAccount_Email_Deleted") { IsUnique = true, Order = 1 }));

Property(loginAccount =&gt; loginAccount.SingleSignOnIdentifier)
    .HasColumnAnnotation(
        IndexAnnotation.AnnotationName,
        new IndexAnnotation(
            new IndexAttribute("UIDX_LoginAccount_SingleSignOnIdentifier_Deleted") { IsUnique = true, Order = 1 }));

Property(loginAccount =&gt; loginAccount.Deleted)
    .HasColumnAnnotation(
        IndexAnnotation.AnnotationName,
        new IndexAnnotation(new[]
        {
            new IndexAttribute("UIDX_LoginAccount_Email_Deleted") { IsUnique = true, Order = 2 },
            new IndexAttribute("UIDX_LoginAccount_SingleSignOnIdentifier_Deleted") { IsUnique = true, Order = 2 }
        }));

Viz též EntityFramework specs – IndexAttribute.

C# 6.0: Ostatní (extension-Add v initializers, overload resolution) [10/10]

Do C# 6.0 se dostaly ještě dvě „drobotiny“, které většině vývojářů život nijak zásadně nezmění.

Collection-initializers podporují extension-podobu Add() metody

V C# 5.0, v inicializaci kolekcí, nemohla být použita extension-podoba metody Add(), přestože VB.NET toto měl. Byl to spíše bug/opomenutí. Každopádně nyní je to napraveno.

Vylepšené vyhodnocování overloadů

Drobná vylepšení, kterých si málokdo všimne. O něco málo méně míst, kde se můžete setkat s výtkou kompilátoru ohledně nejednoznačnosti volby overloadu (např. u nullable hodnotových typů parametrů, atp.)

Série článků o novinkách v C# 6.0:

  1. Auto-property initializers a getter-only (read-only) auto-properties
  2. Expression-bodied function members
  3. Using static
  4. Null-conditional operators (?., ?[], …)
  5. String inerpolation – zkrácený String.Format()
  6. nameof() expressions
  7. Index initializers – přehlednější inicializace slovníků
  8. Exception filters, await v catch/finally blocích
  9. Konstruktor struct bez parametrů
  10. Ostatní (extension-Add v initializers, overload resolution)