Da ich in vielen kleineren Projekten immer wieder die selben Code-Fragmente benötigt habe, war es Zeit diese in eine eigene Sammlung zu stecken.
Implementierung: .NET Standard 2.0
In diesem NuGet Paket befinden sich alle allgemeinen Erweiterungen.
AsyncQueue<T>
Die AsyncQueue ist der Versuch die guten Eigenschaften einer Queue<T>
von C# mit den asynchronen Eigenschaften von async/await
zu vereinen.
Erweitert wurde die Queue noch um die Möglichkeit ein Element zwischendrin auch wieder zu entfernen (wie bei einer Liste). Dies liegt an einer speziellen Anwendung, für die ich sie gebaut habe.
Normale Queue Methoden:
void Clear()
bool Contains(T)
void CopyTo(T[], int)
T Dequeue()
void Enqueue(T)
T Peek()
T[] ToArray()
void TrimExcess()
Zusätzliche Methoden:
bool TryDequeue(out T)
bool TryPeek(out T)
bool Remove(T)
void Enqueue(T[])
Asynchrone Methoden:
Task<T[]> DequeueAvailableAsync(int, CancellationToken)
Task<T[]> DequeueManyAsync(int, CancellationToken)
Task<T> DequeueAsync(CancellationToken)
Task WaitAsync(CancellationToken)
CryptographyHelper
In dieser Klasse wird die Kryptografie ein bisschen gekapselt, sodass ein paar (mehrere!) using
-Direktiven wegfallen und es in sich etwas handlicher wird.
Um eine einfachere symmetrische Verschlüsselung zu ermöglichen, kann eine Instanz der Klasse erzeugt werden, die einen kryptografischen Schlüssel einliest oder erzeugt: new CryptoHelper(string keyFile = null)
.
Ist der Parameter leer (null
) oder ein relativer Pfad angegeben, so wird das BaseDirectory als Referenz genommen. Bei leerem Parameter lautet der Dateiname crypto.key
.
Generell gilt:
password
-Parameter optionalcrypto.key
verwendetVer-/Entschlüsselung:
EncryptAes
, DecryptAes
AesEncrypt
, AesDecrypt
EncryptTripleDes
, DecryptTripleDes
TripleDesEncrypt
, TripleDesDecrypt
Die Daten werden als Byte-Array entgegen genommen, um mögliche Encoding-Fehler zu umgehen.
Für die Ver-/Entschlüsselung von Text kann z.B. Encoding.UTF8
verwendet werden:
// set
string text = "Hello World!";
string password = "SuperSecureP@ssword!";
var encoding = Encoding.UTF8;
// encrypt
byte[] encPlain = encoding.GetBytes(text);
byte[] encCipher = CryptographyHelper.AesEncrypt(encPlain, password);
string encryptedText = Convert.ToBase64String(encCipher); // can be saved somewhere
// decrypt
byte[] decCipher = Convert.FromBase64String(encryptedText); // was saved somewhere
byte[] decPlain = CryptographyHelper.AesDecrypt(decCipher, password);
string decodedText = encoding.GetString(decPlain);
// text == decodedText => True
Hashing:
Md5
und Md5File
für DateienSha1
und Sha1File
für DateienSha256
und Sha256File
für DateienSha512
und Sha512File
für DateienBei *File
wird die Datei eingelesen und ein Hashwert von dessen Inhalt erstellt.
Randomness:
Manchmal werden zufällige Bytes oder Buchstabenfolgen benötigt (z.B. Passwörter).
Daher hier eine Abkürzung
GetRandomBytes(count)
GetRandomString(length)
Sicherer Vergleich:
Zeichenketten, sowie Arrays werden für gewöhnlich so verglichen, dass bei der ersten Abweichung der Vergleich abbricht und das Ergebnis zurückgegeben wird. Somit lässt sich über die Laufzeit herausfinden an welcher Stelle der Fehler ist. Hierfür gibt es SecureEquals
.
DelayedTask
DelayedTask.Create
erstellt einen neuen DelayedTask
, aber startet ihn nichtDelayedTask.Run
erstellt einen neuen DelayedTask
und startet den Delay-TimerReset
setzt den Timer zurück und startet ihn erneutCancel
stoppt den laufenden TimerExcecutePending
führt einen wartenden Task unmittelbar ausNetworkHelper
ResolveHost
versucht einen Hostnamen aufzulösen und die IP Adressen dafür zurückzugeben.ResolveInterface
versucht die IP Adressen einer Netzwerkschnittstelle auszulesen.ParseNetwork
und TryParseNetwork
nimmt eine Netzwerkdefinition im CIDR Format (z.B. 192.168.178.0/24) und versucht den Datentypen IPNetwork
daraus zu erzeugen.ExpandNetwork
nimmt ein geparstes IPNetwork und erzeugt daraus eine Liste mit allen im Netzwerk gültigen IP Adressen.DateTime
und TimeSpan
AsUtc
definiert ein DateTime
als UTC oder wandelt es von der lokalen Zeit um in UTC.AsLocal
definiert ein DateTime
als lokale Zeit oder wandelt es in die lokale Zeit um.RoundToSecond
rundet die Zeitangabe auf SekundenRoundToMinute
rundet die Zeitangabe auf MinutenRoundToHour
rundet die Zeitangabe auf StundenRoundToDay
rundet die Zeitangabe auf TageGetAlignedIntervalUtc
Richtet ein TimeSpan
an der Uhr in UTC aus, ein Offset kann angegeben werden. Die Beachtung von Sommer-/Winterzeit exisitert nicht.GetAlignedIntervalLocal
Richtet ein TimeSpan
an der Uhr in lokaler Zeit aus, ein Offset kann angegeben werden. Die Beachtung von Sommer-/Winterzeit findet stattToShortString
ermöglicht die Anzeige eines TimeSpan
Enum
GetAttributes<T>
gibt die Attribute des gewünschten Typs zurückGetAttribute<T>
gibt das erste Attribut (oder null
) zurückGetDescription
gibt den Text des [Description()]
des Enum zurückGetDisplayName
gibt den Text des [Display(Name = "")]
des Enum zurückException
GetMessage
sorgt dafür, dass die Fehlermeldung der InnerException
ausgegeben wird, sofern vorhanden.AggregateException
.GetRecursiveMessage
verkettet die Meldungen der InnerException
s.IPAddress
Increment
erhöht die vorhandene IP Adresse um eins.Decrement
verringert die vorhandene IP Adresse um eins.JSON
Die Erweiterungen basieren auf dem NuGet Paket Newtonsoft.Json
!
Die Namen erfolgen in camelCase, wie es für JSON üblich ist.
SerializeJson
kann auf alles ausgeführt werden (sofern es keine Loop-Referenzen hat) und erzeugt einen JSON-String.DeserializeJson<T>
deserialisiert den JSON-String wieder in ein Objekt vom Typ T
. Optional kann eine fallbackValue
angegeben werden.ConvertToJObject
konvertiert ein beliebieges Objekt in ein JObject
.ConvertToJArray
konvertiert eine beliebige Auflistung in ein JArray
.GetValue<T>
kann auf ein JObject
ausgeführt werden.GetValue<T>("Test:Sub")
. Der Split-Parameter kann verändert werden.ReaderWriterLockSlim
Der ReaderWriterLockSlim
ist eine praktische Sache, doch es nervt unheimlich, dass anschließend immer wieder explizit mit Exit*
herausgegangen werden muss. Nun kann dies mit einem using
-Block behoben werden. Wenn ein Timeout gesetzt wird, fliegt unter Umständen eine TimeoutException
.
GetReadLock
GetUpgradeableReadLock
GetWriteLock
ReflectionExtensions
Kleine Erweiterung für System.Reflection
, um den Aufruf von asynchronen Methoden zu vereinfachen.
Task methodInfo.CallAsync(object obj, params object[] parameters)
Task<TResult> methodInfo.InvokeAsync<TResult>(object obj, params object[] parameters)
Stream
ReadLine
liest einen Stream Zeichen für Zeichen, bis ein Zeilenende erkannt wirdReadLineAsync
wie ReadLine
, jedoch asynchronousString
Base16:
HexToBytes
BytesToHex
HexEncode
HexDecode
Base64:
Base64Encode
Base64Decode
Replacement:
ReplaceStart
ersetzt einen Text, sofern er sich am Beginn befindetReplaceEnd
ersetzt einen Text, sofern er sich am Ende befindetE-Mail Adresse:
IsValidEmailAddress
gibt es in zwei Ausführungen
emailStr.IsValidEmailAddress()
emailStr.IsValidEmailAddress(checkForDnsRecord: true)
oder emailStr.IsValidEmailAddress(new[] { nameserver1IpEndPoint, nameserver2IpEndPoint })
Sonstige:
ParseDecimal()
sucht nach dem letzten Trennzeichen (Komma oder Punkt) und definiert daraus, mit welcher CultureInfo
das Parsen durchgeführt werden sollStringBuilder.AppendLine()
mit weiterem Parameter, um die Zeichenfolge für "NewLine" anzugebenFileLogger
Es gibt für Microsoft.Extensions.Logging
verschiedene Erweiterungen, wie die Meldungen geschrieben werden können. Sei es eine Datenbank, zur Konsole, etc., doch eine stupide Datei ist nicht mit im Portfolio.
Dem wurde hier Abhilfe geschaffen: AMWD.Common.Logging.FileLogger
.
Es gibt noch erweiterte JsonConverter, die für Newtonsoft.Json
verwendet werden können.
Manchmal braucht man für keine cmd Programme einen anderen Zugang, als es IConfiguration.AddCommandLine()
ermöglicht. Hierfür bietet sich dann der CommandLineParser
an.
var cmd = new CommandLineParser();
var helpOpt = cmd.RegisterOption("help").Alias("h", "?");
cmd.Parse(args);
if (helpOpt.IsSet)
Console.WriteLine("Help me!");
In diesem Snippet kann der Text "Help me!" über folgende Argumente ausgegeben werden: --help
, -h
, /?
(grundsätzlich wird erkannt --
, -
und /
).
Implementierung: .NET 6.0, .NET 8.0
Hier lassen sich Erweiterungen für Web-Anwendungen finden.
Die Basic Authentication ist eine sehr einfach und doch auch nützliche Art der Anmeldung für einen Dienst. Daher ist diese Authentifizierung in verschiedenen Varianten implementiert.
Allen gemein ist eine Anwendungs-spezifische Implementierung des IBasicAuthenticationValidator
s. Dieses Interface wird in allen Varianten für die Prüfung verwendet.
// Registering validation
services.AddScoped<IBasicAuthenticationValidator, BasicAuthCheck>();
Als Attribut
Als Attribut kann der Validierung neben dem bereits erwähnten Validator auch noch hart codierte Credentials mitgegeben werden, die vorrangig sind. [BasicAuthentication(Username = "admin", Password = "passwd", Realm = "Restricted Area")]
Als Handler
Eine Implementierung als AuthenticationHandler
ist auch vorhanden: BasicAuthenticationHandler
. Hier muss die Validierung über den IBasicAuthenticationValidator
erfolgen.
Als Middleware
Als dritte Vairante noch die eigene Middleware (BasicAuthenticationMiddleware
), die vor jede weitere Middleware gesetzt werden kann, um einzelne Bereiche abzusichern.
Es sollte relativ einfach möglich sein, jegliche Pfade mit einer Authentifizierung zu schützen. Doch zumindest für statische Dateien ist das nicht so einfach möglich.
Dies wird über die ProtectedPathMiddleware
einfacher gestaltet.
Konfiguration der Services:
services.AddAuthentication();
services.AddAuthorization(options =>
{
options.AddPolicy("ProtectedPathAccess", policy => policy.RequireAuthenticatedUser());
});
Konfiguration im AppBuilder anschließend:
app.UseAuthentication();
app.UseProtectedPath(new ProtectedPathOptions
{
PolicyName = "ProtectedPathAccess",
Path = "/protected/path"
});
app.UseStaticFiles();
GoogleReCaptcha
Es ermöglicht die Auswertung eines Google ReCaptcha mittels Attribut zu aktivieren. Der weitere Code (z.B. JavaScript) muss natürlch weiterhin eingebunden werden.
Eintrag in der appsettings.json
:
{
"Google": {
"ReCaptcha": {
"PrivateKey": "__insert private ReCaptcha key here__",
"PublicKey": "__insert public ReCaptcha key here__"
}
}
}
Anschließend kann in der Methode auf den Score zugegriffen werden:
[HttpPost]
[GoogleReCaptcha]
public IActionResult Test()
{
decimal? score = (decimal?)HttpContext.Items[GoogleReCaptchaAttribute.ScoreKey];
// if score is null, there is an error message in the ModelState
if (score == null)
return View();
}
Es gibt zwei Attribute, um IP Adressen zu filtern. Einmal als Allowlist, um explizit IP Adressen zu erlauben (häufigster Fall) und natürlich auch als Blocklist, um bestimmte IP Adressen zu blockieren.
Die Listen können dabei auf mehrere Arten definiert werden:
[IPAllowList(AllowedIpAddresses = "127.0.0.0/8,::1")]
[IPBlockList(BlockedIpAddresses = "123.123.123.123,fd00::321")]
{
"<config key>": [
"127.0.0.0/8",
"::1"
]
}
ApplicationBuilder
UseProxyHosting
setzt diverse Eigenschaften, um einen Proxy zu erkennen und mögliche HTTP-Header Informationen auszuwerten. Über optionale Parameter können Anpassungen vorgenommen werden.
ASPNETCORE_APPL_PATH
ein Pfad gesetzt sein, wird eine PathBase
angelegt (wirkt sich wie ein Unterverzeichnis im Webspace aus)HttpContext
GetAntiforgeryToken
gibt ein Triple zurück, dass Name (Form und Header) und Wert des Antiforgery Tokens enthält, um z.B. dennoch valide AJAX Anfragen zu ermöglichenGetRemoteIpAddress
liest die IP Adresse des anfragenden Browsers ausIsLocalRequest
prüft, ob die Anfrage vom selben System kommtClearSession
leert alle Inhalte einer Session, sofern existentGetReturnUrl
sucht in HttpContext.Items
nach "OriginalRequest" oder im Query nach "ReturnUrl"Für die Remote IP Adresse werden standardmäßig folgende Header auf Existenz geprüft:
ModelStateDictionary
AddModelError
in einer etwas komplexeren Schreibweise.
Es ermöglicht für eine Eigenschaft in einem tieferen Objekt eine Fehlermeldung zu schreiben, die anschließend auch in der View nach der Validierung auch angezeigt werden kann.
ServiceCollection
AddSingeltonHostedService
ermöglicht es, dass ein HostedService nur genau eine Instanz erzeugt und diese dann startet/stoppt.
Session
Abstrahiert die sehr begrenzten Möglichkeiten der Session etwas.
Zur Speicherung von Objekten wird auf das NuGet Pakekt Newtonsoft.Json
zurückgegriffen.
SetValue<T>
speichert ein Objekt in der SessionGtValue<T>
holt ein gespeichertes Objekt aus der SessionHasKey
prüft, ob ein Schlüssel in der Session vorhanden istDiese Implementierung geht auf ein größeres Problem in der Implementierung von Microsoft zurück.
Das zugehörige Issue auf GitHub: ASP.NET Core #6566
Es befasst sich mit der Formatierung bzw. dem Parsen von Dezimalzahlen.
Um es einzubinden, muss der ModelBinder an Position eins in die Liste eingesetzt werden.
Startup.cs:
// [...]
public void ConfigureServices {
// [...]
services.AddControllersWithViews(options =>
{
options.ModelBinderProviders.Insert(0, new InvariantFloatingPointModelBinderProvider());
// [...]
});
// [...]
}
// [...]
So lassen sich bedingte Klassen über ein Model steuern:
cshtml:
<div class="d-block" condition-class-d-hidden="@Model.IsHidden">
Some Text
</div>
IsHidden = false
:
<div class="d-block">
Some Text
</div>
IsHidden = true
:
<div class="d-block d-none">
Some Text
</div>
Ermöglicht das einfügen eines neuen HTML Tags: <email />
Die E-Mail Adresse wird hierbei leicht verschleiert, sodass Bots diese nicht ganz so einfach auslesen können.
<email asp-address="max.mustermann@example.com" />
Ermöglicht es recht einfach integrity hashes für die auszuliefernden Dateien zu erzeugen. Es kann sowohl in <link rel="stylesheet" />
als auch in <script />
Tags verwendet werden.
Über den Parameter asp-integrity="<true|false>"
kann die Berechnung der Hashes de-/aktiviert werden.
Über den Parameter asp-integrity-strength="<256|384|512>"
kann die Stärke des zu berechnenden Hashs eingestellt werden.
Hinweis: Bei Quellen von externen Servern (beginnend mit http) wird versucht die Datei herunterzuladen und den Hash zu berechnen.
Dies ist quasi die Frontend-Erweiterung zum InvariantFloatingPoint.
Hier wird die Konfiguration für alle Zahlenwerte in das HTML Rendering etwas präziser umgesetzt (z.B. passende Grenzwerte).
HtmlHelper
Der HtmlHelper
erweitert die HTML Tools von .NET ein wenig.
IsDarkColor
bekommt die Farbe wie CSS und prüft gewichtet, ob es sich um eine helle oder dunkle Farbe handelt.PasswordHelper
Der PasswordHelper
abstrahiert den PasswordHasher
der AspNetCore.Identity
etwas und ermöglicht somit ein einfacheres Handling ohne Instanzgenerierung.
HashPassword
erzeugt einen sicheren Hash aus dem PasswortVerifyPassword
prüft das Passwort gegen den HashImplementierung: .NET 6.0, .NET 8.0
Hier gibt es Erweiterungen für das EntityFramework Core. Größte Erweiterung: Migration der Datenbank mittels SQL-Dateien.
DatabaseIndex
Dieses Attribut ermöglicht es einen Index für eine Spalte in einer Tabelle hinzuzufügen.
Dies ist hauptsächlich für den Code-First Ansatz relevant, da sonst die Datenbank nicht korrekt erzeugt wird.
DateOnly
Dieser Datentyp wurde mit .NET 6.0 eingeführt und bietet die Möglichkeit rein die Date-Komponente eines DateTime
-Objekts abzubilden. Dies spart Speicherkapazitäten und vereinfacht z.B. Vergleiche, da die Zeit-Komponente weggelassen wird.
Microsoft hat ausgerechnet für seinen eigenen SQL Server ein Problem in der Implementierung des EntityFrameworks.
Einbinden: Siehe ModelConfigurationBuilderExtensions
.
Manuell ginge es über
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
base.ConfigureConventions(builder);
if (Database.IsSqlServer())
{
builder.Properties<DateOnly>()
.HaveConversion<DateOnlyConverter>()
.HaveColumnType("date");
builder.Properties<DateOnly?>()
.HaveConversion<NullableDateOnlyConverter>()
.HaveColumnType("date");
}
}
TimeOnly
Dieser Datentyp wurde mit .NET 6.0 eingeführt und bietet die Möglichkeit rein die Time-Komponente eines DateTime
-Objekts abzubilden. Im Gegensatz zu einem TimeSpan
sind bei diesem Datentypen die Grenzen Minute zu Stunde oder auch die Grenze der 24h bereits validiert.
Microsoft hat nun ausgerechnet für seinen eigenen SQL Server ein Problem in der Implementierung des EntityFrameworks.
Einbinden: Siehe ModelConfigurationBuilderExtensions
.
Manuell ginge es über
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
base.ConfigureConventions(builder);
if (Database.IsSqlServer())
{
builder.Properties<TimeOnly>()
.HaveConversion<TimeOnlyConverter>()
.HaveColumnType("time");
builder.Properties<TimeOnly?>()
.HaveConversion<NullableTimeOnlyConverter>()
.HaveColumnType("time");
}
}
DatabaseProviderException
Diese Exception wird während des eigenen Migrationsprozesses ausgelöst, wenn es zu Problemen kommen sollte.
Sie leitet sich von der Mutter aller Fehler ab: Exception
.
DatabaseFacade
ApplyMigrationsAsync
tut genau dies.
Über die DatabaseMigrationOptions
werden die benötigten Informationen übergeben, wie z.B. der Pfad zu den SQL-Dateien. Anschließend wird eine Migrations-Tabelle angelegt, in der die durchgeführten Migrationen gespeichert werden.
Die Aktualisierung der Datenbank erfolgt in der Reihenfolge der Dateien im Dateisystem. Es empfiehlt sich daher die Migrationsskrite immer mit einem Datum beginnen zu lassen: 2020-10-10_Initialize.sql
.
DbContext
Um den Schreibaufwand zu reduzieren und zeitgleich auch die Kompatibilität mit InMemory
zu wahren, wurden Extensions für Transaktionen geschrieben:
DbContext.BeginTransaction()
DbContext.BeginTransactionAsync()
DbContextOptionsBuilder
UseDatabaseProvider
abstrahiert die sonst benötigten Aufrufe von z.B. UseSqlServer
oder UseSqlite
.
Im Hintergrund wird geprüft, ob die entsprechenden Bibliotheken vorhanden sind und dann dynamisch geladen.
Es wurde durchaus auch ein bisschen Zeit investiert, dass die Mechanismen mit einer InMemory
Datenbank funktionieren sollten.
Dies soll die Unterstützung von mehreren Datenbanksystemen für eine Anwendung ermöglichen. So könnte dann eine Anwendung sowohl die Daten aus einer MySQL Datenbank, als auch einer Oracle Datenbank beziehen. Je nachdem, was konfiguriert ist.
Unterstützte Datenbanksysteme und deren NuGet Pakete
Datenbanksystem | NuGet Paket |
---|---|
MySQL | Pomelo.EntityFrameworkCore.MySql (empfohlen), MySql.EntityFrameworkCore |
Oracle | Oracle.EntityFrameworkCore |
PostgreSQL | Npgsql.EntityFrameworkCore.PostgreSQL |
SQLite | Microsoft.EntityFrameworkCore.Sqlite |
(MS) SQLServer | Microsoft.EntityFrameworkCore.SqlServer |
Eine appsettings.json
könnte so aussehen:
{
"Database": {
"Provider": "PostgreSQL",
"Host": "localhost",
"Port": 5432,
"Name": "test_database",
"Schema": "public",
"Username": "user",
"Password": "user-pw",
"File": ""
}
}
ModelBuilder
ApplyIndexAttribute
prüft auf das [DatabaseIndex()]
Attribut und fügt die Informationen zum Datenbank Modell hinzuApplySnakeCase
sorgt dafür, dass alle Tabellen und Spalten dem snake_case Format entsprechen, um eine einheitliche Datenbank zu ermöglichen. Sollte ein expliziter Name angegeben sein, so wird es nicht angewendet.ModelConfigurationBuilder
Mit den Erweiterungen AddDateOnlyConverters
und AddTimeOnlyConverters
werden die oben bereits genannten Converter an den Builder gebunden. Die Prüfung des Datenbanktreibers muss außerhalb noch erfolgen.
Implementierung: .NET Standard 2.0
In dieser Bibliothek werden nach und nach Erweiterungen für UnitTests gesammelt.