Skip to Content
Technical Articles
Author's profile photo Bruno Mulinari

B1SLayer: A clean and easy way to consume SAP Business One Service Layer with .NET

In this blog post I will present to you an overview of B1SLayer, an open source .NET library created by me that aims to make Service Layer integrations as clean and easy as possible.


If you, as a developer, ever worked in multiple .NET projects that consume 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(

    static ServiceLayer() { }

    private ServiceLayer() { }

    public static SLConnection Connection => _serviceLayer;

If you are developing an ASP.NET Core application, like a Web API, it’s even simpler. You can take advantage of the dependency injection (DI) design pattern for later requesting the SLConnection instance where you need it:

builder.Services.AddSingleton(serviceLayer => new SLConnection(

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 bellow, 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 bellow, 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
    .Filter("CreateDate gt '2010-05-01'")

A POST request is even simpler, as you can see bellow. 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

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 = "" };

// 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();


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(
    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);


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 bellow. I will try to reply as soon as possible.

Assigned Tags

      You must be Logged on to comment or reply to a post.
      Author's profile photo Hackerman temper
      Hackerman temper

      This is great, I have a question about the updates,
      I want to implement it in the company where I am. I won't have any problems later if SAP does an update. Are you going to be constantly updating to improve it or is it up to us as developers to solve if we have any problems

      Author's profile photo Bruno Mulinari
      Bruno Mulinari
      Blog Post Author

      I plan to keep updating it with improvements and fixes for the time being. So far, I've yet to see an SAP update break anything in the way B1SLayer works, so I believe that's unlikely.

      Also, keep in mind B1SLayer is open source, so anyone can contribute to it. If you encounter any problem, open an issue on GitHub detailing what's happening and I'll get to it as soon as possible.

      Author's profile photo Hackerman temper
      Hackerman temper

      I'm trying it right now thank you very much for the answer

      Author's profile photo Beka Latsabidze
      Beka Latsabidze

      Very good project, Thanks Bruno.


      Only thing I feel missing is "IncludeCount" .


      Good Luck

      Author's profile photo Bruno Mulinari
      Bruno Mulinari
      Blog Post Author

      Hi, Beka. Thank you for your comment.

      Can you explain what exactly you mean by "IncludeCount"?

      Author's profile photo Beka Latsabidze
      Beka Latsabidze

      Hi Bruno.


      For example:

      var query = slContext.Items.IncludeCount().ToList()

      returns item list with count property.



      Author's profile photo Adan Garcia
      Adan Garcia

      Muchas gracias. Lo estoy implementando en un nuevo proyecto y es excelente

      Author's profile photo Bruno Mulinari
      Bruno Mulinari
      Blog Post Author

      Thank you, Adan!

      Author's profile photo Hermann V. Rösch M.
      Hermann V. Rösch M.

      Dear Bruno,

      Thanks for your article and

      Excellent piece of work!

      I have a scenario where I want to process a huge amount of rows.  i.e.

      serviceLayer.AfterCall(async call =>


      Console.WriteLine($"Call duration: {call.Duration.Value.TotalSeconds} seconds");



      var invoiceList = await serviceLayer.Request("Invoices").WithPageSize(100)


      .Filter($"DocEntry ge 1000")



      Question: As it processes each 100 page chunk, is there a way to access the returned invoiceList and save the returned data before going on to get the next 100 pr less chunk?  I tried to do it without success within the

      serviceLayer.AfterCall(async call => {});

      It looks like the parameters used in the serviceLayer.Request() were immutable.  It does not matter if I make invoiceList as a global static variable.  I even tried to make the

      .Filter($”DocEntry ge {nextDocEntryChunk}”) variable,

      but it did not change.   It looks like it is a single request that will be processed as N calls until the last result set arrives.

      Greetings from Panama,


      Author's profile photo Bruno Mulinari
      Bruno Mulinari
      Blog Post Author

      Hi, Hermann. Thanks for your comment.

      In your case, if you want to process the page chunks as they are obtained, instead of using the GetAllAsync method, it's probably better if you loop the requests yourself using the GetAsync method in conjunction with Skip until you to go through all records.

      The AfterCall method is intended to be used for logging and monitoring the requests.

      Author's profile photo Hermann V. Rösch M.
      Hermann V. Rösch M.

      Hi Bruno,

      Thanks for your quick response. I did it as you advise. So, at the end the code worked fine doing something like:

      int lastDocNum = someNumber;
      for (;;)
      	var serviceLayer = new SLConnection(... // my stuff
      	var invoiceList = await serviceLayer.Request("Invoices")
      		.Filter($"DocNum gt {lastDocNum}")
      	if (invoiceList.IsNullOrEmpty())
      	foreach (var i in invoiceList)
      		// more of my stuff
      		lastDocNum = i.DocNum;	
      	await serviceLayer.LogoutAsync();
      Author's profile photo George Pahakis
      George Pahakis

      Dear Bruno


      Thank you for your Article on Service Layer and the tools you provided . Is there any chance to provide a version in VB.Net? I believe it will very much appreciated.


      Best Regards

      George Pachakis

      Author's profile photo Bruno Mulinari
      Bruno Mulinari
      Blog Post Author

      Hi, George. Thank you for your comment.

      Although B1SLayer is developed in C#, I believe there's nothing stopping you from using it in your VB.NET projects. You can install it from NuGet the same way as I described in this blog post.

      I'm not a VB developer, so let me know if you encounter any issues.

      Author's profile photo Christian Peter
      Christian Peter

      Dear Bruno

      I’m using B1SLayer since May 2021 and I just want to say thank you for this excellent piece of software!

      Greetings from Switzerland


      Author's profile photo Bruno Mulinari
      Bruno Mulinari
      Blog Post Author

      Hi, Christian! Thank you so much for the kind words.

      I'm glad that B1SLayer is proving to be useful for you. Cheers!