Az API implementálását a végpontok és visszatérési kódok meghatározása után az adatok definiálásával szeretem folytatni, hogy melyik végponton milyen adatok lesznek elérhetőek. Ezekhez adatátviteli objektumokat definiálunk. Ezek az objektumok lesznek majd JSON-be alakítva az átküldéshez, hogy a rendszerünk többi részével együttműködjenek. Közvetlenül azért nem a domain modell osztályainkat rakjuk ki a végpontra, hogy függetlenek maradjunk az üzleti logikától. Az üzleti logika metódusainak nem kell a HTTP-re reflektálna, mivel az adatátvitel és a működés két külön dolog.
Ha külön adatátviteli objektumokat, úgynevezett DTO-kat definiáltunk, akkor egy nagyobb objektumot visszaadó publikus API kiszolgálása történhet több üzleti logika metódusból és típusból, amit függetlenül módosíthatunk később anélkül, hogy a külvilág számára eltörnénk bármit is. Ezeket az osztályokat egy külön névtérben szokás tárolni.
namespace UrlShortner.Web.Dto;
public class UrlResponseDto
{
public required string ShortUrl { get; init; }
public required string LongUrl { get; init; }
public required DateTime? ValidTill { get; init; }
public required DateTime CreatedAt { get; init; }
public required long ClickCount { get; init; }
}
public class CreateUrlDto
{
public required string LongUrl { get; set; }
public DateTime? ValidTill { get; set; }
}
public class UpdateUrlDto
{
public DateTime? ValidTill { get; set; }
}
Mappelés
Ezen DTO objektumok és az üzleti logika típusai között kelleni fog egy konverziós réteg, egy mapper. Ezt megírhatjuk kézzel is. Jelen API alkalmazásban teljesen helytálló lenne, mivel három osztályról beszélünk, amiből kettőt (CreateUrlDto és UpdateUrlDto) nem is nagyon kell konvertálgatnunk, mivel az üzleti logikánknak csak a bennük tárolt tulajdonságokra van szüksége. Az egyetlenegy típus, ahol foglalkoznunk kell konverzióval, az a UrlResponseDto és UrlModel esete.
A kézi megíráson kívül alkalmazhatunk valami automatikus konverziós megoldást. Ez implementációtól függően történhet reflection vagy kód generálás segítségével. A reflection alapú megoldások közül az egyik legnépszerűbb az AutoMapper. Ez jó pár éve létezik és gyakorlatilag nem nincs olyan szituáció, amit ne támogatna. A forráskód generálás alapú megoldások közül a legnépszerűbb a Mapperly. Ez, mivel nem létezik annyira régóta, mint az AutoMapper, kevésbé elterjedtnek számít.
Azt, hogy melyiket érdemes használni, ismételten csak szituációfüggő. Jelen alkalmazásban az AutoMapper is bőven jó lenne, mivel az NFR-ekben nem szerepelnek bődületesen nagy számok. Viszont a jövőbe mutatás jegyében a mappelésre a Mapperly-t választottam.
A mapper megvalósítása:
using Riok.Mapperly.Abstractions;
using UrlShortner.Core.Models;
using UrlShortner.Web.Dto;
namespace UrlShortner.Web;
[Mapper]
public partial class DtoMappers
{
[MapPropertyFromSource(nameof(UrlResponseDto.LongUrl), Use = nameof(ConvertUri))]
[MapPropertyFromSource(nameof(UrlResponseDto.ShortUrl), Use = nameof(ConvertShortUrl))]
public static partial UrlResponseDto MapToDto(UrlModel model);
private static string ConvertShortUrl(UrlModel urlModel)
=> urlModel.ShortUri;
private static string ConvertUri(UrlModel urlModel)
=> urlModel.LongUri.ToString();
}
A Mapperly kódgenerálás közben Mapper attribútummal megjelölt osztályokat keres, amelyekben a partial metódusokhoz készíti el a generálást. Jelen esetben ez a MapToDto metódus lesz. Az efelett elhelyezkedő MapPropertyFromSource attribútumok egyedi mappelést konfigurálnak. Ha a név és típus egyezik a két osztályban, akkor ezekre semmi szükség, de ahol típuskonverzió vagy néveltérés van, ott kézzel kell megadnunk, hogy mit mire és hogyan konvertáljon. Ezen célt szolgálják a ConvertShortUrl és a ConvertUri metódusok.
Validáció és kiegészítések
A FluentValidation a fluent builder tervezési mintára épül. A központi osztálya a AbstractValidator<T>, aminél a T típus az az osztály, amit validálni szeretnénk. Jelen esetben a CreateUrlDto osztályt fogjuk validálni, hogy megfelelő paraméterekkel lett-e feltöltve. A validációs szabályokat az osztály konstruktorában kell megadnunk és a tényleges validációt majd az örökölt ValidateAsync vagy Validate metódus meghívásával tudjuk elvégezni. A szabályok között rengeteg beépítettet találunk, de a Must metódussal lényegében bármilyen komplex logikát le tudunk implementálni.
using FluentValidation;
using UrlShortner.Web.Dto;
namespace UrlShortner.Web.Validation;
public class CreateUrlValidator : AbstractValidator<CreateUrlDto>
{
public CreateUrlValidator()
{
RuleFor(x => x.LongUrl)
.Must(LongUrl => Uri.TryCreate(LongUrl, UriKind.Absolute, out _))
.WithMessage("Please provide a valid url");
RuleFor(x => x.ValidTill)
.Must(date => date == null || date > DateTime.UtcNow)
.WithMessage("Valid till date must be in the future or null.");
}
}
public class UpdateUrlValidator : AbstractValidator<UpdateUrlDto>
{
public UpdateUrlValidator()
{
RuleFor(x => x.ValidTill)
.Must(date => date == null || date > DateTime.UtcNow)
.WithMessage("Valid till date must be in the future or null.");
}
}
A FluentValidation könyvtár dokumentációját a https://docs.fluentvalidation.net/en/latest/ címen találjuk meg.
A végpontok implementálásához készítettem egy pár extension metódust, ami az életünket megkönnyíti. A ToListAsync és a SelectAsync LINQ metódusok, amik IAsyncEnumerable támogatással rendelkeznek. Ezek egyenlőre (.NET 9 alatt) nem részei a .NET keretrendszernek, így ezeket nekünk kell megvalósítanunk. A GetProblems metódus pedig a FluentValidation könyvtár ValidationResult eredményéből csoportosítja tulajdonságonként a problémákat, hogy majd hiba esetén könnyen vissza tudjuk őket adni. A TryGetUserName metódus majd azon API végpontok esetén lesz hasznos, ahol a felhasználónévre szükségünk van.
using System.Diagnostics.CodeAnalysis;
using FluentValidation.Results;
namespace UrlShortner.Web;
internal static class Extensions
{
public static async Task<List<T>> ToListAsync<T>(this IAsyncEnumerable<T> asyncEnumerable)
{
List<T> list = new();
await foreach (T item in asyncEnumerable)
{
list.Add(item);
}
return list;
}
public static async IAsyncEnumerable<TOut> SelectAsync<TOut, TIn>(this IAsyncEnumerable<TIn> source, Func<TIn, TOut> selector)
{
await foreach (TIn item in source)
{
yield return selector(item);
}
}
public static IEnumerable<KeyValuePair<string, string[]>> GetProblems(this ValidationResult validationResult)
{
return validationResult.Errors
.GroupBy(x => x.PropertyName)
.ToDictionary(x => x.Key, x => x.Select(x => x.ErrorMessage).ToArray());
}
public static bool TryGetUserName(this HttpContext context, [NotNullWhen(true)] out string? username)
{
username = context.User?.Identity?.Name;
return !string.IsNullOrEmpty(username);
}
}
Végpontok
A végpontok implementálása történhetne akár a főprogramban lambda metódusokkal is, de hogy valami struktúra legyen az alkalmazásunkban, érdemes őket szétbontani. Egy bontási stratégia lehet például, hogy ha URL-ekkel és felhasználókkal dolgozik az API, akkor egy URL API műveletekhez tartozó osztályban helyezzük el az URL végpontok implementálását, míg a felhasználókhoz tartozó dolgokat egy másikban.
Jelen esetben azonban egy teljesen más megoldást választottam. A műveleteim implementációi egyenként egy külön Handler osztályban kaptak helyet, mégpedig azért, hogy az egyes műveletek implementációi önálló egységet alkossanak és több száz sor kód egyben átlátása helyett legyen lehetőség a hozzátartozó magyarázat olvasására.
A legegyszerűbb API végpontunk a /v1/{shortcode} útvonalhoz tatozik.
Ez az implementációjában a RedirectUrl nevet kapta, mivel átirányítás fog történni. A metódus bemeneti paraméterei között a HttpContext típusú context paraméter a kérés és a válasz kontextus objektuma, amiből rengeteg információt ki tudunk nyerni, például a beléptetett felhasználó adatait, a kliens IP címét és még sorolhatnám.
Ugyanennek a context objektumnak a megfelelő metódusaival közvetlenül válaszolhatunk a kérésekre, de ezt megtehetjük kulturáltabb módon egy IResult visszaadásával. Az IResult egy HTTP API választ reprezentál. A gyakran használt válaszkódok a Results nevű factory osztályból érhetőek el.
A metódus egyes paramétereinél szerepel a FromServices attribútum. Ez az ASP.NET-et arra utasítja, hogy az adott paramétert a DI konténerből helyettesítse be. A FromRoute attribútum pedig az URL útvonal egy darabját helyettesíti be.
A logoláshoz egy ILoggerFactory kerül beinjektálásra, amiből egy konkrét logger kikérhető. Erre azért van így szükség, mert a logger példányosításakor megadható egy kategória név, egyfajta szkóp, ami alapján majd később könnyen kereshetünk a hibaüzenetekben.
A metódusban két hibás eset van, amit le kell kezelnünk. Ha az URL üres vagy csupa szóköz, akkor az egy 400-as Bad request, ha pedig az URL nem található az adatbázisban, akkor egy 404-es hibakódot kell visszaadnunk. Ezeket a Results.BadRequest és a Results.NotFound metódusok megfelelő eseti meghívásával tudjuk lekezelni. Ha pedig minden jól ment, akkor a Results.Redirect hívással tudunk egy 307-es választ produkálni, amennyiben paraméterben megadjuk, hogy az nem egy permanens átirányítás.
using Microsoft.AspNetCore.Mvc;
using UrlShortner.Core;
namespace UrlShortner.Web.Endpoints;
public static class RedirectUrlHandler
{
public static async Task<IResult> RedirectUrl(HttpContext context,
[FromServices] IUrlShortner urlShortner,
[FromServices] ILoggerFactory loggerFactory,
[FromRoute] string shortcode)
{
var logger = loggerFactory.CreateLogger("UrlShortner.Web.Endpoints.RedirectUrl");
if (string.IsNullOrWhiteSpace(shortcode))
{
logger.LogWarning("RedirectUrl called with empty shortcode by {connectionid}", context.Connection.Id);
return Results.BadRequest("Shortcode cannot be empty.");
}
Uri? redirectUri = await urlShortner.GetRedirectUriAsync(shortcode);
if (redirectUri is null)
{
logger.LogWarning("RedirectUri not found for shortcode {shortcode} by {connectionid}", shortcode, context.Connection.Id);
return Results.NotFound();
}
return Results.Redirect(redirectUri.ToString(), permanent: false);
}
}
A következő legegyszerűbb kiszolgáló metódus a /v1/urls lekérdezéséhez tartozó. Ennek a logikája egyszerűbbnek tűnik, de itt már szükségünk van a felhasználónévre. Ezt a TryGetUserName extension metódussal teszem meg, hogy ne kelljen minden egyes alkalommal a context.User?.Identity?.Name tagokat kiírnom és megfelelően null ellenőriznem. A SelectAsync és ToListAsync extension metódusokal pedig az üzleti logika modelljeit át tudom konvertálni egy DTO listára, amit eredményként visszaad a metódus, becsomagolva egy HTTP 200-as válaszkódba.
using Microsoft.AspNetCore.Mvc;
using UrlShortner.Core;
using UrlShortner.Web.Dto;
namespace UrlShortner.Web.Endpoints;
public static class GetUrlHandler
{
public static async Task<IResult> GetUrls(HttpContext context,
[FromServices] IUrlShortner urlShortner,
[FromServices] ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger("UrlShortner.Web.Endpoints.GetUrls");
if (!context.TryGetUserName(out string? username))
{
logger.LogError("Unauthorized access attempt from: {ip}", context.Connection.LocalIpAddress);
return Results.Unauthorized();
}
List<UrlResponseDto> urls = await urlShortner
.GetUrlModelsAsync(username)
.SelectAsync(DtoMappers.MapToDto)
.ToListAsync();
return Results.Ok(urls);
}
}
A CreateUrl metódus a /v1/urls POST hívással történő kiszolgálásáért felelős. Ez egy CreateUrlDto formában várja a szükséges adatokat. Itt a FromBody attribútum jelzi a keretrendszernek, hogy ezt az objektumot a POST kérés törzséből szedje ki. A felhasználónév ellenőrzése és az objektum validálása után az üzleti logika eredménye alapján vagy 200-as OK választ adunk vissza a létrejött URL-el, vagy egy 400-as Bad Request hibát.
Ha a validáció nem sikerült, akkor a Results.ValidationProblem segítségével adunk vissza egy 400-as Bad Request hibát. A különbség a ValidationProblem és a BadRequest hívásban a néven kívül a visszaadott adat. A ValidationProblem egy speciális JSON dokumentumot állít elő, amit Problem detailsnek hívnak. Ez egy RFC (https://datatracker.ietf.org/doc/html/rfc7807) dokumentált adatformátum, amit specifikusan arra találtak ki, hogy a HTTP API-k egységes, könnyen feldolgozható formátumban tudjanak jelezni problémákat.
using Microsoft.AspNetCore.Mvc;
using UrlShortner.Core;
using UrlShortner.Core.Models;
using UrlShortner.Web.Dto;
using UrlShortner.Web.Validation;
namespace UrlShortner.Web.Endpoints;
public static class CreateUrlHandler
{
public static async Task<IResult> CreateUrl(HttpContext context,
[FromServices] IUrlShortner urlShortner,
[FromServices] ILoggerFactory loggerFactory,
[FromBody] CreateUrlDto dto)
{
var logger = loggerFactory.CreateLogger("UrlShortner.Web.Endpoints.CreateUrl");
if (!context.TryGetUserName(out string? username))
{
logger.LogError("Unauthorized access attempt from: {ip}", context.Connection.LocalIpAddress);
return Results.Unauthorized();
}
CreateUrlValidator validator = new();
var validationResult = await validator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(validationResult.GetProblems());
}
UrlModel? createdUrl = await urlShortner.CreateAsync(dto.LongUrl, username, dto.ValidTill);
return createdUrl is not null
? Results.Ok(createdUrl)
: Results.BadRequest("Failed to create URL.");
}
}
A /v1/{shortcode} DELETE hívásához a DeleteUrl metódus tartozik és igazából semmi érdekes nincs benne, amiről eddig nem volt szó.
using Microsoft.AspNetCore.Mvc;
using UrlShortner.Core;
namespace UrlShortner.Web.Endpoints;
public static class DeleteUrlHandler
{
public static async Task<IResult> DeleteUrl(HttpContext context,
[FromServices] IUrlShortner urlShortner,
[FromServices] ILoggerFactory loggerFactory,
[FromRoute] string shortcode)
{
var logger = loggerFactory.CreateLogger("UrlShortner.Web.Endpoints.DeleteUrl");
if (!context.TryGetUserName(out string? username))
{
logger.LogError("Unauthorized access attempt from: {ip}", context.Connection.LocalIpAddress);
return Results.Unauthorized();
}
if (string.IsNullOrWhiteSpace(shortcode))
{
logger.LogWarning("DeleteUrl called with empty shortcode by {connectionid}", context.Connection.Id);
return Results.BadRequest("Shortcode cannot be empty.");
}
await urlShortner.DeleteByShortUriAsync(shortcode, username);
return Results.NoContent();
}
}
A frissítésért felelős UpdateUrl metódusról ugyanez mondható el:
using Microsoft.AspNetCore.Mvc;
using UrlShortner.Core;
using UrlShortner.Web.Dto;
using UrlShortner.Web.Validation;
namespace UrlShortner.Web.Endpoints;
public static class UpdateUrlHandler
{
public static async Task<IResult> UpdateUrl(HttpContext context,
[FromServices] IUrlShortner urlShortner,
[FromServices] ILoggerFactory loggerFactory,
[FromRoute] string shortcode,
[FromBody] UpdateUrlDto dto)
{
var logger = loggerFactory.CreateLogger("UrlShortner.Web.Endpoints.UpdateUrl");
if (!context.TryGetUserName(out string? username))
{
logger.LogError("Unauthorized access attempt from: {ip}", context.Connection.LocalIpAddress);
return Results.Unauthorized();
}
if (string.IsNullOrWhiteSpace(shortcode))
{
logger.LogWarning("UpdateUrl called with empty shortcode by {connectionid}", context.Connection.Id);
return Results.BadRequest("Shortcode cannot be empty.");
}
UpdateUrlValidator validator = new();
var validationResult = await validator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
logger.LogError("Validation failed for UpdateUrl with shortcode {shortcode}: {errors}", shortcode, validationResult.Errors);
return Results.ValidationProblem(validationResult.GetProblems());
}
await urlShortner.UpdateAsync(shortcode, username, dto.ValidTill);
return Results.NoContent();
}
}
Lényegében ennyi az endpoint-ok implementációja. Ezt követően nincs más dolgunk, mint összerakni ezeket a Program.cs állományban.
A main metódusunk
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using UrlShortner.Core;
using UrlShortner.Core.Services;
using UrlShortner.Database;
using UrlShortner.Web.Endpoints;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddOpenApi();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthentication().AddBearerToken();
builder.Services.AddAuthorization();
builder.Services.AddLogging();
builder.Services.AddScoped(x => TimeProvider.System);
builder.Services.AddScoped<IUrlRepository, UrlRespository>();
builder.Services.AddScoped<IUrlShortner, UrlShortner.Core.UrlShortner>();
builder.Services.AddScoped<IShortNameGenerator, ShortNameGenerator>();
builder.Services.AddDbContext<UrlShortnerDbContext>(options => options.UseSqlite(connectionString, b => b.MigrationsAssembly("UrlShortner.Database")));
builder.Services.AddIdentityApiEndpoints<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedEmail = false;
options.SignIn.RequireConfirmedAccount = false;
options.SignIn.RequireConfirmedPhoneNumber = false;
options.Lockout.MaxFailedAccessAttempts = 5;
}).AddEntityFrameworkStores<UrlShortnerDbContext>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwaggerUI();
}
//https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-9.0
app.MapIdentityApi<IdentityUser>();
app.UseHttpsRedirection();
app.UseSwagger();
app.UseAuthentication();
app.UseAuthorization();
app.MapPost("/v1/urls", CreateUrlHandler.CreateUrl).RequireAuthorization();
app.MapGet("/v1/urls", GetUrlHandler.GetUrls).RequireAuthorization();
app.MapPut("/v1/{shortcode}", UpdateUrlHandler.UpdateUrl).RequireAuthorization();
app.MapDelete("/v1/{shortcode}", DeleteUrlHandler.DeleteUrl).RequireAuthorization();
app.MapGet("/v1/{shortcode}", RedirectUrlHandler.RedirectUrl);
app.Run();
Az ASP.NET alkalmazásunk egy HTTP/HTTPS szerverrel együtt egy WebApplication osztályban fog futni. Ehhez tartozik egy Builder, amivel a különböző aspektusait tudjuk konfigurálni.
Első körben a szolgáltatásokat konfiguráljuk. Ezek a builder.Services objektuma alatt érhetőek el és az Extensions.DependencyInjection Add metódusain kívül lehetőségünk van beszédes nevű extension metódusok segítségével szolgáltatásokat könnyen konfigurálni. Az AddOpenApi és a AddSwaggerGen az API dokumentálásáért felelős szolgáltatásokat húzza magával.
Az AddAuthentication és a AddAuthorization beszédes nevükkel az autentikációt és authorizációt húzzák magukkal. Itt felhívnám a figyelmet az AddBearerToken hívásra, ami bearer típusú azonosítást konfigurál. Ha JWT-t akarnánk használni, akkor ezt a hívást kellene lecserélni. Az AddLogging hívás után pedig a saját szolgáltatásaink következnek.
Mivel a szolgáltatásaink mindegyike az adatbázisra épül, fontos, hogy az AddScoped hívással konfiguráljuk őket. Ha nem így történne, akkor az első feloldás esetén kapnánk egy kivételt, mivel a függőségek megfelelő regisztrációját validálja a DI konténer.
A DbContext regisztrálására az AddDbContext metódus alkalmazható. Itt szükségünk lesz egy ConnectionString-re, ami a kapcsolat beállításait specifikálja. Ezt az app.config-ból a GetConnectionString metódussal tudjuk felolvasni. Mivel a DbContext implementációnk nem a fő futtatható állományunkban van, ezért itt konfigurálnunk kell a migrációkhoz használt szerelvény nevét is.
Ezt követően a bejelentkezéshez és a felhasználó menedzsmenthez szükséges szolgáltatások az AddIdentityApiEndpoints hívással beállíthatóak. Itt meg kell adnunk a felhasználó típusunkat, aminek egy IdentityUser leszármazottnak kell lennie. Mivel mi extra tulajdonságokat, adatokat nem rendeltünk hozzá, ezért szimplán az IdentityUser típust adtam meg. Ugyanitt számos opciót konfigurálhatunk az azonosítást illetően. Ezek közül a legfontosabbak a megerősítésre vonatkozóak. Alapértelmezetten a felhasználónak meg kellene erősítenie az adatait egy visszaigazoló mail-ben kiküldött linkre kattintással. Éles környezetben ez mindenképp hasznos, de mivel belső hálózaton lesz csak elérhető a termék, ezért ettől a megrendelő kérésére eltekintettünk.
Itt megjegyzem, hogy az ajánlott biztonsági beállítások áthágásáról, ha ügyfél kérésre történnek, akkor mindenképpen legyen írásos bizonyíték, a későbbi esetleges nézeteltérések és felelősségek tisztázása érdekében.
A konfigurálás után meg kell adni egy DBContext típust, ami majd a felhasználói adatok elérését tárolja.
A builder.Build() hívás után az alkalmazásunk konfigurált szolgáltatások tekintetében, de nem jelenti még azt, hogy futásra kész, mivel ezt követően történhet meg a HTTP kérés feldolgozások konfigurálása.
A MapIdentityApi hívás adja hozzá az alkalmazásunkhoz a bejelentkezéshez és regisztrációhoz szükséges végpontokat. A UseAuthentication és a UseAuthorization pedig használatba veszi a korábban konfigurált autentikációs és authorizációs szolgáltatásokat. A UseSwagger a dokumentáció publikálásért felelős, a UseHttpsRedirection pedig a sima HTTP kérésre érkező hívásokat HTTPS-re irányítja át.
A saját útvonalaink a különböző Map hívásokkal konfigurálhatóak. Ezek közül némelyiknél szerepel még a RequireAuthorization is. Ez kikényszeríti, hogy a felhasználó be legyen jelentkeztetve.
De hogyan is kényszeríti ezt ki? ASP.NET esetén a HTTP kérések feldolgozása egy pipeline-on keresztül történik. Ebben a pipeline-ban middleware-nek nevezett osztályok sokasága szerepel, amelyek ha nem tudják feldolgozni a HTTP kérést, akkor továbbadják azt a sorban utánuk következőnek. Az autentikáció és az autorizáció is ilyen middleware, ami ha azt érzékeli, hogy nincs érvényes felhasználó egy konfigurált URL-hez, akkor saját hatáskörben lerendezi a hívást és a vezérlés a tényleges feldolgozó metódushoz elvileg nem is kerülhet át. Ebből adódóan feleslegesnek tűnhet a HTTP végpontokhoz társított metódusokban ellenőrizni, hogy van-e felhasználó, mivel kellene lennie. Azonban ha biztonságról van szó, sosem lehetünk elég alaposak, illetve egyfajta szándékot is kifejez a kódunk, amit később olvasva tisztán látszik, hogy adott metódust csak bejelentkezett felhasználó tudja meghívni.
Swagger
A swaggerről nem esett még szó. Ez dokumentáció mellett a /swagger címen egy felhasználói felületet kínál, ahol könnyen áttekinthetjük az API végpontokat és az egyes végpontokhoz tartozó URL sémákat. Ez azonban csak akkor áll rendelkezésre, ha az alkalmazásunkban szerepel a UseSwaggerUI metódushívás.
Ezen felül tesztelési lehetőségeket is biztosít a felület. Éppen ezért a UseSwaggerUI biztonsági okokból csak fejlesztői környezetben legyen kipublikálva. Ezt a kódban az app.Environment.IsDevelopment() hívással tudjuk ellenőrizni.