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, 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:

  • B1SLayer has an auto-retry logic in a few specific cases. For example, if a request was made and the response was 401 Unauthorized, it's probably because the current session is no longer valid, so B1SLayer will perform a new login and reattempt the previous request automatically. In cases where the reattempt is successful, no exception will be thrown. However, if the reattempts are still unsuccessful, only then an exception will be thrown in your code.
  • Batch requests. As explained in the post, a single batch request can be composed of multiple inner HTTP requests, each one potentially resulting in a different status code. So, whether an inner HTTP request of a batch fails or not, no exception is thrown and I leave it up to you to handle the result of a batch request through the array of HttpResponseMessage that it returns.

I hope that clears your doubt.

0 Kudos
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
brunomulinari
Participant
0 Kudos

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:

var reqOrder = new SLBatchRequest(
HttpMethod.Post,
"Orders",
orderBody,
1); // Content-ID for this entity

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.

padilla_jhon
Discoverer
0 Kudos
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
brunomulinari
Participant
0 Kudos
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.
padilla_jhon
Discoverer
0 Kudos
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
brunomulinari
Participant
0 Kudos

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:

var orderBody = new
{
CardCode = "C20000",
DocDueDate = "20230914",
Document_ApprovalRequests = new[]
{
new
{
ApprovalTemplatesID = 98,
Remarks = "Testing"
}
},
DocumentLines = new[]
{
new
{
ItemCode = "A00001",
Quantity = 1,
UnitPrice = 50,
},
new
{
ItemCode = "A00002",
Quantity = 1,
UnitPrice = 50
}
}
};

var newOrder = await serviceLayer.Request("Orders").PostAsync<MyOrderModel>(orderBody);
padilla_jhon
Discoverer

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?

 

brunomulinari
Participant
0 Kudos

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:

await serviceLayer.Request("Orders").AllowHttpStatus(HttpStatusCode.NotFound).PostAsync(orderBody);

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:

await serviceLayer.Request("Orders").WithReturnNoContent().PostAsync(orderBody);
padilla_jhon
Discoverer
Hi Bruno

Your code works very well:
await serviceLayer.Request("Orders").AllowHttpStatus(HttpStatusCode.NotFound).PostAsync(orderBody);

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.

 
0 Kudos

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:

 var serviceLayer = new SLConnection("https://127.0.0.1:50000/b1s/v1", "SBODemoPT", "manager", "1234");

var PickBody = new
{
PickDate = "20230920",
Remarks = "Test Service Layer",
Name = "Test User",
PickListsLines = new[]
{
new
{
BaseObjectType = 17,
OrderEntry = 339,
OrderRowID = 0
},
new
{
BaseObjectType = 17,
OrderEntry = 339,
OrderRowID = 1
}
}
};

var PickBody2 = new
{
PickDate = "20230920",
Remarks = "Test Service Layer",
Name = "Test User",
PickListsLines = new[]
{
new
{
LineNum = 0,
PickedQuantity = 5
},
new
{
LineNum = 1,
PickedQuantity = 5
}
}
};

var postRequest = new SLBatchRequest(
HttpMethod.Post, // HTTP method
"PickLists", // resource
PickBody);// request body

var postRequest2 = new SLBatchRequest(
new HttpMethod("PATCH"), // HTTP method
"PickLists(38)", // resource
PickBody2);// request body

HttpResponseMessage[] batchResult = await serviceLayer
.PostBatchAsync(postRequest, postRequest2);

 

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.

brunomulinari
Participant
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:
await serviceLayer.Request("PickLists", 38).PatchAsync(PickBody2);

 
0 Kudos
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:
var serviceLayer = new SLConnection("https://127.0.0.1:50000/b1s/v1", "SBODemoPT", "manager", "1234");

var PickBody = new
{
PickDate = "20230920",
Remarks = "Test Service Layer",
Name = "Test User",
PickListsLines = new[]
{
new
{
BaseObjectType = 17,
OrderEntry = 339,
OrderRowID = 0
},
new
{
BaseObjectType = 17,
OrderEntry = 339,
OrderRowID = 1
}
}
};

var PickBody2 = new
{
PickDate = "20230920",
Remarks = "Test Service Layer",
Name = "Test User",
PickListsLines = new[]
{
new
{
LineNumber = 0,
PickedQuantity = 5
},
new
{
LineNumber = 1,
PickedQuantity = 5
}
}
};

var postRequest = new SLBatchRequest(
HttpMethod.Post, // HTTP method
"PickLists", // resource
PickBody,// request body
1);

var postRequest2 = new SLBatchRequest(
new HttpMethod("PATCH"), // HTTP method
"PickLists($1)", // resource
PickBody2, // request body
2);

HttpResponseMessage[] batchResult = await serviceLayer
.PostBatchAsync(postRequest, postRequest2);

I tried like this but it did not work, do you believe this is possible?

 

Thanks a lot in advance.
brunomulinari
Participant
It's possible, but you don't need to specify the resource name when using the Content-ID. It should work like shown below:
var postRequest2 = new SLBatchRequest(
new HttpMethod("PATCH"), // HTTP method
"$1", // resource
PickBody2, // request body
2);

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.

tomersha717
Explorer
0 Kudos
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
public class MyItemWarehouseModel
{
public List<Item> Items { get; set; }
}


public class Item
{
public string ItemCode { get; set; }
public string ItemName { get; set; }
public List<ItemWarehouseInfo> ItemWarehouseInfo { get; set; }
}

public class ItemWarehouseInfo
{
public string WarehouseCode { get; set; }
public double InStock { get; set; }
}
brunomulinari
Participant
0 Kudos
Tomer, your model class should match the entity definition of the resource you are requesting. For instance:
List<OrderModel> orderList = await slConnection.Request("Orders").GetAllAsync<OrderModel>();

Where the OrderModel class definition is like:
public class OrderModel
{
public int DocEntry { get; set; }
public string CardCode { get; set; }
// (...) add all the properties you wish to map
}

The serialization/deserialization (mapping) of each field is based on the property name or the JsonProperty attribute.
tomersha717
Explorer
0 Kudos
Thanks Bruno, appreciate.
tomersha717
Explorer
0 Kudos
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.
brunomulinari
Participant
0 Kudos

Hi, Tomer. For views it will depend on your database, as shown below.

View Service Endpoint in SQL Server version:

var result = await serviceLayer.Request("view.svc/YourViewName").GetAsync();

Semantic Layer Service in HANA version:

var result = await serviceLayer.Request("sml.svc/YourViewName").GetAsync();


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.

tomersha717
Explorer
0 Kudos
Yes, I want to get BP bank account details with conditions via view or store procedure

Example
select * from v_GetBPBankAccounts where CardCode = 'Sapak1' and BankCode = '13'
--Or
Exec sp_GetBPBankAccounts 'V90002', '20'

--This is the actual query
select
T0.CardCode, T0.CardName, T1.BankCode, T2.BankName,
T1.SwiftNum, T3.Name [Country], T1.AcctName, T1.Account, T1.IBAN, T1.ABARoutNum
from OCRD T0
inner join OCRB T1 on T0.CardCode = T1.CardCode
inner join ODSC T2 on T1.BankCode = T2.BankCode
inner join OCRY T3 on T2.CountryCod = T3.Code

 

Thank you.

Tomer.

 
brunomulinari
Participant
0 Kudos

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:

var result = await serviceLayer.Request("SQLQueries('yourQueryCode')/List")
.SetQueryParam("cardCode", "'C00001'")
.SetQueryParam("bankCode", "'13'")
.GetAllAsync<YourModelClass>();

 
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.

tomersha717
Explorer
0 Kudos
Is it possible to use store procedures with params in this example Or is it another method?
brunomulinari
Participant
0 Kudos

You can't use stored procedures in Service Layer.

tomersha717
Explorer
0 Kudos
Thanks king
GurayMollov
Explorer
0 Kudos
Hi Bruno,

Will you continue to develope your packade ? Can I use it without worry ?
brunomulinari
Participant
0 Kudos
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.
GurayMollov
Explorer
0 Kudos
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 🙂
brunomulinari
Participant
0 Kudos
I don't see why not, go ahead.
GurayMollov
Explorer
That is great, thanks Bruno :))
GurayMollov
Explorer
0 Kudos
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 ?
brunomulinari
Participant
0 Kudos
Hi, Guray. Can you share more details on the issue you are facing?
GurayMollov
Explorer
0 Kudos
B1SLayer.SLLoginResponse.SessionId.get returned null.
brunomulinari
Participant
0 Kudos
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.
GurayMollov
Explorer
0 Kudos
Is there a method to check if connection is done or not ?
idu25102022_
Member
0 Kudos
hello sir brunomulinari, 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>>();
brunomulinari
Participant
If "serviceLayer.LoginResponse.SessionId" is null, no connection is currently active.
brunomulinari
Participant
0 Kudos
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.
GurayMollov
Explorer
0 Kudos
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 !");
}
}
}
}
brunomulinari
Participant
0 Kudos
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.
GurayMollov
Explorer
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());
}
}
}
GurayMollov
Explorer
0 Kudos
Hi Bruno,

How can I post a GRPO with Bin Locations ?
GurayMollov
Explorer
0 Kudos
I did it 🙂
GurayMollov
Explorer
0 Kudos
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()
{
}
}
brunomulinari
Participant
0 Kudos
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.
GurayMollov
Explorer
0 Kudos

Hi Bruno,

Postman has returned the following json structure.
{
"@odata.context": "$metadata#Collection(Edm.ComplexType)",
"value": [
{
"UnitOfMeasurements": {
"Code": "100 pcs."
}
},
{
"UnitOfMeasurements": {
"Code": "1000 pcs."
}
}
]
}

brunomulinari
Participant
0 Kudos
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.
public class UnitOfMeasurementModel
{
public UnitOfMeasurementDetail UnitOfMeasurements { get; set; }
}

public class UnitOfMeasurementDetail
{
public string Code { get; set; }
}

var uomData = 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<UnitOfMeasurementModel>>();
GurayMollov
Explorer
0 Kudos
You are great ! It works now 🙂
egil1977
Explorer
0 Kudos

Love your framework! ❤️ 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 😮

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?

Labels in this area