Technical Articles
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.
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 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(
"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.
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
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.
I'm trying it right now thank you very much for the answer
Very good project, Thanks Bruno.
Only thing I feel missing is "IncludeCount" .
Good Luck
Hi, Beka. Thank you for your comment.
Can you explain what exactly you mean by "IncludeCount"?
Hi Bruno.
For example:
var query = slContext.Items.IncludeCount().ToList()
returns item list with count property.
Regards
Hi, Beka. Just to let you know that this feature was added in version 1.3.1.
Muchas gracias. Lo estoy implementando en un nuevo proyecto y es excelente
Thank you, Adan!
Dear Bruno,
Thanks for your article and https://www.nuget.org/packages/B1SLayer
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)
.Select("DocEntry,DocDate,DocTime,CardCode,DocTotal,DocumentLines")
.Filter($"DocEntry ge 1000")
.OrderBy("DocEntry")
.GetAllAsync<SapInvoice>();
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,
Hermann
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.
Hi Bruno,
Thanks for your quick response. I did it as you advise. So, at the end the code worked fine doing something like:
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
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.
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
Christian
Hi, Christian! Thank you so much for the kind words.
I'm glad that B1SLayer is proving to be useful for you. Cheers!
Dear Bruno
As I am trying to develop a web API using your library, some questions need to ask you.
My scenario is to get the InStock value from all warehouses, did your library support passing the query (QueryService_PostQuery)? As I need to cross-join the Items/ItemWarehouseInfoCollection to get all warehouses In stock quantity or just use the select option on Items to get the full ItemWarehouseInfoCollection array?
result
Hi, Karmen.
I believe there's two ways you could perform your request with B1SLayer. See below:
Or like this, which I would preffer because you can take advantage of GetAllAsync:
In both I'm assuming you have defined the model class MyItemWarehouseModel to deserialize the JSON response from Service Layer.
Let me know if that helps!
Dear Bruno
It's is working, thanks for the help
Hi Bruno,
hope I found you well
I'm trying to use your example with the "GetAllAsync" for learning purposes
I'm kind of lost with the Model class
I tried to create the model but wasn't sure by what convention.
So I did something like that and the response was lots of null with flat JSON
I have 2 questions hopefully we can answer
1. What am I doing wrong, How the class structure should be?
2. are the class fields mapping automatically by names?
Thanks
Tomer, your model class should match the entity definition of the resource you are requesting. For instance:
Where the OrderModel class definition is like:
The serialization/deserialization (mapping) of each field is based on the property name or the JsonProperty attribute.
Thanks Bruno, appreciate.
Hi Bruno,
I couldn't get the list from the request below. uom.Count is not zero but the content of the list is null
List<Uom> uom== await serviceLayer
.Request("$crossjoin(UnitOfMeasurements,Items/ItemUnitOfMeasurementCollection)")
.Apply("groupby ((UnitOfMeasurements/Code))")
.Filter("UnitOfMeasurements/AbsEntry eq Items/ItemUnitOfMeasurementCollection/UoMEntry and Items/ItemUnitOfMeasurementCollection/UoMType eq 'P'")
.GetAsync<List<Uom>>();
public partial class Uom
{
public string Code { get; set; }
public Uom()
{
}
}
Hi, Guray.
Cross-joins will result in a new complex type that is returned by Service Layer, meaning that the JSON structure is very different and your model classes should match this structure in order for the deserialization (conversion from JSON to object) to work correctly. I recommend performing this request in Postman to check the JSON structure of the response, then generating your classes based on it.
Hi Bruno,
Postman has returned the following json structure.
{
"@odata.context": "$metadata#Collection(Edm.ComplexType)",
"value": [
{
"UnitOfMeasurements": {
"Code": "100 pcs."
}
},
{
"UnitOfMeasurements": {
"Code": "1000 pcs."
}
}
]
}
Exactly. You then base your model classes on this JSON format (considering that B1SLayer already 'unwraps' the value array). There are online tools that can help you with that like json2csharp.com.
You are great ! It works now 🙂
Dear Bruno!
Once again big thanks for the great B1SLayer product you have developed!
We are trying to send a BATCH request to create Sales Order and then a Down Payment based on it in one transaction.
Below is our code, but …
The question is how to specify that the down payment should be based on the order that will be created with the first request in the batch?
Or in general: Is it possible to create a sales order, then based on created sales order create a down payment or Invoice documents in one batch request?
Best Regards
Vardan
Hi, Vardan.
In this case you need to specify the Content-ID in your request to have a reference for the entity you want. See below:
In case this doesn't work for you, try using OData V4 by simply changing from "b1s/v1" to "b1s/v2" in your Service Layer URL.
Here is the result, both documents created and linked in Business One:
Let me know if that works for you!
It works!!!
Thank you so much!
Hello Bruno
A good contribution, congratulations.
One question, how can I close an "Order" document?, using SLBatchRequest
Hi, Wilton.
I did some tests and for some reason, when in a batch, this request requires an empty body (not null) for it to work, otherwise I get HTTP error 400 (Bad Request). I'm not sure why, but this should work:
Hi, Bruno
It's works. I missed the third parameter new {}
Thank you so much!
Hi Bruno
Id = 84, Status = WaitingForActivation, Method = "{null}", Result = "{Aún no computado}"
AsyncState: null
CancellationPending: false
CreationOptions: None
Exception: null
Id: 84
Result: {System.Threading.Tasks.VoidTaskResult}
Status: WaitingForActivation
Option 2: Batch
var Quotation = new SLBatchRequest(
HttpMethod.Patch,
"Quotations(23119)",
Body);
HttpResponseMessage[] batchResult = ServiceLayer.PostBatchAsync(newPostQuotation1).Result;
Answer:
{StatusCode: 204, ReasonPhrase: 'No Content', Version: 1.1, Content: <null>, Headers:
{
DataServiceVersion: 3.0
}}
NOTE: There is no error, but it does not update the quote
Neither way works for me.
I hope you can help me
you want to change the Lines in the existing document, since this is a collection you need to send the header value B1S-ReplaceCollectionsOnPactch as true, you can do this simply by adding the extension .WithReplaceCollectionsOnPatch() to your Request.
Hello Sven
Thanks for answering.
I did the test (adding WithReplaceCollectionsOnPatch)with option 1:Patch
ServiceLayer.Request("Quotations", 3481)
.WithReplaceCollectionsOnPatch()
.PatchAsync(Body);
now i get error:
"10000111 - On \"Contents\" tab, enter item or items"
I will appreciate your idea, I could do the test.
Dear Sven
If it works as you indicate in asynchronous mode.
I wanted in synchronous mode.
I will work in asynchronous mode.
Thank you
Hello, happy that I could help. I don’t know exactly the reason why this works in async request only. Have you set the CORS allowed headers in your ServiceLayer configuration? Maybe, if you explicitly add the b1s-replacecollectionsonpatch header there -if it’s not already set- then this works for non async requests. At least you could check there. I remember there was also an issue with this header in batch requests, SAP note 2850921.
Hello everybody
I hope you can help me. I am creating business partner from Service Layer. Using B1Slayer, everything perfect.
But, from time to time, I am starting to receive an error saying: "This entry already exists in the following tables (ODBC -2035)".
The only change I made, is to use Service Layer (before I was using DI SERVER), to create clients.
What could be happening?
We use SAP Business One 10.0 (10.00.191) FP 2202 (64-bit) with MS SQL Server 2017.
I look forward to your help or idea to test.
Hi! Bruno Mulinari , Wilton Tejada
Big thanks for the great B1SLayer product you have developed!
i'm working with Quotations update using Patch with out any luck.
i want to be able to modify a line (qty , price)
and be able to delete existing lines
and be able to add a new lines
i have try two way without luck.
I create the Quotation document Object
i only add the DocEntry and the DocumentLines object list
cosidering the events Add,delete,update
To Add
a add a new Document line to the DocumentLines object list
Add
To delete
i subtract the line form the DocumentLines object list
delete
To Update
I modify the content on specific DocumentLines
one action or all actions can happended and the json will reflect them.
option one :
Code;
var UpdateQuotation=ServiceLayer.slConnection.Request("Quotations",quotation.DocEntry).WithReplaceCollectionsOnPatch().PatchAsync(quotation.DocumentLines);
Result :
Id = 27, Status = WaitingForActivation, Method = "{null}", Result = "{Aún no computado}"
----------------------
option 2
code:
await ServiceLayer.slConnection.Request("Quotations", quotation.DocEntry).PatchAsync(quotation.DocumentLines);
result:
B1SLayer.SLException: 'BadFormat'
FlurlHttpException: Call failed with status code 400 (Bad Request): PATCH https://192.168.47.147:50000/b1s/v1/Quotations(486)
his exception was originally thrown at this call stack:
[External Code]
B1SLayer.SLRequest.PatchAsync.AnonymousMethod__0() in SLRequest.cs
B1SLayer.SLConnection.ExecuteRequest<T>(System.Func<System.Threading.Tasks.Task<T>>) in SLConnection.cs
i appreciate any orientations.
Hi, Guillermo.
In order to update a document or its lines, you need to send the document object as the JSON body, not just the lines. I believe the code below should work:
Hello Bruno is there any possibility to connect without ssl?
Hi, Gerardo. I'm not sure I understand your question. By default, the Service Layer API supports HTTPS only, so it won't accept HTTP.
This is a good thing since HTTP is not secure for sensitive data.
Hi Bruno,
I was talking about sending requests without SSL verification. Currently, our service layer does not have any SSL certificate configuration since it is installed on the intranet exclusively for company staff. However, I discovered that it has already been configured to disable SSL verification.
Gerardo, in B1SLayer the certificate validation is disabled since the default certificate for Service Layer is self-signed and the connection would not work without doing this. This is also done in the .NET example provided in the Service Layer user manual.
However, even though the certificate validation is disabled, SSL will still be used regardless.
Hello, great job Bruno !
I'm a fresh technical B1 consultant and still in my learning phase.
I was wondering if this would help me expose functions of an add-on I have on SAP Business One ? the end goal is to create a loosely coupled solution in a form of a web app that performs like the add-on (it's a sales add-on where you chose from products categories colors ... and then chose a markup and update the price) will it help me expose the functionalities of the said above add-on ?
If you need any further clarification, I can provide.
Regards,
El Mehdi
Hello, Thank Bruno.
I want to delete master data states, but need 2 parameters Country and Code. How to declare with many parameters on the B1Slayer that you created. Regard, Arista
Hi, Arista.
In case of multiple parameters, the request can be made like this:
I hope this helps.
Thanks You Bruno, Successfully
Hi Bruno,
can you help me with this code, i want to create two or more Items in single request(postasync) , the single item works, but when i send two or more in <List> give me bad request
here is the code:
public async void createItems()
{
var serviceLayer = new SLConnection(uriOrigen, companyDb, userSAP, passwordSAP);
await serviceLayer.LoginAsync();
try
{
List<DMItem> Items = new List<DMItem>();
DMItem Item = new DMItem();
Item.ItemCode = "TST1606231";
Item.ItemName = "Test Item";
Item.ForeignName = "PRUEBA";
Item.ItemsGroupCode = 108;
Item.VatLiable = "tYES";
Item.PurchaseItem = "tYES";
Item.SalesItem = "tYES";
Item.InventoryItem = "tNO";
//Items.SalesUnit = ""
//Items.NumInSale =
Item.AvgStdPrice = 10.70;
//Items.DfltWH =
Items.Add(Item);
Item = new DMItem();
Item.ItemCode = "TST1606232";
Item.ItemName = "Test Item 2";
Item.ForeignName = "Item Prueba 2 ";
Item.ItemsGroupCode = 108;
Item.VatLiable = "tYES";
Item.PurchaseItem = "tYES";
Item.SalesItem = "tYES";
Item.InventoryItem = "tNO";
//Items.SalesUnit = ""
//Items.NumInSale =
Item.AvgStdPrice = 10.00;
//Items.DfltWH =
Items.Add(Item);
// Enviar la solicitud POST para crear la nota de entrega
var VItem = await serviceLayer.Request("Items").PostAsync<List<DMItem>>(Items);
// Performs a POST on /Logout, ending the current session
await serviceLayer.LogoutAsync();
}
Hi, Julio.
I'm afraid Service Layer does not support the creation of multiple entities simultaneously like you are doing.
If you want to create multiple entities in a single HTTP request, you can use batch requests:
Hi Bruno,
it is possible to initiate 2 DB connections on dotnet Core 7?
I saw in the previous comments that you mentioned that it is possible with contentId, but I'm not sure how to apply it with the dependency injection
How can I choose which entity to update?
program class
services
Then in the controller, I injected the SLConnection and use it like this
the object updated successfully but only in one company
Controller
Thanks in advenced
Tomer.
Hi, Tomer. Yes, you can connect to multiple databases or use multiple users, you just need a different SLConnection instance for each and a way to manage and access these multiple instances in your application.
I suppose you could use something like a Dictionary for that. This way, you will have a key to identify each different connection you wish to access:
Then, you can inject this Dictionary where you need it and access any specific SLConnection instance based on the key you previously defined:
Keep in mind this is just an example I quickly put together, the code can surely be improved based on your specific needs.
Let me know if this helps you.
Thank Bruno, it looked promising, and I got the 204 result,
but for some reason, it did not update the UDF which on a single connection it did.
any idea why?
In general, I'm trying to update UDF on line level in a purchase request
Program class
The controller
Thanks, Tomer.
I managed to understand what was the problem
I noticed that the object actually updated (Change Log) but without actual changes.
The problem was:
1. wrong structure in the body request (Line level)
2. Case-sensitive
Bruno, Thank you so much for your help!
Hello Bruno Mulinari, can a query be registered by blocks of 50 documents in the different endpoints? You will have an example where an array with several objects passes through the body
Hi, Segundo. I'm not sure I understand your question.
Could you please rephrase it or maybe provide an example of what you are trying to do?
Hi, Vinicius.
That is correct. Generally speaking, if a request to Service Layer was unsuccessful (non 2xx HTTP code), that would result in an exception, so you should handle that accordingly in your code (try-catch). This is a behavior that B1SLayer inherits from Flurl.
However, there are two things to keep in mind:
I hope that clears your doubt.
Hello brother, it's me again, I'm trying to use batch submissions, but every request I try returns the error: Content ID should be specified in each change set request. Have you got this error before? I confess that I couldn't solve it by debugging
Hi, Vinicius.
As described the the user manual, in case you are using OData v4 (b1s/v2 in your URL), the Content ID is mandatory and should be specified in your SLBatchRequest instances. For example:
Hi Bruno,
How can I post a GRPO with Bin Locations ?
I did it 🙂
Got it, thank you very much, I managed to fix it and integrate it into my code.
Taking advantage, I saw your work and integrated it with what I was using, I took the liberty of making a junction, for the company I work for.
We are migrating from DIAPI to ServiceLayer The idea is to place the functions in the SAP models, and it works similarly to the DIAPI Doing ModelSAP.Add(), .Update etc.
For the time being, few templates are standardized, document patterns, Business Partners and so on. The user fields follow the pattern of List<Dictionary<string, object>> with a function to form the request, integrating the fields in the code.If you want have a look -> LAB1 Service Layer
If you don't like using it, contact me.Thanks for the contribution.
Hi Bruno
Your work helped simplify mine. Thank you very much for sharing your knowledge.
I have a question, I hope you can answer it
I'm creating a sales order that goes through the authorization process. It's not being created, and I'm not sure what I'm doing wrong (if I create it from Postman, it does get created). This is my code:
var orderBody = new
{
CardCode = "C1",
DocDueDate = "20230914",
Document_ApprovalRequests= new[]
{
new { ApprovalTemplatesID=98, Remarks="Probando" }
},
DocumentLines = new[]
{
new
{
ItemCode = "A1",
Quantity = 1,
UnitPrice= 50,
},
new
{
ItemCode = "A2",
Quantity = 1,
UnitPrice= 50
}
}
};
var reqOrder = new SLBatchRequest(
HttpMethod.Post,
"Orders",
orderBody);
HttpResponseMessage[] batchResult = await ServiceLayer.Connection.PostBatchAsync(reqOrder);
error:
Header:Location: https://local:50000/b1s/v1/Drafts(6010)
ReasonPhrase: "Not Found"
I hope you can give me some ideas to come up with a solution
Hi, Jhon.
Can you share your Postman request that's working as you expect? I see you are creating a single entity here, so I don't really se a reason to use a batch request.
Hi Bruno
This is the Postman request and response. Despite the error, the document is created and goes through authorization. In other tools similar to Postman, it doesn't show any errors.
See image:
If I use the batch request. To simplify the example, I used a single entity.
Thank you very much
John, the behavior in B1SLayer should be the same, as it doesn't really do anything out of the ordinary for a simple POST request like this. I suggest you double check if the JSON data your are sending through B1SLayer, Postman or other tools is correct and in the expected format by Service Layer, as it's not normal to get a different result from the same request simply because of the client you used. If all the request parameters are the same, the response should be the same.
I tested the code below based on your code and the document was created without errors:
Hi Bruno
You're right. In single mode (not batch), it does create it. It works the same as Postman. It throws the same error as in Postman. See image:
Thank you so much
I found this SAP note 3066294.
How could I control the error in B1SLayer?
John, thanks for the SAP note. That's a very odd behavior from Service Layer that I had not anticipated for B1SLayer. It doesn't really make sense to return an error when the request was actually processed and the document created, don't you think?
Right now, the only way to read the full response with its headers in B1SLayer is from a batch request like you were doing originally (from the HttpResponseMessage object). Maybe there you'll find the "Location" header that is mentioned in the SAP note. The thing is, by definition, batch requests are automatically rolled back in case of an error (as I mentioned in the blog post above), so that's probably why you don't see the document created when using a batch request.
If you have no use for this "Location" header, I guess you could simply ignore the error like so:
Another thing you could try, is to configure the request to not return any content, just the HTTP status code. Maybe this way you don't get the 404 error? Not sure, but it's worth trying and preferable to the solution above, in case it works:
Hi Bruno
Your code works very well:
It is important for me to work with batch transactions (creating more than one document).
In the batch request, I see the headers location 'https://localhost:50000/b1s/v1/Drafts(6040)' according to the SAP note. However, I can't do anything because it rolled back.
Thank you for your time. If you have any ideas, please let me know. I will continue researching.
Hello Bruno,
First thing, thanks a lot for this tool, I've been using and it works great. But now I'm facing some issues while using Batch requests to create and update a Pick List. The error I'm getting is "Bad Request", which is not helpful. Please see the code I'm using:
If I try the Post request only it works fine, but only the Patch request or both at the same time it does not work.
The PickList is based on a Sales Order with 2 lines with Quantity 5 on both lines.
Can you help figuring this out?
Thanks a lot in advance.
Hi, Luis.
Does your patch request work when performing it normally (not through a batch)? There could be something wrong with your patch request and that's why the batch fails. Test the request like shown below and see what's the result:
Hello Bruno,
Thanks a lot for your fast response. You were right, the correct Property Name on the PickListLines List is "LineNumber" and not "LineNum", when I tried the request normally (not through a batch), it gave a message a lot more meaningful than on the Batch Request and I was able to figure it out. I wonder why the batch does not give useful error messages...
Now the last step is that I want to Patch the PickList that I created in the step before. I saw your examples using the Content-ID, below is updated code:
I tried like this but it did not work, do you believe this is possible?
Thanks a lot in advance.
It's possible, but you don't need to specify the resource name when using the Content-ID. It should work like shown below:
Check the user manual for more information on this.
Hello Bruno,
I just tested and it works great!
Thank you very much for your support.
Dear Bruno,
Can you give more examples with the Get method?
For example, how can I use the Get method to achieve complex query, view, or SP
Thanks in advanced
Tomer.
Hi, Tomer. For views it will depend on your database, as shown below.
View Service Endpoint in SQL Server version:
Semantic Layer Service in HANA version:
Is there a particular request that you have doubts that you can share? Basically, pretty much any request you can perform to Service Layer (with Postman, for instance), you can perform it with B1SLayer with the help of the extension methods I show in the blog post.
Yes, I want to get BP bank account details with conditions via view or store procedure
Example
Thank you.
Tomer.
Hi, Tomer.
In this case, you could use the SQLQueries resource to first register your query and then you call it by its code on Service Layer/B1SLayer. Check the following blog post for more info on this: NEW!!! SAP Business One Service Layer – SQL Query
Once your query is registered (don't forget the parameters), you can call it with B1SLayer like so:
Another option is to use the SQL View Exposure, check the user manual for more info on this. In this case, the request with B1SLayer is like I demonstrated in my previous response.
Is it possible to use store procedures with params in this example Or is it another method?
You can't use stored procedures in Service Layer.
Thanks king
Hi Bruno,
Will you continue to develope your packade ? Can I use it without worry ?
Hi, Guray.
I plan to keep maintaining it for the foreseeable future. Currently, I consider B1SLayer to be stable and production-ready, but I'm always looking for ways to improve it based on community feedback.
Anyone that encounters an issue, has a suggestion or a doubt, is welcome to contact me on GitHub and I'll try respond as soon as possible.
However, it's worth noting that I maintain B1SLayer during my free time, so the pace of updates may not be very swift due to my other commitments.
I am about to start a serious project for a stock management application that will run on a mobile Android OS. Service Layer is inevitable for me. Can I prefer your package without fear 🙂
I don't see why not, go ahead.
That is great, thanks Bruno :))
Hi Bruno,
My project is a .NET MAUI project running on .NET 7.0
I could not manage to set a connection with your library.
Is there anything I missing ?
Hi, Guray. Can you share more details on the issue you are facing?
B1SLayer.SLLoginResponse.SessionId.get returned null.
Guray, the session ID will only be available once you perform a request to Service Layer, as the login request is performed only when needed and not on the creation of the SLConnection object. If you want to share more details about this specific issue, I recommend opening an issue on GitHub with a code snippet that demonstrates the problem.
Is there a method to check if connection is done or not ?
If "serviceLayer.LoginResponse.SessionId" is null, no connection is currently active.
Exactly that was my method for testing connection status. It returns null. I am sharing my code here.
What happens is the method immediately retuns null without trying to connect. I have managed to connect to the same SL with OData.
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
B1SLayer.SLConnection serviceLayer = new B1SLayer.SLConnection("https://10.56.20.1:50000/b1s/v2", "PRODDB", "manager", "1234");
if (serviceLayer.LoginResponse.SessionId == null)
{
MessageBox.Show("Connection can not be established !");
}
}
}
}
Guray, the general idea with B1SLayer is that you don't have to worry about the session management at all, it's all automatic. The login will be performed in the background automatically before you perform a GET on /Orders, for example, and if it sees that you don't have an active session.
Like I said before, simply creating the SLConnection object will not result in a login request, as B1SLayer only performs the login request when it's needed.
However, if you still want to login manually, you can simply call serviceLayer.LoginAsync(), like I show in the blog post.
Thanks Bruno for your fast response. Now I have managed to login. Here is my last code, it works 🙂
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
B1SLayer.SLConnection serviceLayer = new B1SLayer.SLConnection("https://10.56.20.1:50000/b1s/v2", "PRODDB", "manager", "1234");
B1SLayer.SLLoginResponse slresponse=await serviceLayer.LoginAsync(true);
if (serviceLayer.LoginResponse.SessionId == null)
{
MessageBox.Show("Connection can not be established !");
}
else
MessageBox.Show(serviceLayer.LoginResponse.SessionId.ToString());
}
}
}
hello sir Bruno Mulinari, i have a problem. The data pull that I got only showed up 20 rows. if pulling from UDT (User Defined Table). What is the solution so that the data appears completely?
The following is the code I use
await _slsap.Request("UDO_PRODUCT_TEST").GetAsync<List<UDT>>();
Hello. This is the standard behavior from Server Layer as the results are retrieved in pages.
If you wish to retrieve all the data, you can loop the requests using the Skip method until you go through all records, or simply use GetAllAsync, which does all that automatically.
Love your framework! <3 Simple, yet powerful!
Maybe not the right spot, but in lack of a better place, I will ask you about a problem I have run into lately....
I have quite a few timed threads doing automated things like approving or removing approval on SO's based on parameters and conditions. This works just fine (in theory), but if a sales person has the document open in SAP the patch does not work. It logs the change in the Change log in SAP, but the values are not changed :O
Also the salesperson will be notified that there is changes on the SO and they can't update it. This is really messing up my functions 😛 And I'm very reluctant going back to DI API to do these updates :/
Do you have a workaround for this? A check for if the document is open in SAP? Or any thoughts about this issue?
Hi, Egil Hansen. 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.
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!