Enterprise Resource Planning Blogs by Members
Gain new perspectives and knowledge about enterprise resource planning in blog posts from community members. Share your own comments and ERP insights today!
cancel
Showing results for 
Search instead for 
Did you mean: 
brunomulinari
Participant

Introduction

If you, as a developer, ever worked in multiple .NET projects that consume the Service Layer, you probably faced the issue where different projects may have different implementations on how they communicate with Service Layer. Often times these implementations are less than ideal and can lead to a number of issues and headaches that could be avoided.

Also, although RESTful APIs have concepts that are fairly easy to grasp, Service Layer has some particularities that can make it difficult to integrate with, specially for new developers. There is also the option to use the WCF/OData client, but this approach can be cumbersome, which is probably why most developers I know have chosen to write their own solutions instead.

B1SLayer aims to solve all that by abstracting Service Layer's complexity and taking care of various things under the hood automatically, saving precious development time and resulting in a cleaner and easier to understand code. B1SLayer seamlessly takes care of the authentication and session management, provides an easy way to perform complex requests with its fluent API, automatically retries failed requests and much more.


Getting started

The first thing to do is install it to your project. B1SLayer is available on NuGet and can be easily installed through the NuGet Package Manager in Visual Studio or the .NET CLI. The library is based on the .NET Standard 2.0 specification and therefore it is compatible with various .NET implementations and versions.

Once installed, you start by creating your SLConnection instance, providing it the required information for it to be able to connect to the API, that is, the Service Layer URL, database name, username and password.

The SLConnection instance is the most important object when using B1SLayer. All requests originate from it and it's where the session is managed and renewed automatically. Therefore, once initialized, this instance needs to be kept alive to be reused throughout your application's lifecycle. A common and simple way to achieve this is implementing a singleton pattern:

 

using B1SLayer;

public sealed class ServiceLayer
{
    private static readonly SLConnection _serviceLayer = new SLConnection(
        "https://localhost:50000/b1s/v1/",
        "SBO_COMPANYDBNAME",
        "username",
        "password");
    static ServiceLayer() { }
    private ServiceLayer() { }
    public static SLConnection Connection => _serviceLayer;
}

 

If your project supports the dependency injection (DI) design pattern, it's even simpler. You can add the SLConnection instance as a singleton service and later request it where you need it:

 

builder.Services.AddSingleton(serviceProvider => new SLConnection(
    "https://localhost:50000/b1s/v1/",
    "SBO_COMPANYDBNAME",
    "username",
    "password"));

 

For the following content in this post, I'm going to assume you have your SLConnection instance named "serviceLayer".


Performing requests

As mentioned earlier, B1SLayer manages Service Layer's authentication and session for you. This means you don't need to perform a login request as it is done automatically whenever necessary, although you can still login manually if you want to.

Another thing to keep in mind, is that B1SLayer does not include model classes for Service Layer's entities. This means you will need to create model classes like BusinessPartner or PurchaseOrder yourself.

Most B1SLayer requests can be divided into three parts: creation, configuration and execution, all done chaining calls in a fluent manner.

    • Creation is where you call the Request method from your SLConnection instance to specify which Service Layer resource you wish to request to, for instance: "BusinessPartners";
    • Configuration is optional, it's where you can specify parameters for the request, like query string, headers, etc. For instance: "$filter", "$select", "B1S-PageSize";
    • Execution is where the the HTTP request is actually performed (GET, POST, PATCH, DELETE). It's also where you can specify the return type or the JSON body, optionally.

In the example below, a GET request is created to the "PurchaseOrders" resource for a specific document with DocEntry number 155. The request is then configured to select only a couple fields of this entity and lastly the request is executed, deserializing the JSON result into a MyPurchaseOrder type object named purchaseOrder.

 

// Resulting HTTP request:
// GET /PurchaseOrders(155)?$select=DocEntry,CardCode,DocTotal
MyPurchaseOrderModel purchaseOrder = await serviceLayer // SLConnection object
    .Request("PurchaseOrders", 155) // Creation
    .Select("DocEntry,CardCode,DocTotal") // Configuration
    .GetAsync<MyPurchaseOrderModel>(); // Execution

 

Pretty straight forward, right? How about a more complex request? In the example below, a GET request is created to the "BusinessPartners" resource, then configured with Select, Filter and OrderBy query string parameters, then WithPageSize which adds the "B1S-PageSize" header parameter to the request to specify the number of entities to be returned per request, overwriting the default value of 20. Lastly, the request is performed and the JSON result is deserialized into a List of MyBusinessPartnerModel type named bpList.

 

// Resulting HTTP request:
// GET /BusinessPartners?$select=CardCode,CardName,CreateDate&$filter=CreateDate gt '2010-05-01'&$orderby=CardName
// Headers: B1S-PageSize=100
List<MyBusinessPartnerModel> bpList = await serviceLayer
    .Request("BusinessPartners")
    .Select("CardCode,CardName,CreateDate")
    .Filter("CreateDate gt '2010-05-01'")
    .OrderBy("CardName")
    .WithPageSize(100)
    .GetAsync<List<MyBusinessPartnerModel>>();

 

A POST request is even simpler, as you can see below. The "Orders" resource is requested and a new order document is created with the object newOrderToBeCreated serialized as the JSON body. If the entity is created successfully, by default Service Layer returns the created entity as the response, this response then is deserialized into a new a MyOrderModel type object named createdOrder.

 

// Your object to be serialized as the JSON body for the request
MyOrderModel newOrderToBeCreated = new MyOrderModel { ... };

// Resulting HTTP request:
// POST /Orders
// Body: newOrderToBeCreated serialized as JSON
MyOrderModel createdOrder = await serviceLayer
    .Request("Orders")
    .PostAsync<MyOrderModel>(newOrderToBeCreated);

 

What about PATCH and DELETE requests? Here I'm using an anonymous type object that holds the properties that I want to update in my business partner entity. PATCH and DELETE requests don't return anything, so there is nothing to deserialize.

 

// Your object to be serialized as the JSON body for the request
var updatedBpInfo = new { CardName = "SAP SE", MailAddress = "sap@sap.com" };

// Resulting HTTP request:
// PATCH /BusinessPartners('C00001')
// Body: updatedBpInfo serialized as JSON
await serviceLayer.Request("BusinessPartners", "C00001").PatchAsync(updatedBpInfo);

// Resulting HTTP request:
// DELETE /BusinessPartners('C00001')
await serviceLayer.Request("BusinessPartners", "C00001").DeleteAsync();

 


Specialized requests

Although the majority of requests to Service Layer will follow the format I presented above, there are some exceptions that needed a special implementation, these are called directly from your SLConnection object. Let's get into them.

Login and logout

Just in case you want to handle the session manually, here's how to do it:

 

// Performs a POST on /Login with the information provided in your SLConnection object
await serviceLayer.LoginAsync();

// Performs a POST on /Logout, ending the current session
await serviceLayer.LogoutAsync();

 


Attachments

Uploading and downloading attachments is very streamlined with B1SLayer. Have a look:

 

// Performs a POST on /Attachments2, uploading the provided file and returning the
// attachment details in a SLAttachment type object
SLAttachment attachment = await serviceLayer.PostAttachmentAsync(@"C:\temp\myFile.pdf");

// Performs a GET on /Attachments2({attachmentEntry}) with the provided attachment entry,
// downloading the file as a byte array
byte[] myFile = await serviceLayer.GetAttachmentAsBytesAsync(953);

 

Keep in mind that to be able to upload attachments through Service Layer, first some configurations on the B1 client and server are required. Check out the section "Setting up an Attachment Folder" in the Service Layer user manual for more details.


Ping Pong

This feature was added in version 9.3 PL10, providing a direct response from the Apache server that can be used for testing and monitoring. The result is a SLPingResponse type object containing the "pong" response and some other details. Check section "Ping Pong API" in the Service Layer user manual for more details.

 

// Pinging the load balancer
SLPingResponse loadBalancerResponse = await serviceLayer.PingAsync();

// Pinging a specific node
SLPingResponse nodeResponse = await serviceLayer.PingNodeAsync(2);

 


Batch requests

Although a powerful and useful feature, batch requests can be quite complicated to implement, but thankfully this is also very simple to do with B1SLayer. If you are not familiar with the concept, I recommend reading section "Batch Operations" in the user manual. In essence, it's a way to perform multiple operations in Service Layer with a single HTTP request, with a rollback capability if something fails.

Here each individual request you wish to send in a batch is represented as an SLBatchRequest object, where you specify the HTTP method, resource and optionally the body. Once you have all requests created, you send them through the method PostBatchAsync. The result is an HttpResponseMessage array containing the responses of each request you sent.

 

var postRequest = new SLBatchRequest(
    HttpMethod.Post, // HTTP method
    "BusinessPartners", // resource
    new { CardCode = "C00001", CardName = "I'm a new BP" }); // request body

var patchRequest = new SLBatchRequest(
    HttpMethod.Patch,
    "BusinessPartners('C00001')",
    new { CardName = "This is my updated name" });

var deleteRequest = new SLBatchRequest(HttpMethod.Delete, "BusinessPartners('C00001')");

// Here I'm passing each request individually, but you can also
// add them to a collection and pass it instead.
HttpResponseMessage[] batchResult = await serviceLayer
    .PostBatchAsync(postRequest, patchRequest, deleteRequest);

 


Conclusion

This is my first post here and it ended up longer than I expected, and even still, I couldn't possibly fit every feature of B1SLayer here, otherwise this post would be even longer. Nevertheless, I hope it gives you a good overall understanding of how it works and what it offers. I'll do my best to keep this post updated and up to the community standards, so any feedback is appreciated!

The source code for B1SLayer is available on GitHub. If you want to contribute, have any suggestions, doubts or encountered any issue, please, feel free to open an issue there or leave a comment below. I will try to reply as soon as possible.

112 Comments
brunomulinari
Participant
Hi, egil1077. Thank you for your kind words.

From what you have described, this is a scenario of high concurrency that is leading to update conflicts. You might want to check out the usage of ETag in Service Layer.

With ETag you can guarantee that you are handling the most up-to-date version of an entity.
egil1977
Explorer
Thank you for the quick response and taking the time to answer something off-topic!

Had a quick look at ETag, seems like it's what I need! 🙂

Thanks again!
GurayMollov
Explorer
0 Kudos

Hi Bruno,

The following model doesn't work for me. I couldn't find what is wrong.

When I make a request to Service Layer I can't get list InventoryCountingLines, it returns 0 count. On the other hand I can get the values InventoryCountingDocument such as DocumentEntry,DocumentNumber

List<InventoryCountingDocument> invCountLines = null;
invCountLines =await NINOVA.App.serviceLayer
.Request("InventoryCountings")
.Filter("DocumentNumber eq " + DocNumEntry.Text)
.GetAsync<List<InventoryCountingDocument>>();

Here is the json structure returned by PostMan

{
"@odata.context": "https://10.150.100.4:50000/b1s/v2/$metadata#InventoryCountings/$entity",
"DocumentEntry": 84,
"DocumentNumber": 1003366,
"InventoryCountingLines": [
{
"DocumentEntry": 84,
"LineNumber": 1,
"ItemCode": "Mobile Item 1",
"ItemDescription": "Mobile Item 1",
"Freeze": "tNO",
"WarehouseCode": "100",
"BinEntry": 1,
"InWarehouseQuantity": 52.0,
"Counted": "tNO",
"UoMCode": "бр.",
"BarCode": "3800051700013",
"UoMCountedQuantity": 0.0,
"ItemsPerUnit": 1.0,
"CountedQuantity": 0.0,
"LineStatus": "clsOpen",
"CounterType": "ctUser",
"MultipleCounterRole": "mcrIndividualCounter",
"InventoryCountingLineUoMs": [
{
"DocumentEntry": 84,
"LineNumber": 1,
"ChildNumber": 1,
"UoMCountedQuantity": 0.0,
"ItemsPerUnit": 1.0,
"CountedQuantity": 0.0,
"UoMCode": "бр.",
"BarCode": "3800051700013",
"CounterType": "ctUser",
"MultipleCounterRole": "mcrIndividualCounter"
}
],
"InventoryCountingSerialNumbers": [],
"InventoryCountingBatchNumbers": []
}
],
"InventoryCountingDocumentReferencesCollection": []
}

public class InventoryCountingDocument
{
public int DocumentEntry { get; set; }
public int DocumentNumber { get; set; }
public DateTime CountDate { get; set; }
public List<InventoryCountingLine> InventoryCountingLines { get; set; }
}
public class InventoryCountingLine
{
public int DocumentEntry { get; set; }
public int LineNumber { get; set; }
public string ItemCode { get; set; }
public string Freeze { get; set; }
public string WarehouseCode { get; set; }
public int BinEntry { get; set; }
public double InWarehouseQuantity { get; set; }
public string Counted { get; set; }
public string UoMCode { get; set; }
public string BarCode { get; set; }
public double UoMCountedQuantity { get; set; }
public double ItemsPerUnit { get; set; }
public double CountedQuantity { get; set; }
public string LineStatus { get; set; }
public string CounterType { get; set; }
public string MultipleCounterRole { get; set; }
public List<InventoryCountingLineUoM> InventoryCountingLineUoMs { get; set; }
}

public class InventoryCountingLineUoM
{
public int LineNumber { get; set; }
public int ChildNumber { get; set; }
public double UoMCountedQuantity { get; set; }
public double ItemsPerUnit { get; set; }
public double CountedQuantity { get; set; }
public string UoMCode { get; set; }
public string BarCode { get; set; }
public string CounterType { get; set; }
public string MultipleCounterRole { get; set; }
}

brunomulinari
Participant
0 Kudos

Hi, Guray. If you are selecting a single document by its ID, you should use the appropriate request:

 

GurayMollov
Explorer
0 Kudos
The solution is here I should use the proper request. Thanks Bruno, long live 🙂
fbada
Discoverer
0 Kudos
Love your contribution! ❤️ Simple and magnific!

I have two questions:

1º - When uploading attachments is there no option to indicate to overwrite the file if it exists?

2º - How long does it usually take you on average to create an order/delivery/... with basic data? it takes me between 3-5 seconds, is this normal?

 

Thanks!
brunomulinari
Participant
0 Kudos
Thanks!

The attachment line is replaced if it already exists when performing a PATCH request. The behavior when uploading attachments is detailed in the Service Layer User Manual.

Regarding the time it takes to create new documents, it will depend on a number of factors, like your database performance and object size. Usually, the more lines a document has, the longer it takes. I would say 3-5 seconds is about normal.
mucipilbuga
Participant
0 Kudos

Dear Bruno,

Perfect job. Thanks.

Is there any good tutorial or sample file to use your library in Visual Basic?

Regards,

Mucip:)

MUCHIRAJUNOIR
Explorer
0 Kudos

Hey Bruno

Thanks for this, i have been trying to work with SAPDIAPI and its so deprecated to work with anything  .NET 6 and above 

I gonna try this out but how do we know the names of the service Layers like BusinessPartners, Invoices , Orders etc. is there like a list to follow. 

Bình
Explorer
0 Kudos

Dear Bruno

Thanks for this library. 

My SAP BO DI API save a new document very slow (5-10 minus, sometime deadlock ..). When using B1SLayer time consumming is 3-7 minus and sometime display timeout ...

My problems is sometime B1SLayer create doublicate document (2, 3 ... documents), i don't know why.

Please help me. Thanks in advance.

jtregue
Discoverer
0 Kudos

Hi, first of all let me congratulate for this great tool.

Now, my question is, can I access this method, "SBOBobService_GetCurrencyRate"?

I want to query the currency, it seems to work on Postman but I get the following error with code:

"Error reading JObject from JsonReader. Current JsonReader item is not an object: Float. Path '', line 1, position 9."

Here is the sample, my best guess is it has something to do with this line:

var resPago = await serviceLayer.Request("SBOBobService_GetCurrencyRate").PostAsync<TipoDeCambio>(model);

Thanks in advance.

 

MUCHIRAJUNOIR
Explorer
0 Kudos

MUCHIRAJUNOIR_0-1714214017307.png

Am getting such when i try quering data

 

Labels in this area