B1 Service Layer: Develop extensions by embedding JavaScript
Tags:
Since 9.2 Patch 04, Service Layer (SL) allows users to develop their own extension application by embedding JavaScript in the server side.
1. JavaScript Parsing Engine
SL makes use of Chrome V8 Engine as the JavaScript parsing engine due to the following considerations.
- The parsing performance is of significant importance for SL. V8 is just the script engine known for its excellent performance.
- SL and V8 are both written in C++. This would make the integration more seamless and more easily.
The V8 JavaScript Engine is an open source JavaScript engine developed by The Chromium Project for the Google Chrome web browser. To get more details of V8, access https://developers.google.com/v8 .
2. JavaScript Extension Framework
To facilitate the development of extension application, SL provides a JavaScript framework for users to easily operate the business objects and services. The diagram as below shows the basic structure of the framework.
[Note]
- Besides the DICore and SLCore, V8, as a new C++ component, is integrated into SL.
- SL adds the C++/JavaScript interop layer to be responsible for the interaction between JavaScript and C++.
- On top of the interop layer, JavaScript SDK is designed to hiding the interactive details and provide a high level and simplified API for the application layer.
- Considering the fact that switching the context between C++ and JavaScript stack is not good for the performance, one target of providing the SDK is to decrease the context switching frequency.
- Users' JavaScript Extension application is suggested to be developed based on the JavaScript SDK.
3. JavaScript Entry Function
As each executable file has a main
entry function, each script file has to define entry functions. Conventionally, it is better to define four entry functions in each script file, just corresponding to the CRUD operations on entities.
Each entry function has the same name with the HTTP method. On receiving a request, the entry function having the same name with the http method of this request is triggered.
//The entry function for http request with the GET method
function GET(){
...
}
//The entry function for http request with the POST method
function POST(){
...
}
//The entry function for http request with the PATCH method
function PATCH(){
...
}
//The entry function for http request with the DELETE method
function DELETE(){
...
}
[Note]
- Due to a keyword compatibility issue in JavaScript, each entry function should be in upper-case, otherwise the function would be unrecognized.
4. JavaScript URL Mapping
Script files are triggered to run by sending requests to the specific script URL. To differentiate the script URL from the normal URL, SL provides a specific URL resource path for script by appending /script
to the original path /b1s/v1
as below:
/b1s/v1/script/
Considering the fact that different partner might define the script with the same name, SL identifies which script to run by combining the partner name and the script name as the unique identifier. The mapping rule for the URL pattern is:
/b1s/v1/script/{{partner name}}/{{script name}}
Requests sending to URLs with the above pattern are dispatched to the corresponding script function defined by the corresponding partner.
[Example]
The following request
POST /b1s/v1/script/mtcsys/items
will trigger the execution of the function POST
defined in item.js
provided by partner mtcsys
.
[Note]
- The prerequisite is to ensure the script file with the corresponding
ard
file are deployed into SLD by the partner. For more details about how to deploy scripts, please refer to chapter [JavaScript Deployment] .
5. JavaScript SDK
Similar with the DIAPI, the JavaScript SDK is intended to provide a group of APIs for programmers to easily operate on business services and business Objects. The APIs consist of entity CRUD, entity query, transactions, exceptions and http request/response.
JavaScript, as a weak-typed programming language, is built in with many favorable dynamic features. However, for the sake of programming experience and coding efficiency, the JavaScript SDK is designed like a static-language library, so as to make most use of the auto-complete and IntelliSense functionalities provided by the modern IDE. The recommended one is the Visual Studio 2013/2015 with a Node.js plug-in (https://www.visualstudio.com/en-us/features/node-js-vs.aspx), as shown below:
Admittedly, you can also choose to program dynamically and enjoy the flexible features born with JavaScript.
[Note]
- This SDK is designed to purposely follow the Common JavaScript Specification and approximates the Node.js grammar, which is exactly the reason why the Node.js plug-in is recommended.
5.1 Http Request API
Http request functions as below are packaged in the module HttpModule.js
, which is an essential module to be required to handle http request.
API Name | API Description |
---|---|
getContet() | Returns the raw content from the request payload. |
getJsonObject() | Returns the JSON format of the request payload. |
getMethod() | Returns the Htttp Verb, e.g. GET, POST, PATCH, DELETE. |
getContentType() | Returns the MIME type of the request body (e.g. APPLICATION/JSON) |
getParameter(name) | Returns the value of a request parameter as a String, or null if the parameter does not exist. |
getParameterNames() | Returns an array of String objects containing the names of the parameters contained in this request. |
getEntityKey() | Returns the entity key from the URL resource part. |
getHeader(name) | Returns the value of the specified request header as a String. |
5.2 Http Response API
Http response functions as below are packaged in the module HttpModule.js
, which is an essential module to be required to handle http response.
API Name | API Description |
---|---|
setHeader(name, value) | Adds a response header with the given name and value. |
setContentType(contentType) | Set the content type of the response being sent to client. |
setCharSet(charset) | Sets the character encoding (MIME charset) of the response being sent to the client, for example, to UTF-8. |
setStatus(status) | Sets the status code for this response. |
setContent(content) | Set the content in the response body |
send(status, content) | Send back the response to the client with the optional http status and content |
[Example]
For such a request as below,
PATCH /b1s/v1/script/mtcsys/items('i001')?key1=val1 & key2=val2
DataServiceVersion:3.0
{ "ItemName": "new name" }
apply the following script to handle this request:
var http = require('HttpModule.js');
function PATCH() {
console.log("testing the http request and http response API...")
var ret = {};
ret.content = http.request.getJsonObj();
ret.method = http.request.getMethod();
ret.contentType = http.request.getContentType();
ret.dataServiceVersion = http.request.getHeader("DataServiceVersion");
ret.paramNames = http.request.getParameterNames();
if (ret.paramNames && ret.paramNames.length) {
ret.paramNames.forEach(function (param) {
ret[param] = http.request.getParameter(param);
});
}
ret.key = http.request.getEntityKey();
http.response.setContentType(http.ContentType.APPLICATION_JSON);
http.response.setStatus(http.HttpStatus.HTTP_OK);
http.response.setContent(ret);
http.response.send();
}
On success, SL returns:
HTTP/1.1 200 OK
{
"content": { "ItemName": "new name" },
"method": "PATCH",
"contentType": "text/plain;charset=UTF-8",
"dataServiceVersion": "3.0",
"paramNames": [ "key1", "key2" ],
"key1": "val1",
"key2": "val2",
"key": "'i001'"
}
[Note]
- Similar as Node.js,
require
is a global function to import a module and return a reference to that module. The above example indicateshttp
is a reference of the moduleHttpModule.js
. request
andresponse
are two members ofhttp
, representing a pre-createdHttpRequest
instance of and aHttpResponse
instance respectively.- To facilitate HTTP programming, module
HttpModule.js
also defines HTTP utility constants, e.g.HttpStatus
,ContentType
.
5.3 Entity CRUD API
Each exposed entity supports CRUD operations by default. The relevant APIs are packaged in the module ServiceLayerContext.js
.
- For most cases, to perform CRUD operations on an entity, an entity instance has to be created at first if the entity name is known in advance. Then call the following group of APIs defined in the prototype of EntitySet:
[Prototype of EntitySet]
API Name | API Description |
---|---|
add(content, callback) | Create an entity by the content and the optional callback function on creation. |
get(key, callback) | Retrieve an entity by the key and the optional callback function on retrieval. |
update(content, key, callback) | Update an entity by the content, key and the optional callback function on update. |
remove(key, callback) | Remove an entity by the key and the optional callback function on removal. |
... | ... |
- For the scenario where the entity name is not know in advance or the entity is a dynamically created UDO, a ServiceLayerContext instance has to be created at first. Then call the following group of APIs against this instance.
[Prototype of ServiceLayerContext]
API Name | API Description |
---|---|
add(name, content, callback) | Create an entity by the name, content and the optional callback function on creation. |
get(name, key, callback) | Retrieve an entity by the name, key, and the optional callback function on retrieval. |
update(name, content, key, callback) | Update an entity by the name, content and key and the optional callback function on update. |
remove(name, key, callback) | Remove an entity by the name, key and the optional callback function on removal. |
... | ... |
[Example]
For such a request as below,
POST /b1s/v1/script/mtcsys/test_items_more
apply the following script to handle this request:
var ServiceLayerContext = require('ServiceLayerContext.js');
var Item = require('EntityType/Item.js');
var http = require('HttpModule.js');
var test_item_code = "i001";
function POST() {
var slContext = new ServiceLayerContext();
var ret = [];
var item = new Item();
item.ItemCode = test_item_code;
var dataSrvRes = slContext.Items.add(item);
if (!dataSrvRes.isOK()) {
throw http.ScriptException(http.HttpStatus.HTTP_BAD_REQUEST, "create entity failure")
}
ret.push({ "operation": dataSrvRes.operation, "status": dataSrvRes.status });
var key = test_item_code;
var dataSrvRes = slContext.Items.get(key);
if (!dataSrvRes.isOK()) {
throw http.ScriptException(http.HttpStatus.HTTP_INTERNAL_SERVER_ERROR, "retrieve entity failure")
}
ret.push({ "operation": dataSrvRes.operation, "status": dataSrvRes.status });
item.ItemName = 'new_item_name';
dataSrvRes = slContext.update("Items", item, key);//equivalent to slContext.Items.update(item, key);
if (!dataSrvRes.isOK()) {
throw http.ScriptException(http.HttpStatus.HTTP_INTERNAL_SERVER_ERROR, "update entity failure")
}
ret.push({ "operation": dataSrvRes.operation, "status": dataSrvRes.status });
dataSrvRes = slContext.remove("Items", key);//equivalent to slContext.Items.remove(key);
if (!dataSrvRes.isOK()) {
throw http.ScriptException(http.HttpStatus.HTTP_INTERNAL_SERVER_ERROR, "delete entity failure")
}
ret.push({ "operation": dataSrvRes.operation, "status": dataSrvRes.status });
http.response.send(http.HttpStatus.HTTP_OK, ret);
}
On success, SL returns:
HTTP/1.1 200 OK
[
{ "operation": "add", "status": 201 },
{ "operation": "get", "status": 200 },
{ "operation": "update", "status": 204 },
{ "operation": "remove", "status": 204 }
]
5.4 Entity Query API
Query APIs are packaged in the module ServiceLayerContext.js
, and similar with the CRUD API, they are defined both on the EntitySet
and the ServiceLayerContext
prototype.
[Prototype of EntitSet]
API Name | API Description |
---|---|
query(queryOption, isCaseInsensitive) | Perform a case-sensitive or case-insensitive query and return the entities satisfying the query options. |
count(queryOption, isCaseInsensitive) | Perform a case-sensitive or case-insensitive query and return the number of the entities satisfying the query options. |
... | ... |
[Prototype of ServiceLayerContext]
API Name | API Description |
---|---|
query(name, queryOption, isCaseInsensitive) | Perform a case-sensitive or case-insensitive query and return the entities with the given name and satisfying the query options. |
count(name, queryOption, isCaseInsensitive) | Perform a case-sensitive or case-insensitive query and return the number of the entities with the given name and satisfying the query options. |
... | ... |
[Note]
- By default, the query is case-sensitive, due to the default Unicode collation for Hana database.
- Specifying the flag isCaseInsensitive as true would issue a case insensitive query. However, the query performance would not be as efficient as the case-insensitive case.
[Example]
For such a request as below,
GET /b1s/v1/script/mtcsys/test_query_businesspartner
apply the following script to handle this request:
var ServiceLayerContext = require('ServiceLayerContext.js');
var http = require('HttpModule.js');
function GET() {
var queryOption = "$select=CardName, CardCode & $filter=contains(CardCode, 'c1') & $top=5 & $orderby=CardCode";
var slContext = new ServiceLayerContext();
var retCaseSensitive = slContext.BusinessPartners.query(queryOption);
var retCaseInsensitive = slContext.query("BusinessPartners", queryOption, true);
http.response.setStatus(http.HttpStatus.HTTP_OK);
http.response.setContent({ "CaseSensitive": retCaseSensitive.toArray(), "CaseInsensitive": retCaseInsensitive.toArray() });
http.response.send();
}
On Success, SL returns:
HTTP/1.1 200 OK
{
"CaseSensitive": [
{
"CardCode": "c1",
"CardName": "customer c11"
}
],
"CaseInsensitive": [
{
"CardCode": "c1",
"CardName": "customer c11"
},
{
"CardCode": "C11",
"CardName": null
},
{
"CardCode": "C12",
"CardName": null
}
]
}
5.5 Transaction API
Transaction APIs as below are packaged in the module ServiceLayerContext.js
, which is an essential module to be required to control transactions.
API Name | API Description |
---|---|
startTransaction | Start a transaction. |
commitTransaction | Commit a transaction. |
rollbackTransaction | Rollback a transaction |
isInTransaction | return true if the current operation is in a transaction |
[Example]
For such a request as below,
POST /b1s/v1/script/mtcsys/test_create_businesspartner
[
{
"CardCode": "c001",
"CardName": "c001"
},
{
"CardCode": "c002",
"CardName": "c002"
},
{
"CardCode": "c003",
"CardName": "c003"
},
{
"CardCode": "c004",
"CardName": "c004"
},
{
"CardCode": "c005",
"CardName": "c005"
}
]
apply the following script to handle this request:
var ServiceLayerContext = require('ServiceLayerContext.js');
var http = require('HttpModule.js');
var BusinessPartner = require('EntityType/BusinessPartner.js');
function POST() {
var slContext = new ServiceLayerContext();
var bpList = http.request.getJsonObj();
if (!(bpList instanceof Array)) {
throw http.ScriptException(http.HttpStatus.HTTP_BAD_REQUEST, "invalid format of payload");
}
slContext.startTransaction();
for (var i = 0; i < bpList.length; ++i) {
var res = slContext.BusinessPartners.add(bpList[i]);
if (!res.isOK()) {
slContext.rollbackTransaction();
throw http.ScriptException(http.HttpStatus.HTTP_BAD_REQUEST, res.getErrMsg());
}
};
slContext.commitTransaction();
http.response.setContentType(http.ContentType.TEXT_PLAIN);
http.response.send(http.HttpStatus.HTTP_OK, "transaction committed");
}
On Success, SL returns:
HTTP/1.1 200 OK
transaction committed
Send this request again, SL returns:
HTTP/1.1 400 Bad Request
{
"error": {
"code": 600,
"message": {
"lang": "en-us",
"value": "1320000140 - Business partner code 'c001' already assigned to a business partner; enter a unique business partner code"
}
}
}
[Note]
- Programmers should be aware of the transaction operations are expensive and big transactions are bad for web service throughput. Thus, SL imposes a limitation on the transaction size. The total operations in one transaction should be no more than 10.
- Please keep in mind
startTransaction
/commitTransaction
orstartTransaction
/rollbackTransaction
should be called in pair.
5.6 Exception API
5.6.1 Compile Exception
SL responds error message to client if there is compilation error in users' script.
[Example]
var Document = require('EntityType/Document.js');
//type mistake: ';' should be ','
var line = Document.DocumentLine.create({
ItemCode: 'i001'; Quantity: 2, UnitPrice: 10
});
var lines = new Document.DocumentLineCollection();
lines.add(line);
The above code would result in such an error message as below:
{
"error": {
"code": 511,
"message": {
"lang": "en-us",
"value": "Script error: compile error [SyntaxError: Unexpected token ;]."
}
}
}
5.6.2 Runtime Exception
SL responds error message to client if there is runtime error in users' script.
[Example]
var ServiceLayerContext = require('ServiceLayerContext.js');
//var Bank = require('EntityType/Bank.js');
var bank = new Bank();
bank.BankCode = 'bank01';
var res = new ServiceLayerContext().Banks.add(bank);
if (!res.isOK) {
}
The above code would result in such an error message as below:
HTTP/1.1 400 Bad Request
{
"error": {
"code": 512,
"message": {
"lang": "en-us",
"value": "Script error: runtime error [ReferenceError: Bank is not defined]."
}
}
}
5.6.3 Users' Exception
SL also allows users to explicitly propagate exception by throwing ScriptException
exported from the http module.
[Example]
var ServiceLayerContext = require('ServiceLayerContext.js');
var Order = require('EntityType/Document.js');
var http = require('HttpModule.js');
var slContext = new ServiceLayerContext();
var res = slContext.Orders.get(10000);
if (!res.isOK()) {
throw new http.ScriptException(http.HttpStatus.HTTP_NOT_FOUND, "the given order is not found");
}
The above code would result in such an error message as below:
HTTP/1.1 404 Not Found
{
"error": {
"code": 600,
"message": {
"lang": "en-us",
"value": "the given order not found"
}
}
}
6 Logging
Currently debugging script is not supported. However, uses are allowed to log the key information during script programming by using the API console.log
console.log('Hello, Service Layer Scripting!');
[Note]
console
is a global object. Literally, the output of this object should be printed in the console. However, considering SL is a backend service, the output is redirected to log files under{SL Installation Path}/logs/script/
7 JavaScript SDK Generator Tool
Considering in each patch there might be new business objects exposed or new changes made on the existing objects, the SDK would be adjusted to adapt to the changes accordingly.
To manually maintain the SDK would not only need huge efforts, but also be error-prone. To automatically address this issue, a tool named Metadata2JavaScript
is provided to generate the SDK according to the metadata, as metadata reflects all changes on the business objects.
This tool supports to generate the SDK in two ways:
[ From a local metadata file ]
Metadata2JavaScript -a {local metadata file} -o {output folder, default is ./b1s_sdk}
or
Metadata2JavaScript --addr {local metadata file} --output {output folder, default is ./b1s_sdk}
For example:
Metadata2JavaScript -a metadata.xml -o ./output
[ From a remote SL instance ]
Metadata2JavaScript -a {SL base url} -u {user} -p {password} -c {company} -o {output folder, default is ./b1s_sdk}
or
Metadata2JavaScript --addr {SL base url} --user {user} --password {password} --company {company} --output {output folder, default is ./b1s_sdk}
For example:
Metadata2JavaScript --addr https://10.58.136.174:50000/b1s/v1/ --user manager --password 1234 --company SBODEMOUS
[Note]
- This tool is released together with SL and available in the bin folder of the SL installation path.
8 JavaScript Deployment
SL reuses the extension manager to manage the life cycle of script files. Similar with the DIAPI add-on, extension applications developed by SL are deployed to SLD as well.
Assume you have a script file Items.js
, take the following steps to deploy it.
1. Create an ard
file named Items.ard as the below format to describe the meta of this script file. Meanwhile, the ard
file is specifically used for the purpose of determining the script URL path.
<?xml version="1.0" encoding="utf-8"?>
<AddOnRegData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
SlientInstallation="" SlientUpgrade="" Partnernmsp="mtcsysnm" SchemaVersion="3.0"
Type="ServiceLayerScript" OnDemand="" OnPremise="" ExtName="ItemsExt"
ExtVersion="1.00" Contdata="sa" Partner="mtcsys" DBType="HANA" ClientType="S">
<ServiceLayerScripts>
<Script Name="items" FileName="Items.js"></Script>
</ServiceLayerScripts>
<XApps>
<XApp Name="" Path="" FileName="" />
</XApps>
</AddOnRegData>
2. Compress the ard
file and script file into a zip file (e.g. Items.zip).
3. Upload Items.zip to the extension manager from the Extension Import Wizard.
4. Assign the extension application to one company from the Extension Assignment Wizard.
5. Login the company with SL and access the script by the following URL.
/b1s/v1/script/mtcsys/items
[Note]
- The script URL is a combination of partner name and the script name attribute separated by a '/' appended to the SL base URL
/b1s/v1/
- Currently, SL does not support compressing multiple script files into one
ard
file. - About more details about how to deploy extension applications, please look up the release documentation of
SAP Business One Extension Manager
. - Do not name the value of the attribute
Partner
astest
, astest
is a reserved word for internal testing.
9 Typical User Cases to Apply Script
9.1 Complex Transactions
Scripting can be used in the transaction scenarios, which is an important complement to the OData Batch operations. The following example is to add an order and a delivery based on the order in one transaction, which would be impossible without scripting.
[Example]
var ServiceLayerContext = require('ServiceLayerContext.js');
var http = require('HttpModule.js');
var Order = require('EntityType/Document.js');
var DeliveryNote = require('EntityType/Document.js');
/*
* Entry function for the POST http request.
*
*/
function POST() {
var order = new Order();
order.CardCode = 'c1';
order.DocDate = new Date();
order.DocDueDate = new Date();
var line = new Order.DocumentLine();
line.ItemCode = 'i1';
line.Quantity = 1;
line.UnitPrice = 10;
var line2 = new Order.DocumentLine();
line2.ItemCode = 'i2';
line2.Quantity = 1;
line2.UnitPrice = 10;
order.DocumentLines = new Order.DocumentLineCollection();
order.DocumentLines.add(line);
order.DocumentLines.add(line2);
var slContext = new ServiceLayerContext();
//start the transaction
slContext.startTransaction();
var res = slContext.Orders.add(order);
if (!res.isOK()) {
slContext.rollbackTransaction();
return http.response.send(http.HttpStatus.HTTP_BAD_REQUEST, res.body);
}
//get the newly created order from the response body.
var newOrder = Order.create(res.body);
//create a delivery based on the order
var deliveryNote = new DeliveryNote();
deliveryNote.DocDate = newOrder.DocDate;
deliveryNote.DocDueDate = newOrder.DocDueDate;
deliveryNote.CardCode = newOrder.CardCode;
deliveryNote.DocumentLines = new DeliveryNote.DocumentLineCollection();
for (var lineNum = 0; lineNum < order.DocumentLines.length; ++lineNum) {
var line = new DeliveryNote.DocumentLine();
line.BaseType = 17;
line.BaseEntry = newOrder.DocEntry;
line.BaseLine = lineNum;
deliveryNote.DocumentLines.add(line);
}
res = slContext.DeliveryNotes.add(deliveryNote);
if (!res.isOK()) {
slContext.rollbackTransaction();
return http.response.send(http.HttpStatus.HTTP_BAD_REQUEST, res.body);
} else {
slContext.commitTransaction();
return http.response.send(http.HttpStatus.HTTP_CREATED, res.body);
}
}
9.2 Customized Business Logic (e.g. UDO)
Another typical case for scripting is to add customized business logic during the process of operating UDO. The following example is to do some validations and calculate the DocTotal
when creating the UDO named MyOrder
.
POST /b1s/v1/script/mtcsys/test_myorder
{
"U_CustomerName": "c1",
"U_DocTotal": 0,
"MyOrderLinesCollection": [
{
"U_ItemName": "i1",
"U_Price": 100,
"U_Quantity": 3
},
{
"U_ItemName": "i2",
"U_Price": 80,
"U_Quantity": 4
}
]
}
Apply the following script to handle this above request:
function POST() {
//Before creating the UDO, users are allowed to add extra logic.
var myOrder = http.request.getJsonObj();
var slContext = new ServiceLayerContext();
//Example 1 : added logic to validate if each item exists and the item stock is enough.
myOrder.MyOrderLinesCollection.forEach(function (line) {
var dataSvcRes = slContext.Items.get(line.U_ItemName);
if (!dataSvcRes.isOK()) {
throw new http.ScriptException(http.HttpStatus.HTTP_NOT_FOUND, "item not found");
} else {
//Convert weak type to strong type by calling Item.create. The conversion is not a must.
//You can also use dataSvcRes.body.QuantityOnStock
var item = Item.create(dataSvcRes.body);
if (item.QuantityOnStock < line.U_Quantity) {
throw new http.ScriptException(http.HttpStatus.HTTP_BAD_REQUEST, "not enough items on stock");
}
}
});
//Example 2 : added logic to calculate the DocTotal
myOrder.U_DocTotal = 0;
myOrder.MyOrderLinesCollection.forEach(function (line) {
myOrder.U_DocTotal += (line.U_Price * line.U_Quantity);
});
//Add this UDO
var res = slContext.add("MyOrder", myOrder);
if (res.isOK()) {
http.response.send(http.HttpStatus.HTTP_CREATED, res.body);
} else {
http.response.send(http.HttpStatus.HTTP_BAD_REQUEST, res.body);
}
}
10 Consume Script Service from .Net Application
For the consideration of flexibility, SL allows the response from the script is highly-customized. It is not appropriate to define the fixed meta data for the scripting, and as such, using the single WCF
framework is not possible to consume the script service. As an alternative, it is suggested to turn to programming with the .NET Web Http library mixed with WCF, illustrated with the below code snippet.
[TestFixture]
class ScriptOrdersTest : AppCommon.GeneralTestGroup
{
[SetUp]
public void setup()
{
ServicePointManager.ServerCertificateValidationCallback += delegate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors ssl) { return true; };
ServicePointManager.Expect100Continue = false;
ServicePointManager.MaxServicePointIdleTime = 2000;
}
private string m_cookie = AppCommon.WebConnection.Instance.SessionID;
private Uri m_baseUri = new Uri(AppCommon.ConfigInfo.Instance().SL_URL);
private int m_docEntry = 0;
[Test]
public void test01_create()
{
Document order = new Document();
order.CardCode = "c1";
order.DocDate = DateTime.Now;
order.DocDueDate = DateTime.Now;
{
DocumentLine line = new DocumentLine();
line.LineNum = 1;
line.ItemCode = "i1";
line.Quantity = 1;
line.UnitPrice = 10;
order.DocumentLines.Add(line);
}
{
DocumentLine line = new DocumentLine();
line.LineNum = 2;
line.ItemCode = "i2";
line.Quantity = 1;
line.UnitPrice = 10;
order.DocumentLines.Add(line);
}
try
{
var setting = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore };
string json = JsonConvert.SerializeObject(order, setting);
var data = Encoding.ASCII.GetBytes(json);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(m_baseUri, "script/test/test_orders"));
request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);
request.Method = "POST";
request.KeepAlive = false;
request.Headers["Cookie"] = m_cookie;
request.ContentType = "application/json;odata=minimalmetadata";
request.ContentLength = data.Length;
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Assert.AreEqual(response.StatusCode, HttpStatusCode.Created);
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
Document newEntity = JsonConvert.DeserializeObject<Document>(responseString);
Assert.IsTrue(newEntity.DocEntry > 0);
Assert.AreEqual(newEntity.DocumentLines.Count(), order.DocumentLines.Count());
response.Close();
m_docEntry = newEntity.DocEntry;
}
catch (WebException ex)
{
WebResponse response = ex.Response;
if (response == null)
{
throw SetResultMessage(ex);
}
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
throw SetResultMessage(new Exception(responseString));
}
catch (Exception ex)
{
throw SetResultMessage(ex);
}
}