Search

    Multitenancy

    This guide explains how CAP supports multi-tenant SaaS applications and what you as an implementor and provider of such should know.

    Content

    How It Works

    SAP BTP exposes a method for implementing and provisioning SaaS applications on Cloud Foundry. To effectively operate and run SaaS applications and to make them economically successful (TCO), such applications can be deployed once by the application provider, and then be shared by many customers. Such sharing of applications and related computational resources between many tenants is addressed by the multitenancy capabilities of SAP BTP. One important aspect is to guarantee separation between different tenants, so that a tenant isn’t allowed to access any data of other tenants. Apart from data separation, Identity and Access Management must be isolated between tenants.

    CAP supports application developers in implementing the APIs required by SAP BTP. If the application uses a SAP HANA database, multitenancy is available out-of-the-box. CAP features provided:

    • Expose endpoints for un-/subscribing tenants, for returning subscription dependencies and for tenant database updates
    • Out of the box default implementation for un-/subscribing using SAP HANA HDI containers
    • Out of the box default implementation for tenant database updates
    • Application can implement handlers for its own un-/subscribing logic
    • Tenant-specific routing of service requests → use tenant-specific metadata and tenant-specific database connection

    The cds-mtx Module

    CAP provides the npm module for Node.js published as @sap/cds-mtx on npmjs.com.:

    Java SaaS application deployment

    It provides a number of APIs for implementing SaaS applications on SAP BTP. All APIs are based on CDS. They can be exposed through plain REST, and/or consumed by other Node.js modules when running on the same server as cds-mtx.

    • provisioning: implements the subscription callback API as required by SAP BTP. If a tenant subscribes to the SaaS application, the onboarding request is handled. cds-mtx contacts the SAP Service Manager to create a new HDI container for the tenant. Then, database artifacts get deployed into this HDI container. In addition, the unsubscribe operation and the “get dependencies” operations are supported.

    • metadata: can be used to get CSN- and EDMX-models, get a list of available services and languages.

    • model: is used to extend existing CDS models and to perform tenant model upgrades after having pushed a new version of the SaaS application.

    Setup

    As a starting point you can use the bookshop sample project, which you can find on GitHub.

    Prerequisites

    To get started, you need a space in the SAP BTP and a HANA Cloud or HANA-as-a-service database connected to it. Further information can be found here.

    Configuration

    1. Add required services to your .cdsrc.json or package.json:
      "cds": {
        "requires": {
       "db": {
         "kind": "hana",
         "vcap": {
           "label": "service-manager"
         }
       },
       "multitenancy": true,
       "uaa": {
         "kind": "xsuaa"
       }
        }
      }
      
    2. In case of a Node.js project, add the following entries to the dependencies in your package.json:

      "@sap/cds-mtx": "^2",
      "@sap/xssec": "^3",
      "hdb": "^0.18.3",
      "passport": "^0.4.0"
      
    3. Add one of the following alternative annotations to the AdminService (admin-service.cds) and the CatalogService (cat-service.cds) of the bookshop app:

      • If you intend to use the Postman collection for testing (see Testing with Postman), enable access by technical users:
          @requires: 'system-user'
        
      • Otherwise, enable access by authenticated natural persons:
         @requires: 'authenticated-user'
        

    Deployment

    Setup

    Each SaaS application must have bindings to at least three SAP BTP service instances:

    Service Description
    User Account and Authentication Service (service xsuaa) Binding information contains the OAuth client ID and client credentials. The XSUAA security library can be used to validate JWT token from requests and to retrieve the tenant context from the JWT.
    Service Manager (service service-manager) CAP uses this service for creating a new SAP HANA Deployment Infrastructure (HDI) container for each tenant and for retrieving the tenant-specific database connection.
    SaaS Provisioning Service (service saas-registry) To make a SaaS application available for subscription to SaaS consumer tenants, the application provider must register the application in the SAP BTP, Cloud Foundry environment through the SaaS Provisioning Service.

    Deploy Using MTA

    The following mta.yaml is a minimal example creating all the services required for a multitenant bookshop application. It can be used to build and deploy the project as described in Deploy Using MTA.

    mta.yaml

    _schema-version: '3.1'
    ID: bookshop
    version: 1.0.0
    description: "A simple multitenant bookshop application"
    parameters:
      enable-parallel-deployments: true
    
    build-parameters:
      before-all:
       - builder: custom
         commands:
          - npm install --production
          - npx -p @sap/cds-dk cds build --production
    
    modules:
     - name: bookshop-srv
       type: nodejs
       path: gen/srv
       properties:
         EXIT: 1 # required by deploy.js task to terminate
         SAP_JWT_TRUST_ACL: [{"clientid":"*","identityzone":"sap-provisioning"}] # Trust between server and SaaS Manager
       requires:
        - name: bookshop-db
        - name: bookshop-uaa
        - name: bookshop-registry
       provides:
        - name: srv-api # required by consumers of CAP services (e.g. approuter)
          properties:
            srv-url: ${default-url}
        - name: mtx-api # potentially required by approuter
          properties:
            mtx-url: ${default-url}
    
    resources:
     - name: bookshop-db
       type: org.cloudfoundry.managed-service
       parameters:
         service: service-manager
         service-plan: container
       properties:
         hdi-service-name: ${service-name} # required for Java apps
    
     - name: bookshop-uaa
       type: org.cloudfoundry.managed-service
       parameters:
         service: xsuaa
         service-plan: application
         path: ./xs-security.json
    
     - name: bookshop-registry
       type: org.cloudfoundry.managed-service
       requires:
           - name: bookshop-uaa
           - name: mtx-api
             properties:
               prop: ~{mtx-url}
             parameters:
               param: ~{mtx-url}
       parameters:
           service: saas-registry
           service-plan: application
           config:
             xsappname: bookshop-${space}
             appName: bookshop-${space}
             displayName: bookshop
             description: A simple CAP project.
             #category: "Category"
             appUrls:
                 onSubscription: ~{mtx-api/mtx-url}/mtx/v1/provisioning/tenant/{tenantId}
                 onSubscriptionAsync: false
                 onUnSubscriptionAsync: false
                 callbackTimeoutMillis: 300000
    

    Additionally, an xs-security.json file to specify the XSUAA service settings has to be created, with roles for subscription and deployment.

    xs-security.json

    {
      "xsappname": "bookshop-${space}",
      "tenant-mode": "shared",
      "scopes": [
        { "name": "$XSAPPNAME.MtxDiagnose", "description": "Diagnose MTX sidecar" },
        { "name": "$XSAPPNAME.mtdeployment", "description": "Deploy applications" },
        { "name": "$XSAPPNAME.mtcallback", "description": "Subscribe to applications", "grant-as-authority-to-apps": [
          "$XSAPPNAME(application,sap-provisioning,tenant-onboarding)"
        ]}
      ],
      "authorities": [
        "$XSAPPNAME.MtxDiagnose"
      ],
      "role-templates": [
        {
          "name": "MultitenancyAdministrator",
          "description": "Administrate multitenant applications",
          "scope-references": [
            "$XSAPPNAME.MtxDiagnose",
            "$XSAPPNAME.mtdeployment",
            "$XSAPPNAME.mtcallback"
          ]
        }
      ]
    }
    

    You can now build the project using

    mbt build -t ./
    

    and deploy it via

    cf deploy bookshop_1.0.0.mtar
    

    Testing

    Testing with Server Running Locally

    You can run the bookshop server (bookshop-srv) locally and have it connected to the remote database. To achieve this, you need to provide database connection details, which you can obtain as follows:

    1. Deploy the MTA as described previously.
    2. Run cf env bookshop-srv to obtain output similar to the following:
       [...]
       {
        "VCAP_SERVICES": {
         "service-manager": [
          {
           "binding_name": null,
           "credentials": {
             [...]
           },
           [...]
          }
         ],
         "xsuaa": [
          [...]
         ]
        }
       }
      
       [...]
      
    3. Copy the VCAP_SERVICES definition, that is, the part between and including the outermost curly braces.
    4. Create a file default-env.json in the db subfolder of your app.
    5. Paste the copied data.

    To start the server, run npm run start in the root directory of bookshop-srv. By default, the server listens on localhost:4004. (The port can be changed through the PORT environment variable.)

    This address can be used as server URL in the remainder of this documentation. You can also run the Postman requests against it by simply adjusting the appurl accordingly.


    Postman

    Import this Collection into Postman. It contains a set of variables, which you have to set.

    • From XSUAA binding information you get using cf env bookshop-srv, extract clientid, clientsecret, paasTenantId (= tenantid), and paasSubdomain (= identityzone).
    • appurl is the URL of the bookshop-srv application.
    • mtxappurl is the URL of the application providing the mtx APIs. If you are using Node.js as runtime, mtxappurl is the same as appurl. If you are using Java as runtime, mtxappurl is the URL of the sidecar application.

    Send requests:

    1. Fetch a JWT token with request “Authenticate → Get Token w/ Client Credentials”. Check whether the test results are ok, indicating that the JWT token has been stored.
    2. Subscribe the PaaS tenant with request “mtx API → Onboard PaaS tenant
    3. Try all other requests …


    Event Handlers


    If you’re using a CAP Java server, it re-exposes the APIs required by SAP BTP’s SaaS Manager (the Provisioning API). We recommended leveraging the corresponding Java-based mechanisms to add handlers to these APIs. Handlers for the Model-API of cds-mtx must always be implemented on the Node.js server, because this API isn’t re-exposed by the CAP Java runtime.


    cds-mtx serves its APIs by using CDS technology. Therefore, any additional application logic can be implemented as CDS event handlers. Event handlers can be implemented in JavaScript files. For cds-mtx APIs, custom handlers have to be registered on bootstrap of CDS in a custom server.js file, like

    const cds = require('@sap/cds');
    
    cds.on('bootstrap',async(app) => {
        await cds.mtx.in(app);
        const provisioning = await cds.connect.to('ProvisioningService');
        await provisioning.impl(require('./handlers/provisioning'));
        
        await cds.serve('all').in(app); // serve all CAP business services
    });
    
    // Delegate bootstrapping to built-in server.js
    module.exports = cds.server
    

    See the following use cases for examples.


    Use Case: Implement a Tenant Provisioning Handler

    You can set an application entry point for the subscription in the SAP BTP Cockpit (usually a UI).

    • Create the file provisioning.js in folder srv:
      module.exports = (service) => {
      service.on('UPDATE', 'tenant', async (req, next) => {
        const res = await next(); // first call default implementation which performs the HDI container creation
        return '<bookshop-srv-url>/admin';
      });
      }
      

      In the provided code sample, you have to replace <bookshop-srv-url> with the URL of your bookshop-srv application on Cloud Foundry. The /admin endpoint is returned then. It’s important that this endpoint isn’t protected (doesn’t require a JWT token to be called).

    • Rebuild (mbt build -t ./) and redeploy (cf deploy bookshop_1.0.0.mtar) your application.

    See also here in case you are using asynchronous provisioning.

    Use Case: Application-Specific Scope Checks

    Another common task is to implement application-specific scope checks for the subscription requests. This scope check can be implemented by the “before - UPDATE -tenant” event handler within provisioning.js:

    module.exports = (service) => {
      // event handler for doing some application specific scope checks
      service.before ('UPDATE', 'tenant', async (req) => {
        console.log('[INFO ][BEFORE_UPDATE_TENANT] ' + req.data.subscribedTenantId);
        if (!req.user.is('Callback')) {     // check for the scope "Callback"
          console.log('[ERROR][BEFORE_UPDATE_TENANT]: JWT does not contain required scope.');
          // Reject request
          const e = new Error('Forbidden');
          e.code = 403;
          return req.reject(e);
        } else {
          console.log('[INFO ][BEFORE_UPDATE_TENANT]: JWT contains relevant scope');
        }
      });
    ...
    }
    


    Use Case: Conditional Offboarding

    Applications may want to prevent the deletion of a tenant database container, if certain conditions are met (for instance, there are still subscribers accessing this database). This behavior can be implemented by replacing the default “DELETE tenant” implementation:

    module.exports = (service) => {
      ...
      // Event handler checking a condition before deleting the tenant DB container as a response to an offboarding request.
      // This handler overwrites the default handler!
      service.on ('DELETE', 'tenant', async (req, next) => {
        console.log('[INFO ][ON_DELETE_TENANT] ' + req.data.subscribedTenantId);
        if (1 === 2) {    // put your condition here
          console.log('[INFO ][ON_DELETE_TENANT]: Offboarding will not be executed');
        } else {
           await next();  // call default implementation which is doing regular offboarding
        }
      });
      ...
    }
    


    Use Case: Handler for asynchronous provisioning

    As described previously, applications can create handlers for the tenant provisioning. If the provisioning is called asynchronously, implementing a handler for the provisioning endpoint unfortunately doesn’t work. In that case, the handler is called before the provisioning is run.
    To allow applications to add custom code after asynchronous provisioning, applications can add handlers for the internal tenant creation endpoint. This endpoint is called in both synchronous and asynchronous provisioning. The following API is called internally:

    @protocol:'rest'
    service TenantPersistenceService {
        type JSON {
            // any json
        }
    
        action createTenant(tenantId: UUID, subscriptionData: JSON) returns String;
        action deleteTenant(tenantId: UUID);
    }
    

    This is a sample of a handler using that API:

    module.exports = (service) => {
    service.on('createTenant', async (req, next) => {
        const res = await next(); // first call default implementation which performs the HDI container creation
    
        // some more custom code
        // req.data.subscriptionData contains the original provisioning request payload
        return '<bookshop-srv-url>/admin';
      });
    }
    

    Advanced

    SAP BTP Dependencies

    If you require SAP reuse services use cds.mtx.dependencies in your package.json and pass an array of their xsappnames. The SaaS Provisioning service (getDependencies handler) needs this information.

    "mtx": {
      "dependencies": ["xsappname-1", "xsappname-2"]
    }
    

    Job Queue Size

    The job queue size can be modified to allow for more concurrently running operations. For instance, its size could be increased to 10 by adding

    "mtx": {
      "jobqueue": {
        "size": 10
      }
    }
    

    to your package.json or .cdsrc.json, or by setting the environment variable CDS_MTX_JOBQUEUE_SIZE=10.

    Advanced Deployment Scenarios

    API Reference

    All APIs receive and respond with JSON payloads. Application-specific logic (for example, scope checks) can be added using event handlers.

    Provisioning API

    • Subscribe tenant

      PUT /mtx/v1/provisioning/tenant/<tenantId>
      

      Minimal request body:

      {
      "subscribedSubdomain": "<subdomain>",
      "eventType": "CREATE"
      }
      

      Only if eventType is set to CREATE, the subscription is performed . An application can mix in application-specific parameters into this payload, which it can interpret within application handlers. Use the _application_ object to specify those parameters. There’s one predefined sap object, which is interpreted by cds-mtx default handlers. With that object, it’s possible to set service creation parameters to be used by the SAP Service Manager when creating HDI container service instances. A typical use case is to provide the database_id to distinguish between multiple SAP HANA DBs mapped to one Cloud Foundry space.

      {
      "subscribedSubdomain": "<subdomain>",
      "eventType": "CREATE",
      "_application_": {
        "sap": {
          "service-manager": {
            "provisioning_parameters": { "database_id" : "<HANA DB GUID>" },
            "binding_parameters": {"<key>" : "<value>"}
        }
      }
      }
      

      Having more than one SAP HANA databases mapped to one space, subscription doesn’t work out of the box, unless you’ve specified a default database. This can be achieved by specifying a default when mapping a database to a space (not available for HANA Cloud). See Share an Instance with another Cloud Foundry Space for more details.


    • Unsubscribe tenant

      DELETE /mtx/v1/provisioning/tenant/<tenantId>
      
    • Subscription dependencies

      GET /mtx/v1/provisioning/dependencies
      

      Response body: Array of String. The default implementation returns an empty array.

    • GET subscribed tenants

      GET /mtx/v1/provisioning/tenant/
      

      Returns the list of subscribed tenants. For each tenant, the request body is returned which have been used for subscribing the tenant.

    Model API

    • Get cds model content

      GET mtx/v1/model/content/<tenantId>
      

      Returns the two objects base and extension in response body:

      {
      "base": "<base model cds files>",
      "extension": "<extension cds files>"
      }
      
    • Activate extensions

      POST mtx/v1/model/activate
      

      Request body (example):

      {
      "tenant": "tenant-extended",
      "extension": "<cds extension files>",
      "undeployExtension": false
      }
      

      The extension element must be a JSON array of arrays. Each first-level array element corresponds to a CDS file containing CDS extensions. Each second-level array element must be a two entry array. The first entry specifies the file name. The second entry specifies the file content. Extension files for data models must be placed into a db folder. Extensions for services must be placed into srv folder. Entities of the base model (the nonextended model) are imported by using ... from '_base/...'.

      If the undeployExtension flag is set, all extensions are undeployed from the database that are no longer part of the extensions in the current activation call.

      ❗ Warning
      undeployExtension has to be used with care as it potentially removes tables and their content from the database.

      Request body detailed sample:

      {
      "tenant": "tenant-extended",
      "extension": [
      [
      "db/ext-entities.cds",
      "using my.bookshop from '_base/db/data-model'; \n extend entity bookshop.Books with { \n ISBN: String; \n rating: Integer \n  }"
      ],
      [
      "db/new-entities.cds",
      "namespace com.acme.ext; \n entity Categories { \n key ID: String; \n description: String; \n }"
      ],
      [
      "srv/ext-service.cds",
      "using CatalogService from '_base/srv/cat-service'; \n using com.acme.ext from '../db/new-entities'; \n extend service CatalogService with { \n  @insertonly entity Categories as projection on ext.Categories; \n }"
      ]
      ],
      "undeployExtension": false
      }
      
    • Deactivate Extension

      POST /mtx/v1/model/deactivate
      

      Request body (example):

      {
      "tenant": "tenant-extended",
      "extension_files": [
        "srv/ext-service.cds"
      ]
      }
      

      extension_files is an array of the files that are to be removed from the extensions.

    Use this API to deactivate extension. To activate and deactivate extension in one call, use activate with undeployExtension: true.

    ❗ Warning
    The API has to be used with care as it removes tables and their content from the database.

    • Upgrade base model from filesystem (asynchronous)

      POST mtx/v1/model/asyncUpgrade
      

      Request body:

      {
      "tenants": ["tenant-extended-1", "tenant-non-extended-2", ...],
      "autoUndeploy": <boolean>
      }
      

      Upgrade all tenants with request body { "tenants": ["all"] }.

      If autoUndeploy is set to true, the auto-undeploy mode of the HDI deployer is used. See HDI Delta Deployment and Undeploy Allowlist for more details.

      Response (example):

      { "jobID": "iy5u935lgaq" }
      

      The jobID can be used to query the status of the upgrade process:

      GET /mtx/v1/model/status/<jobID>
      

      During processing, the response can look like:

      {
      "error": null,
      "status": "RUNNING",
      "result": null
      }
      

      Once a job is finished, the collective status is reported like this:

      {
      "error": null,
      "status": "FINISHED",
      "result": {
          "tenants": {
              "<tenantId1>": {
                  "status": "SUCCESS",
                  "message": "",
                  "buildLogs": "<build logs>"
              },
              "<tenantId2>": {
                  "status": "FAILURE",
                  "message": "<some error log output>",
                  "buildLogs": "<build logs>"
              }
          }
      }
      }
      

    The status of a job can be QUEUED (not started yet), RUNNING, FINISHED, and FAILED.

    The result status of the upgrade operation per tenant can be RUNNING, SUCCESS, or FAILURE.


    Logs are persisted for a period of 30 minutes before they get deleted automatically. If you request the job status after that, you’ll get a 404 Not Found response.


    Metadata API

    All metadata APIs support eTags. By setting the corresponding header, you can check for model updates.

    • GET edmx

      GET /mtx/v1/metadata/edmx/<tenantId>
      

      Returns the edmx metadata of the (extended) model of the application.

      Optional URL parameters

      name=<service-name>
      language=<language-code>
      
    • GET csn

      GET /mtx/v1/metadata/csn/<tenantId>
      

      Returns the compiled (extended) model of the application.

    • GET languages

      GET /mtx/v1/metadata/languages/<tenantId>
      

      Returns the supported languages of the (extended) model of the application.

    • GET services

      GET /mtx/v1/metadata/services/<tenantId>
      

      Returns the services of the (extended) model of the application.

    Extensibility

    Extend Tenant Data and Service Models

    cds-mtx provides an API to extend data and service models in a tenant-specific way. The CDS Software Development Kit, @sap/cds-dk, contains a command line client with which an extension developer can maintain and activate cds model extensions files. See section Extending SaaS Applications for more details.

    Perform the following steps to enable and perform extensions of the Getting Started Bookshop SaaS application:

    1. Add required scopes to XSUAA configuration.
    2. Authorize Extension Developer on SAP BTP.
    3. Set up an Extension Project.
    4. Create extension cds files and activate.

    Add Required Scopes

    Tenant-specific CDS model extensions are performed by an extension developer, usually a customer. Therefore, those activities are protected by XSUAA scopes. You can add the following configuration to your xs-security.json:

    {
      "scopes": [
        { "name": "$XSAPPNAME.ExtendCDS", "description": "Create extensions" },
        { "name": "$XSAPPNAME.ExtendCDSdelete", "description": "Undeploy extensions" }
      ],
      "role-templates": [
        {
          "name": "ExtensionDeveloper",
          "description": "CDS Extension Developer",
          "scope-references": [
            "$XSAPPNAME.ExtendCDS",
            "$XSAPPNAME.ExtendCDSdelete"
          ]
        }
      ]
    }
    

    Rebuild the application while applying the extension descriptor with:

    mbt build -t ./ -e scopes.mtaext
    

    Then redeploy while just updating the bookshop-uaa service instance:

    cf deploy bookshop_1.0.0.mtar -r bookshop-uaa
    

    You need to have the User & Role Administrator role to be able to define role templates. If the previous command gives you a related error message, assign this role to you.



    Authorize Extension Developer

    • Open the overview screen of a Subaccount in SAP BTP Cockpit. This can be any Subaccount, which is already subscribed to your application. For first testing, you can choose your Paas/Provider Subaccount into which you’ve deployed your application. Note down the value for “Subdomain”. In the following, we refer to it as <subdomain>. It represents the tenant.

    • Choose Trust Configuration and click on the active Default identity provider name. Trust Configuration

    • Enter your E-Mail address and choose Show Assignments.
    • Choose Assign Role Collection.
    • From the popup, assign CAP-Getting-Started.

    You’re now authorized to use the extension API to extend cds models for your PaaS/Provider tenant.


    Set Up an Extension Project

    Initialize a new extension project for tenant <subdomain>:

    • Execute cds extend <bookshop-srv-url> -s <subdomain>
    • The system will respond with the message “Invalid passcode”.
    • Fetch a one-time passcode used for authentication by following the link given in the system message. Log on at the browser logon screen. Note down the shown authentication code <passcode>.
    • Execute cds extend <bookshop-srv-url> -s <subdomain> -p <passcode>. A project structure will be generated in a new folder with name <subdomain>.

    Create and Activate an Extension

    • In the db/ folder of this project, create the new file ext.cds:
      using sap.capire.bookshop from '_base/db/schema.cds';
      extend entity bookshop.Books with {
      ISBN : String;
      }
      
    • Execute cds activate <subdomain> (from the folder one above the folder <subdomain>). For the tenant <subdomain>, the new field ISBN gets activated on the database.
    • Verify that the new field is exposed for Books. Use Postman to fetch a JWT for the extended tenant and send a GET request to <bookshop-srv-url>/browse/$metadata.


    Extensibility Configuration

    Extensibility settings can be added to the mtx settings of your .cdsrc.json file:

    "mtx": {
      "element-prefix": ["Z_", "ZZ_"],
      "namespace-blocklist": ["com.sap.", "sap."],
      "extension-allowlist": [
        {
            "for": ["my.bookshop.Authors", "my.bookshop.Books"],
            "new-fields": 2
        }, {
            "for": ["CatalogService"],
            "new-entities": 2
        }
      ]
    }
    

    Element Prefixes and Namespace Blocklist

    Field extensions can only be performed with prefixes listed in the `element-prefix’ prefix list.

    Similarly, new entities and services can’t be created within the blocked CDS namespaces listed in namespace-blocklist.
    Namespace restrictions are “inherited”, that is, protecting “com.sap.” also protects “com.sap.*”

    Entity Allowlist

    The mtx section also lists the entities and services that can be extended; their fully qualified names are listed in the entity-allowlist.
    The entity-allowlist can contain restrictions to limit the number of extensions that can be created. With new-fields and new-entities, the maximum number of fields for entities and the number of new entities for services can be set.
    It is also possible to specify the entity and service names as namespaces. The restrictions then apply to all entities services in that namespace.

    "extension-allowlist": [
      {
        "for": ["my.bookshop"],
        "new-fields": 2
      },
      {
        "for": ["*"],
        "kind": "service"
      }
    ]
    

    The example allows two new fields for all entities in namespace my.bookshop and an unlimited number of new entities for all services.

    If this list isn’t provided, no entities or services can be extended.

    Show/Hide Beta Features