Overview
OData
Integrazione OData nel framework per route metadata-driven, con fallback controllato su provider alternativi. Supporto completo read + CRUD + $expand + $count=true wrapper.
Scope
- Uso di endpoint OData come sorgente dati di
wuic-data-source. - Allineamento con metadata tabella/colonna per rendering UI e comportamento runtime.
- Relazione con flusso di scaffolding iniziale e bootstrap progetto.
- Navigation properties auto-generate da colonne
lookupByIDper abilitare$expandnativo.
Esempi
- Pattern 3c — OData Cities grid (read + CRUD + `$expand` + lookup puro-OData) — esempio completo con
$top/$skip/$filter/$orderby/$expand/$count=truecontro/odata/Citiese/odata/Stateprovinces.
Riferimenti Metadata
- Il routing e la configurazione base restano guidati dai metadati (
md_route_name, colonne, filtri, sort, paging). - In
md_props_bagpuoi forzare il provider tramiteendpoint(esempio:{"endpoint":{"type":"odata","uri":"/odata/Orders"}}). - Anche le colonne lookup possono puntare a un endpoint OData custom via
mc_props_bag.lookup.endpoint(vedi Widget Lookup). - Il datasource continua a usare
filterInfo,sortInfo,pageInfo; lato provider OData avviene la traduzione in query OData. - Per i dettagli metadata trasversali vedi Metadata.
Navigation properties auto-generate (lookupByID → $expand)
Il generatore EF dinamico (MetadataModelGenerator) emette automaticamente una navigation property su ogni colonna con mc_ui_column_type = 'lookupByID' e mc_ui_lookup_entity_name che punta a una route esposta in WebAPI. Convenzione di naming: strip del suffisso Id / ID dal nome FK → nome nav property (es. stateProvinceID → StateProvince). Con EnableLowerCamelCase() attivo l'EDM espone la nav in camelCase (stateProvince).
Effetti pratici:
- il
$metadatacontiene<NavigationProperty Name="stateProvince" Type="...Stateprovinces" />sull'entityCities; GET /odata/Cities?$expand=stateProvincerestituisce ogni riga con l'oggetto correlato inline ({... "stateProvince": { "stateProvinceID": 1, "stateProvinceName": "Alabama" }});- il controller
EntitiesController.Gettraduce$expand=navPropin.Include(navProp)EF Core (LEFT JOIN SQL).
Limiti attuali: solo $expand top-level (niente nested $expand=a($expand=b)); sub-options OData dopo la nav name (es. $expand=stateProvince($select=...)) vengono ignorate — .Include include l'intera entita' correlata. Le colonne dell'entita' espansa negate all'utente tornano comunque vuote.
Response shape e $count=true wrapper
- Default (senza
$count): endpoint ritorna un plain JSON array ([{...}, {...}]) per compatibilita' con i consumer legacy del framework (DataProviderOdataService.select,DataProviderWebserviceService, ecc.) che leggonoresponse as any[]. - Opt-in OData v4 (
$count=truenella query): endpoint ritorna il wrapper standard{ "value": [...], "@odata.count": N }doveNe' il totale calcolato dopo$filter/$orderbyma prima di$skip/$top. Usare quando serve il totale paginato (es. pager UI).
CRUD completo via EntitiesController
L'endpoint OData generico espone l'intera suite RESTful su /odata/{EntitySet}:
GET /odata/{EntitySet}— list con$top/$skip/$filter/$orderby/$select/$expand/$count=true.POST /odata/{EntitySet}— insert, body JSON con le colonne (le colonnenullnel payload vengono skippate cosi' SQL applica ilDEFAULTlato DB, supportando PK IDENTITY/SEQUENCE/GUID automaticamente e colonne con default temporali/audit).PATCH /odata/{EntitySet}({key})— update parziale, body JSON con subset colonne; la PK in path.DELETE /odata/{EntitySet}({key})— delete per chiave singola.
Gating lato metadata (riga _metadati__tabelle):
mdexposeinwebapi = 1(obbligatorio per qualsiasi CUD, altrimenti 403).mdserviceenableinsert,mdserviceenableedit,mdserviceenabledelete= 1 per le rispettive operazioni.- Vedi anche tab metadata "Servizio web" nel metadata-editor.
Permessi e vincoli di riga (dalla 1.7.21)
OData applica all'utente della sessione (o del token) gli stessi permessi e vincoli del CRUD della UI: via OData non si legge e non si scrive niente che la UI non mostrerebbe. Il superadmin non ha vincoli per utente, ruolo o azienda né filtro di default; come nel CRUD non vede però le righe cancellate logicamente.
- Route: senza permesso di lettura sulla route (autorizzazioni per utente, ruolo o azienda,
md_grant_by_default) la risposta e' 403errors.auth.route_read_forbidden. - Colonne: una colonna che l'utente non vede, citata in
$select,$filter,$orderbyo$expand, da' 403 (filtrare su una colonna nascosta ne rivelerebbe il valore). Senza$selectla colonna torna vuota. Il server legge comunque il valore dal DB e lo svuota nella risposta, quindi non arriva mai al client; il CRUD invece la esclude gia' dalla SELECT. Con$select(senza$expand) si leggono solo le colonne richieste. - Righe: la base della query e' la SELECT del CRUD con i vincoli di sistema dell'utente: restrizione per utente, ruolo o azienda (
md_record_restriction_key_user_field_list), filtro di default (md_default_filter, sempre applicato) e cancellazione logica.$filter,$orderby,$skip,$tope$countdel client si applicano fuori, in AND, e possono solo restringere: con la restrizione per utente,$filter=name eq 'X' or 1 eq 1restituisce comunque solo le righe dell'utente.
SELECT ... FROM ( SELECT t.* FROM t <join> WHERE <user constraints> ) s
WHERE <client $filter>
ORDER BY ... OFFSET ... FETCH ...- `$expand`: la JOIN la fa EF (
.Include, LEFT JOIN) sulla tabella fisica della route di destinazione, senza la WHERE del framework. Per chi non e' superadmin e' ammesso solo verso una route leggibile senza cancellazione logica e senza vincoli di riga, altrimenti 403; le colonne negate dell'entita' espansa tornano vuote. Le route reticolari rispondono 403. - Scritture:
POST,PATCHeDELETEpassano dal CRUD del framework (insertRecord,updateRecord,deleteRecord). Valgono quindi i permessi di inserimento, modifica e cancellazione, le colonne non modificabili, trigger e callback server, i campi di logging (utente e data di inserimento e modifica), il change log, i workflow e i controlli contro l'escalation dei privilegi su utenti e ruoli. I flagmd_service_enable_*restano un interruttore in piu'.
- DELETE su una route con cancellazione logica imposta il flag invece di cancellare la riga, anche per il superadmin.
- PATCH o DELETE su una riga che i vincoli nascondono all'utente: 404. DELETE senza permesso di lettura sulla route: 403.
- La risposta di POST mantiene la forma di prima (payload piu' la chiave generata).
- Una colonna del payload che l'utente non vede: 403. Una colonna visibile ma non modificabile viene ignorata dal CRUD, come nella UI.
- Entity set senza route esposta: solo il superadmin, che scrive con EF direttamente (senza CRUD).
- Multi-azienda: la connessione dati si sceglie a ogni richiesta sull'azienda dell'utente (anche dopo
switchAziendadel superadmin), come nel CRUD. Un'azienda con un DB dati configurato ma non risolvibile da' errore, mai il DB principale. Un'azienda senza un DB dati proprio usa il DB principale, come nel CRUD. - Vincoli non costruibili (filtro di default malformato, parametro
{user.x}che l'utente non ha): 403errors.auth.route_read_forbidden, mai 500. - Superadmin: niente controllo sugli
$expand, quindi le righe espanse possono includere quelle cancellate logicamente della destinazione. Le righe cancellate gli sono nascoste solo tramite la chiave di cancellazione logica della route (md_has_logic_delete), non con l'impostazione globalelogicDeleteField. Route reticolari ed entity set senza route: accesso pieno come prima. $applynon viene applicato: valgono solo$filter,$orderby,$skip,$top,$count,$selecte$expand.
La stessa regola sulle righe vale ora anche nel CRUD: updateRecord, deleteRecord e deleteRecordByID su una riga nascosta dalla restrizione rispondono 403 errors.auth.route_read_forbidden (reason row_not_visible). Fino alla 1.7.20 la WHERE della UPDATE/DELETE conteneva solo la chiave.
Autenticazione e token personali (dalla 1.7.21)
- Tutti gli endpoint
/odatachiedono una sessione valida (cookiek-user) o un token personale: senza, 401errors.auth.unauthenticated. Vale anche per/odata,/odata/$metadatae/odata/openapi.json, salvoodataPublicMetadata=true(vedi AppSettings), che riapre solo la discovery e non i dati. - Token personali (
apiTokensEnabled=true): ogni utente li crea dal menu utente, voce "Token API" (nome, durata in giorni, sola lettura oppure lettura e scrittura). Il token (wuic_pat_...) si vede una volta sola: sul DB restano solo il suo SHA-256 e un prefisso riconoscibile.
- Durata: di default 90 giorni, al massimo apiTokenMaxLifetimeDays (default 365). Nessun token senza scadenza; al massimo 20 attivi per utente.
- Agisce con i permessi del proprietario, nell'azienda in cui e' stato creato, e solo su /odata: non vale su /api/*, quindi un token non puo' gestire altri token.
- Scope odata.read: POST, PATCH, PUT, DELETE e $batch (anche di sola lettura) rispondono 403 errors.auth.api_token_scope. Scope odata.read odata.write: scritture con i controlli normali di quell'utente.
- Power BI / Excel: "Feed OData" con l'URL https://<host>/odata, credenziali "Di base" (Basic): utente qualsiasi, il token come password. Con i token attivi il 401 senza credenziali porta WWW-Authenticate: Basic, cosi' il client le chiede.
- Script: header Authorization: Bearer wuic_pat_....
curl -H "Authorization: Bearer wuic_pat_..." "https://<host>/odata/Cities?\$top=5" - Errori: 401 errors.auth.api_token_invalid (token sconosciuto, revocato, scaduto o con il proprietario eliminato); 401 errors.auth.api_tokens_disabled (funzione spenta); 429 dopo 5 tentativi falliti dallo stesso IP in 5 minuti, per 60 secondi.
- La revoca vale subito; il superadmin vede e revoca i token di tutti. L'ultimo uso e' aggiornato al massimo una volta al minuto.
Metadati Tab "Servizio web" (OData/API)
Nel metadata editor tabella, il tab Servizio web governa l'esposizione API e i gate operativi lato servizio.
md_expose_in_webapi: espone la route nei servizi Web API/OData.md_service_apply_default_filter: deprecato dalla 1.7.21. OData applica sempremd_default_filtera chi non e' superadmin, come il CRUD della UI (vedi "Permessi e vincoli di riga"): il flag non lo spegne piu'.md_service_enable_delete: abilita delete da endpoint servizio.md_service_enable_edit: abilita update/edit da endpoint servizio.md_service_enable_insert: abilita insert da endpoint servizio.md_service_enable_detail: abilita endpoint dettaglio record.md_service_enable_clone: abilita clone record lato servizio.md_service_enable_logging: abilita logging operazioni lato servizio.md_service_page_size: page size lato servizio (Pagesizenel tab UI).
md_expose_in_webapi=true,md_service_enable_insert=true,md_service_enable_edit=true,md_service_enable_delete=true,md_service_page_size=100.
Nota pratica:
md_pagesizeregola il paging UI standard;md_service_page_sizeregola il paging lato endpoint servizio.- Per OData conviene tenere allineati i flag servizio con le reali capability del backend (read-only vs read/write).
Riferimenti Scaffolding
- Il bootstrap (
firstRun) prepara contesto applicativo e connessioni iniziali; dopo scaffolding puoi agganciare route a endpoint OData. - Nel setup iniziale conviene validare subito route, metadati tabella/colonne e mappature minime per evitare mismatch OData/metadata.
- Riferimento operativo: Scaffolding iniziale.
Flusso Operativo Consigliato
1. Definisci/valida route metadata e colonne minime.
2. Configura provider endpoint OData in md_props_bag.endpoint.
3. Verifica lettura lista con filtro/sort/paging dalla UI.
4. Se usi auth OData, allinea le chiavi appsettings (enableODATAAuthentication e policy correlate).
Note pratiche
- OData e metadata non sono alternativi: il metadata resta la sorgente di comportamento UI, OData la sorgente dati.
- In caso di differenze schema, aggiorna prima metadati colonna (
mc_*) e poi rifinisci endpoint/query OData. - Dalla 1.7.13 gli endpoint dati
/odata/{EntitySet}richiedono una sessione valida (cookiek-userche supera la stessa validazione del resto dell'API; la sessione guest non basta), indipendentemente daenableODATAAuthentication: senza, rispondono 401errors.auth.unauthenticated. Dalla 1.7.21 anche/odata,/odata/$metadatae/odata/openapi.jsonchiedono la sessione (o un token personale), salvoodataPublicMetadata=true: vedi "Autenticazione e token personali". - Le colonne spaziali (
point,geometry,polygon,sdo_geometrye simili) non sono esposte negli entity set, con lo stesso comportamento su SQL Server, MySQL, PostgreSQL e Oracle: per i dati geografici usa la griglia o la mappa del framework. Le altre colonne si espongono con lo stesso tipo su tutti e quattro i DBMS.