Convalida e autorizzazione nell'architettura a strati


13

So che stai pensando (o forse urlando), "non un'altra domanda che ti chiede dove appartiene la validazione in un'architettura a strati?!?" Bene, sì, ma spero che questo sia un po 'diverso dall'argomento.

Sono fermamente convinto che la convalida abbia molte forme, sia basata sul contesto e vari a ogni livello dell'architettura. Questa è la base per il post - che aiuta a identificare quale tipo di validazione dovrebbe essere eseguita in ogni livello. Inoltre, una domanda che spesso si pone è dove appartengono i controlli di autorizzazione.

Lo scenario di esempio proviene da un'applicazione per un'azienda di catering. Periodicamente durante il giorno, un conducente può consegnare all'ufficio qualsiasi eccesso di denaro accumulato durante il trasporto del camion da un sito all'altro. L'applicazione consente a un utente di registrare il "calo di cassa" raccogliendo l'ID del conducente e l'importo. Ecco un po 'di codice scheletro per illustrare i livelli coinvolti:

public class CashDropApi  // This is in the Service Facade Layer
{
    [WebInvoke(Method = "POST")]
    public void AddCashDrop(NewCashDropContract contract)
    {
        // 1
        Service.AddCashDrop(contract.Amount, contract.DriverId);
    }
}

public class CashDropService  // This is the Application Service in the Domain Layer
{
    public void AddCashDrop(Decimal amount, Int32 driverId)
    {
        // 2
        CommandBus.Send(new AddCashDropCommand(amount, driverId));
    }
}

internal class AddCashDropCommand  // This is a command object in Domain Layer
{
    public AddCashDropCommand(Decimal amount, Int32 driverId)
    {
        // 3
        Amount = amount;
        DriverId = driverId;
    }

    public Decimal Amount { get; private set; }
    public Int32 DriverId { get; private set; }
}

internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
    internal ICashDropFactory Factory { get; set; }       // Set by IoC container
    internal ICashDropRepository CashDrops { get; set; }  // Set by IoC container
    internal IEmployeeRepository Employees { get; set; }  // Set by IoC container

    public void Handle(AddCashDropCommand command)
    {
        // 4
        var driver = Employees.GetById(command.DriverId);
        // 5
        var authorizedBy = CurrentUser as Employee;
        // 6
        var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
        // 7
        CashDrops.Add(cashDrop);
    }
}

public class CashDropFactory
{
    public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
    {
        // 8
        return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
    }
}

public class CashDrop  // The domain object (entity)
{
    public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
    {
        // 9
        ...
    }
}

public class CashDropRepository // The implementation is in the Data Access Layer
{
    public void Add(CashDrop item)
    {
        // 10
        ...
    }
}

Ho indicato 10 posizioni in cui ho visto i controlli di convalida inseriti nel codice. La mia domanda è quali controlli dovresti eseguire, se presenti, in base alle seguenti regole aziendali (insieme ai controlli standard per lunghezza, intervallo, formato, tipo, ecc.):

  1. L'importo del calo di cassa deve essere maggiore di zero.
  2. Il drop in contanti deve avere un driver valido.
  3. L'utente corrente deve essere autorizzato ad aggiungere gocce di denaro (l'utente corrente non è il conducente).

Per favore, condividi i tuoi pensieri, come hai o ti avvicinerai a questo scenario e le ragioni delle tue scelte.


SE non è esattamente la piattaforma giusta per "favorire una discussione teorica e soggettiva". Votare per chiudere.
tdammers,

Dichiarazione scarsamente formulata. Sto davvero cercando le migliori pratiche.
SonOfPirate

2
@tdammers - Sì, è il posto giusto. Almeno vuole esserlo. Dalle FAQ: "Sono ammesse domande soggettive". Ecco perché hanno creato questo sito anziché Stack Overflow. Non essere un nazista vicino. Se la domanda fa schifo, svanirà nell'oscurità.
FastAl,

@ FastAI: Non è tanto la parte "soggettiva", quanto piuttosto la "discussione" che mi dà fastidio.
martedì

Penso che potresti sfruttare gli oggetti valore qui avendo un CashDropAmountoggetto valore piuttosto che usare a Decimal. Controllare se il driver esiste o no verrebbe eseguito nel gestore dei comandi e lo stesso vale per le regole di autorizzazione. È possibile ottenere l'autorizzazione gratuitamente facendo qualcosa di simile al punto in Approver approver = approverService.findById(employeeId)cui viene lanciato se il dipendente non ricopre il ruolo di responsabile dell'approvazione. Approversarebbe solo un oggetto valore, non un'entità. Si potrebbe anche sbarazzarsi del vostro metodo di fabbrica in fabbrica o l'uso su un AR invece: cashDrop = driver.dropCash(...).
Plalx,

Risposte:


2

Sono d'accordo che ciò che stai convalidando sarà diverso in ogni livello dell'applicazione. In genere convalido solo ciò che è necessario per eseguire il codice nel metodo corrente. Provo a trattare i componenti sottostanti come scatole nere e non convalidare in base alla modalità di implementazione di tali componenti.

Quindi, ad esempio, nella tua classe CashDropApi, verificherei solo che "contratto" non è nullo. Ciò impedisce NullReferenceExceptions ed è tutto ciò che è necessario per garantire che questo metodo venga eseguito correttamente.

Non so che convalideremmo nulla nelle classi di servizio o comando e il gestore verificherebbe che 'command' non è nullo per gli stessi motivi della classe CashDropApi. Ho visto (e fatto) la convalida in entrambi i modi rispetto alle classi factory e entità. L'uno o l'altro è il punto in cui si desidera convalidare il valore di "importo" e che gli altri parametri non sono nulli (le regole aziendali).

Il repository deve solo confermare che i dati contenuti nell'oggetto sono coerenti con lo schema definito nel database e l'operazione daa avrà esito positivo. Ad esempio, se hai una colonna che non può essere nulla o ha una lunghezza massima, ecc.

Per quanto riguarda il controllo di sicurezza, penso che sia davvero una questione di intenti. Poiché la regola ha lo scopo di impedire l'accesso non autorizzato, desidero effettuare questo controllo il più presto possibile per ridurre il numero di passaggi non necessari che ho intrapreso se l'utente non è autorizzato. Probabilmente lo metterei in CashDropApi.


1

La tua prima regola commerciale

L'importo del calo di cassa deve essere maggiore di zero.

sembra un invariante della tua CashDropentità e della tua AddCashDropCommandclasse. Ci sono un paio di modi in cui impongo un invariante come questo:

  1. Prendi il percorso Design By Contract e usa i Contratti di codice con una combinazione di precondizioni, postcondizioni e un [ContractInvariantMethod] a seconda del tuo caso.
  2. Scrivi codice esplicito nel costruttore / setter che genera ArgumentException se passi un importo inferiore a 0.

La tua seconda regola è di natura più ampia (alla luce dei dettagli nella domanda): significa che l'entità conducente ha una bandiera che indica che può guidare (cioè che la patente di guida non è stata sospesa), significa che il conducente era effettivamente lavorando quel giorno o significa semplicemente che driverId, passato a CashDropApi, è valido nell'archivio di persistenza.

In ognuno di questi casi dovrai navigare nel tuo modello di dominio e ottenere l' Driveristanza dal tuo IEmployeeRepository, come fai location 4nel tuo esempio di codice. Quindi, qui è necessario assicurarsi che la chiamata al repository non restituisca null, nel qual caso il driverId non era valido e non è possibile procedere con l'elaborazione.

Per gli altri 2 (i miei ipotetici) controlli (il conducente ha una patente di guida valida, il conducente stava lavorando oggi) stai eseguendo le regole commerciali.

Quello che tendo a fare qui è usare una raccolta di classi di validazione che operano su entità (proprio come il modello di specifica del libro di Eric Evans - Domain Driven Design). Ho usato FluentValidation per creare queste regole e validatori. Posso quindi comporre (e quindi riutilizzare) regole più complesse / più complete da regole più semplici. E posso decidere quali layer nella mia architettura eseguirli. Ma li ho tutti codificati in un unico posto, non sparsi in tutto il sistema.

La tua terza regola riguarda una preoccupazione trasversale: l'autorizzazione. Dal momento che stai già utilizzando un contenitore IoC (supponendo che il tuo contenitore IoC supporti l'intercettazione del metodo) puoi eseguire alcuni AOP . Scrivi un aspetto che fa l'autorizzazione e puoi usare il tuo contenitore IoC per iniettare questo comportamento di autorizzazione dove deve essere. La grande vittoria qui è che hai scritto la logica una volta, ma puoi riutilizzarla nel tuo sistema.

Per utilizzare l'intercettazione tramite un proxy dinamico (Castle Windsor, Spring.NET, Ninject 3.0, ecc.), La classe target deve implementare un'interfaccia o ereditare da una classe base. Si intercetterebbe prima della chiamata al metodo target, si controlla l'autorizzazione dell'utente e si impedisce alla chiamata di procedere al metodo effettivo (lancia un'escissione, registra, restituisce un valore che indica un errore o qualcos'altro) se l'utente non ha i ruoli giusti per eseguire l'operazione.

Nel tuo caso potresti intercettare la chiamata a entrambi

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Forse qui i problemi che CashDropServicenon possono essere intercettati perché non esiste un'interfaccia / classe base. O AddCashDropCommandHandlernon viene creato dal tuo IoC, quindi il tuo IoC non può creare un proxy dinamico per intercettare la chiamata. Spring.NET ha una funzione utile in cui puoi scegliere come target un metodo su una classe in un assembly tramite regex, quindi potrebbe funzionare.

Spero che questo ti dia alcune idee.


Puoi spiegarmi come "utilizzerei il tuo contenitore IoC per iniettare questo comportamento di autorizzazione dove deve essere"? Sembra interessante, ma ottenere AOP e IoC insieme mi sfugge finora.
SonOfPirate,

Per quanto riguarda il resto, sono d'accordo con l'immissione della convalida nel costruttore e / o nei setter per impedire all'oggetto di entrare in uno stato non valido (gestione degli invarianti). Ma oltre a questo e un riferimento al controllo null dopo essere passati a IEmployeeRepository per individuare il driver, non si forniscono dettagli su dove eseguire il resto della convalida. Dato l'uso di FluentValidation e il riutilizzo, ecc., Dove applicheresti le regole nel modello dato?
SonOfPirate,

Ho modificato la mia risposta - vedi se questo aiuta. Per quanto riguarda "dove applicheresti le regole nel modello dato?"; probabilmente circa 4, 5, 6, 7 nel gestore dei comandi. Hai accesso ai repository che possono fornire le informazioni necessarie per eseguire la convalida a livello aziendale. Ma penso che ci siano altri che non sarebbero d'accordo con me qui.
RobertMS,

Per chiarire, tutte le dipendenze vengono iniettate. L'ho lasciato fuori per mantenere breve il codice di riferimento. La mia indagine ha più a che fare con la dipendenza all'interno dell'aspetto poiché gli aspetti non vengono iniettati tramite il contenitore. Quindi, in che modo AuthorizationAspect ottiene un riferimento a AuthorizationService, ad esempio?
SonOfPirate,

1

Per le regole:

1- L'importo del calo di cassa deve essere maggiore di zero.

2- Il drop in contanti deve avere un driver valido.

3- L'utente corrente deve essere autorizzato ad aggiungere gocce di denaro (l'utente corrente non è il conducente).

Farei la convalida in posizione (1) per la regola aziendale (1) e assicurerei che l'id non sia nullo o negativo (supponendo che lo zero sia valido) come controllo preliminare per la regola (2). Il motivo è la mia regola di "Non attraversare un limite di livello con dati errati che è possibile verificare con le informazioni disponibili". Un'eccezione a ciò sarebbe se il servizio esegue la convalida come parte del proprio dovere verso gli altri chiamanti. In tal caso, sarà sufficiente avere la convalida solo lì.

Per le regole (2) e (3), questo deve essere fatto al livello di accesso al database (o al livello db stesso) solo poiché implica l'accesso al database. Non è necessario viaggiare intenzionalmente tra i livelli.

In particolare, la regola (3) può essere evitata se si consente alla GUI di impedire agli utenti non autorizzati di premere il pulsante per abilitare questo scenario. Mentre questo è più difficile da codificare, è meglio.

Buona domanda!


+1 per l'autorizzazione: inserirla nell'interfaccia utente è un'alternativa che non ho menzionato nella mia risposta.
RobertMS,

Pur avendo controlli di autorizzazione nell'interfaccia utente fornisce un'esperienza più interattiva per l'utente, sto sviluppando un'API basata sui servizi e non posso fare ipotesi su quali regole il chiamante abbia o non abbia implementato. È perché molti di questi controlli possono essere facilmente delegati all'interfaccia utente che ho scelto di utilizzare il progetto API come base per il post. Sto cercando le migliori pratiche piuttosto che il libro di testo semplice e veloce.
SonOfPirate

@SonOfPirate, INMO, l'interfaccia utente deve eseguire le convalide perché è più veloce e contiene più dati del servizio (in alcuni casi). Ora il servizio non dovrebbe inviare dati al di fuori dei propri confini senza fare le proprie convalide poiché fa parte delle sue responsabilità fintanto che si desidera che il servizio non si fidi del cliente. Di conseguenza, suggerisco di effettuare (di nuovo) controlli non-db nel servizio prima di inviare i dati al database per ulteriori elaborazioni.
NoChance,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.