ASP.NET Identity Grundlagen

Benutzer Authentifzierung begleitet den Entwickler im Web ein Leben lang. Kann eine Desktop Anwendung in der Regel sich auf den angemeldeten Windows User verlassen, so wird das im Web schon schwierig. Windows nutzt NTLM und dafür braucht es ein lokales Netzwerk. Sobald man Web Apps (oder auch mobile Apps) abgesichert nutzen will landet man beim Kundenwunsch Single Sign On (SSO). Eine Anmeldung für alles. Hier hat sich OAuth2 zum Standard etabliert und zig Anbieter buhlen darum ihre Tokens (Achtung Erklärung später) anbieten zu dürfen. So auch Microsoft als B2C Dienst oder in Unternehmensanwendung mit einer Brücke zum Active Directory –Azure AD. In diesem Artikel geht es allerdings um die konkrete Implementierung in ASP.NET core (bis 7) mittels Identity. Es gibt verschiedene Wege dies in ein Visual Studio Projekt einzubinden. Hier werde ich aber von Grund auf aufschlüsseln was genau im Hintergrund passiert. Wir starten mit einem leeren (oder beliebigen) ASP.NET Project in Visual Studio. Das was wir als nächstes tun, erledigt entweder ein Wizard oder ist in einer DLL ausprogrammiert die per Wizard eingefügt wird. Um den Status des angemeldeten Benutzers zu visualisieren wird ein Partial angelegt. Dies erfüllt zudem die Aufgabe die Navigation für Login und Logout bereit zu stellen. 1: <ul class="navbar-nav"> 2: @if (User.Identity.IsAuthenticated) 3: { 4: <li class="nav-item"> 5: <span class="nav-text text-dark">Hello @User.Identity.Name!</span> 6: </li> 7: <li class="nav-item"> 8: <a class="nav-link text-dark" asp-page="/account/SignOut">Sign out</a> 9: </li> 10: } 11: else 12: { 13: <li class="nav-item"> 14: <a class="nav-link text-dark" asp-page="/account/Login">Sign in</a> 15: </li> 16: } 17: </ul> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Wir sehen das User Objekt das Zugriff auf verschiedene Eigenschaften wie den Login Name bietet. Das Partial nennen wir _LoginPartial und speichern es im Shared Folder. Dann fügen wir in Layout.cshtml am Ende des Bootstrap Navigations Menü das Partial auch ein <partial name="_LoginPartial" /> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Ruft man nun die Seite auf, wird man im Menü einen neuen Eintrag sehen, zu den man aber nicht hin navigieren kann. 1: namespace BlogAuth.Pages.Account 2: { 3: public class Credentials 4: { 5: public string UserName { get; set; } 6: public string Password { get; set; } 7: public bool Remember { get; set; } 8: } 9: } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Als nächstes legt man das Bootstrap basierte Login Formular an. Ebenso im Account Verzeichnis als login.cshtml 1: @page 2: @model BlogAuth.Pages.Account.LoginModel 3: @{ 4: } 5: <div class="row"> 6: <div class="col-md-4"> 7: <section> 8: <form id="account" method="post"> 9: <h2>Login</h2> 10: <hr /> 11: <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div> 12: <div class="form-floating mb-3"> 13: <input asp-for="Input.UserName" class="form-control" autocomplete="username" aria-required="true" placeholder="name@ppedv.de" /> 14: <label asp-for="Input.UserName" class="form-label">UserName</label> 15: <span asp-validation-for="Input.UserName" class="text-danger"></span> 16: </div> 17: <div class="form-floating mb-3"> 18: <input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" /> 19: <label asp-for="Input.Password" class="form-label">Password</label> 20: <span asp-validation-for="Input.Password" class="text-danger"></span> 21: </div> 22: <div class="checkbox mb-3"> 23: <label asp-for="Input.Remember" class="form-label"> 24: <input class="form-check-input" asp-for="Input.Remember" /> 25: @Html.DisplayNameFor(m => m.Input.Remember) 26: </label> 27: </div> 28: <div> 29: <button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button> 30: </div> 31:   32: </form> 33: </section> 34: </div> 35: </div> 36: @section Scripts { 37: <partial name="_ValidationScriptsPartial" /> 38: } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Nun fehlt noch ein wenig C# Login Logik. Minimal um das Formular zu validieren und einfach mal einen Testlauf des Projekts zu starten. 1: [BindProperty] 2: public Credentials Input { get; set; } 3: 4: public async Task<IActionResult> OnPostAsync(string returnUrl) 5: { 6: if (!ModelState.IsValid) 7: { 8: return BadRequest(ModelState); 9: } 10:   11:   12: if (Input.UserName == "admin" && Input.Password == "x") 13: { 14: return RedirectToPage(returnUrl); 15: } 16: else 17: { 18: return Page(); 19: } 20: } 21: } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Das sieht dann im Browser wie folgt aus und erzeugt einen Fehler beim Login      "The returnUrl field is required." Hier beginnt schon die ASP.NET Identity Magie und unser Forschungsprojekt. Wenn man eine Page oder Service schützen möchte, muss man das Authorize Attribut anwenden. Das kann im Razor View erfolgen @page @using Microsoft.AspNetCore.Authorization @attribute [Authorize] .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } oder im Code vor der entsprechenden HTTP Methode oder Page Klasse 1: [Authorize(Roles ="Admins")] 2: public class AdminModel : PageModel .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Ruft man nun diese Website auf, erhält man wiederum eine Fehlermeldung. invalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions). Es gibt noch einiges zu tun. Nämlich in der HTTPPipeline des Kestrel Web Servers die nötige Konfiguration vornehmen. Ob es mir gefällt oder nicht (es gefällt mir nicht) das geht nur im Code der program.cs. Wie schon seit Urzeiten von Benutzerverwaltung gilt auch hier zu klären, wer bin ich (Authentication) und was darf ich (Authorization). Wie in ASP.NET Core Dependency Injection üblich muss zuerst das Objekt auf der Dependency Liste (IServiceCollection) angemeldet werden. builder.Services.AddAuthentication().AddCookie("MyCookie"); builder.Services.AddAuthorization(); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Die Konfiguration der Objekte lassen wir noch unberücksichtigt. Allerdings erinnern Sie sich noch an die Fehlermeldung bezüglich des Authentication Schemas. Um das zu lösen müssen, wir angeben welches wir wollen. Da gäbe es OAuth oder wie hier gewählt Cookie. Das bedeutet das ein Cookie genutzt wird um alles Notwendige (später Token genannt) zu speichern. Letztendlich müssen die beiden Objekte auch genutzt werden. Auch dies in der program.cs ein Stück später. Die Reihenfolge spielt nun eine Rolle! app.UseAuthentication(); app.UseAuthorization(); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Der Zeitpunkt kurz inne zu halten. Bzw den Breakpoint (ich nehme hier index die Get Methode) Wir sehen am User Objekt eine Identity. Die braucht man auch im den anonymen Case abzubilden. Auch kein User ist ein User- im IIS liefert den der W3C Prozess. Wir sehen auch Claims und Roles. Das ist ein Punkt der vielleicht neu für Sie ist. In der Windows Benutzer Verwaltung ist man Mitglied in der Rolle Admin. Dieses Konzept passt in offenen Authentifizierungssystemen nicht. Man wählt (alternativ Role geht auch noch) einen Claim den ein User besitzt. Oder eher wahrscheinlich viele Claims. Oft fühlen sich diese ident zu Rollen an. Die Claims werden im Token gespeichert. Das Token erhält man nach Authentifizierung. Speicherort des Tokens ist hier ein Cookie. Um eine Analogie zu bemühen. Sie erhalten von der Behörde einen Führerschein, der begrenzt gültig ist. Mit dieser Plastikkarte (=Token) können sie jederzeit die verschiedenen Fahrberechtigungen nachweisen. Ganz ohne bei der Behörde jedes mal anrufen zu müssen. Die Helper Klasse die uns das sichere Token erzeugt, heißt SignIn. Wir haben schon festgestellt (per Debug) das es ein Identity Objekt gibt. Daraus formen wir einen ClaimsPrinzipal mit dem man SignIn Füttert. Da der Token durch unsere Konfiguration in einem Cookie landet, auch noch der Name desselben (immer der gleiche von program.cs bis hier). Tja und dann die Liste der Claims. Es gibt eine Reihe vordefinierte und hier der frei definierte “admins”. Da Key Value mit Fake Wert true. Entsprechend ergänzen wir den Code in der Login.cshtml.cs 1: if (Input.UserName == "admin" && Input.Password == "x") 2: { 3: var claims = new List<Claim> 4: { 5: new Claim(ClaimTypes.Name,Input.UserName), 6: new Claim(ClaimTypes.Email, "test@ppedv.de"), 7: new Claim("admins","true") 8: }; 9: var identity = new ClaimsIdentity(claims, "MyCookie"); 10: var principal = new ClaimsPrincipal(identity); 11: var authpro = new AuthenticationProperties { IsPersistent = Input.Remember }; 12: await HttpContext.SignInAsync("MyCookie", principal, authpro); 13: return RedirectToPage(returnUrl); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Tatsächlich, die Webanwendung leitet den Benutzer bei Aufruf der “/Privacy” Page auf Login um. Nach erfolgreichen Login erscheint der Name im Menü. Ja ja, Design Note 2. Der Debugger verrät nun, wir sind Authentifiziert und die Claims sind da. Das ist selbst dann der Fall, wenn die Debug Session endet und neu gestartet wird. Die Anmeldung ist persistent. Jedenfalls theoretisch. Ein Blick in die Browser Tools (F12- Application – Cookies) verrät, das Cookie ist da und enthält verschlüsselt Daten. Allerdings wer genau hinsieht, wird erkennen, das die Cookie Gültigkeit auf die Session beschränkt ist. Hier ist ein Eingriff in die Konfiguration des Webservers (Seufz, ja program.cs) nötig. 1: builder.Services.AddAuthentication().AddCookie("MyCookie", 2: o => { 3: o.ExpireTimeSpan = TimeSpan.FromDays(1); 4: }); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Nun klappts auch mit dem Cookie länger. Wenden wir uns den Rechten zu. Wir erkennen, das das Authorize Attribut Roles oder Policy als Parameter erlaubt. Claims suchen wir vergeblich. Trägt man nun eine Rolle zum Test in die Privacy Page ein [Authorize(Roles ="admins")] wird man nach Zugriff auf eine nicht existierende Url umgeleitet. Account/AccessDenied?ReturnUrl=%2FPrivacy Da ist sie wieder die Identity Magie. Also füttern wir diese und legen die fehlende Seite einfach an und zeigen dem Benutzer was sinnvolles. Tatsächlich spricht einiges dafür den HTTP Status Code 403 zurück zu geben. Ist aber kein muss. Das löst natürlich das ursprüngliche Problem nicht, wie man die Rechte im Claim überprüft. Und wieder gehts in die Program.cs, wo eine Policy hinzugefügt werden muss. 1: builder.Services.AddAuthorization(o => 2: { 3: o.AddPolicy("IsAdmin", p => p.RequireClaim("admins")); 4: }); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Dort können beliebig viele und beliebig komplexe Bedingungen formuliert und kombiniert werden. Eine winzige Änderung in der privacy Razor Page auf  [Authorize(Policy ="isAdmin")] und schon gehts. Seitennotiz- groß Kleinschreibung scheint egal zu sein. Claims haben eine Lebenszeit. Fügt man nun weitere Eigenschaften hinzu, werden diese nicht im Cookie aktualisiert. Wenn Sie mit ein neues ASP.NET Core Projekt von Grund auf mit Authentication starten, werden keine cshtml Dateien angelegt, sondern ein Nuget Paket Microsoft.AspNetCore.Identity.UI genutzt.

Taghelper Scrollposition wieder herstellen

In meinem letzten Blog Post zu MaintainScrollPosition habe ich die Grundlagen per JavaScript erarbeitet. Eine Website soll egal ob refresh oder Postback dem Benutzer die gleiche Stelle zeigen wie vorher. Also wohin man gescrollt ist. Folgende Lösung für ASP.NET 7 Razor Pages. Per einfachen Tag Helper in der Scripts Sektion soll das Feature Position im Browser wieder herstellen aktiviert sein. @section Scripts { <ScrollPosition></ScrollPosition> } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Dazu erstellt man im Visual Studio ASP.NET Projekt eine Klasse die von TagHelper erbt und definiert das zukünftige HTML Element oder auch ggf Attribut. Dazu wird per Schraubenziehersymbol die Überschreibung generiert. Der konzeptionelle C# Code definiert in Zeile 1 das <ScrollPosition> und in Zeile 6 die gerenderte Ausgabe. 1: [HtmlTargetElement("ScrollPosition", TagStructure = TagStructure.Unspecified)] 2: public class MaintainScrollposition : TagHelper 3: { 4: public override void Process(TagHelperContext context, TagHelperOutput output) 5: { 6: output.PostContent.Append("<script>"); 7: } 8: } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Wer diesen Code probiert wird erkennen, das die HTML Entitys statt der <> im HTML Code erscheinen. Das Problem lässt sich mit AppendHTML lösen. Nächste Herausforderung: Wie lese ich den Wert der Form Elemente aus. Reminder: die Wiederherstellung der Scroll Position, erfolgt über ein per JavaScript dynamisch zum Form das den Post auslöst, hinzugefügtes Input Element. Dazu dient der ViewContext, der wiederum Zugriff auf den HTTPContext liefert und somit auf den Request und das Formular. Also zunächst für das Verständnis der Code um die die Scroll Methode (ident zu ScrollTo) zu setzen. 1:[HtmlTargetElement("ScrollPosition", TagStructure = TagStructure.Unspecified)] 2:public class ScrollPositionTagHelper : TagHelper 3: { 4: [ViewContext] 5: public ViewContext ViewContext { get; set; } 6: public override void Process(TagHelperContext context, TagHelperOutput output) 7: { 8: float scrollpos = 0; 9: if (ViewContext.HttpContext.Request.HasFormContentType) 10: { 11: scrollpos = float.Parse(ViewContext.HttpContext.Request.Form["scrollpos"], CultureInfo.CurrentCulture); 12: } 13: output.PostContent.AppendHtml("<script>"); 14: if (scrollpos > 0) 15: { 16: output.PostContent.AppendHtml($" window.scroll({{top: {scrollpos.ToString("0.0", CultureInfo.InvariantCulture)}, left: 0, behavior: 'instant'}});"); 17: } 18: output.PostContent.AppendHtml("</script>"); 19: } 20: } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Das rendert dann, wenn gescrollt, das JavaScript in der Page, wie im anderen Blog Post beschrieben. Aber noch fehlt der Teil, der das INPUT in den DOM unterhalb des FORM höngt und mit der ScrollY Position füllt. Hier ist es einfacher den JavaScriipt Code in einen Single Block zu hängen. Auch eine .JS Datei zu laden, sehe ich als Option. Dieser Code als Ergänzung zum vorigen c# Listing nach Zeile 13. 1: var js = @" document.addEventListener('DOMContentLoaded', function (event) { 2: window.onsubmit = function (event) { 3: var input = document.createElement('input'); 4: input.name = 'scrollpos'; 5: input.id = 'scrollpos'; 6: input.value = this.scrollY.toLocaleString(); 7: input.type = 'hidden'; 8: event.target.appendChild(input); 9: };});"; 10: ... 11: output.PostContent.AppendHtml("<script>"); 12: output.PostContent.AppendHtml(js); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Das wars schon.

WYSIWYG - in Asp .Net Core integrieren

Wie integriert man einen WYSIWYG (What You See Is What You Get) Editor in eine Asp.Net Core Webanwendung zum einfachen Editoren von HTML wie zum Beispiel für E-Mail Templates. [Mehr]

Responsive Image Preview List mit Bootstrap 4 und ASP.NET Webforms

Im folgenden Lab erstellen sie eine Bildervorschau im Grid Layout. Dieses ändert je nach verfügbarer Breite die Anzahl der Spalten. Erstellen Sie mit Visual Studio ein neues ASP.NET Webforms Projekt. Die Templates verwenden allerdings noch die UI Library Bootstrap 3. Wählen Sie den Nuget Paket Manager und aktualisieren Sie alle Pakete. Die aktuelle Version 4.x erfordert ein neues Layout für das Menü. Bearbeiten Sie dazu die Datei Site.Master im Visual Studio Editor. Ersetzen Sie den vorhanden HTML Code für das Navigation Menü 1: <header> 2: <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> 3: <div class="container"> 4: <a class="navbar-brand" href="/Index">ppedv AG</a> 5: <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent" 6: aria-expanded="false" aria-label="Toggle navigation"> 7: <span class="navbar-toggler-icon"></span> 8: </button> 9: <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse"> 10: <ul class="navbar-nav flex-grow-1"> 11: <li class="nav-item"> 12: <a class="nav-link text-dark" href="/Index">Home</a> 13: </li> 14: <li class="nav-item"> 15: <a class="nav-link text-dark" href="/Privacy">Privacy</a> 16: </li> 17: </ul> 18: </div> 19: </div> 20: </nav> 21: </header> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Erstellen Sie eine Klasse Bilder, die später als Model Klasse für das ASP.NET Modelbinding in einer generischen Liste genutzt wird. Ein Property soll reichen um das Konzept zu erläutern. 1: Public Class Bilder 2: Public Property Pfad As String 3: End Class .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Bilder können mehrere MB groß sein. Deswegen wird jedes Bild verkleinert und auf einheitliche Größe formatiert. Dazu wird eine ASHX Handler Klasse erzeugt, die den Dateinamen als Querystring ausliest. Fügen Sie dem Visual Studio Projekt eine neue Datei (Handler) hinzu mit dem Namen Thumbnail. Fügen Sie folgenden Code in ProcessRequest Methode ein. Ersetzen Sie dabei den automatisch generierten VB.NET Code im Methodenrumpf. Es wird nun der Parameter aus der Url ausgelesen. Das Bild geladen und ein miniaturisiertes Vorschaubild erstellt. Aus Performancegründen wird ein einmal so komprimiertes Image in den Cache des Webservers gespeichert und wenn vorhanden daraus abgerufen. 1: Dim img As String = context.Request.QueryString("img") 2: Dim cacheImg As String = context.Request.CurrentExecutionFilePath + ":" + img 3: Dim pic() As Byte 4: Dim cachedPic As Object = context.Cache.Get(cacheImg) 5: If IsNothing(cachedPic) Then 6: Dim im = New Bitmap(context.Server.MapPath("~\images\" + img)) 7: Dim stream = New MemoryStream() 8: im = im.GetThumbnailImage(200, 140, Nothing, Nothing) 9: im.Save(stream, ImageFormat.Jpeg) 10: stream.Close() 11: pic = stream.GetBuffer() 12: context.Cache.Add(cacheImg, pic, Nothing, DateTime.MaxValue, New TimeSpan(2, 0, 0), CacheItemPriority.Normal, Nothing) 13: Else 14: pic = CType(cachedPic, Byte()) 15: End If 16: context.Response.ContentType = "image/jpg" 17: context.Response.Cache.SetCacheability(HttpCacheability.Public) 18: context.Response.BufferOutput = False 19: context.Response.OutputStream.Write(pic, 0, pic.Length) .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Diese Handler muss in einem IMG Element einer HTML Seite als Source eingebettet werden. Dazu erstellen Sie eine ASPX Seite, mit Referenz auf die Masterpage. Innerhalb des Content Elements wird folgender HTML Code platziert. Erstellen Sie ein Repeater Control, weisen den Itemtype zu und lassen sich die SelectMethod automatisch generieren. Im Itemtemplate wird das Bootstrap Feature Cards genutzt um Bild und Bildnamen darzustellen. Wenn das Binding korrekt ist, wird nach tippen von #Item durch Intellisense das Property Pfad vorgeschlagen. 1: <div class="row"> 2: <asp:Repeater ID="rptBilder" runat="server" 3: ItemType="ImageGrid1.Bilder" SelectMethod="rptBilder_GetData"> 4: <ItemTemplate> 5: <div class="card" style="width: 18rem;"> 6: <img class="card-img-top" src="/modules/thumbnail.ashx?img=<%#Item.Pfad %>" 7: alt="<%#Item.Pfad %>"> 8: <div class="card-body"> 9: <p class="card-text"><%#Item.Pfad %></p> 10: </div> 11: </div> 12: </ItemTemplate> 13: </asp:Repeater> 14: </div> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Nun fehlt noch im Codebehind der aspx Seite der Code der zum Laden der Daten ausgeführt wird. Es wird das Verzeichnis ausgelesen und eine neue generische Liste vom Typ Bilder erzeugt und als Return Wert an den Repeater geliefert. 1: Public Function rptBilder_GetData() As IEnumerable(Of Bilder) 2: Dim files As String() = Directory.GetFiles(Server.MapPath("~\images"), "*.jpg") 3: Dim imgs = From item In files 4: Select New Bilder With {.Pfad = Path.GetFileName(item)} 5: Return imgs 6: End Function .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

ASP.NET Web Api Key authentication

Das Konzept der verteilten Anwendungen wie mit z.B. Azure Functions als Serverless computing bezeichnet, hat es mir angetan. Besonders einfach ist es mit ASP.NET Web Api (egal ob Core oder .net framework). API Calls werden speziell im App to App Szenario meist mit einer ApplicationID  Secret Kombination abgesichert. So ein Beispiel habe ich mit VB.NET implementiert um Datenaustausch zwischen zwei ASP.NET Webforms Anwendungen über Server grenzen hinweg, halbwegs sicher zu gestalten. Im Kern wird in den HTTP Header beim Request (POST, GET …) ein zusätzlicher Key und Wert eingefügt. Bei mir schlicht X-ApiKey genannt.   1: Dim hc As New HttpClient() 2: hc.DefaultRequestHeaders.Add("X-ApiKey", "000000000") 3: ... 4: Dim ret = hc.DeleteAsync(New Uri("https://domain.de/api/booking/1111" .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Der Artikel geht davon aus, das bekannt ist, wie ein API Controller (hier BookingController.vb) angelegt wird. Häufig sieht man das Attribut Authorize vor der Controller Klasse um eine Anmeldung zu erzwingen. Das bezieht sich aber auf einen Benutzer Prinzipal. Kann man per Code erstellen. Mein Weg ist einfacher mit einer Klasse geerbt von DelegatingHandler. Darin wird geprüft, ob ein Api Key im Header existiert und der Wert den Erwartungen entspricht. Andernfalls wird eine 403 Fehlermeldung zurückgegeben und die Ausführung in der Request Pipeline unterbrochen. Die Methode SendAsync (Zeile 18) leitet im Erfolgsfall an das nächste Modul in der HTTP Request Pipeline weiter. 1: Imports System.IdentityModel.Claims 2: Imports System.Net.Http 3: Imports System.Security.Claims 4: Imports System.Threading 5: Imports System.Threading.Tasks 6: Imports System.Web.Http 7: Imports System.Web.Http.Dispatcher 8: Imports Microsoft.VisualBasic 9:   10: Public Class AuthorizationApiKeyHandler 11: Inherits DelegatingHandler 12:   13: Protected Overrides Function SendAsync(request As HttpRequestMessage, cancellationToken As CancellationToken) As Task(Of HttpResponseMessage) 14: Dim apiKeyHeaderValues As IEnumerable(Of String) 15: If (request.Headers.TryGetValues("X-ApiKey", apiKeyHeaderValues)) Then 16: Dim apiKeyHeaderValue = apiKeyHeaderValues.First() 17: If apiKeyHeaderValue = "000000000" Then 18: Return MyBase.SendAsync(request, cancellationToken) 19: End If 20: End If 21:   22: Dim response = New HttpResponseMessage(Net.HttpStatusCode.Forbidden) 23: Dim tsc = New TaskCompletionSource(Of HttpResponseMessage)() 24: tsc.SetResult(response) 25: Return tsc.Task 26: End Function 27: End Class .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Um nun die Klasse mit dem Controller zu verbinden, muss das Routing konfiguriert werden. Das passiert in der Datei WebApiConfig.vb. Diese sollte automatisch erstellt werden, wenn man in Visual Studio Projekt per Nuget die ASP.NET Web Api installiert. Um für alle API Calls das neue Modul zu aktivieren, reicht eine Zeile Code, die den selbst geschriebenen Handler registriert. 1: config.MessageHandlers.Add(New AuthorizationApiKeyHandler()) .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Da aber viele API’s offen sein müssen um z.B. einer JavaScript Bibliothek einen CallBack zu erlauben, kommt ein selektiver Ansatz zum Einsatz. Obige Zeile wird nicht verwendet. Statt dessen wird spezifisch eine Route mit dem Authorization Handler als Parameter angelegt. 1: config.Routes.MapHttpRoute( 2: name:="DefaultApi", 3: routeTemplate:="api/{controller}/{id}", 4: defaults:=New With {.id = RouteParameter.Optional}, 5: constraints:=Nothing, 6: handler:=New AuthorizationApiKeyHandler() With {.InnerHandler = New Dispatcher.HttpControllerDispatcher(config)} 7: ) .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Natürlich soll eine gesicherte verschlüsselte HTTPS Verbindung genutzt werden um den Key zu übertragen, analog zu Passwörtern. Mehr zu ASP.NET Web Api lernen Sie in unserem Training.

Visual Basic 14 und Webforms

Bereits seit 2015 hat Visual Basic (und auch C#) einige neue Sprach Features. Zwei VB.NET Funktionen haben es mir dabei angetan in meiner täglichen Entwickler Arbeit. Multi Line String Literals Bereits die XML Literals haben Jahrelang dem C# die leichte Zornesröte des Neides ins Gesicht getrieben. Gibt es meines Wissens bis heute nicht, wobei natürlich XML heute sowieso Bäh ist und durch JSON zu ersetzen. Mein Problem sind immer mehrzeiligen komplexe SQL Abfragen im Source Code. Mit Multiline String Literals geht nun Copy Paste und das lästige schachten in Hochkommas und das verketten mit dem Plus Operator entfällt. String Interpolation Nicht ganz so nützlich, aber deutlicher Gewinn an Lesbarkeit. Es wird das $ Zeichen verwendete um direkt Variablen, samt Format Expression in geschweifte Klammern platzieren zu können. $ ist die Kurzform von String.Format. 1: Dim s = $"hello {p.Name} you are {p.Height:0.00}m tall" .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Nun setzen diese Sprach Features auf den neuen Kompiler (Rosyln) auf. Der fehlt in Web Forms Projekten und Websites. Dort gilt praktisch noch VB 12. Das war mein Wissensstand bis gestern. Ich war wieder einmal unzufrieden mit dem 10 zeiligen SQL Script im Quellcode. Relativ schnell bin ich auf die Lösung für Roslyn in ASP.NET Webforms Projekten gestoßen. Um den VB 14 Compiler verwenden zu können, wird das Nuget Paket Microsoft.CodeDom.Providers.DotNetCompilerPlatform  in das Projekt installiert. Nun arbeite ich in der Regel nicht mit Projekten sondern Websites. Der feine Unterschied wird manch einem nicht bekannt sein. Websites betrachten jede ASPX Datei als eigenes “Projekt” und erstellen einzelne Assemblys und nicht wie bei Projects eine dicke DLL ins BIN Verzeichnis. Man erkennt in der ASPX Datei dies am Attribut Codefile (Website) statt Codebehind (Projekt). Außerdem müssen Klassen im APP_CODE Ordner abgelegt werden. Das bedeutet ich kann einen ASPX Seite oder die zugehörige .vb Datei in Notepad editieren. Kompiliert wird diese (und nur die geänderten) Automatisch beim erstem Aufruf. Razor Pages haben später das Konzept übernommen. Jedenfalls klappt das mit Websites nicht, trotz installation des NUGET Pakets. Eine kleine Änderung in der web.config bring dann doch auch eine ASP.NET Website auf die VB 14 Language Version. Einfach “default” auf “14” ändern. 1: <system.codedom> 2: <compilers> 3: <compiler language="c#;cs;csharp" extension=".cs" 4: type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" 5: warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701"/> 6: <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" 7: type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" 8: warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+"/> 9: </compilers> 10: </system.codedom> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

URL Pfad, Querystring und Segmente

Da ich mich aktuell mit einer Programmieraufgaben mit ASP.NET Web API, Webforms und FriendlyUrls beschäftige, stehe ich dauernd vor Fragen wie Wie bekomme ich die Page? Wie erhalte ich den Querystring, das FriendlyUrlsegment? Wie komme ich auf den vollen Pfad? Zunächst kommt es darauf an, wo man den Code platziert. In einer ASPX Page ist das Request Objekt direkt greifbar 1: Request.Url .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Damit erhält man ein Objekt vom Typ Url. In Modulen, Handlern und Services, muss man über den HTTP Context gehen. 1: HttpContext.Current.Request.Url .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Am Beispiel Url http://localhost:50458/test.aspx?Id=demo&val=0 ApplicationPath: / CurrentExecutionFilePath:/test.aspx FilePath: /test.aspx Path: /test.aspx PathInfo: PhysicalApplicationPath: C:\WebSites\companyII\ RawUrl: /test.aspx?Id=demo&val=0 Url.AbsolutePath: /test.aspx Url.AbsoluteUri: http://localhost:50458/test.aspx?Id=demo&val=0 Url.Fragment: Url.Host: localhost Url.Authority: localhost:50458 Url.LocalPath: /test.aspx Url.PathAndQuery: /test.aspx?Id=demo&val=0 Url.Port: 50458 Url.Query: ?Id=demo&val=0 Url.Scheme: http Url.Segments: / test.aspx

Die USTID mit JavaScript und Webforms automatisieren

Die EU wird aktuell (DSVGO oder GDPR) viel gescholten. Ein positiver Aspekt unserer Gemeinschaft ist, das der Handel zwischen Mitgliedsländern einfacher von statten geht, sofern man eine gültige USTID (Umsatzsteuer Identifikation) auf beiden Seiten besitzt. Lieferant kann so innergemeinschaftlich ohne den müßigen Vorsteuerabzug bzw. Einzug liefern. Diese ID wird vom Finanzamt vergeben und muss tagesaktuell geprüft werden, wofür ein EU Portal genannt VIES kostenfrei, bereit steht. Es gibt dort auch einen Webservice mit gutem alten SOAP Ansatz. Etwas was man sich mit JavaScript nicht antun will, um die Client Validierung auszuführen. Entsprechend beinhaltet die ASP.NET Lösung folgende Aufgaben ASP.NET Formular zur Eingabe der ID JavaScript Funktion für Lostfocus und Ajax Call ASP.NET Service der den SOAP Call kapselt JavaScript + Bootstrap um den Erfolg zu visualisieren Web Service hinzufügen Früher, da waren Webservices etwas sehr fixes und definierten die Verbindlichkeit (Contract) per Interface. Entsprechend lassen sich Proxy Klassen per Visual Studio –Add Service Reference- generieren, die als Zugriffsobjekte dienen. Dies allerdings nur wenn der Service per WSDL sich selbst beschreiben kann. Im Visual Studio Web Projekt werden so Verzeichnisse und Dateien angelegt, von denen man die Finger lassen sollte. Der Wizard macht schon. Ab in den Code. Mein Ziel ist ein Service der einen Service kapselt. Da wir nur true oder false auf die Frage “Ustid korrekt”, reicht es eine ASPX Webforms Seite anzulegen. Diese wird bis auf zwei Zeilen komplett geleert. Dies hat noch einen weiteren dramatischen Vorteil. 1: <%@ Page Language="VB" AutoEventWireup="false" CodeFile="CheckUstid.aspx.vb" Inherits="CheckUstid" %> 2: <%@ OutputCache Duration="600" VaryByParam="ustid" %> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Wie der Kenner sehen kann, nutze ich hier Caching und reduziere damit die Requests zum EU Server dramatisch. In 600 Sekunden wird sich die Gültigkeit der USTID nicht ändern. Als Parameter wird der QueryString “ustid” übergeben. Im VB.NET Codebehind wird das Service Objekt instanziiert, der QueryString ausgelesen und zerlegt und dann an den EU Service per checkVat weiter gereicht. Die Rückgabe ist dann eine Zeichenkette true oder false. 1: Private Sub cachy_CheckUstid_Load(sender As Object, e As EventArgs) Handles Me.Load 2: Dim ws As New CheckVatServiceReference1.checkVatPortTypeClient 3: Dim ustid = Request.QueryString("ustid") 4: Dim isvalid As Boolean 5: Dim Name As String 6: Dim adresse As String 7: Dim land As String = Left(ustid, 2) 8: Dim id As String = ustid.Substring(2, ustid.Length - 2) 9: ws.checkVat(land, id, isvalid, Name, adresse) 10: Response.Write(isvalid) .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Im HTML Teil der ASPX Seite kommt das Bootstrap Framework in der Version 3 zum Einsatz. Um im Erfolgsfalle die Box grün und im Fehlerfall rot umrandet zu bekommen, hilft das div ustidform. Das Lostfocus Event in JavaScript nennt sich onblur. Dies sollte ausgelöst werden, wenn der Benutzer das Eingabefeld verlässt. Das kann häufiger sein, als man denkt, z.B. wenn vom Browserfenster in ein anderes Programm gewechselt wird. 1: <div id="ustidform"> 2: <label class="control-label col-md-1" for="ustid">USTID</label> 3: <div class="col-md-2"> 4: <input class="form-control" id="ustid" placeholder="Ustid" 5: onblur="CheckUSTID(this);"> 6: </div> 7: </div> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Nun kommt der Sadomaso Teil: JavaScript. Wenn man den Code klein hält, geht das gerade noch. Um die ID eines ASP.NET Steuerelements, wie die Textbox, aus JavaScript auch sicher ansteuern zu können, hilft das Attribut ClientIDMode="Static".  Was hier nicht nötig ist, weil nur pur HTML Input Element. Der HTML Trick sind zwei CSS Klassen aus Bootstrap für Error und success. Diese werden dem DIV hinzugefügt, wenn der ASP.NET Webforms “Service” seine Antwort liefert. Das $ ist die Kurzform für die JQuery Bibliothek. 1: function CheckUSTID(input) { 2: $.ajax({ 3: url: "checkustid.aspx", 4: data: { "ustid": input.value }, 5: success: function (result) { 6: if (result == 'False') { 7: $('#ustidform').addClass("has-error"); 8: console.log("invalid ustid"); 9: } 10: else { 11: $('#ustidform').addClass("has-success"); 12: console.log("valid ustid"); 13: } 14: 15: } 16: }); 17: } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } War doch nicht so schlimm. Benutzer tippt, verlässt das Feld mit Tab und tippt weiter und im Hintergrund findet die Prüfung statt, die im Erfolgsfall so aussieht oder so Dann muss die Rechnung mit MWst ausgestellt werden.

Autofill Produktivitätsboost Web Formular

Amazon lebt davon, es dem Kunden so einfach wie möglich zu machen. Eine tägliche Qual ist das ausfüllen von Formularen im Web, Name, Strasse, PLZ usw. Das muss nicht sein, mit Autofill. Zunächst kann der Benutzer (ja das bist Du) in seinem Browser der Wahl Daten hinterlegen. Für Chrome Einstellungen – Erweitert (unten) –Passwörter und Formulare- Autofill. Dort hinterlegt man seine Adressdaten oder Kreditkarten. Autovervollständigen bei Microsoft EDGE – Einstellungen. Erweiterte Einstellungen –Formulareinträge speichern. Edge lernt die Daten aus der Nutzung und bietet keine Möglichkeit diese manuell zu ändern. Der Website Entwickler kann einfach die ID Name Werte der HTML Page passend eintragen oder besser er deklariert sie per Autocomplete Attribut. Das klappt auch mit ASP.NET Webforms, hier mit Bootstrap hervorragend. 1: <div class="form-group"> 2: <asp:Label runat="server" AssociatedControlID="email">Email</asp:Label> 3: <asp:TextBox runat="server" ClientIDMode="Static" ID="email" TextMode="email" CssClass="form-control" AutoComplete="email" /> 4: <asp:RequiredFieldValidator runat="server" ControlToValidate="Password" Display="Dynamic" 5: CssClass="text-danger" ErrorMessage="Email ist erforderlich." /> 6: </div> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   Die dabei wichtigsten Werte sind für Name und Email email tel name fname mname Für Adressdaten address city state zip country und für mutige die Kreditkartendaten cc-name  cc-number cc-csc cc-exp-month cc-exp-year cc-exp cc-type Demosite für einen Test https://greenido.github.io/Product-Site-101/form-cc-example.html Dabei kann man auch in eine beliebige Zeile des Formulars springen und Autfill entdeckt ob es einen passenden Datensatz gibt und fügt diese auf Benutzerwunsch ein. Die farbliche Markierung der automatischen gefüllten Felder in gelb ist dann noch Herausforderung für einen Designer.   Der Unterschied zu Edge ist, das die ausgefüllten Felder blau hinterlegt werden.

ASP.NET Identity

Da Microsoft in den letzten Monaten durchaus den ein oder anderen Blick über den Tellerrand gewagt hat, hat sich doch so einiges getan. Besonders in der Web-Welt von ASP.NET. Alle sprechen nur noch von One ASP.NET, und das alles einfacher und klarer ist und werden soll. Die ersten Schritte sind schon getan, man findet beim erstellen eines neuen Projekts nur noch eine ASP.NET WebApplication Vorlage welche dann die optionale Auswahl der verschiedensten Technologien ermöglicht. Doch der Fokus dieses Blogbeitrags soll auf das neue IdentityModel abzielen. Bisher war es wohl eher eine Qual mit dem Membership Modell von Microsoft etwas eigenes bzw. angepasstes auf die Beine zu stellen. Doch nun hat das ASP.NET Team erkannt, dass es an der Zeit für etwas frischen Wind ist und so einige Änderungen/Anpassungen vollzogen welche es uns Entwicklern leichter ermöglichen sollen unsere User mit den dazugehörigen Profildaten zu verknüpfen. Mit ASP.NET WebForms Zunächst schauen wir uns mal die Projektvorlage an welche wir beim erstellen einer neuen WebApplication erhalten. Im App_Start Ordner ist die Identityconfig.cs und im Models ordner, welcher selbst auch neu ist, kam die IdentityModel.cs neu hinzu. Beide zusammen bilden so zu sagen das Grundgerüst für das neue IdentityModel. IdentityConfig In der IdentityConfig.cs befinden sich mehrere Klassen, eine davon ist die ApplicationUserManager. Diese ist der Dreh und Angelpunkt der IdentityConfig. Hier werden mehrere Einstellungen und Richtlinien getroffen, hauptsächlich bei der Erzeugung eines neuen Users. Hier wird z. B. die Passwortstärke oder die Anzahl an maximalen fehlversuchen beim Login festgelegt. IdentityModel Im IdentityModel befindet sich einerseits das UserModel (ApplicationUser) und der DbContext welcher von der EntityFramework CodeFirst Migration verwendet wird. Falls wir nun unseren Usern weiter Information beim Login aus den Fingern kitzeln wollen fügen wir diese einfach dem Application User als einfaches Property hinzu. In unserem Fall fügen wir mal ein Property Ort vom Typ string hinzu. 1: public class ApplicationUser : IdentityUser 2: { 3: public string Ort { get; set; } 4:  5: public ClaimsIdentity GenerateUserIdentity(ApplicationUserManager manager) 6: { 7: // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType 8: var userIdentity = manager.CreateIdentity(this, DefaultAuthenticationTypes.ApplicationCookie); 9: // Add custom user claims here 10: return userIdentity; 11: } 12:  13: public Task<claimsidentity> GenerateUserIdentityAsync(ApplicationUserManager manager) 14: { 15: return Task.FromResult(GenerateUserIdentity(manager)); 16: } 17: } Im ApplicationUserContext steht nicht großartig viel Code. Den Context verwenden wir eig. nur um mit Hilfe der CodeFirst Migration auf die Datenbank los zu gehen. Hierzu 2 einfache Befehle: Enable-Migrations –EnableAutomaticMigration Update-Database EnableeAutomaticMigration deshalb, weil wir dann keine Zeit damit verschwenden müssen uns selbst um die Migrationen zu kümmern. Standardgemäß wird die entsprechende Datenbank als lokaler MDF-File im App_Data Verzeichnis erzeugt, dies kann man natürlich auch ändern wie man es gerne möchte in der Web.config  z. B. Letzte Schritte Da wir nun die Config und das Model soweit konfiguriert haben wie wir es gerne möchten und unsere Datenbank geupdated wurde, steht uns nicht mehr viel im Weg bevor wir diese Daten vom User erfassen lassen können. Der nächste Schritt wäre es, in der Register.aspx eine weitere Zeile mit entsprechendem Input Element hinzu zu fügen welche dem Nutzer die Eingabe ermöglicht. 1: <div class="form-group"> 2: <asp:label runat="server" AssociatedControlId="Ort" CssClass="col-md-2 control-label">Email</asp:label> 3: <div class="col-md-10"> 4: <asp:TextBox runat="server" ID="Ort" CssClass="form-control" /> 5: <asp:RequiredFieldValidator runat="server" ControlToValidate="Ort" 6: CssClass="text-danger" ErrorMessage="The Ort field is required." /> 7: </div> 8: </div> Nachdem wir das Input-Element hinzugefügt haben müssen wir noch in der CodeBehind beim erzeugen des neuen Users die jeweiligen Daten mit angebenen. 1: var user = new ApplicationUser() { UserName = Email.Text, Email = Email.Text, Ort = Ort.Text };   Und das wars schon. Mehr muss man gar nicht machen um die Userdaten zu erweitern. Mit MVC sieht das ganze recht ähnlich aus nur anstelle der Register.aspx hat man im Views/Account/ Verzeichnis eine Register.cshtml und die CodeBehind haben wir dort auch nicht. Anstelle davon ist der AccountController da mit der [HttpPost] Register Action. Ein kleiner zusätlicher Schritt ist noch bei MVC zu machen, und zwar man sollte noch das RegisterViewModel anpassen, dort kann man auch direkt mit Data Annotatinos Arbeiten und weitere ValidierungsLogik hinzufügen. Fazit Einfach zu handhaben und gar nicht so kompliziert wie es den meisten im ersten Moment vll. vorkommt. Viel Spaß