RAP Build-Along Part 6: OData V4 Service Binding and EML Testing
Part 6 of the RAP Bootcamp exposes the Travel + Booking BO through a service definition and OData V4 UI service binding, validates $metadata, and tests the BO end to end with EML — read, modify, create by association, actions, MAPPED/FAILED/REPORTED, COMMIT ENTITIES at the outer boundary — plus how to tell BO failures from exposure or UI failures.
Explanation
📌 KEY POINTS
✅ Service definition exposes projections; binding (OData V4 – UI) publishes the service.
✅ Test with the preview and $metadata, then with EML in a console class.
✅ EML: MODIFY ENTITIES → COMMIT ENTITIES; evaluate FAILED and REPORTED.
⚠️ Validations run at COMMIT ENTITIES, not at MODIFY.
▶ SAP RAP ODATA V4 TUTORIAL: SERVICE BINDING AND EML TESTING (PART 6)
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU WILL BUILD
| Artifact | Name |
|---|---|
| Service definition | ZUI_RAP_TRAVEL |
| Service binding (OData V4 – UI) | ZUI_RAP_TRAVEL_O4 |
| EML test class (classrun) | ZCL_RAP_TRAVEL_E2E |
| ABAP Unit test class | ZTC_RAP_TRAVEL (test class in behavior pool or separate) |
━━━━━━━━━━━━━━━━━━━━
▶ PREREQUISITES / PREVIOUS PART
- [Part 5](/lessons/rap-bootcamp-part-5-projection-fiori-elements): projections
ZC_RAP_Travel,ZC_RAP_Booking, projection behavior and metadata extensions active.
━━━━━━━━━━━━━━━━━━━━
▶ WHAT SERVICE DEFINITION AND BINDING ARE — AND ARE NOT
A service definition lists which projection entities are exposed and under which names. A service binding chooses the protocol and scenario (OData V2/V4, UI or Web API) and publishes the endpoint. Neither contains business logic. Validations, determinations, actions and authorization all live in the BO (Parts 3–4); consumer restrictions live in the projection (Part 5). If behavior is wrong, you fix the BO or projection, never the service.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 1 — SERVICE DEFINITION
*New* → *Business Services* → *Service Definition* ZUI_RAP_TRAVEL, referenced object ZC_RAP_Travel.
┏━━ CODE ━━━━━━━━━━━━
┃ define service ZUI_RAP_TRAVEL {
┃ expose ZC_RAP_Travel as Travel;
┃ expose ZC_RAP_Booking as Booking;
┃ expose I_Currency as Currency; // for value help, if used
┃ }
┗━━━━━━━━━━━━━━━━━━━
Expose the child explicitly; otherwise navigation _Booking has no target entity set. Expose value-help views referenced in annotations.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 2 — SERVICE BINDING (ODATA V4 – UI)
Right-click the service definition → *New Service Binding* ZUI_RAP_TRAVEL_O4, binding type OData V4 – UI. Activate, then press Publish.
- In SAP BTP ABAP environment and S/4HANA Cloud, Publish registers the service locally in the dev system.
- In on-premise / private S/4HANA, Publish performs the service registration for the local gateway (older releases required manual steps in
/IWFND/V4_ADMIN; release-dependent).
The binding editor lists entity sets and navigations. Select Travel → Preview to open the Fiori Elements preview.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 3 — VALIDATE $METADATA
Open the service URL + /$metadata (link in the binding editor). Check:
| Look for | Confirms |
|---|---|
| EntitySet Name="Travel" and Name="Booking" | Both exposed |
| NavigationProperty Name="_Booking" on Travel, ContainsTarget or navigation binding | Composition exposed |
| Action Name="acceptTravel" bound to Travel | Action exposed through projection use |
| Draft* properties, DraftAdministrativeData | Draft enabled |
| Core.OptimisticConcurrency / ETag property | ETag active |
| UI annotations (UI.LineItem, UI.Facets) | Metadata extension picked up |
If something is missing in $metadata, the problem is in exposure (service definition/projection), not in the BO.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 4 — PREVIEW END-TO-END IN FIORI ELEMENTS
- Go on the list → rows from
ZC_RAP_Travel. - Create → draft opens; enter Customer, dates, fee, currency.
- In the Bookings table, Create → add a booking; TotalPrice updates (determination from Part 4 — draft side effects may require refresh on older releases).
- Enter End < Begin → Save → validation message highlights date fields; activation blocked.
- Fix dates → Save → draft activated; TravelID assigned.
- Accept → status becomes Accepted, criticality green, Accept disabled, BookingFee read-only.
- Edit with a second user → "locked by" message.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 5 — EML: THE SAME SCENARIOS IN CODE
The UI is only one consumer. Test the BO directly with EML so failures are isolated from the UI. The classrun in the code section does, in order:
MODIFY ENTITIES ... CREATEa Travel with%cid, andCREATE BY \_Bookingtwo bookings using%cid_ref.- Inspect
MAPPEDto get the generated UUIDs;FAILED/REPORTEDfor interaction errors. COMMIT ENTITIES RESPONSE OF→ check commitFAILED(save-sequence validations).READ ENTITIESthe Travel andBY \_Booking→ verify TotalPrice and TravelID.EXECUTE acceptTravel→ checkRESULTstatus; commit.- Negative: create with invalid dates → expect commit failure and a message;
ROLLBACK ENTITIES.
━━━━━━━━━━━━━━━━━━━━
▶ MAPPED, FAILED, REPORTED
- MAPPED —
%cid→ key. For managed UUID numbering, the UUID is already available. - FAILED — per entity: instances that could not be processed, with
%fail-cause(not_found,unauthorized,disabled,locked, ...). - REPORTED — messages; read
%msg->if_message~get_text( ).
Interaction-phase failures (auth, feature control, locks) appear on MODIFY. Validation failures appear on COMMIT ENTITIES.
━━━━━━━━━━━━━━━━━━━━
▶ WHERE COMMIT ENTITIES BELONGS
COMMIT ENTITIES belongs only at the outer transaction boundary — the classrun, a report, a background job, a unit test. It is forbidden inside behavior handlers and inside code called from a RAP transaction (another BO's handler). OData requests commit automatically through the runtime; the UI never calls it.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 6 — ABAP UNIT FOR THE BO
Unit tests use EML too. In ABAP Cloud, use cl_botd_txbufdbl_bo_test_env / cl_botd_mockemlapi_bo_test_env (release-dependent test doubles) or simply test against the real BO in a test environment with cl_osql_test_environment doubles for the tables. Minimal test: create invalid Travel → expect failed-travel after commit; ROLLBACK ENTITIES in teardown.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 7 — RUNTIME LOGS AND DIAGNOSTICS
| Platform | Where to look |
|---|---|
| BTP ABAP / S/4HANA Cloud | ADT *Feed Reader* for runtime errors/dumps, *ABAP Cross Trace* for RAP/OData request tracing |
| On-premise / private | /IWFND/ERROR_LOG, /IWFND/GW_CLIENT to replay requests, ST22 for dumps, ABAP Cross Trace (SAT for performance) |
| Browser | DevTools Network tab: $batch response bodies contain OData error messages |
━━━━━━━━━━━━━━━━━━━━
▶ STEP 8 — BO FAILURE VS EXPOSURE/UI FAILURE
| Symptom | Test with EML | Conclusion |
|---|---|---|
| Error in UI, EML also fails | Fails | BO problem — handler, BDEF, mapping, auth |
| UI fails, EML works | Works | Exposure or UI — projection use, service definition, binding, annotations |
| Entity/action missing in $metadata | n/a | Service definition or projection behavior |
| Button missing but action in $metadata | n/a | Metadata extension dataAction |
| 403 on create in UI | EML without IN LOCAL MODE also unauthorized | Authorization in BO, role assignment |
| 412 Precondition Failed | n/a | ETag mismatch — concurrent change, refresh |
This split saves hours: always reproduce with EML before touching annotations.
━━━━━━━━━━━━━━━━━━━━
▶ CHECKPOINT
- [ ] Service definition exposes Travel, Booking (and value-help views).
- [ ] OData V4 UI binding active and published; preview opens.
- [ ]
$metadatashows entity sets, navigation, actions, draft and ETag. - [ ] EML test creates root + child, fails on invalid dates, executes Accept, reads back.
- [ ]
COMMIT ENTITIESonly in the test/classrun. - [ ] You can classify a failure as BO vs exposure vs UI.
━━━━━━━━━━━━━━━━━━━━
▶ TRY IT YOURSELF
Expose your read-only projection from Part 5 as ZAPI_RAP_TRAVEL_RO with an OData V4 – Web API binding. Confirm in $metadata that no create/update or actions are available, and that no UI annotations are needed.
━━━━━━━━━━━━━━━━━━━━
▶ INTERVIEW TAKEAWAY
"The service definition only lists exposed projection entities; the binding picks OData V4 UI or Web API and publishes. Neither contains logic. I validate $metadata for entity sets, navigation, actions, draft and ETag, preview in Fiori Elements, and test the BO with EML — create by association, actions, MAPPED/FAILED/REPORTED — committing only at the outer boundary. If EML works but the UI fails, it's exposure or annotations; if EML fails, it's the BO."
━━━━━━━━━━━━━━━━━━━━
▶ NEXT PART
[Part 7: Production Hardening, Clean Core, Security, Performance and Transport](/lessons/rap-bootcamp-part-7-production-hardening)
Code example
" ============================================================" Service definition ZUI_RAP_TRAVEL (no logic - exposure only)" ============================================================@EndUserText.label: 'Travel UI service - RAP Bootcamp'define service ZUI_RAP_TRAVEL { expose ZC_RAP_Travel as Travel; expose ZC_RAP_Booking as Booking; expose I_Currency as Currency;} " Service binding ZUI_RAP_TRAVEL_O4: created in ADT form editor" Binding type: OData V4 - UI -> Activate -> Publish -> Preview " ============================================================" End-to-end EML test - outer transaction boundary" ============================================================CLASS zcl_rap_travel_e2e DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. INTERFACES if_oo_adt_classrun. PRIVATE SECTION. METHODS print_reported IMPORTING reported TYPE RESPONSE FOR REPORTED LATE zi_rap_travel out TYPE REF TO if_oo_adt_classrun_out.ENDCLASS. CLASS zcl_rap_travel_e2e IMPLEMENTATION. METHOD if_oo_adt_classrun~main. DATA(today) = cl_abap_context_info=>get_system_date( ). " ---- 1) Create root + 2 children by association -------------- MODIFY ENTITIES OF zi_rap_travel ENTITY Travel CREATE FIELDS ( AgencyID CustomerID BeginDate EndDate BookingFee CurrencyCode Description ) WITH VALUE #( ( %cid = 'T1' AgencyID = '070001' CustomerID = '000010' BeginDate = today + 20 EndDate = today + 27 BookingFee = '30.00' CurrencyCode = 'EUR' Description = 'E2E happy path' ) ) CREATE BY \_Booking FIELDS ( CarrierID ConnectionID FlightDate FlightPrice CurrencyCode ) WITH VALUE #( ( %cid_ref = 'T1' %target = VALUE #( ( %cid = 'B1' CarrierID = 'LH' ConnectionID = '0400' FlightDate = today + 20 FlightPrice = '400.00' CurrencyCode = 'EUR' ) ( %cid = 'B2' CarrierID = 'LH' ConnectionID = '0401' FlightDate = today + 27 FlightPrice = '420.00' CurrencyCode = 'EUR' ) ) ) ) MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported). IF failed IS NOT INITIAL. out->write( 'Interaction phase failed (auth/feature/lock?)' ). ROLLBACK ENTITIES. RETURN. ENDIF. COMMIT ENTITIES RESPONSE OF zi_rap_travel FAILED DATA(c_failed) REPORTED DATA(c_reported). IF c_failed IS NOT INITIAL. print_reported( reported = c_reported out = out ). RETURN. ENDIF. DATA(travel_uuid) = mapped-travel[ %cid = 'T1' ]-TravelUUID. out->write( |1) Created travel { travel_uuid }| ). " ---- 2) Read back root + children ---------------------------- READ ENTITIES OF zi_rap_travel ENTITY Travel ALL FIELDS WITH VALUE #( ( TravelUUID = travel_uuid ) ) RESULT DATA(travels) ENTITY Travel BY \_Booking ALL FIELDS WITH VALUE #( ( TravelUUID = travel_uuid ) ) RESULT DATA(bookings). out->write( |2) TravelID { travels[ 1 ]-TravelID }, status { travels[ 1 ]-OverallStatus }, | && |total { travels[ 1 ]-TotalPrice } { travels[ 1 ]-CurrencyCode }, | && |{ lines( bookings ) } bookings| ). " Expected: status O, total 850.00 EUR, 2 bookings, TravelID assigned " ---- 3) Execute action --------------------------------------- MODIFY ENTITIES OF zi_rap_travel ENTITY Travel EXECUTE acceptTravel FROM VALUE #( ( TravelUUID = travel_uuid ) ) RESULT DATA(action_result) FAILED failed REPORTED reported. COMMIT ENTITIES. out->write( |3) After accept: { action_result[ 1 ]-%param-OverallStatus }| ). " A " ---- 4) Second accept must be rejected by feature control ---- MODIFY ENTITIES OF zi_rap_travel ENTITY Travel EXECUTE acceptTravel FROM VALUE #( ( TravelUUID = travel_uuid ) ) FAILED failed. out->write( |4) Re-accept failed as expected: { xsdbool( failed-travel IS NOT INITIAL ) }| ). ROLLBACK ENTITIES. " ---- 5) Negative: invalid dates must fail at save ------------- MODIFY ENTITIES OF zi_rap_travel ENTITY Travel CREATE FIELDS ( CustomerID BeginDate EndDate CurrencyCode ) WITH VALUE #( ( %cid = 'BAD' CustomerID = '000010' BeginDate = today + 10 EndDate = today + 5 CurrencyCode = 'EUR' ) ) FAILED failed. COMMIT ENTITIES RESPONSE OF zi_rap_travel FAILED c_failed REPORTED c_reported. out->write( |5) Invalid dates rejected: { xsdbool( c_failed-travel IS NOT INITIAL ) }| ). print_reported( reported = c_reported out = out ). ROLLBACK ENTITIES. ENDMETHOD. METHOD print_reported. LOOP AT reported-travel INTO DATA(r) WHERE %msg IS BOUND. out->write( | Travel msg: { r-%msg->if_message~get_text( ) }| ). ENDLOOP. LOOP AT reported-booking INTO DATA(rb) WHERE %msg IS BOUND. out->write( | Booking msg: { rb-%msg->if_message~get_text( ) }| ). ENDLOOP. ENDMETHOD.ENDCLASS. " ============================================================" ABAP Unit (Test Classes tab of ZBP_I_RAP_TRAVEL) - minimal" ============================================================CLASS ltc_travel DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. PRIVATE SECTION. METHODS invalid_dates_fail FOR TESTING. METHODS teardown.ENDCLASS. CLASS ltc_travel IMPLEMENTATION. METHOD invalid_dates_fail. DATA(today) = cl_abap_context_info=>get_system_date( ). MODIFY ENTITIES OF zi_rap_travel ENTITY Travel CREATE FIELDS ( CustomerID BeginDate EndDate CurrencyCode ) WITH VALUE #( ( %cid = 'X' CustomerID = '000010' BeginDate = today + 9 EndDate = today + 1 CurrencyCode = 'EUR' ) ). COMMIT ENTITIES RESPONSE OF zi_rap_travel FAILED DATA(f) REPORTED DATA(r). cl_abap_unit_assert=>assert_not_initial( act = f-travel msg = 'Validation should fail' ). ENDMETHOD. METHOD teardown. ROLLBACK ENTITIES. ENDMETHOD.ENDCLASS.Real project scenario
After a transport to QAS, users reported that the Accept button had vanished. Developers spent a day changing UI annotations. Running the EML classrun in QAS showed the action worked, and $metadata still contained acceptTravel — but the metadata extension had not been included in the transport. Adopting the rule "reproduce with EML, then check $metadata, then annotations" turned similar incidents into ten-minute diagnoses.
Common mistakes
- Putting logic or restrictions into the service layer thinking it is "the API" — the service only exposes.
- Forgetting to expose the child entity or value-help views in the service definition.
- Activating the binding but not publishing it (on-premise).
- Calling
COMMIT ENTITIESinside a handler or inside code called from another BO's transaction. - Ignoring commit-time
FAILED; validation failures only appear there. - Debugging annotations before confirming the BO works via EML.
- Using OData V2 UI binding for a new draft-enabled app without a reason; V4 is the modern default.
Best practices
- Keep service definitions thin: expose projection entities only.
- One binding per consumer type (UI, Web API).
- Always check
$metadataafter changes. - Test the BO with EML before and after UI changes; keep the classrun in the package.
- Commit only at outer boundaries; roll back in tests.
- Add ABAP Unit for key validations and actions.
- Use ABAP Cross Trace / gateway error logs rather than guessing.
Interview angle
Questions: "What does a service binding do?" "Difference between OData V4 UI and Web API binding?" "When do validation errors surface in EML?" "Where is COMMIT ENTITIES allowed?" "How do you debug a missing action button?" Answer with the BO-vs-exposure-vs-UI split and $metadata checks.
Practice this lesson
Quick check
1. A RAP OData request fails with HTTP 500. Where do you look first?
2. A console test changes a travel with MODIFY ENTITIES, but the change is not in the database. What is missing?
3. After a transport, an action button disappeared in QAS but works in DEV. What is the most likely cause?
Interview questions
1. A RAP OData call returns HTTP 500. How do you proceed?
Answer it in your own words first, then compare.
2. After changing a projection, the Fiori app still shows the old fields. Why and what do you do?
Answer it in your own words first, then compare.
Reference-verified: the steps are checked against official SAP documentation and tutorials, and object and field names in the sample code are checked for consistency. The code has not been compiled or run on an SAP system by ERPClimb. Syntax marked release-dependent varies by SAP release, so check it in your own system.
More lessons in this topic
- RAP Basics Part 1: RAP Architecture — Layers, Artifacts and When RAP Fits
- RAP Basics Part 10: EML — Entity Manipulation Language
- RAP Basics Part 11: Draft, Locking and Authorization
- RAP Basics Part 2: Data Model Design — Tables, Keys, Administrative Fields and Interface Views
- RAP Basics Part 3: Projection Layer and Metadata Extensions
- RAP Basics Part 4: Implementation Types — Managed, Unmanaged, Additional Save and Unmanaged Save
- RAP Basics Part 5: Root Entity, Child Entity and Composition
- RAP Basics Part 6: Behavior Definition — CRUD, Field Control and Features