Zum Inhalt springen

C# CSharp Programmierung fortgeschrittene Techniken – Von Variablen zu Azure Cloud Techniken.

C# Charp Programmierung Tutorial

Werbung: Wenn Du ein gutes C# Lern-Buch benötigst, können wir Dir folgendes Buch von Amazon.de empfehlen:

Inhaltsverzeichnis

51. Event Sourcing mit MediatR

Event Sourcing ist eine fortgeschrittene Technik, bei der alle Zustandsänderungen eines Systems als Ereignisse gespeichert werden. Diese Ereignisse können dann wieder abgespielt werden, um den aktuellen Zustand des Systems zu rekonstruieren. MediatR eignet sich hervorragend für Event Sourcing, da es Befehle und Ereignisse leicht handhaben kann.

Beispiel für ein Event Sourcing-Modell:
 public interface IEvent { }

public class BenutzerErstelltEvent : IEvent
{
public string Benutzername { get; set; }
public DateTime Zeitpunkt { get; set; }
}

// Event Handler für das BenutzerErstelltEvent
public class BenutzerErstelltEventHandler : INotificationHandler<BenutzerErstelltEvent>
{
public Task Handle(BenutzerErstelltEvent benutzerEvent, CancellationToken cancellationToken)
{
// Logik zum Verarbeiten des Ereignisses
Console.WriteLine($"Benutzer erstellt: {benutzerEvent.Benutzername} um {benutzerEvent.Zeitpunkt}");
return Task.CompletedTask;
}
}

// Senden eines Events
public class BenutzerErstellungService
{
private readonly IMediator _mediator;

public BenutzerErstellungService(IMediator mediator)
{
_mediator = mediator;
}

public async Task ErstelleBenutzerAsync(string benutzername)
{
// Logik zum Erstellen des Benutzers...

var benutzerErstelltEvent = new BenutzerErstelltEvent
{
Benutzername = benutzername,
Zeitpunkt = DateTime.UtcNow
};

await _mediator.Publish(benutzerErstelltEvent); // Event über MediatR veröffentlichen
}
}

In diesem Beispiel wird ein BenutzerErstellt-Ereignis veröffentlicht, nachdem ein Benutzer erfolgreich erstellt wurde. Der BenutzerErstelltEventHandler verarbeitet dieses Ereignis und führt die notwendige Logik aus, etwa das Speichern des Ereignisses in einer Event-Store-Datenbank.

52. Integration von C# mit Kafka für Event-Streaming

Für verteilte Systeme und Microservices ist Kafka ein leistungsstarkes Event-Streaming-Tool, das hohe Durchsatzraten und skalierbare Echtzeit-Datenströme ermöglicht. Kafka kann mit C# und .NET problemlos verwendet werden, indem das Confluent Kafka .NET Client-Paket eingebunden wird.

Kafka-Produzent in C#:
 using Confluent.Kafka;

public async Task SendeNachrichtZuKafka(string topic, string nachricht)
{
var config = new ProducerConfig
{
BootstrapServers = "localhost:9092"
};

using (var producer = new ProducerBuilder<Null, string>(config).Build())
{
var deliveryResult = await producer.ProduceAsync(topic, new Message<Null, string> { Value = nachricht });
Console.WriteLine($"Nachricht zu {deliveryResult.TopicPartitionOffset} gesendet.");
}
}

Dieser Code zeigt, wie man eine Nachricht an einen Kafka-Cluster sendet. Du kannst das ProducerConfig-Objekt konfigurieren, um dich mit deinem Kafka-Cluster zu verbinden und dann Nachrichten an das gewünschte Topic zu senden.

Kafka-Konsument in C#:

 using Confluent.Kafka;

public async Task EmpfangeNachrichtenVonKafka(string topic)
{
var config = new ConsumerConfig
{
GroupId = "test-consumer-group",
BootstrapServers = "localhost:9092",
AutoOffsetReset = AutoOffsetReset.Earliest
};

using (var consumer = new ConsumerBuilder<Null, string>(config).Build())
{
consumer.Subscribe(topic);

while (true)
{
var consumeResult = consumer.Consume();
Console.WriteLine($"Nachricht empfangen: {consumeResult.Message.Value}");
}
}
}

In diesem Beispiel wird ein Kafka-Konsument eingerichtet, der Nachrichten von einem bestimmten Topic konsumiert. Der Konsument kann kontinuierlich Nachrichten empfangen und verarbeiten.

53. C# und gRPC für Hochleistungs-RPC

gRPC ist ein leistungsstarkes Remote Procedure Call (RPC)-Framework, das stark typisierte APIs und bidirektionale Streaming-Kommunikation unterstützt. Es wird häufig für die Kommunikation zwischen Microservices verwendet und kann in C# einfach integriert werden.

gRPC-Dienst:

Zuerst definierst du den Dienst in einer .proto-Datei:

 syntax = "proto3";

service BenutzerService {
rpc HoleBenutzer (BenutzerAnfrage) returns (BenutzerAntwort);
}

message BenutzerAnfrage {
int32 id = 1;
}

message BenutzerAntwort {
string name = 1;
int32 id = 2;
}

Dann generierst du den C#-Code aus dieser Datei und implementierst den gRPC-Dienst:

 public class BenutzerServiceImpl : BenutzerService.BenutzerServiceBase
{
public override Task<BenutzerAntwort> HoleBenutzer(BenutzerAnfrage request, ServerCallContext context)
{
return Task.FromResult(new BenutzerAntwort { Name = "Max", Id = request.Id });
}
}

gRPC-Client in C#:

 var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new BenutzerService.BenutzerServiceClient(channel);

var antwort = await client.HoleBenutzerAsync(new BenutzerAnfrage { Id = 1 });
Console.WriteLine($"Benutzer: {antwort.Name}, ID: {antwort.Id}");

gRPC bietet sehr gute Performance und ist eine bevorzugte Wahl für die Kommunikation zwischen Microservices in verteilten Systemen.

54. Clean Architecture mit C#

Clean Architecture ist ein Architekturansatz, der die Trennung von Verantwortlichkeiten und Abhängigkeiten fördert. Es ist eine Form der Schichtenarchitektur, bei der die Abhängigkeiten immer von äußeren Schichten zu den inneren Schichten fließen. Die Geschäftslogik und die Domänenmodelle bleiben unabhängig von den Frameworks und Datenbankimplementierungen.

Aufbau von Clean Architecture:

  • Entities: Die Kern-Geschäftslogik und Modelle, die unabhängig von anderen Schichten sind.
  • Use Cases: Die Anwendungslogik, die die Geschäftsregeln ausführt.
  • Interface Adapters: Schicht, die die Anwendungslogik mit der äußeren Welt (Datenbanken, Web, UI) verbindet.
  • Frameworks and Drivers: Datenbankzugriff, Webframeworks, UI, externe Systeme.

Beispiel für eine Clean Architecture-Struktur:

 // Entität
public class Benutzer
{
public int Id { get; set; }
public string Name { get; set; }
}

// Use Case
public class ErstelleBenutzerUseCase
{
private readonly IBenutzerRepository _benutzerRepository;

public ErstelleBenutzerUseCase(IBenutzerRepository benutzerRepository)
{
_benutzerRepository = benutzerRepository;
}

public void Handle(Benutzer benutzer)
{
_benutzerRepository.ErstelleBenutzer(benutzer);
}
}

// Interface Adapter (Repository-Interface)
public interface IBenutzerRepository
{
void ErstelleBenutzer(Benutzer benutzer);
}

// Datenbank-Implementierung
public class BenutzerRepository : IBenutzerRepository
{
public void ErstelleBenutzer(Benutzer benutzer)
{
// Datenbanklogik hier
}
}

Die Idee hinter Clean Architecture ist, dass die Geschäftslogik unabhängig bleibt von Frameworks und technischen Implementierungen, sodass sie leicht änderbar und testbar bleibt.

55. Async/Await mit ValueTask zur Optimierung

In besonders performancekritischen Bereichen

Das async– und await-Schlüsselwort in C# ermöglicht es, asynchrone Methoden zu schreiben, die auf Aufgaben (Tasks) warten. Die Standard-Rückgabe für asynchrone Methoden ist Task<T>, aber in bestimmten performancekritischen Szenarien kannst du ValueTask<T> verwenden, um unnötige Speicherzuweisungen zu vermeiden, wenn das Ergebnis einer Methode sofort verfügbar ist.

Wann sollte man ValueTask verwenden?

  • Task hat immer Overhead, da es ein Objekt auf dem Heap erzeugt, selbst wenn die Aufgabe sofort abgeschlossen wird.
  • ValueTask spart Speicher und vermeidet unnötige Objektallokationen, besonders bei wiederholten synchronen Rückgaben.

Beispiel für die Verwendung von ValueTask:

 ublic async ValueTask<int> BerechneErgebnisAsync(bool sofortVerfügbar)
{
if (sofortVerfügbar)
{
return 42; // Ergebnis ist sofort verfügbar, kein Task notwendig
}

await Task.Delay(1000); // Simuliere asynchrone Arbeit
return 42;
}

Hier wird nur dann ein Task-Objekt erzeugt, wenn die Berechnung wirklich asynchron erfolgen muss. Andernfalls wird direkt ein ValueTask<int> mit dem Ergebnis zurückgegeben.

56. Thread-Sicherheit mit ConcurrentDictionary und anderen Concurrent-Klassen

In Multithreading-Umgebungen musst du sicherstellen, dass Datenstrukturen thread-sicher sind, um Race Conditions und Datenkorruption zu vermeiden. .NET bietet verschiedene Concurrent Collections, die speziell für solche Szenarien entwickelt wurden.

ConcurrentDictionary

Ein ConcurrentDictionary ist eine thread-sichere Version von Dictionary, die gleichzeitige Lese- und Schreibvorgänge unterstützt.

Beispiel:

 using System.Collections.Concurrent;

ConcurrentDictionary<int, string> dictionary = new ConcurrentDictionary<int, string>();

// Element hinzufügen
dictionary.TryAdd(1, "Max");

// Element abrufen
if (dictionary.TryGetValue(1, out string name))
{
Console.WriteLine($"Name: {name}");
}

// Element entfernen
dictionary.TryRemove(1, out string entfernteWert);

BlockingCollection

Die BlockingCollection ist eine threadsichere Sammlung, die häufig in Producer-Consumer-Szenarien verwendet wird. Sie blockiert den Aufrufer, wenn der Puffer voll ist (beim Hinzufügen) oder leer ist (beim Entfernen).

Beispiel:

 using System.Collections.Concurrent;
using System.Threading.Tasks;

BlockingCollection<int> collection = new BlockingCollection<int>(5); // Puffergröße 5

Task producer = Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
collection.Add(i);
Console.WriteLine($"Produziert: {i}");
}
collection.CompleteAdding(); // Signalisiert, dass der Producer fertig ist
});

Task consumer = Task.Run(() =>
{
foreach (var item in collection.GetConsumingEnumerable())
{
Console.WriteLine($"Konsumiert: {item}");
}
});

Task.WaitAll(producer, consumer);

Hier wird eine BlockingCollection verwendet, um Daten sicher zwischen einem Produzenten und einem Konsumenten zu übertragen.

57. C# Source Generators zur Compilezeit-Code-Generierung

Source Generators sind eine relativ neue Funktion in C#, mit der du Code zur Compilezeit generieren kannst. Dies ist besonders nützlich, um Boilerplate-Code zu vermeiden und eine bessere Performance zu erreichen, da zur Laufzeit weniger Reflexion und dynamische Aufrufe notwendig sind.

Beispiel für einen Source Generator:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public class BeispielGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }

public void Execute(GeneratorExecutionContext context)
{
var source = @"
namespace GenerierterCode
{
public static class GenerierteKlasse
{
public static void SagHallo()
{
System.Console.WriteLine(""Hallo, Welt!"");
}
}
}";

context.AddSource("GenerierteKlasse.g.cs", SourceText.From(source, Encoding.UTF8));
}
}

Dieser Generator erzeugt eine Klasse GenerierteKlasse zur Compilezeit, die eine Methode SagHallo enthält. Solche Source Generators sind besonders nützlich, um Boilerplate-Code für komplexe Datenstrukturen oder häufig genutzte Schnittstellen zu vermeiden.

58. Span<T> und Memory<T> für speichereffizientes Arbeiten

Span<T> und Memory<T> sind spezielle Typen in C#, die eine speichereffiziente Arbeit mit Arrays und Buffern ermöglichen, ohne zusätzliche Speicherzuteilungen vorzunehmen. Sie sind besonders nützlich für Szenarien, in denen hohe Leistung erforderlich ist, wie etwa beim Verarbeiten großer Datenmengen.

Span<T> Beispiel:

 public void VerarbeiteDaten(Span<int> daten)
{
for (int i = 0; i < daten.Length; i++)
{
daten[i] *= 2;
}
}

int[] array = { 1, 2, 3, 4, 5 };
VerarbeiteDaten(array.AsSpan());

Console.WriteLine(string.Join(", ", array)); // Ausgabe: 2, 4, 6, 8, 10

Span<T> arbeitet direkt auf dem Speicher, ohne dass eine Kopie des Arrays erstellt wird, was zu einer besseren Speicherverwaltung führt. Es kann sowohl auf dem Stack als auch auf dem Heap verwendet werden.

Memory<T> Beispiel:

Memory<T> ist ein heap-basiertes Pendant zu Span<T>, das außerhalb der synchronen Methoden verwendet werden kann (z.B. in asynchronen Methoden).

csharpCode kopierenpublic async Task VerarbeiteDatenAsync(Memory<int> daten)
{
    for (int i = 0; i < daten.Length; i++)
    {
        daten.Span[i] *= 2;  // Zugriff auf den Speicherbereich über `Span`
    }
}

int[] array = { 1, 2, 3, 4, 5 };
await VerarbeiteDatenAsync(array.AsMemory());

Console.WriteLine(string.Join(", ", array));  // Ausgabe: 2, 4, 6, 8, 10

59. Records in C# 9+ für unveränderliche Datenmodelle

Records sind eine neue Ergänzung in C# 9, die speziell für unveränderliche Datentypen entwickelt wurden. Sie bieten eine kompakte Möglichkeit, Klassen zu definieren, die nur aus Daten bestehen, und enthalten automatisch Implementierungen für Gleichheit und Vergleich.

Beispiel für ein Record:

 public record Person(string Name, int Alter);

// Erzeugung eines Records
var person1 = new Person("Max", 25);

// Kopie des Records mit einem geänderten Wert
var person2 = person1 with { Name = "Anna" };

Console.WriteLine(person1); // Ausgabe: Person { Name = Max, Alter = 25 }
Console.WriteLine(person2); // Ausgabe: Person { Name = Anna, Alter = 25 }

Records sind besonders nützlich für DTOs (Data Transfer Objects) und andere unveränderliche Datenmodelle, bei denen du möchtest, dass Objekte als Werttypen behandelt werden.

Werbung: Wenn Du ein gutes C# Lern-Buch benötigst, können wir Dir folgendes Buch von Amazon.de empfehlen:

60. Blazor für moderne Web-UI-Entwicklung

Blazor ist ein modernes Framework von Microsoft, das es dir ermöglicht, interaktive Web-User-Interfaces in C# zu schreiben, anstatt auf JavaScript zurückzugreifen. Es unterstützt sowohl clientseitige als auch serverseitige Render-Modelle.

Beispiel für eine einfache Blazor-Komponente:
razorCode kopieren@page "/counter"

<h3>Counter</h3>

<p>Aktueller Zählerstand: @count</p>

<button class="btn btn-primary" @onclick="ErhoeheZaehler">Erhöhe</button>

@code {
    private int count = 0;

    private void ErhoeheZaehler()
    {
        count++;
    }
}

Blazor ermöglicht dir, C# für die Webentwicklung zu verwenden, ohne dass du JavaScript kennen musst. Mit Blazor WebAssembly kannst du sogar komplette Single-Page-Applications (SPAs) auf dem Client ausführen, was Blazor zu einer attraktiven Wahl für .NET-Entwickler macht.

Wir sind noch nicht fertig, Weiteres später

Seiten: 1 2 3 4 5 6