Transakcje baz danych stanowią kluczowy element w zarządzaniu danymi, zapewniając integralność, spójność i niezawodność aplikacji. Zrozumienie ich istoty jest niezbędne do tworzenia aplikacji, które przetwarzają dane w sposób bezpieczny i stabilny, szczególnie w systemach, gdzie operacje są wykonywane równocześnie przez wielu użytkowników. W tym kontekście, Entity Framework Core (EF Core) w wersji 9 stanowi efektywne narzędzie do obsługi transakcji w aplikacjach ASP.NET Core.

Transakcja bazodanowa to jednostka pracy, która jest traktowana jako całość. Oznacza to, że jeśli jakikolwiek element transakcji nie powiedzie się, cała transakcja jest wycofywana, a stan bazy danych pozostaje niezmieniony. Takie podejście gwarantuje, że dane w bazie będą zawsze w spójnym stanie, a błędy, które mogą wystąpić w trakcie operacji, nie wprowadzą aplikacji w niepożądany stan.

Zasady działania transakcji

W kontekście baz danych, transakcja ma cztery podstawowe właściwości, które są określane akronimem ACID:

  1. Atomowość: Transakcja jest jednością – albo wszystkie operacje w jej ramach są wykonane pomyślnie, albo żadna. Gdy jedna z operacji zakończy się niepowodzeniem, cała transakcja zostaje anulowana.

  2. Spójność: Po zakończeniu transakcji baza danych musi znajdować się w spójnym stanie. Transakcja nie może pozostawić bazy danych w pośrednim, nieprawidłowym stanie.

  3. Izolacja: Transakcje wykonywane równolegle nie mogą się nawzajem zakłócać. Izolacja zapewnia, że dane widziane przez jedną transakcję nie będą zmieniane przez inną, dopóki transakcja pierwsza nie zostanie zakończona.

  4. Trwałość: Po zakończeniu transakcji i jej zatwierdzeniu (commit), zmiany wprowadzone do bazy danych są trwałe, niezależnie od tego, co stanie się z systemem.

Obsługa transakcji w EF Core

EF Core umożliwia łatwe zarządzanie transakcjami za pomocą DbContext. W przypadku, gdy chcemy przeprowadzić operacje na bazie danych jako część jednej transakcji, możemy użyć metody BeginTransaction(), a następnie wykonać wszystkie operacje, które mają być częścią tej transakcji. W przypadku sukcesu transakcję zatwierdzamy za pomocą Commit(), a w przypadku błędu wycofujemy ją za pomocą Rollback().

csharp
using (var transaction = dbContext.Database.BeginTransaction()) { try { dbContext.Add(product); dbContext.SaveChanges(); transaction.Commit(); } catch (Exception) { transaction.Rollback(); throw; } }

Dzięki tej metodzie mamy pełną kontrolę nad tym, które operacje są częścią transakcji, oraz możemy zapewnić, że dane będą spójne, nawet w przypadku wystąpienia błędów.

Obsługa transakcji w aplikacjach minimal API

W przypadku aplikacji ASP.NET Core Minimal API, proces zarządzania transakcjami nie różni się zbytnio od tradycyjnych aplikacji API. Ważne jest, aby każdą operację, która wpływa na stan bazy danych (np. dodawanie, aktualizowanie lub usuwanie danych), traktować jako część transakcji. W EF Core transakcje są stosunkowo łatwe do zaimplementowania, zwłaszcza w kontekście minimal API, które umożliwiają szybkie i zwięzłe tworzenie endpointów.

Przykład użycia transakcji w ramach operacji PUT:

csharp
app.MapPut("/products/{id}", async (EfCoreLabContext dbContext, int id, Product product) =>
{ var p = await dbContext.Products.Where(p => p.Id == id).FirstOrDefaultAsync(); if (p != null) { using (var transaction = await dbContext.Database.BeginTransactionAsync()) { try { p.Price = product.Price; if (!string.IsNullOrEmpty(product.Name)) p.Name = product.Name; dbContext.Products.Update(p); await dbContext.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } } return Results.Ok(p); } return Results.NotFound(); });

W tym przypadku, operacje zapisu do bazy danych są wykonywane w ramach transakcji. Jeśli jakakolwiek operacja się nie powiedzie, zmiany zostaną wycofane, co zapewni integralność danych.

Testowanie API z wykorzystaniem transakcji

Po zaimplementowaniu operacji CRUD (Create, Read, Update, Delete) na danych, warto przetestować je za pomocą odpowiednich narzędzi, jak np. REST Client w Visual Studio Code lub Scalar UI, które ułatwiają interakcję z API. Przy testowaniu API ważne jest, aby upewnić się, że wszystkie operacje bazodanowe są realizowane poprawnie, a w przypadku błędów system właściwie reaguje, wycofując zmiany.

Dla poprawnego testowania operacji transakcyjnych warto stworzyć specjalny plik .http, w którym zdefiniujemy przykładowe zapytania:

http
### Test PUT request
PUT {{efcoredbfirst_HostAddress}}/products/1 Content-Type: application/json { "name": "Updated Product", "price": 150 }

Znaczenie transakcji w aplikacjach bazodanowych

Kiedy aplikacja wykonuje operacje na bazie danych, szczególnie w systemach, które obsługują wielu użytkowników jednocześnie, transakcje odgrywają kluczową rolę w zapewnianiu integralności danych. Bez odpowiedniego zarządzania transakcjami, aplikacja może napotkać problemy związane z niespójnością danych, np. poprzez częściowe zapisanie operacji lub nadpisywanie danych przez różne procesy równocześnie.

Zrozumienie i poprawne implementowanie transakcji jest kluczowe, aby stworzyć aplikację, która nie tylko działa efektywnie, ale także gwarantuje bezpieczeństwo danych. Dzięki właściwej obsłudze transakcji można uniknąć wielu typowych błędów, które mogą wystąpić w aplikacjach wielowątkowych lub rozproszonych.

Jak zaimplementować bezpieczną autentykację REST API w aplikacji .NET z użyciem JWT

Aby stworzyć bezpieczne API, niezbędna jest implementacja odpowiednich mechanizmów autentykacji i autoryzacji. W tym przypadku, w aplikacji .NET, możemy wykorzystać tokeny JWT (JSON Web Tokens) do zapewnienia bezpiecznego dostępu do danych. Poniżej przedstawiono kroki, które prowadzą przez konfigurację aplikacji z użyciem JWT, implementację rejestracji i logowania użytkowników, a także zakończenie procesu integracji z bazą danych.

W pierwszej kolejności należy skonfigurować usługę OpenAPI. Za pomocą odpowiedniego kodu w Program.cs, możemy dodać schemat bezpieczeństwa JWT do dokumentacji API. W tym przypadku konfigurujemy tytuł, wersję oraz opis dokumentu API, a także dodajemy schemat bezpieczeństwa Bearer, który będzie używany w procesie uwierzytelniania. Schemat ten ma przypisany typ Http z opcją Bearer oraz format JWT, a także prośbę o wpisanie tokenu JWT.

csharp
builder.Services.AddOpenApi(options => { options.AddDocumentTransformer((document, context, cancellationToken) => { document.Info.Title = "Secure REST API"; document.Info.Version = "v1"; document.Info.Description = "A secure REST API with JWT authentication"; document.Components ??= new OpenApiComponents(); document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", Description = "Enter JWT Bearer token" }; return Task.CompletedTask; }); });

Następnie należy zainstalować i skonfigurować Scalar UI, które umożliwia interaktywną dokumentację API. W tym celu dodajemy odpowiednią konfigurację przed metodą builder.Build(), a po jej wywołaniu aktywujemy środowisko uwierzytelniania oraz autoryzacji.

csharp
var app = builder.Build();
if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(options => { options.Title = "Secure REST API"; options.Theme = ScalarTheme.Purple; options.ShowSidebar = true; options.DefaultHttpClient = new(ScalarTarget.CSharp, ScalarClient.HttpClient); }); } // ... app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization();

W kolejnym kroku musimy skonfigurować połączenie z bazą danych oraz ustawić tajny klucz JWT w pliku appsettings.json. Ważne jest, aby używać odpowiedniego łańcucha połączenia oraz klucza sekretu JWT, który powinien być wystarczająco skomplikowany i losowy (32 znaki). Po dokonaniu tych zmian, należy przeprowadzić migrację bazy danych.

json
{
"ConnectionStrings": { "MyDB": "server=localhost; database=TrainingDB; uid=tester; pwd=pass123;" }, "AppSettings": { "Secret": "aaaaabbbbbcccccddddd11234df4444sd" } }

Warto zauważyć, że klucz JWT powinien być trzymany w tajemnicy i nie może być ujawniany publicznie.

Po skonfigurowaniu bazy danych, przeprowadzamy migrację oraz aktualizację bazy za pomocą poniższych komend:

bash
dotnet ef migrations add InitialCreate dotnet ef database update

Po tych krokach możemy zaimplementować punkty końcowe API: rejestracji oraz logowania użytkowników. Podczas rejestracji, hasło użytkownika jest haszowane przed zapisaniem go w bazie danych. Do przechowywania haseł używamy popularnej biblioteki BCrypt.

csharp
app.MapPost("/register", async (AppDbContext dbContext, ApiUser usr) => {
var user = new ApiUser { Username = usr.Username, Password = BC.HashPassword(usr.Password), Email = usr.Email, Name = usr.Name }; dbContext.Users.Add(user); await dbContext.SaveChangesAsync(); return Results.Ok(); });

W przypadku logowania, użytkownik jest autentykowany na podstawie danych przechowywanych w bazie. Jeśli dane są prawidłowe, generowany jest token JWT, który może być użyty do autentykacji w przyszłych żądaniach.

csharp
app.MapPost("/login", (AppDbContext dbContext, IConfiguration configuration, UserModel model) => { var usr = dbContext.Users.Where(o => o.Username == model.UserName).FirstOrDefault(); if (usr != null) { if (BC.Verify(model.Password, usr.Password)) { var key = configuration.GetValue("AppSettings:Secret"); var keyBytes = Encoding.ASCII.GetBytes(key ?? "aaaaabbbbbcccccddddd11234df4444sd"); var expiredAt = DateTime.Now.AddDays(2); var tokenHandler = new JwtSecurityTokenHandler(); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, usr.Username) }), Expires = expiredAt, SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(keyBytes), SecurityAlgorithms.HmacSha256Signature ) }; var token = tokenHandler.CreateToken(tokenDescriptor); var userToken = new UserToken { Token = tokenHandler.WriteToken(token), ExpiredAt = expiredAt.ToString(), Message = "" }; return userToken; } } return new UserToken { Message = "Username or password is invalid" }; });

Po zaimplementowaniu tych dwóch punktów końcowych możemy stworzyć punkt końcowy, który pozwala na dostęp do profilu użytkownika, ale tylko wtedy, gdy użytkownik dostarczy ważny token JWT w nagłówku Authorization.

csharp
app.MapGet("/profile", [Authorize] async (HttpContext httpContext, AppDbContext dbContext) => {
var username = httpContext.User.Identity?.Name; var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Username == username); return user != null ? Results.Ok(new { user.Username, user.Name, user.Email }) : Results.NotFound(); });

Dzięki tym krokom zbudowane zostało pełne API z wykorzystaniem JWT do autentykacji i autoryzacji użytkowników. Całość aplikacji powinna być następnie skompilowana i uruchomiona przy użyciu komendy:

bash
dotnet run

Aby ułatwić testowanie, warto stworzyć plik .http, który pozwoli na szybsze sprawdzenie funkcjonalności rejestracji, logowania oraz dostępu do profilu użytkownika.

http
@secrestapi_HostAddress = http://localhost:5124 @token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InVzZXIxIiwibmJmIjoxNzAyMjg1NjMxLCJleHAiOjE3MDI0NTg0MzEsImlhdCI6MTcwMjI4NTYzMX0.mE7s2rSIIT78b-bjp-5hhbgGEPGr1eJTww8Wg0DpRMo ### register POST {{secrestapi_HostAddress}}/register Accept: application/json Content-Type: application/json { "Email": "[email protected]", "Password": "pass123", "Username": "user1", "Name": "User 1" } ### login POST {{secrestapi_HostAddress}}/login Accept: application/json Content-Type: application/json { "Password": "pass123", "Username": "user1" } ### GET profile GET {{secrestapi_HostAddress}}/profile Accept: application/json Content-Type: application/json Authorization: Bearer {{token}}

Powyższe kroki stanowią kompletną konfigurację oraz implementację API z użyciem JWT, a także umożliwiają łatwe testowanie aplikacji przy użyciu dedykowanych narzędzi.

Jak skonfigurować i chronić dane w aplikacjach ASP.NET Core: Przewodnik

W nowoczesnych aplikacjach webowych ochrona danych jest jednym z najważniejszych aspektów, który wymaga szczególnej uwagi. Zastosowanie odpowiednich mechanizmów ochrony pozwala na zabezpieczenie wrażliwych informacji, takich jak dane osobowe użytkowników czy hasła. W tej części przedstawimy, jak skonfigurować mechanizmy ochrony danych w aplikacjach ASP.NET Core, bazując na wykorzystaniu bazy danych do przechowywania kluczy ochrony oraz zarządzania wrażliwymi informacjami.

Aby odpowiednio chronić dane, musimy skonfigurować odpowiednie usługi ochrony danych. Jednym z kluczowych elementów jest przechowywanie kluczy ochrony w bazie danych. W tym celu tworzymy niestandardowego dostawcę ochrony danych, który będzie zarządzać tymi kluczami w bazie. Proces zaczynamy od stworzenia klasy SqlServerDataProtectionProvider, która implementuje interfejs IDataProtectionProvider. W tej klasie będziemy implementować logikę do tworzenia i przechowywania kluczy ochrony danych w bazie danych.

Implementacja dostawcy ochrony danych

Pierwszym krokiem jest utworzenie klasy SqlServerDataProtectionProvider, która będzie odpowiedzialna za zarządzanie kluczami ochrony w bazie danych. Zaczniemy od utworzenia klasy w folderze Models. Klasa ta będzie korzystać z kontekstu bazy danych, aby przechowywać i pobierać klucze ochrony. W metodzie CreateProtector() sprawdzamy, czy klucz ochrony już istnieje w bazie danych. Jeśli go nie ma, tworzymy nowy klucz. Działa to w następujący sposób:

csharp
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; namespace privdata.Models { public class SqlServerDataProtectionProvider : IDataProtectionProvider { private readonly AppDbContext _dbContext;
public SqlServerDataProtectionProvider(AppDbContext dbContext)
{ _dbContext = dbContext; }
public IDataProtector CreateProtector(string purpose) { var key = _dbContext.DataProtectionKeys .Where(o => o.FriendlyName == purpose) .FirstOrDefault(); if (key == null) { key = CreateNewKey(purpose); } var protector = DataProtectionProvider.Create(key.FriendlyName ?? purpose) .CreateProtector(purpose); return protector; }
private DataProtectionKey CreateNewKey(string purpose)
{
var key = new DataProtectionKey { FriendlyName = purpose }; _dbContext.DataProtectionKeys.Add(key); _dbContext.SaveChanges(); return key; } } }

W powyższym kodzie, metoda CreateProtector() tworzy ochronę danych dla określonego celu, natomiast CreateNewKey() tworzy nowy klucz, jeśli nie istnieje w bazie danych. Ochrona danych jest kluczowym elementem przy pracy z wrażliwymi informacjami.

Konfiguracja ochrony danych

Kolejnym krokiem jest konfiguracja usług ochrony danych w aplikacji. W tym celu, w pliku Program.cs, musimy dodać odpowiednią konfigurację, która wykorzysta nasz niestandardowy dostawca ochrony danych. Warto zauważyć, że korzystamy z bazy danych, aby przechowywać klucze ochrony.

csharp
builder.Services.AddDataProtection()
.PersistKeysToDbContext<AppDbContext>(); // Dodanie dostawcy ochrony builder.Services.AddTransient<SqlServerDataProtectionProvider>();

Dzięki tej konfiguracji aplikacja będzie mogła przechowywać klucze ochrony w bazie danych i w razie potrzeby je wykorzystać.

Enkrypcja i maskowanie danych wrażliwych

Po skonfigurowaniu ochrony danych, należy również zadbać o właściwą ochronę samych danych w aplikacji. W tym przypadku pomocna będzie klasa SensitiveDataService, która będzie odpowiedzialna za logikę enkrypcji i maskowania danych. Ta klasa pozwala na bezpieczne przechowywanie wrażliwych informacji, takich jak numery telefonów, daty urodzin czy adresy e-mail.

W metodach tej klasy implementujemy algorytmy szyfrowania danych w sposób, który zapewnia ich bezpieczeństwo, a jednocześnie umożliwia ich wykorzystanie w aplikacji.

csharp
public class SensitiveDataService
{ private readonly IDataProtector _protector; public SensitiveDataService(IDataProtectionProvider provider) { _protector = provider.CreateProtector("SensitiveData"); } public Employee EncryptEmployeeData(Employee employee) { employee.Phone = _protector.Protect(employee.Phone); employee.Email = _protector.Protect(employee.Email); employee.Birthdate = _protector.Protect(employee.Birthdate.ToString()); return employee; } public Employee MaskEmployeeData(Employee employee) { employee.Phone = MaskData(employee.Phone); employee.Email = MaskData(employee.Email); employee.Birthdate = MaskData(employee.Birthdate.ToString()); return employee; } private string MaskData(string data) {
return data.Substring(0, 3) + "*****" + data.Substring(data.Length - 3);
} }

Testowanie i uruchamianie aplikacji

Po skonfigurowaniu mechanizmów ochrony danych i enkrypcji, przyszedł czas na przetestowanie aplikacji. W tym celu warto stworzyć odpowiednie punkty API, które będą odpowiedzialne za dodawanie i pobieranie danych z bazy.

csharp
app.MapPost("/employees", (AppDbContext dbContext, SensitiveDataService service, Employee employee) => { dbContext.Employees.Add(service.EncryptEmployeeData(employee)); dbContext.SaveChanges(); return Results.Ok(); }); app.MapGet("/employees", (AppDbContext dbContext, SensitiveDataService service) => { var employees = dbContext.Employees.AsEnumerable().Select(service.MaskEmployeeData); return Results.Ok(employees); });

Dzięki tym endpointom, będziemy mogli dodawać nowych pracowników do bazy oraz pobierać dane pracowników, gdzie wrażliwe informacje będą odpowiednio zabezpieczone.

Testowanie aplikacji

Aby przeprowadzić testy, można użyć narzędzia Scalar UI, które pozwala na łatwe sprawdzenie działania API. Za pomocą Scalar UI można dodać nowych pracowników, a następnie sprawdzić, jak dane są prezentowane w bazie. Wszystkie wrażliwe dane, takie jak numery telefonów czy adresy e-mail, będą odpowiednio zaszyfrowane i zamaskowane w odpowiednich miejscach.

Co warto dodać do aplikacji?

Ważnym elementem, o którym warto pamiętać przy budowie aplikacji tego typu, jest odpowiednie zarządzanie kluczami ochrony danych. Klucze te powinny być przechowywane w sposób bezpieczny, a dostęp do nich powinien być ograniczony do odpowiednich osób i procesów. Również proces aktualizacji kluczy ochrony danych może być istotny, szczególnie w kontekście dbałości o bezpieczeństwo wrażliwych informacji.

Ponadto, należy zadbać o odpowiednią politykę przechowywania danych w aplikacji, by spełniała ona wymagania ochrony prywatności użytkowników, takie jak regulacje RODO (GDPR) czy inne przepisy ochrony danych osobowych. Przechowywanie wrażliwych danych powinno odbywać się tylko wtedy, gdy jest to absolutnie konieczne, a dostęp do tych danych powinien być dobrze kontrolowany.

Jak zrealizować przesyłanie i pobieranie plików w ASP.NET Core 9.0?

Wraz z rozwojem aplikacji webowych, obsługa przesyłania i pobierania plików staje się podstawową funkcjonalnością, którą należy uwzględnić w wielu projektach. ASP.NET Core 9.0 zapewnia minimalne API, które ułatwia implementację takich operacji. W poniższym przykładzie pokazano, jak zaimplementować przesyłanie plików na serwer oraz ich późniejsze pobieranie przez użytkowników.

Do rozpoczęcia musimy stworzyć odpowiednią strukturę aplikacji, która umożliwi obsługę plików. Podstawowym zadaniem jest zapewnienie, by pliki były przechowywane w odpowiednim katalogu, w tym przypadku w katalogu wwwroot. Kolejnym krokiem jest zapewnienie, że serwer będzie w stanie obsługiwać zarówno wysyłanie, jak i pobieranie plików za pomocą odpowiednich endpointów API.

Implementacja API do przesyłania plików

Rozpoczynamy od stworzenia metody obsługującej przesyłanie plików. Najpierw musimy zdefiniować endpoint, który będzie przyjmować pliki. Następnie, przy pomocy strumienia pamięci, zapisujemy plik w wyznaczonym katalogu. Oto przykład implementacji:

csharp
var memoryStream = new MemoryStream(); using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { await stream.CopyToAsync(memoryStream); } memoryStream.Position = 0; return Results.File(memoryStream, "application/octet-stream", fileName);

W powyższym kodzie najpierw otwieramy plik z określonej ścieżki, a następnie kopiujemy go do strumienia pamięci (memoryStream). Po tej operacji ustawiamy pozycję strumienia na początek, aby umożliwić zwrócenie pliku w odpowiedzi HTTP.

Kolejnym krokiem jest obsługa wysyłania plików za pomocą odpowiedniego formularza, który umożliwi użytkownikowi przesyłanie pliku wraz z dodatkowymi danymi, jak na przykład opis pliku. Poniżej przykład kodu do testowania przesyłania plików w narzędziu takim jak Postman:

http
@fileapi_HostAddress = https://localhost:7010
### Upload File POST {{fileapi_HostAddress}}/api/file/upload Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW Accept: application/json ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="description" This is a test file ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="file"; filename="example.txt" Content-Type: text/plain < ./example.txt ------WebKitFormBoundary7MA4YWxkTrZu0gW--

Pobieranie plików

Z kolei pobieranie plików z serwera realizowane jest za pomocą metod GET, gdzie użytkownik wskazuje ścieżkę do pliku. W odpowiedzi serwer zwraca zawartość pliku:

http
GET {{fileapi_HostAddress}}/api/file/download/example.txt

Przykład powyżej pokazuje prostą metodę, która umożliwia użytkownikowi pobranie pliku example.txt z serwera.

Testowanie API

Po stworzeniu odpowiednich endpointów, należy przeprowadzić testy. Można to zrobić za pomocą narzędzi takich jak Postman lub REST Client w Visual Studio Code. W przypadku testowania w Postmanie, wystarczy zaimportować plik OpenAPI, który automatycznie wygeneruje wszystkie dostępne endpointy. Następnie tworzymy nowe żądanie i wypełniamy odpowiednie pola, takie jak adres URL oraz nagłówki. Upewniamy się, że plik jest poprawnie przesyłany oraz pobierany.

Używanie Serilog do logowania

Podczas implementacji API warto także zadbać o odpowiednie logowanie błędów oraz informacji o przesyłanych i pobieranych plikach. Możemy do tego wykorzystać bibliotekę Serilog, która pozwala na zapisywanie logów do pliku oraz konsoli. Aby zainstalować Serilog, wykonujemy polecenia NuGet:

bash
dotnet add package Serilog dotnet add package Serilog.AspNetCore dotnet add package Serilog.Sinks.File dotnet add package Serilog.Sinks.Console

Następnie konfigurujemy Serilog w pliku Program.cs:

csharp
var logger = new LoggerConfiguration()
.WriteTo.Console() .WriteTo.File("log-.txt", rollingInterval: RollingInterval.Day) .CreateLogger(); builder.Logging.AddSerilog(logger);

Dzięki temu wszystkie błędy i informacje o przesyłanych plikach będą zapisywane w pliku dziennika. Umożliwi to lepsze monitorowanie aplikacji oraz szybsze rozwiązywanie problemów.

Obsługa wyjątków i błędów HTTP

Kolejnym ważnym elementem jest zapewnienie odpowiedniej obsługi wyjątków, takich jak błędy 404 (plik nie znaleziony) czy 500 (błąd serwera). W tym celu możemy wykorzystać middleware w ASP.NET Core. Przy pomocy middleware przechwycimy wyjątki i zwrócimy odpowiednie informacje użytkownikowi, jak również zapiszemy błąd w dzienniku:

csharp
app.UseExceptionHandler("/error");
app.MapGet("/error", (HttpContext httpContext) => { var exceptionFeature = httpContext.Features.Get<IExceptionHandlerFeature>(); var exception = exceptionFeature?.Error; var problemDetails = new ProblemDetails { Status = 500, Title = "An error occurred while processing your request." }; if (exception is FileNotFoundException) { problemDetails.Status = 404; problemDetails.Title = "File not found."; } return Results.Problem(problemDetails.Title, statusCode: problemDetails.Status); });

Dzięki tej obsłudze aplikacja nie tylko będzie w stanie przechwycić błędy, ale także odpowiednio zareagować, przekazując użytkownikowi precyzyjne informacje.

Co warto dodać?

Ważnym aspektem przy pracy z plikami jest dbałość o bezpieczeństwo, szczególnie w przypadku, gdy aplikacja umożliwia przesyłanie plików przez użytkowników. Warto zabezpieczyć aplikację przed przesyłaniem nieautoryzowanych plików (np. poprzez sprawdzanie rozszerzeń plików) oraz rozważyć implementację limitów rozmiaru przesyłanych plików, aby uniknąć przeciążenia serwera. Ponadto, przy implementacji takich funkcji warto pamiętać o odpowiednich uprawnieniach dostępu do plików i katalogów, aby uniemożliwić nieautoryzowany dostęp do wrażliwych danych.

Jak zaimplementować middleware i filtry w aplikacji Minimal API w ASP.NET Core 9.0?

Implementacja middleware w aplikacjach webowych stała się nieodłącznym elementem współczesnego rozwoju aplikacji na platformie ASP.NET Core. W ramach tego rozdziału skupimy się na procesie implementacji własnego middleware oraz wykorzystaniu filtrów w projekcie typu Minimal API, co pozwala na skuteczne zarządzanie cyklem życia żądań HTTP, zapewniając elastyczność w zakresie autoryzacji, pamięci podręcznej czy formatowania odpowiedzi.

W przypadku aplikacji typu Minimal API, dostępność funkcji znanych z tradycyjnych kontrolerów MVC jest ograniczona. Niemniej jednak, odpowiednikiem dla filtrów są tu mechanizmy middleware, które umożliwiają dodanie logiki pośredniczącej w obiegu żądań HTTP.

W celu rozpoczęcia pracy z projektem Minimal API, należy stworzyć nowy projekt przy użyciu narzędzia dotnet. W terminalu należy uruchomić poniższe polecenia:

bash
mkdir middlewareapi cd middlewareapi dotnet new webapi

Po utworzeniu projektu, możemy otworzyć go w wybranym edytorze kodu, np. Visual Studio Code, używając komendy:

bash
code .

Z kolei implementacja prostego middleware, które będzie logować informacje o przychodzących i wychodzących żądaniach HTTP, może wyglądać następująco. W pliku Program.cs wprowadzamy poniższy kod:

csharp
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // Custom Middleware for Logging app.Use(async (context, next) => { Console.WriteLine("Request Incoming"); await next(); Console.WriteLine("Response Outgoing"); }); // Define routes app.MapGet("/", () => "Hello World"); app.Run();

Można także zorganizować middleware w osobnej klasie. Aby to zrobić, tworzymy nowy plik LoggingMiddleware.cs, a w jego wnętrzu umieszczamy następujący kod:

csharp
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks; public class LoggingMiddleware { private readonly RequestDelegate _next; public LoggingMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { Console.WriteLine("LoggingMiddleware>> Request Incoming"); await _next(context); Console.WriteLine("LoggingMiddleware>> Response Outgoing"); } }

Następnie, w metodzie Main w Program.cs, należy dodać wywołanie tego middleware:

csharp
app.UseMiddleware<LoggingMiddleware>();

Aby przetestować nasze rozwiązanie, możemy skorzystać z narzędzia REST Client dostępnego w Visual Studio Code. Tworzymy plik .http, w którym określamy adres API, a następnie wysyłamy zapytanie GET:

http
@middlewareapi_HostAddress = http://localhost:5104 GET {{middlewareapi_HostAddress}}/weatherforecast Accept: application/json

Po uruchomieniu aplikacji (np. komendą dotnet run), powinniśmy zobaczyć w konsoli logi związane z przychodzącymi i wychodzącymi żądaniami HTTP, co potwierdza działanie naszego middleware.

Warto także zauważyć, że Minimal API w ASP.NET Core nie wspiera filtrów w taki sam sposób jak kontrolery MVC. Zamiast tego, do obsługi podobnej logiki, możemy wykorzystać middleware, które pozwala na łatwą manipulację w trakcie obsługi żądań. Alternatywnie, w bardziej zaawansowanych przypadkach, można rozważyć konwersję aplikacji na podejście oparte na kontrolerach, gdzie filtry są pełnoprawną częścią architektury.

Implementacja filtrów, choć formalnie niedostępna w Minimal API, może zostać zrealizowana poprzez usługę wstrzykiwaną do delegatów endpointów, co daje dodatkową elastyczność w tworzeniu aplikacji. Jednak w kontekście tego labu, głównie koncentrujemy się na wykorzystywaniu middleware.

Po zakończeniu implementacji middleware warto zadbać o odpowiednią konfigurację aplikacji i testowanie jej za pomocą narzędzi takich jak Swagger UI. Dzięki temu, będziemy mogli w prosty sposób zobaczyć, jak middleware wpływa na odpowiedzi serwera.

Middleware jest bardzo potężnym narzędziem, które może zostać użyte do szerokiego zakresu zadań, od logowania i monitorowania, po zarządzanie bezpieczeństwem i pamięcią podręczną. Kluczowym aspektem przy implementacji jest dobre zrozumienie kolejności, w jakiej middleware są uruchamiane, co może mieć kluczowe znaczenie dla poprawnego działania aplikacji.

Ważnym aspektem przy pracy z Minimal API i middleware jest również wiedza o tym, że stosowanie odpowiednich narzędzi, takich jak filtry i middleware, pozwala na efektywne zarządzanie żądaniami, a także umożliwia stworzenie skalowalnych i łatwych w utrzymaniu aplikacji. Warto pamiętać, że middleware w ASP.NET Core daje duże możliwości rozbudowy funkcji aplikacji przy zachowaniu prostoty i wydajności.