Πρόσθεσε authorization και telemetry γύρω από CQRS handlers στο .NET, με Dependency Injection, OpenTelemetry και tests, χωρίς MediatR.

Ένας handler εκτελεί ένα use case: διαβάζει δεδομένα, υπολογίζει ένα αποτέλεσμα ή καταχωρίζει μια αλλαγή. Συχνά όμως θέλουμε να ελέγξουμε δικαιώματα πριν από την εκτέλεσή του και να μετρήσουμε τη διάρκεια ολόκληρης της κλήσης.

Με το Decorator pattern μπορούμε να προσθέσουμε αυτές τις συμπεριφορές γύρω από τον handler, κρατώντας το ίδιο interface. Κάθε decorator κρατά έναν εσωτερικό handler και αποφασίζει τι θα κάνει πριν, μετά ή γύρω από την κλήση του.

Σε αυτό το άρθρο θα γράψουμε authorization και telemetry decorators για CQRS handlers στο .NET, θα τους συνδέσουμε με Dependency Injection και θα ελέγξουμε ότι η αλυσίδα λειτουργεί. Θα καλέσουμε τους handlers απευθείας, χωρίς MediatR.

Για να ακολουθήσεις, χρειάζεται βασική εξοικείωση με interfaces, generics, async/await και το DI του ASP.NET Core. Τα snippets προορίζονται για ASP.NET Core project με .NET 10 και ενεργοποιημένα implicit usings. Βάλε τους τύπους σε ξεχωριστά αρχεία· οι προσθήκες στο Program.cs επισημαίνονται στα αντίστοιχα σημεία.

Ένα use case με authorization και telemetry

Ας φανταστούμε ένα εσωτερικό εργαλείο μιας πιτσαρίας. Ένας υπάλληλος ζητά την τιμή μιας πίτσας με συγκεκριμένα υλικά.

Η κύρια δουλειά είναι να υπολογίσουμε την τιμή. Θέλουμε επίσης:

  • Να ελέγξουμε αν ο υπάλληλος έχει δικαίωμα να χρησιμοποιήσει τη λειτουργία.
  • Να μετρήσουμε τη διάρκεια και να καταγράψουμε αν η εκτέλεση ολοκληρώθηκε ή απέτυχε.

Μπορούμε να βάλουμε όλον αυτόν τον κώδικα στην ίδια μέθοδο. Αν όμως οι ίδιοι έλεγχοι και οι ίδιες μετρήσεις χρειάζονται σε πολλές λειτουργίες, θα αρχίσουμε να τους επαναλαμβάνουμε.

Με decorators μπορούμε να χωρίσουμε τη δουλειά:

Telemetry decorator └── Authorization decorator └── Handler που υπολογίζει την τιμή 

Κάθε επίπεδο κρατά το επόμενο, προσφέρει το ίδιο συμβόλαιο κλήσης και προσθέτει τη δική του συμπεριφορά. Ο κώδικας που καλεί τον handler δεν χρειάζεται να γνωρίζει πόσα επίπεδα υπάρχουν.

Οι απαραίτητοι όροι

Ένα query είναι ένα αίτημα για ανάγνωση ή υπολογισμό δεδομένων, χωρίς να αλλάζουμε την αποθηκευμένη κατάσταση της εφαρμογής. Για παράδειγμα: «υπολόγισε την τιμή αυτής της πίτσας».

Ένα command είναι ένα αίτημα που ζητά αλλαγή. Για παράδειγμα: «καταχώρισε αυτή την παραγγελία».

Ένας handler είναι η κλάση που εκτελεί το αντίστοιχο αίτημα.

Ο διαχωρισμός των λειτουργιών ανάγνωσης από τις λειτουργίες αλλαγής είναι η βασική ιδέα του CQRS. Μπορείς να ξεκινήσεις με διαφορετικά requests και handlers στην ίδια εφαρμογή και στην ίδια βάση δεδομένων.

Το CQRS δεν απαιτεί MediatR. Στο παράδειγμα θα καλέσουμε τους handlers απευθείας, μέσα από δικά μας interfaces.

Το κοινό συμβόλαιο των handlers

public interface IRequest<TResult> { } public interface IQuery<TResult> : IRequest<TResult> { } public interface ICommand<TResult> : IRequest<TResult> { } public interface IRequestHandler<TRequest, TResult> where TRequest : IRequest<TResult> { Task<TResult> HandleAsync( TRequest request, CancellationToken cancellationToken = default); } 

Τα TRequest και TResult είναι θέσεις για τύπους που θα ορίσουμε όταν χρησιμοποιήσουμε το interface.

Για παράδειγμα, το IRequestHandler<GetPizzaPriceQuery, PizzaPriceDto> σημαίνει: «handler που δέχεται ένα GetPizzaPriceQuery και επιστρέφει ένα PizzaPriceDto μέσα σε Task».

Η γραμμή where TRequest : IRequest<TResult> περιορίζει ποιοι τύποι μπορούν να χρησιμοποιηθούν ως requests.

Commands και queries έχουν διαφορετικά interfaces για να δηλώνουν την πρόθεσή τους, αλλά οι handlers τους μοιράζονται ένα κοινό συμβόλαιο. Έτσι, ο ίδιος generic decorator μπορεί να χρησιμοποιηθεί και για τα δύο.

Το IRequestHandler<TRequest, TResult> είναι το κοινό interface που θα υλοποιούν τόσο ο πραγματικός handler όσο και οι decorators του.

Δηλώνουμε ποιο δικαίωμα απαιτείται

Θα χρησιμοποιήσουμε ένα μικρό attribute, δηλαδή μια δήλωση που προσθέτει πληροφορία σε μια κλάση:

[AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class RequiresPermissionAttribute : Attribute { public string Permission { get; } public RequiresPermissionAttribute(string permission) { if (string.IsNullOrWhiteSpace(permission)) { throw new ArgumentException( "A permission is required.", nameof(permission)); } Permission = permission; } } 

Το attribute δηλώνει την απαίτηση. Δεν εκτελεί μόνο του κανέναν έλεγχο. Ο authorization decorator θα διαβάσει αυτή την πληροφορία και θα την εφαρμόσει.

Το query και ο handler υπολογισμού της τιμής

Το query περιγράφει τις επιλογές του υπαλλήλου. Το DTO είναι ένα απλό αντικείμενο με τα δεδομένα που θέλουμε να επιστρέψουμε.

[RequiresPermission("pizza.prices.read")] public sealed record GetPizzaPriceQuery( bool ExtraCheese, bool Mushrooms, bool Olives) : IQuery<PizzaPriceDto>; public sealed record PizzaPriceDto( string Description, decimal Total); 

Ο handler παρακάτω υπολογίζει την τιμή στη μνήμη. Η υλοποίηση παραμένει απλή, ώστε να επικεντρωθούμε στους decorators γύρω από την εκτέλεσή του.

public sealed class GetPizzaPriceHandler : IRequestHandler<GetPizzaPriceQuery, PizzaPriceDto> { public Task<PizzaPriceDto> HandleAsync( GetPizzaPriceQuery request, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); string description = "Πίτσα με σάλτσα ντομάτας"; decimal total = 6.00m; if (request.ExtraCheese) { description += ", έξτρα τυρί"; total += 1.50m; } if (request.Mushrooms) { description += ", μανιτάρια"; total += 1.00m; } if (request.Olives) { description += ", ελιές"; total += 0.50m; } return Task.FromResult(new PizzaPriceDto(description, total)); } } 

Ο υπολογισμός γίνεται στη μνήμη, γι’ αυτό χρησιμοποιούμε Task.FromResult. Διατηρούμε το ασύγχρονο συμβόλαιο ώστε ένας άλλος handler να μπορεί, για παράδειγμα, να κάνει await σε μια κλήση προς τη βάση δεδομένων.

Ο συγκεκριμένος handler ασχολείται με τις επιλογές και την τιμή. Δεν ελέγχει τον χρήστη και δεν περιέχει κώδικα μετρήσεων. Σε μια μεγαλύτερη εφαρμογή, οι κανόνες τιμολόγησης μπορούν να βρίσκονται σε ξεχωριστό μοντέλο του domain.

Authorization πριν εκτελεστεί ο handler

Οι πληροφορίες που χρειαζόμαστε για τον χρήστη

Το authorization απαντά στην ερώτηση «επιτρέπεται αυτός ο χρήστης να εκτελέσει αυτή τη λειτουργία;».

Ο decorator χρειάζεται να γνωρίζει αν ο χρήστης έχει συνδεθεί και αν έχει το απαραίτητο δικαίωμα:

public interface ICurrentUser { bool IsAuthenticated { get; } bool HasPermission(string permission); } 

Το interface μάς επιτρέπει να πάρουμε αυτές τις πληροφορίες χωρίς να εξαρτάται ο decorator από το HttpContext του ASP.NET Core.

Θα χρησιμοποιήσουμε επίσης δύο δικά μας exceptions, ώστε να ξεχωρίζουμε τις διαφορετικές απορρίψεις:

public sealed class AuthenticationRequiredException : Exception { public AuthenticationRequiredException() : base("Authentication is required.") { } } public sealed class PermissionDeniedException : Exception { public PermissionDeniedException(string permission) : base($"Missing permission: {permission}.") { } } 

Ο authorization decorator

using System.Reflection; public sealed class AuthorizationHandler<TRequest, TResult> : IRequestHandler<TRequest, TResult> where TRequest : IRequest<TResult> { private static readonly RequiresPermissionAttribute? Requirement = typeof(TRequest).GetCustomAttribute<RequiresPermissionAttribute>(); private readonly IRequestHandler<TRequest, TResult> _inner; private readonly ICurrentUser _currentUser; public AuthorizationHandler( IRequestHandler<TRequest, TResult> inner, ICurrentUser currentUser) { _inner = inner; _currentUser = currentUser; } public Task<TResult> HandleAsync( TRequest request, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); if (Requirement is null) { throw new InvalidOperationException( $"Request {typeof(TRequest).Name} must declare a permission."); } if (!_currentUser.IsAuthenticated) { throw new AuthenticationRequiredException(); } if (!_currentUser.HasPermission(Requirement.Permission)) { throw new PermissionDeniedException(Requirement.Permission); } return _inner.HandleAsync(request, cancellationToken); } } 

Το _inner είναι ο handler που βρίσκεται μέσα στον decorator. Επειδή υλοποιεί το ίδιο interface, μπορεί να είναι είτε ο πραγματικός handler είτε ένας άλλος decorator.

Ο decorator πρώτα ελέγχει τις προϋποθέσεις. Αν όλα είναι εντάξει, καλεί τον εσωτερικό handler:

return _inner.HandleAsync(request, cancellationToken); 

Αν ο χρήστης δεν έχει δικαίωμα, πετά exception και ο εσωτερικός handler δεν εκτελείται. Ένας decorator μπορεί λοιπόν να σταματήσει σκόπιμα την κλήση, όταν αυτό είναι μέρος της ευθύνης του.

Το GetCustomAttribute διαβάζει το attribute από τον τύπο του request. Το αποτέλεσμα αποθηκεύεται στο static readonly πεδίο, οπότε δεν επαναλαμβάνουμε αυτή την αναζήτηση σε κάθε κλήση για τον ίδιο συνδυασμό generic τύπων.

Στο παράδειγμά μας, κάθε request που περνά από αυτόν τον decorator πρέπει να δηλώνει permission. Αν το ξεχάσουμε, η εκτέλεση σταματά με σφάλμα ρύθμισης. Για δημόσια requests θα χρειαζόταν μια ρητή πολιτική που να επιτρέπει την ανώνυμη πρόσβαση.

Από τον χρήστη του API στο ICurrentUser

Σε ένα ASP.NET Core API μπορούμε να διαβάσουμε τα permissions από τα claims του χρήστη:

using Microsoft.AspNetCore.Http; public sealed class HttpCurrentUser : ICurrentUser { private readonly IHttpContextAccessor _httpContextAccessor; public HttpCurrentUser(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public bool IsAuthenticated => _httpContextAccessor.HttpContext? .User.Identity?.IsAuthenticated == true; public bool HasPermission(string permission) { return IsAuthenticated && _httpContextAccessor.HttpContext! .User.HasClaim("permission", permission); } } 

Εδώ υποθέτουμε ότι το authentication της εφαρμογής έχει ήδη ρυθμιστεί και δημιουργεί ένα claim τύπου permission για κάθε δικαίωμα. Αν η εφαρμογή σου χρησιμοποιεί άλλο μοντέλο δικαιωμάτων, αλλάζει αυτή η υλοποίηση του ICurrentUser.

Το API πρέπει επίσης να μεταφράζει τα exceptions στις κατάλληλες HTTP απαντήσεις:

Exception HTTP απάντηση
AuthenticationRequiredException 401 Unauthorized
PermissionDeniedException 403 Forbidden

Αυτή η αντιστοίχιση γίνεται στο κεντρικό error handling του API. Τα exceptions από μόνα τους δεν αλλάζουν τον HTTP status code. Το ASP.NET Core παρέχει το IExceptionHandler γι’ αυτή τη δουλειά, σε συνδυασμό με το exception handling middleware. Δες την τεκμηρίωση για το error handling.

Οι συγκεκριμένοι έλεγχοι αφορούν ένα γενικό permission. Μια λειτουργία που ανοίγει συγκεκριμένη παραγγελία μπορεί να χρειάζεται επιπλέον έλεγχο ιδιοκτησίας ή πρόσβασης στα δεδομένα της.

Telemetry γύρω από την εκτέλεση

Τι θέλουμε να μετρήσουμε

Με τον όρο telemetry εννοούμε εδώ τις πληροφορίες που συλλέγουμε για την εκτέλεση μιας λειτουργίας:

  • Πόσο χρόνο χρειάστηκε.
  • Αν ολοκληρώθηκε, απέτυχε ή ακυρώθηκε.
  • Πού βρίσκεται μέσα στη συνολική διαδρομή ενός request.

Θα χρησιμοποιήσουμε δύο APIs του .NET:

  • Το ActivitySource, για να δημιουργούμε ένα span, δηλαδή μια καταγραφή της συγκεκριμένης εκτέλεσης μέσα σε ένα trace.
  • Το Meter, για να καταγράφουμε μετρήσεις διάρκειας. Το histogram είναι ο τύπος μέτρησης που χρησιμοποιούμε για τις διάρκειες.

Οι πηγές των μετρήσεων

using System.Diagnostics; using System.Diagnostics.Metrics; public static class CqrsDiagnostics { public const string SourceName = "PizzaApp.Cqrs"; public static readonly ActivitySource Activities = new(SourceName); private static readonly Meter Meter = new(SourceName); public static readonly Histogram<double> Duration = Meter.CreateHistogram<double>( name: "pizzaapp.cqrs.duration", unit: "ms", description: "Duration of CQRS requests."); } 

Τα κρατάμε για όλη τη διάρκεια της εφαρμογής, αντί να δημιουργούμε νέες πηγές σε κάθε request.

Ο telemetry decorator

using System.Diagnostics; public sealed class TelemetryHandler<TRequest, TResult> : IRequestHandler<TRequest, TResult> where TRequest : IRequest<TResult> { private readonly IRequestHandler<TRequest, TResult> _inner; public TelemetryHandler(IRequestHandler<TRequest, TResult> inner) { _inner = inner; } public async Task<TResult> HandleAsync( TRequest request, CancellationToken cancellationToken = default) { string requestName = typeof(TRequest).Name; using var activity = CqrsDiagnostics.Activities.StartActivity(requestName); var stopwatch = Stopwatch.StartNew(); string outcome = "success"; try { return await _inner.HandleAsync(request, cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { outcome = "cancelled"; throw; } catch (Exception) { outcome = "failure"; activity?.SetStatus(ActivityStatusCode.Error); throw; } finally { activity?.SetTag("cqrs.outcome", outcome); CqrsDiagnostics.Duration.Record( stopwatch.Elapsed.TotalMilliseconds, new KeyValuePair<string, object?>( "cqrs.request", requestName), new KeyValuePair<string, object?>( "cqrs.outcome", outcome)); } } } 

Ας διαβάσουμε τη ροή με απλά λόγια:

  1. Ξεκινά η καταγραφή και το χρονόμετρο.
  2. Καλείται ο εσωτερικός handler.
  3. Το await περιμένει να ολοκληρωθεί η εργασία του.
  4. Αν υπάρξει σφάλμα, καταγράφουμε την αποτυχία και ξαναπετάμε το exception.
  5. Το finally καταγράφει τη διάρκεια σε κάθε περίπτωση.

Το throw; επιτρέπει στον κώδικα που έκανε την κλήση να λάβει το αρχικό exception. Το telemetry δεν μετατρέπει μια αποτυχία σε επιτυχημένο αποτέλεσμα.

Το using var κλείνει το activity όταν τελειώσει η μέθοδος. Το activity?. σημαίνει «κάνε αυτή την κλήση μόνο αν υπάρχει activity», επειδή η StartActivity() μπορεί να επιστρέψει null όταν δεν υπάρχει ενδιαφερόμενος listener. Δες την τεκμηρίωση του ActivitySource.

Στα tags του histogram χρησιμοποιούμε το όνομα του request και λίγες σταθερές τιμές αποτελέσματος. IDs παραγγελιών και email χρηστών θα δημιουργούσαν πάρα πολλούς διαφορετικούς συνδυασμούς μετρήσεων.

Το cancelled αφορά ακύρωση που ζητήθηκε μέσω του token της κλήσης. Άλλο OperationCanceledException καταγράφεται εδώ ως αποτυχία. Σε μεγαλύτερη εφαρμογή μπορείς να ορίσεις πιο αναλυτική ταξινόμηση των αποτελεσμάτων.

Πώς βλέπουμε το telemetry με OpenTelemetry

Ο decorator παράγει τα δεδομένα. Χρειαζόμαστε επίσης ένα σύστημα που τα συλλέγει και έναν exporter, δηλαδή ένα εξάρτημα που τα στέλνει στον επιλεγμένο προορισμό.

Για ένα τοπικό παράδειγμα σε ASP.NET Core μπορούμε να χρησιμοποιήσουμε τον console exporter:

dotnet add package OpenTelemetry.Extensions.Hosting dotnet add package OpenTelemetry.Exporter.Console dotnet add package OpenTelemetry.Instrumentation.AspNetCore 

Στο Program.cs, πριν από το builder.Build():

using OpenTelemetry.Metrics; using OpenTelemetry.Trace; builder.Services.AddOpenTelemetry() .WithTracing(tracing => tracing .AddAspNetCoreInstrumentation() .AddSource(CqrsDiagnostics.SourceName) .AddConsoleExporter()) .WithMetrics(metrics => metrics .AddMeter(CqrsDiagnostics.SourceName) .AddConsoleExporter()); 

Το AddSource συνδέει τα activities μας με τη συλλογή traces. Το AddMeter συνδέει το histogram με τη συλλογή metrics.

Ο console exporter βοηθά στην τοπική ανάπτυξη. Για αποστολή σε ένα σύστημα παρακολούθησης μπορείς να χρησιμοποιήσεις έναν OTLP exporter και την αντίστοιχη ρύθμιση προορισμού. Δες την τεκμηρίωση των OpenTelemetry exporters.

Συνδέουμε τους decorators με Dependency Injection

Πρώτα η σύνθεση με new

Το Dependency Injection, ή DI, είναι ο μηχανισμός με τον οποίο η εφαρμογή παρέχει σε μια κλάση τις εξαρτήσεις που χρειάζεται.

Πριν το χρησιμοποιήσουμε, ας δούμε τι θέλουμε να κατασκευάσει. Αν έχουμε ήδη ένα ICurrentUser με το όνομα currentUser:

IRequestHandler<GetPizzaPriceQuery, PizzaPriceDto> handler = new GetPizzaPriceHandler(); handler = new AuthorizationHandler<GetPizzaPriceQuery, PizzaPriceDto>( handler, currentUser); handler = new TelemetryHandler<GetPizzaPriceQuery, PizzaPriceDto>(handler); PizzaPriceDto result = await handler.HandleAsync( new GetPizzaPriceQuery( ExtraCheese: true, Mushrooms: true, Olives: false)); 

Αν ο χρήστης έχει το απαιτούμενο δικαίωμα, η τιμή που επιστρέφεται είναι 8,50 €.

Ξεκινάμε με την κανονική υλοποίηση και προσθέτουμε ένα αντικείμενο γύρω της κάθε φορά. Κάθε ανάθεση κρατά ως εσωτερικό handler την αλυσίδα που έχουμε ήδη κατασκευάσει.

Ένα ρητό registration χωρίς επιπλέον βιβλιοθήκη

Στο ASP.NET Core μπορούμε να δηλώσουμε την ίδια κατασκευή μέσα σε ένα factory:

using Microsoft.Extensions.DependencyInjection; public static class PizzaPriceRegistration { public static IServiceCollection AddPizzaPriceQuery( this IServiceCollection services) { services.AddHttpContextAccessor(); services.AddScoped<ICurrentUser, HttpCurrentUser>(); services.AddScoped<GetPizzaPriceHandler>(); services.AddScoped< IRequestHandler<GetPizzaPriceQuery, PizzaPriceDto>>(provider => { IRequestHandler<GetPizzaPriceQuery, PizzaPriceDto> handler = provider.GetRequiredService<GetPizzaPriceHandler>(); handler = new AuthorizationHandler< GetPizzaPriceQuery, PizzaPriceDto>( handler, provider.GetRequiredService<ICurrentUser>()); handler = new TelemetryHandler< GetPizzaPriceQuery, PizzaPriceDto>(handler); return handler; }); return services; } } 

Και στο Program.cs:

builder.Services.AddPizzaPriceQuery(); 

Το factory είναι απλώς ο κώδικας που λέει στο DI πώς να κατασκευάσει την αλυσίδα. Όταν κάποια κλάση ζητήσει IRequestHandler<GetPizzaPriceQuery, PizzaPriceDto>, θα πάρει τον εξωτερικό TelemetryHandler, ο οποίος κρατά τους υπόλοιπους μέσα του.

Χρησιμοποιούμε Scoped lifetime: τα συγκεκριμένα services συνδέονται με το ενεργό DI scope, που στο ASP.NET Core συνήθως αντιστοιχεί σε ένα HTTP request.

Καταχωρούμε και τον πραγματικό handler ως service. Έτσι το container διαχειρίζεται τον κύκλο ζωής του. Αυτό έχει σημασία αν αργότερα ο handler αποκτήσει πόρους που χρειάζονται Dispose. Οι decorators του παραδείγματος δεν κατέχουν τέτοιους πόρους. Δες τις οδηγίες του .NET για το DI και το disposal.

Για πολλούς handlers μπορεί να αξίζει μια δοκιμασμένη λύση αυτόματου decoration. Το ρητό registration εδώ κρατά ορατό τον τρόπο κατασκευής, χωρίς reflection πάνω στα registrations ή πρόσθετο μηχανισμό υποδομής.

Η σειρά αλλάζει τι παρατηρούμε

Στήσαμε αυτή την αλυσίδα:

Κλήση HandleAsync └── TelemetryHandler └── AuthorizationHandler └── GetPizzaPriceHandler 

Η εκτέλεση ξεκινά από τον εξωτερικό decorator:

  1. Το telemetry ξεκινά τη μέτρηση.
  2. Το authorization ελέγχει τα δικαιώματα.
  3. Ο handler υπολογίζει την τιμή, αν επιτραπεί η εκτέλεση.
  4. Το αποτέλεσμα ή το exception επιστρέφει προς τα έξω.
  5. Το telemetry καταγράφει το αποτέλεσμα και τη διάρκεια.

Επειδή το telemetry είναι εξωτερικό, βλέπει και τις απορρίψεις του authorization. Αν αντιστρέφαμε τη σειρά, μια απόρριψη θα συνέβαινε πριν φτάσουμε στον telemetry decorator.

Η σειρά των decorators μπορεί να αλλάξει ουσιαστικά τη συμπεριφορά της εφαρμογής. Για παράδειγμα, ένας έλεγχος πρόσβασης πρέπει να γίνεται πριν επιστρέψουμε προστατευμένα δεδομένα από cache.

Ο κώδικας που καλεί τον handler

Σε ένα API με ρυθμισμένο authentication, authorization και error handling, το endpoint μπορεί να ζητήσει το κοινό interface:

app.MapGet("/pizza/price", async ( bool extraCheese, bool mushrooms, bool olives, IRequestHandler<GetPizzaPriceQuery, PizzaPriceDto> handler, CancellationToken cancellationToken) => { var query = new GetPizzaPriceQuery(extraCheese, mushrooms, olives); var result = await handler.HandleAsync(query, cancellationToken); return Results.Ok(result); }) .RequireAuthorization(); 

Το endpoint γνωρίζει το request και το αποτέλεσμα. Η κατασκευή των decorators βρίσκεται στο registration.

Το RequireAuthorization() εφαρμόζει την πολιτική του HTTP endpoint. Ο decorator επιβάλλει τον έλεγχο του use case όταν αυτό καλείται μέσω της διακοσμημένης αλυσίδας, ακόμη και από άλλο σημείο της εφαρμογής.

Αν ένα background job χρειάζεται να καλέσει το use case, πρέπει να του παρέχουμε κατάλληλη ταυτότητα και υλοποίηση του ICurrentUser. Η HttpCurrentUser απαιτεί ενεργό HTTP context για να αναγνωρίσει χρήστη.

Έλεγχοι ότι η αλυσίδα λειτουργεί Source:https://dev.to/__b63657/decorator-pattern-se-cqrs-handlers-me-authorization-kai-telemetry-sto-net-7do