Skip to content

Prototyping with CAP for RAP

Talking about protypes and mocks you maybe will think about SAP build and today, maybe SAP AppGyver will also be an option for you.

Concerning our B2B requests, most time we had to start with an odata model visualized by a SAPUI5 app. For this reason, SAPUI5 offeres a mock service on top of an existing metadata file, but it was quit affordable to design the service description manually.

Since we are using the SAP BTP, we can easily design service models using CAP and deploy them together with a SAP Fiori Elements (or maybe Freestyle) app to the cloud and the customer is able to get his hands on it. Even applying his identity providers is quite a common task.

Development efficiency and productivity

Skills and knowledge of the developer is one factor, that reflects the prototype. An experienced RAP developer will be faster to implement things than a newbie in CAP.

But competing RAP vs CAP there are other factors, like the need for multiple Tools (VS code, BAS, ADT), that slowdown the development performance. While CAP passthroughs all kind of annotations (see Differences to ABAP), there is an additional overhead using EDMX.

Prototyping is full stack development

A prototype always needs ui and service development, that maybe can easier be handled by a fullstack web developer who can use javascript on ui and for backend servises.

Talking about SAP Fiori Element ui without the need for a custom floorplan, the situation can be different.

Prototype service development efficiency and productivity comparison

Prototype SpeedSlowestSlowModerateFast
Programming ModelGWBOPFRAPCAP
TechnologyABAPJAVANodeJS
ODatav2v2v2 (v4)v4 (v2)v4 (v2)
Release Evolution<=7.40>=7.50>=7.54 (v2)
>=S4H2020FP1 (v4)
>=S4H2021 (guided)
>=Java 8
>=Apache Maven 3.5.0
>=18
Development Tools (Backend)SAPGUI
(SEGW)
ABAP Development Tools (ADT)VS Code or SAP BAS
SAP Fiori tools (UX)VS Code or SAP BAS

The above info graphic is also a time beam from the past to today. The higher level of abstraction inside the programming models allows low-code design in less time.

Hint

To develop an application that connects to SAP data sources, you can use the SAP Fiori tools.

If you have to connect to an SAP On-Premise system and you do not have direct access to the system (VPN), Fiori tools are able to proxy this inside BAS, but not locally inside VS Code!

For this reason, the only known solution is using sap-cf-proxy. If you do not want the additional efford (or are not allowed to deploy sshenabler), go with SAP BAS.

Why choosing CAP?

ReasonDescription
MockingCAP supports local mocking by default without the additional need of a database (using sqlite in memory).
DevelopmentIntegrated development experience with VS Code and SAP BAS for CAP Service model and SAPUI5 Fiori elements or Freestyle application.
DeploymentPrototypes can easily be deployed with cloud native tools.

Why not choosing RAP?

ReasonDescription
Cloud ConnectorOnPremise access to a S/4HANA system needs a working cloud connector setup
More ComplexYou have to choose multiple Tools (ADT vs IDE). Also SideEffects needs to be maintained manually while beeing supported by CAP.
Outdated SystemTo benefit from the latest features, your ABAP system needs to be state-of-the-art. This is valid for cloud based runtimes, but maybe problematic for onPremise systems.
Status QuoMaybe a Business Suite is currently migrating to S/4HANA and today RAP is not available. If i am able to access/use an existing RAP system?

What is the reason or requirement?

A new SAPUI5 Freestyle app should be developed that will be running in the SAP BTP. The service layer should be developed with RAP v4 inside the on-premise S/4HANA runtime, but the system is currently not available. We need a working prototype to finalize and discuss features with the customer. The solution will be developed by multiple companies and developers.

To be able to start with the app, we decided to use CAP for service modelling and a Freestyle SAPUI5 app as the UI, supporting SaaS based Theming in the SAP BTP.

Idea

  • Right now starting with the development while beeing able to switch over to a RAP oData v4 service at any time.
  • Working in parallel with different teams on CAP Mock and RAP Enterprise service.
  • Quickly release new feature prototypes to be discussed before final implementation starts.

Domain Modelling

Since we finally need an oData service that should be compatible with RAP, we can not follow the Naming Conventions - Recommendations inside CAP.

The reason for this is, that ABAP RAP does not allow to control namings, while CAP if more flexible, so we have to follow the ABAP CDS naming conventions. Especially reusing a lot of existing SAP Virtual Data Models (VDM).

Restrictions

CAP 4 RAPCAP Native
Use CamelCase for entity and type names
(CustomerNumber)
Start entity and type names with capital letters
KEY follows entity name
(SalesOrder -> key SalesOrder: string)
KEY mostly id
(SalesOrder -> key id: guid)
Use singular form for entities
(SalesOrder)
Use plural form for entities
Use singular form for types
(RAP adds "Type" to Entity name)
Use singular form for types
Start elements with a uppercase letterStart elements with a lowercase letter
Prefer Unmanaged Compositions and Associations
(controlling reference names)
Prefer Managed Compositions and Associations

Use Unmanaged Associations

Concerning the restrictions, using the managed approach generates element namings that does not fullfill VDM naming. Falling back to unmanaged approach solved this to be able to control target naming.

Navigation Property naming

Do not repeat entity context name in navigation properties, means using to_Items instead of to_SalesOrderItems.

ts
entity SalesOrder {
    key SalesOrder        : String(10)
        // foreign key
        ShipToParty       : Customers : Customer;
        // Associations (Singular)
        to_SalesOrderType : Association to SalesDocumentTypes
            on to_SalesOrderType.SalesDocumentType = SalesOrderType;
        to_ShipToParty    : Association to Customers
            on to_ShipToParty.Customer = ShipToParty;
        // To-many Associations (Plural)
        to_Items          : Composition of many SalesOrderItems
            on to_Items.SalesOrder = SalesOrder;
}

Field Features

Application developers can control important features of the field, see Further Features of the Field.

Text Arrangement

The value displayed within the field can show either only the textual description of the value, or show it together with the ID representation of the value.

TextArrangementDisplay Format
TextFirst - (default value)ABC (A1)
TextLastA1 (ABC)
TextOnlyABC
TextSeparateA1

Property Level

cds
annotate my.Entity with {
    MyProperty @Common : {
        Text            : MyPropertyTextPropertyPath,
        TextArrangement : #TextOnly
    };
}
abapcds
define view <ViewName> as select from <data_source> {
    @ObjectModel.text.Element: ['MyPropertyTextPropertyPath']
    @UI.textArrangement: #TEXT_ONLY
    MyProperty,

    // Maybe you have to flatten deep text links
    // (currently not supported in RAP)
    to_Text.MyPropertyTextPropertyPath as MyPropertyTextPropertyPath
}

Value Help Entity Set Level

cds
annotate my.Entity with {
    MyProperty @Common : {
        Text            : MyPropertyTextPropertyPath,
        TextArrangement : #TextOnly
    };
}
abapcds
@ObjectModel.dataCategory: #TEXT
@EndUserText.label: 'Value Help Title'
define view <ViewName> as select from <data_source> {
    key MyProperty,

    @Semantics.text: true
    to_Text.MyPropertyTextPropertyPath as MyPropertyTextPropertyPath,
}

Editability of the Field

There are different ways in which a field can be marked as editable:

AnnotationsDescription
Core.ComputedIf a property is annotated with Core.Computed true, it is always displayed in a non-editable way.
Core.ImmutableHas the same result in edit mode, but in create mode the property is editable.
Common.FieldControlIs a term using an enumeration with ReadOnly as a possible value. You can statically set a property to read-only (similar to Core.Computed), but you can also refer to a property that uses the enumeration values as possible data values.

While the first two options are only used with the static values true and false, the third option is also used dynamically by referring to another property.

Field Control

In SAP Fiori elements for OData V4, you can use the common.FieldControl annotation within the usage of properties (as in UI.LineItem or UI.FieldGroup). With this option you can control the enablement of a property within a specific field group, for example, or you can define a property to be read only in a tabular visualization, but to be editable as a form field.

@Common.FieldControlBehavior
#MandatoryProperty must have a non-null value whenever save or update is triggered.
#OptionalProperty can have a null value.
#ReadOnlyProperty value cannot be changed. In edit mode, this property will not show up as an editable field.
#InapplicableProperty has no meaning in the current entity state and will show up in disabled state.

Fiori Elements Representation

OData Field Control States

Usage of Common.Inapplicable as UI.Hidden

Regarding the dynamic Inapplicable value, the fields for which the FieldControl evaluates to inapplicable are rendered as disabled.

If you want to hide the field, you have to use UI.Hidden instead by using:

  1. Dynamic Expression on top of Inapplicable.
  2. Server side calculated Virtual Field dependend on Inapplicable
  3. Building Block Binding Expression using visible="{= !!%{Property_FC}}"
    %{PATH} ≙ ${path: 'PATH', targetType: 'any'}
    Property_FC=0 ≙ #Inapplicable ≙ Hidden

SAP Feedback

The SAP Vocabulary teams changed the Common.Hidden behavior (deprecated) some years ago and introduced Common.Inapplicable and UI.Hidden.

With the switch, FieldControl semantically belongs to Edit Screens. SAP Fiori elements can also skip FieldControl annotations on ReadOnly Screens for performance reasons.

Field control allows you to use Virtual Elements in the backend to evaluate the state of an input control. In conjunction with the Side Effect, you can solve cascading field dependencies and also using conditional control states.

Domain Modelling Field Control

cds
type TechnicalFieldControlFlag : Integer @(
    UI.Hidden,
    Core.Computed
) enum {
    Mandatory    = 7; // Property is mandatory from a business perspective
    Optional     = 3; // Property may have a value
    ReadOnly     = 1; // Property value cannot be changed
    Inapplicable = 0; // Property has no meaning in the current entity state
};

entity Cart {
    @Common.FieldControl    : PickLocation_FC
    PickLocation            : String(200);
    virtual PickLocation_FC : TechnicalFieldControlFlag not null default 3;
}

Service Implementation

If you are using virtual elements, the best place to compute the values is in the after hook after data is processed. This can be easily done with the following arrow function.

js
this.after('READ', 'Entity', each => {
    if (each.PickLocation_FC) {
        each.PickLocation_FC = 0;
    }
})

But in most cases, you will have more complex conditions with dependencies to multiple properties. Especially if you are using Side Effects, only the requested properties are extra read after a PATCH operation.

For this reason, you can use a small helper, that will auto read neccessary data if not part of the request. It analyzes the request and only requests data on demand.

js
async function computeFieldControl(data, req, deps, callback) {
    let index = 0
    for (const each of Array.isArray(data) ? data : [data]) {
        // analyze dependencies if missing data needs to be read
        const elements = Object.keys(each)
        let readTarget = false
        for (const fc of Object.keys(deps)) {
            if (elements.includes(fc)) {
                for (const dep of deps[fc]) {
                    if (!elements.includes(dep)) {
                        readTarget = true
                        break
                    }
                }
            }
        }
        const entity = (readTarget)
            ? await SELECT.one.from(req.target).where(req.params[index])
            : each
        callback.apply(each, [entity])
        index++
    }
}

// use after event handler to compute virtual elements
this.after('READ', 'Entity', async (data, req) => {
    // define dependencies to solve field control evaluation
    const deps = {
        "PickLocation_FC": ["ShippingCondition"]
    }
    // use helper to add field control on demand
    await computeFieldControl(data, req, deps, function (entity) {
        if (this.PickLocation_FC) {
            this.PickLocation_FC = (entity.ShippingCondition == '02') 
                ? 1 : 0
        }
    })
})

TIP

You can avoid the extra read by declaring all dependent properties as property targets for the relevant side effect.

Visibility and Enablement

The visibility of the field is affected by the UI.Hidden annotation, which can be given either at property level or field level (path based).

cds
annotate my.Entity with {
    // Property level
    MyProperty @UI.Hidden : true;

    /* OR */

    // Path based
    MyProperty @UI.Hidden : MyPropertyHiddenPath;
}
abapcds
define view <ViewName> as select from <data_source>
{
    // Property level
    @UI.hidden // @UI.hidden: false (default true)
    <view.element>

    /* OR */
    
    // Path based
    @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_VIRTUAL_HIDDEN'
    virtual <view.element> :abap_boolean
}

Dynamic Expression

In particular, when writing annotations for SAP Fiori UIs, you may want to make use of dynamic expressions in annotation values. They are evaluated in the front end and can, for example, be used to avoid status mapping in the backend.

What, if you want to use field control and want to hide all Inapplicable fields? You have to use an additional virtual property to hide the field if Property_FC = 0.

This can be avoided by using a client side dynamic expression.

cds
annotate my.Entity with {
    Inapplicable @UI.Hidden : {
        $edmJson : {
            $If : [{
                $Eq : [
                    {$Path : 'Inapplicable_FC'},
                    0
                ]},
                true,
                false
            ]
        }
    };    
}
xml
<Annotations Target="Namespace.Entity/Inapplicable">
    <Annotation Term="UI.Hidden">
        <If>
            <Eq>
                <Path>Inapplicable_FC</Path>
                <Int>0</Int>
            </Eq>
            <Bool>true</Bool>
            <Bool>false</Bool>
        </If>
    </Annotation>
</Annotations>

Side Effect

If a user changes the content of a field or performs another activity, this change can potentially influence other fields on the UI. This system behavior is called a Side Effect.

Side effects are performed in the back end. However, you need to annotate the side effects implemented in the back end using side effect annotations to "inform" the front end which fields on the UI might be influenced by a change, so that the front end can request new data for these fields. Otherwise, the UI might still display outdated data.

Using Side Effects with RAP

At least supported with version 2302 (see: How to use side effects in RAP).

Former systems need a local annotation.xml file inside UI5 project that is attached to the odata model.

cds
annotate my.Cart with @Common : {SideEffects #SoldToParty : {
    SourceProperties : ['SoldToParty'],
    TargetProperties : [
        'ShipToParty',
        'ShipToParty_FC'
    ],
    TargetEntities   : [
        to_ShippingCondition
        to_ShipToParty
    ]
}};
xml
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
    <edmx:Reference Uri="/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Vocabularies(TechnicalName='%2FIWBEP%2FVOC_COMMON',Version='0001',SAP__Origin='LOCAL')/$value">
        <edmx:Include Namespace="com.sap.vocabularies.Common.v1" Alias="Common"/>
    </edmx:Reference>
    <edmx:Reference Uri="/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Vocabularies(TechnicalName='%2FIWBEP%2FVOC_UI',Version='0001',SAP__Origin='LOCAL')/$value">
        <edmx:Include Namespace="com.sap.vocabularies.UI.v1" Alias="UI"/>
    </edmx:Reference>
    <edmx:Reference Uri="/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Vocabularies(TechnicalName='%2FIWBEP%2FVOC_COMMUNICATION',Version='0001',SAP__Origin='LOCAL')/$value">
        <edmx:Include Namespace="com.sap.vocabularies.Communication.v1" Alias="Communication"/>
    </edmx:Reference>
    <edmx:Reference Uri="/sap/opu/odata4/sap/YOURSERVICE_srvd/0001/$metadata">
        <edmx:Include Namespace="com.sap.gateway.srvd.YOURSERVICE_srvd.v0001" Alias="SAP__metadata"/>
    </edmx:Reference>
    <edmx:DataServices>
        <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="SAP__self">
            <Annotations Target="SAP__metadata.CartType">
                <Annotation Term="Common.SideEffects" Qualifier="SoldToParty">
                    <Record Type="Common.SideEffectsType">
                        <PropertyValue Property="SourceProperties">
                            <Collection>
                                <PropertyPath>SoldToParty</PropertyPath>
                            </Collection>
                        </PropertyValue>
                        <PropertyValue Property="TargetProperties">
                            <Collection>
                                <String>ShipToParty</String>
                                <String>ShipToParty_FC</String>
                            </Collection>
                        </PropertyValue>
                        <PropertyValue Property="TargetEntities">
                            <Collection>
                                <NavigationPropertyPath>to_ShippingCondition</NavigationPropertyPath>
                                <NavigationPropertyPath>to_ShipToParty</NavigationPropertyPath>
                            </Collection>
                        </PropertyValue>
                    </Record>
                </Annotation>
            </Annotations>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

OData Annotation differences to ABAP

In contrast to ABAP CDS, CAP applies a generic, isomorphic approach where names and positions of annotations are exactly as specified in the OData Vocabularies and also SAP odata-vocabularies.

AdvantageDescription
Single source of truthUsers only need to consult the official OData specs.
SpeedDo not need complex case-by-case mapping logic.
No bottlenecksAlways supports the full set of OData annotations.
Bidirectional mappingTranslation of CDS to EDMX and vice versa.

SAPUI5 Freestyle App

The general recommendation (not only for Fiori apps) is to have a 1:1 relationship between an ui5 component (app) and an odata model. While having a CAP and RAP service in parallel, we need a switch to decide, which model should be used.

By default, the main odata model will be defined inside the manifest and automatically instantiated, without further control. To decouple this and to be able to control the instantiation, the instantiation needs to be done manually.

Define Data Sources

The relevant data sources can be regularly defined inside the manifest.json. Since RAP does currently not support Side Effects, see lines [17-19] how to attach a local annotation.xml file for the RAPService data source.

manifest.json

json
"sap.app": {
    // ...
    "dataSources": {
        "CAPService": {
            "uri": "/service/cap/",
            "type": "OData",
            "settings": {
                "odataVersion": "4.0"
            }
        },
        "RAPService": {
            "uri": "/s4/sap/opu/odata4/sap/zmy/srvd/sap/zmy_srvd/0001/",
            "type": "OData",
            "settings": {
                "odataVersion": "4.0",
                "annotationURI": [
                    "annotations/annotation.xml"
                ]
            }
        }
    }
}

Do not define a default model inside the sap.ui5/models section, because we need to instantiate the model inside the Component.js.

Dynamic Model Instantiation

After declaring the model configurations for both models, we need to instantiate the relevant model. While working locally, we generally want to use the CAP mock service (but hybrid mode would also be possible). The deployed version should use the RAP service by default, but if you also deployed the CAP microservice to the cloud, you can switch to it.

URI ParamsDescription
mockThe RAP service is used by default, except running locally on localhost. If you deployed the CAP service to the cloud, you can use this uri param to force the mock service usage. This helps to implement new features inside CAP and showcase them in the cloud.
$directBy default, operations are grouped using Batch Control. Using this param, the SubmitMode is switched to Direct and the groupId is using $direct. This makes is easier to investigate data in the chrome network instead of scrolling through the payload.

DANGER

Take care, that $direct breaks some odata v4 behavior and should only be used for evaluating data. All calls are fired in parallel, so side effects maybe will not represent the correct state after patch.

The UI5 Inspector shows $batch OData Requests as foldable JSON objects!

Component.js

js
sap.ui.define([
    "sap/fe/core/AppComponent",
    "sap/ui/model/odata/v4/ODataModel",
    "sap/base/util/UriParameters"
], function (AppComponent, ODataModel, UriParameters) {
    "use strict";

    return AppComponent.extend("namespace.Component", {

        init: function () {
			// set the main global odata v4 model for CAP (mock) or RAP
			this.setModel(this.getODataModel());
            // CUSTOM CODE...
        },

        getODataModel: function () {
			var oUriParameters = new UriParameters(window.location.href),
				bMock = oUriParameters.has("mock")
					|| window.location.hostname === "localhost",
				bDirect = oUriParameters.has("$direct"),
				mDataSource = this.getManifestEntry("/sap.app/dataSources/"
                    + (bMock) ? "CAPService" : "RAPService"
                ),
				sSubmitMode = (bDirect) ? "Direct" : "Auto",
                mParameters = {
                    "autoExpandSelect": true,
                    "earlyRequests": true,
                    "groupId": (bDirect) ? "$direct" : "$auto",
                    "groupProperties": {
                        "default": { "submit": sSubmitMode },
                        // Custom Groups
                        "CartGroup": { "submit": sSubmitMode }
                    },
                    "operationMode": "Server",
                    "serviceUrl": mDataSource.uri,
                    "synchronizationMode": "None"
                };

            return new ODataModel(
				Object.assign(mParameters, mDataSource.settings)
			);
		}
    }
});

Pain Points

While prototyping with CAP for RAP you will run into some issues, you have to resolve manually. Maybe, this will be addressed in the future to make things easier.

CAP

Initial csv data newline issue

The is currently an open question inside anwsers.sap.com concerning this topic.

CAP Bound Actions and Functions without context

Trying to use oData Operations, CAP and RAP have different restrictions.

RAP only allows Actions and Funtions, that are bound to an entity set. It is not possible to use an unbound action, instead you have to bound an action to an entity set and call it wihtout a context.

Using a RAP action like

POST Cart/com.sap.gateway.srvd.zwebshop_srvd.v0001.simulateAll

leads to an error inside CAP while using

POST Cart/PrivateService.simulateAll

'PrivateService.simulateAll' is neither a structured type nor a bound operation

ts
// bound action handler without context is currently not supported!
this.on('simulateAll', 'Cart', async req => {
    console.log("CART->simulateAll...", req.data);
})

Calling the same service with context is supported:

POST Cart(BP='01',ShopSalesArea='ABCD')/PrivateService.startDatatransTransaction

Therefore, you have to handle services in CAP differently than in RAP

ts
service PrivateService @(path : '/private') {
    // CAP unbound action (RAP bound action without context workaround)
    action simulateAll();

    entity Cart as select from my.Cart actions {
        // SAP: use the following work-in-progress experimental annotation
        @cds.odata.bindingparameter.collection
        action simulateAll();
    }
}

Improvement

Introducing a generic handler, that allows the usage of bound actions without context.

SAP Feedback

There is an undocumented action annotation @cds.odata.bindingparameter.collection for internal use, that solves the issue! We will introduce a more meaningful annotation name in a future release.

You also have to use the unbound handler:

ts
// use unbound action handler
this.on('simulateAll', async req => {
    // req.query: { SELECT: { from: {ref:['PrivateService.Cart']} }}
    // req.data:  { contains parameters }
})

Domain Modelling ignoring Best Practises

Since CAP is more flexible than RAP and in most times, CAP prototypes will be productive in on-premise systems, CAP needs to mimic the naming conventions from RAP.

A prominent example is the new SAP sample cap-sflight which is modelled the same way like the RAP opposite.

As an example, a CDS entity will look like this

cds
entity Booking : managed {
  key BookingUUID   : UUID;
  BookingID         : Integer @Core.Computed;
  ...
  BookingStatus     : Association to BookingStatus;
  to_Flight         : Association to Flight 
                        on  to_Flight.AirlineID = to_Carrier.AirlineID
                        and to_Flight.FlightDate = FlightDate
                        and to_Flight.ConnectionID = ConnectionID;
};

The main difference are the CamelCase property names und the association names like to_AssocName. RAP is auto generating assoc names, so to_* is a mandatory approach.

Further all associations needs to be unmanaged, that means the relationship must be explicitely defined using on.

Improvement

A global switch to be compatible with RAP naming and the possibility to use Managed Associations would be great.

At least, it would be great to make sure inside toolings, that the RAP approach will still be valid with future updates.

RAP

Special RAP null value handling

RAP v4 does not support null values (while v2 does?!?) to unset a property. An empty string "" needs to be used instead.

Also, all string properties are annotated as nullable false

xml
<Property Name="PropertyName" Type="Edm.String" Nullable="false"/>

This implies, using POSTMAN for service calls, these properties are mandatory and a value needs to be send for creating entities.

Improvement

If possible, allow sending null instead empty string and avoid Nullable="false" property declaration inside $metadata.

Declarative Side Effect annotations

Right now, you have to fallback to EDMX if you need to implement side effect dependencies. The problem are scenarios, where different teams are working on UI and RAP.

Improvement

Offering CDS annotations for Side Effects.

SAP Feedback

Configuring side effects will be possible starting with release 2302. How to use side effects in RAP - Andre Fischer

Custom Entities fallback to EDMX

While using custom entities inside RAP, we have Situations where it is not possible to add relevant annotations for UI.TextArrangement. In this case, we have to addtionally add them to the local UI annotation file.

Improvement

To be more flexible, it would be great to use a more generic, isomorphic approach like CAP.

Specially for ABAP On-Premise systems, where update cycles are quit long, it would be great to have the possibility to add some kind of wildcard or freestyle annotations to overcome current restrictions.

Fiori Elements

ValueList navigation property support

If you want to use value helps, you have to annotate @Common:ValueList. You have to define the CollectionPath which needs to be an EntitySet.

This is maybe comming from the FE ABAP history, but with CAP, you can easily model all relevant associations in CDS. For RAP model flavor, you have to go unmanaged and declare JOIN conditions.

cds
entity Cart {
    ...
    SoldToParty      : Customer:Customer default '0000000000';
    to_SoldToParty   : Association to Customer
                    on to_SoldToParty.Customer = SoldToParty;
    to_SoldToParties : Association to many CustomerFunctionVH
                    on  to_SoldToParties.BusinessPartner = BusinessPartner
                    and to_SoldToParties.ShopSalesArea   = ShopSalesArea
                    and to_SoldToParties.PartnerFunction = 'AG';
    ...
}

All relevant information is handily available through navigation properties, but if use FE or Building Blocks and ValueHelps, you can not use to_SoldToPartiesfor the ValueHelp.

You have to fallback using the entityset CustomerFunctionVH by loosing the JOIN conditions. For that reason you have to redundandently declare the JOIN conditions using the ValueListParameter.

cds
annotate my.Cart with {
    SoldToParty       @Common : {
        Text            : to_SoldToParty.CustomerName,
        TextArrangement : #TextOnly,
        // if you want to use ComboBox with less entries use
        // ValueListWithFixedValues, 
        ValueList       : {
            Label          : '{i18n>SoldToParty}',
            // currently using navigation property is not supported!!!
            // CollectionPath : 'to_SoldToParties',
            CollectionPath : 'CustomerFunctionVH',            
            Parameters     : [
            {
                $Type             : 'Common.ValueListParameterInOut',
                LocalDataProperty : SoldToParty,
                ValueListProperty : 'BPCustomerNumber'
            },
            // add JOIN conditions
            {
                $Type             : 'Common.ValueListParameterIn',
                LocalDataProperty : BusinessPartner,
                ValueListProperty : 'BusinessPartner'
            },
            {
                $Type             : 'Common.ValueListParameterIn',
                LocalDataProperty : ShopSalesArea,
                ValueListProperty : 'ShopSalesArea'
            },
            {
                $Type             : 'Common.ValueListParameterConstant',
                ValueListProperty : 'PartnerFunction',
                Constant          : 'AG'
            },
            {
                $Type             : 'Common.ValueListParameterDisplayOnly',
                ValueListProperty : 'CustomerName'
            },
            ...

Besides the additional overhead off redundantly declaring the JOIN conditions, using the ValueHelp you will get this:

SAP Fiori Elements Value List

By default, the user gets all Filter and would be able to compromiss the underlying logic (searching/filtering the wrong data on a generic table). This would not be possible with the navigation property, where the JOIN condition is used on server side.

The current workaround is to hide all JOIN condition filters from the UI, which introduces additional overhead.

cds
annotate my.CustomerFunctionVH with {
    BusinessPartner  @UI : {HiddenFilter : true};
    ShopSalesArea    @UI : {HiddenFilter : true};
    // only for security backup
    // experimental ValueListParameterConstant does not 
    // propagate field to UI!
    PartnerFunction  @UI : {HiddenFilter : true};
}

Finally, you have redesigned the CAP logic for Fiori Elements.

Improvement

For quick prototyping and also for security, enhancing the ValueList with the possibility of using a relative navigation property would make things easier (and more safe). You are also no more forced to publish the underlying entity set for direct usage inside your service.

SAPUI5

Special RAP null value handling

By default, SAPUI5 controls will send a null value to remove/unset a given value. This is working for an odata v2 model with RAP, but no more with v4.

Instead, you have to send an empty string "", to get the same result. This is strange from odata perspective, because also all $metadata string props will be annotated with nullable=false.

xml
<Property Name="PropertyName" Type="Edm.String" Nullable="false"/>

Generic ui's will intepret this as an mandatory field on ui layer!

To teach SAPUI5 to send empyt string "" instead of null, each relevant value help fields needs to set a formatOption and this forced also to set a type:

xml
<Input value="{
    path: 'PropertyName',
    type: 'sap.ui.model.odata.type.String',
    formatOptions: {
        parseKeepsEmptyString: true
    }
}"/>

This is implemented inside sap.fe value help, but for freestyle apps, you have to take care of this.

Improvement

Introducing a model switch to handle this topic in a generic way

SAP Feedback

The topic has been discussed and may be implemented in the future!

Namespace issues with bound OData Ooperations

The OData model heavily removed former v2 APIs by using a more declarative approach allowing binding parameters and results inside XML views.

xml
<Dialog binding="{name.space.CreateInvoice(...)}" id="operation" 
    title="Address Note property of the sales order">
    <form:SimpleForm binding="{$Parameter}">
        <Label text="Note" />
        <Input value="{_it/Note}" />
    </form:SimpleForm>
</Dialog>

The problem is, that using bound actions you have to declare the service namespace as part of the operation binding. If you are using CAP and RAP services, a workaround would be using a CAP services

Anyhow, in situations having two models for the same app (for example a PUBLIC and a PRIVATE model inside the same app), you are no more able to use declarative bindings and have to push this to the controller code.

ODataV4Utils

You can use the following utility class:

js
sap.ui.define([], function () {
    "use strict";
    return class ODataV4Utils {
        constructor(oDataModel, bOnPremise) {
            this._oDataModel = oDataModel;
            this._bOnPremise = bOnPremise;
        }

        // CAP "MyService.EntityContainer"
        // RAP "com.sap.gateway.srvd.my_srvd.v0001.Entity" */
        async getNamespace() {
            const oDataModel = this._oDataModel;
            if (this._sNamespace) {
                return Promise.resolve(this._sNamespace);
            } else {
                var that = this;
                return oDataModel.getMetaModel()
                    .requestObject("/$EntityContainer/")
                    .then(() => {
                        const sValue = oDataModel.getMetaModel()
                            .getData().$EntityContainer;
                        that._sNamespace = sValue.substring(
                            0, sValue.lastIndexOf(".")
                        );
                        return Promise.resolve(that._sNamespace);
                    }
                );
            }
        }

        async getActionPath(sEntitySet, bBound) {
            let sPath = "/";
            if (sEntitySet && (this._bOnPremise || bBound)) {
                const sNamespace = await this.getNamespace();
                if (bBound) {
                    return Promise.resolve(`${sNamespace}.`);
                } else {
                    return Promise.resolve(
                        `/${sEntitySet}/${sNamespace}.`
                    );
                }
            } else {
                return Promise.resolve(sPath);
            }            
        }
    }
});

Improvement

Posibility to get rid of the namespace declaration inside views.

Maybe introducing a variable like %NAMESPACE%that will be substituted during runtime.

SAP Feedback

The topic has been discussed but the usage is to close and will not be followed. In such a use case, you should create the binding inside the controller.

Metadata Driven Controls

The long awaited sap.ui.mdc, that will replace the v2 smart controls are still not availble.

Luckily, SAP has announced to support the Fiori Elements Building Blocks and the Flexible Programming Model also for Freestyle Apps.

Improvement

Availability of sap.ui.mdc to be feature complete with v4 odata model.

API Create key for collection name

Inside the v2 model, a heavily used feature was

ODataV2Model.createKey(sCollection, oKeyProperties)

to create the path for a given entity collection. This is currently not implemented in the v4 model.

You have to escape the key(s) using custom helpers accordingly:

js
sap.ui.define([
    ...
    "sap/ui/model/odata/v4/ODataUtils"
    ], function (..., ODataUtils) {
	"use strict";

    createPath: function (sEntity, mKeys) {
        var sParams = "";

        each(mKeys, function (sKey, sValue) {
            if (sParams.length > 0) {
                sParams += ",";
            }
            sParams += sKey + "=" + encodeURIComponent(sValue);
		});

		return "/" + sEntity + ((sParams.length > 0) 
            ? "(" + sParams + ")" : "");
	},
    
    formatLiteral: function (sKey, sType) {
        // format key value in dependency to odata type
		return ODataUtils.formatLiteral(sKey, (sType) 
            ? sType : "Edm.String");
	},

    getProductPath: function(oParameter) {
        return this.createPath("Product", {
			"BusinessPartner": this.formatLiteral(
                oParameter.BusinessPartner
            ),
			"ViewId": this.formatLiteral(oParameter.ViewId)
		})
    }
});

SAP Feedback

Starting with SAPUI5 1.107.0, the v4 oDataModel introduces methods getKeyPredicate() and requestKeyPredicate(), so you can simply build your context path this way:

js
return `/Product(${oDataModel.getKeyPredicate("/Product", mKeys)})`;