dbosoft / YaNco

.NET SAP RFC API based on SAP Netweaver RFC SDK
MIT License
123 stars 15 forks source link

Call bapis direct from .net framework and .net core #30

Closed clint2627 closed 3 years ago

clint2627 commented 3 years ago

I am trying to use your package to call bapis directly from .net projects. I'm trying to do it from .net framework 4.7.2 web project, and .net core web project. I cannot get it to work from either. I've downloaded and installed all the prerequisites. I am also dropping all the SAP Netweaver DLLs into the bin folder (not sure if that's what I'm supposed to do). I feel like there's something very obvious I'm missing. Or perhaps your package is not intended for what i'm trying to do. But I can't even seem to make a connection to our SAP environment. Any help would be appreciated.

fw2568 commented 3 years ago

mmhm, maybe we are also missed something in our docs. I have also created a blog article to get started with YaNco, see https://www.dbosoft.eu/en-us/blog/creating-a-sap-dms-library-with-yanco-part-1.

If you still cannot solve your issue, please share a repo.

clint2627 commented 3 years ago

Perhaps I'm misunderstanding how to use. That link appears to be referring to cloning the repo and building my own. I am trying to work with the nuget package. So from my .net core web application I'm referencing the dbosoft.yanco nuget package. I'm then trying to create a connection and call a simple bapi. I have included the SAP Netweaver DLLs in my project and I have them set to "Copy if newer". They are getting into the output folder on build like I expect.

clint2627 commented 3 years ago

OK I read that link again. I see where I got confused. I will follow the steps in that link and report back.

clint2627 commented 3 years ago

I see how you MapStructure to get output. But how do I get Table output from bapi ? I see an option for MapTable but I'm not sure how to use it.

fw2568 commented 3 years ago

I think, the best is, if you post me the name of the BAPI you would like to call and what you what to extract from it. Then I can provide you a sample.

As direct answer to your question: a sample how to use MapTable is in the readme.md (this code is from 3.x branch):

using (var context = new RfcContext(ConnFunc))
{
    await context.CallFunction("BAPI_COMPANYCODE_GETLIST",
        Output: func => func.MapTable("COMPANYCODE_LIST",s =>
                from code in s.GetField<string>("COMP_CODE")
                from name in s.GetField<string>("COMP_NAME")
                select (code, name)))
    .ToAsync().Match(
        r =>
            {
                foreach (var (code, name) in r)
                {
                    Console.WriteLine($"{code}\t{name}");
                }
            },
        l=> Console.WriteLine($"Error: {l.Message}"));
}

So MapTable takes the table name as input and a function. The function input will be the structure of each row in the table. Now you just map each field of the table structure to a result of your choice. The return will be a IEnumerable<TResult> where TResult is the data from mapping the structure.

clint2627 commented 3 years ago

What if I want to return the data as a list of objects? So if I have a custom class called Company.cs and it has properties Code, and Name

I want to return the data as List instead of just writing to the console

fw2568 commented 3 years ago

sure, just printing to screen makes no sense ;-)

It's looks like your are struggeling how to convert the Either to the result. Lets take again the example with the company code, but directly return the list of companies or throw a exception.

class for company:

class Company
{
    public string Code { get; set; }
    public string Name { get; set; }
}

and the call:

var companies =await context.CallFunction("BAPI_COMPANYCODE_GETLIST",
        Output: func => func.MapTable("COMPANYCODE_LIST", s =>
            from code in s.GetField<string>("COMP_CODE")
            from name in s.GetField<string>("COMP_NAME")
            select new Company {Code = code, Name = name}))
    .ToAsync().IfLeft(l => throw new Exception(l.Message));

Side comment: the call of ToAsync() after CallFunction will no longer be needed with YaNco 4.0 (currently in pre-release).

About the Either<RfcErrorInfo,TResult> response: It is completely up to you how you want to proceed in case of an error. As you wrote at the beginning, you create a web application. I usually pass the Either<,> back to the chain of calling methods and handle the result finally on the Controller.

Here a sample from a odata controller that turns the RfcErrorInfo into a odata error response:

        public static Task<IActionResult> ToActionResult<T>(this Task<Either<RfcErrorInfo, IQueryable<T>>> self,
            ODataQueryOptions opts, Action<RfcErrorInfo> errorCallback = null)
        {
            return self.ToAsync().MatchAsync(
                Right: r => new OkObjectResult(opts.ApplyTo(r)) as IActionResult,
                LeftAsync: l =>
                {
                    errorCallback?.Invoke(l);

                    return new ObjectResult(new ODataError
                        {
                            //ErrorCode = l.Code.ToString(),
                            Message = l.Message
                        })
                        { StatusCode = StatusCodes.Status500InternalServerError }.AsTask<IActionResult>();
                });

        }
clint2627 commented 3 years ago

https://www.dbosoft.eu/en-us/blog/creating-a-sap-dms-library-with-yanco-part-1

I'm trying to follow the above tutorial. Can you explain it as it relates to the below example:

public static Task<Either<RfcErrorInfo, DocumentData>> DocumentGetDetail( this IRfcContext context, DocumentId documentId) { return context.CallFunction("BAPI_DOCUMENT_GETDETAIL2", Input: func => func .SetField("DOCUMENTTYPE", documentId.Type) .SetField("DOCUMENTNUMBER", documentId.Number) .SetField("DOCUMENTPART", documentId.Part) .SetField("DOCUMENTVERSION", documentId.Version) .SetField("GETDOCDESCRIPTIONS", "X"), Output: func => func .HandleReturn() .MapStructure("DOCUMENTDATA", docData => from status in docData.GetField("STATUSEXTERN") from description in docData.GetField("DESCRIPTION") select new DocumentData(documentId, description, status) ) ); }

But my bapi returns a table with multiple rows. The .ToAsync() is giving me errors.

fw2568 commented 3 years ago

here a extended version of same example that also extracts table table for documents:

            return context.CallFunction("BAPI_DOCUMENT_GETDETAIL2",
                Input: func => func
                    .SetField("DOCUMENTTYPE", documentId.Type)
                    .SetField("DOCUMENTNUMBER", documentId.Number)
                    .SetField("DOCUMENTPART", documentId.Part)
                    .SetField("DOCUMENTVERSION", documentId.Version)
                    .SetField("GETDOCDESCRIPTIONS", "X"),
                Output: func =>
                (
                    from docData in func.MapStructure("DOCUMENTDATA", docData =>
                        from status in docData.GetField<string>("STATUSEXTERN")
                        from description in docData.GetField<string>("DESCRIPTION")
                        select (status, description))

                    from longTexts in func.MapTable("LONGTEXTS", s => s.GetField<string>("TEXTLINE"))
                    from statusLog in func.MapTable("STATUSLOG", s =>
                        from logDate in s.GetField<DateTime>("LOGDATE")
                        from logTime in s.GetField<DateTime>("LOGTIME")
                        from userName in s.GetField<string>("USERNAME")
                        select (logDate, logTime, userName))

                    select new DocumentData(
                        documentId, 
                        docData.description, 
                        docData.status,
                        longTexts, 
                        statusLog))); 

Its always the same concept. MapStructure transforms the structure into the result that is returned from your mapping function. Mapping function for DOCUMENTDATA is this:

       docData =>
              from status in docData.GetField<string>("STATUSEXTERN")
              from description in docData.GetField<string>("DESCRIPTION")
              select (status, description)

So docData will be a (string status, string description)

MapTable works same way, but for every row in structure, so result will be a enumerable of mapping function return.

                    from longTexts in func.MapTable("LONGTEXTS", s => s.GetField<string>("TEXTLINE"))

that will generate a IEnumerable<string>

               from statusLog in func.MapTable("STATUSLOG", s =>
                        from logDate in s.GetField<DateTime>("LOGDATE")
                        from logTime in s.GetField<DateTime>("LOGTIME")
                        from userName in s.GetField<string>("USERNAME")
                        select (logDate, logTime, userName))

that will generate a IEnumerable<(DateTime logDate, DateTime logTime, string userName>

and finally put all together in one output:

                    select new DocumentData(
                        documentId, 
                        docData.description, 
                        docData.status,
                        longTexts, 
                        statusLog))); 
clint2627 commented 3 years ago

select new DocumentData( documentId, docData.description, docData.status, longTexts, statusLog)));

So what does longTexts end up mapping to in the DocumentData class? What is the type?

fw2568 commented 3 years ago
        public DocumentData(DocumentId documentId, 
            string description, string status, 
            IEnumerable<string> longTexts, 
            IEnumerable<(DateTime logDate, DateTime logTime, string userName)> statusLog)
        {

        }

longtext will be a IEnumerable in that case.

Again: that's just a sample, you could of course transform StatusLog first to another type, and so on...

clint2627 commented 3 years ago

https://www.dbosoft.eu/en-us/blog/creating-a-sap-dms-library-with-yanco-part-1

var runtime = new RfcRuntime(); using var context = new RfcContext(() => Connection.Create(rfcSettings, runtime));

        var documentResult = await context.DocumentGetDetail(documentId);

Can you help me understand how context is able to call DocumentGetDetail() ? I'm not making the connection

fw2568 commented 3 years ago

not sure if I understand your question.

with

var runtime = new RfcRuntime();
using var context = new RfcContext(() => Connection.Create(rfcSettings, runtime));

you create a context that will use the function () => Connection.Create(rfcSettings, runtime) to open a connection when it needs one. It will also recreate the connection if it brokes in between. So, no, you don't have to create it directly, you just telling the RfcContext how you would like the connection to be created.

It that's what confused you?

clint2627 commented 3 years ago

var documentResult = await context.DocumentGetDetail(documentId);

I don't know how context has any idea what DocumentGetDetail() is. This is a custom method in a completely different project.

fw2568 commented 3 years ago

No that's a extension method that is also part of the blog post: https://github.com/dbosoft/sap-dmsclient/blob/blog_series/part-1/src/SAPDms.Core/Functions/ReadDocument.cs

In the blog search for "ReadDocument.cs", that will directly bring you to the implementation.

clint2627 commented 3 years ago

Correct. That is in a completely different project. I am not understanding how the apps project is calling a method from the Core project.

fw2568 commented 3 years ago

Yes, but VS should automatically detect that and suggest to add a reference to it like this: https://github.com/dbosoft/sap-dmsclient/blob/0b5e97a2111572e7c76a184826f67b53be7ced4c/src/Apps.ExportDocument/Apps.ExportDocument.csproj#L21-L23

but you are right, that step missing in blog post.

clint2627 commented 3 years ago

I'm still missing a piece of the puzzle. What makes DocumentGetDetail() an extension method of RfcContext ?

DocumentGetDetail() is a part of partial class DmsRfcFunctionExtensions

When I type in context. Intelisense doesn't give me the option for DocumentGetDetail() method.

clint2627 commented 3 years ago

Ahh I figured out my issue. Missing this part:

using Dbosoft.SAPDms.Functions;

I cloned the project. Making it much easier to figure things out now. For some reason I didn't realize it was available for cloning. :)

clint2627 commented 3 years ago

Running the application Now our application implementation is complete and we could run it to access the SAP system. But what is still missing are the library files from the SAP Netweaver RFC library. We have to make them available for the application at runtime. The easiest way to achieve this is to add them to project and enable the copy to output directory option. Therefore we now copy the following files from the downloaded nwrfcsdk package to the project root of Apps.ExportDocument:

lib/icudt50.dll lib/icuin50.dll lib/icuuc50.dll lib/libicudecnumber.dll lib/libsapucum.dll lib/sapnwrfc.dll

In the apps.ExportDocuments project, do I create a folder called lib to put all these DLLs in ? Or do they need to go in the root ?

fw2568 commented 3 years ago

no lib folder, same as executeable unless you change load path (don't do it).

clint2627 commented 3 years ago

One or more errors occurred. (Method not found: 'System.Threading.Tasks.Task1<!!1> LanguageExt.CondAsyncExt.Apply(System.Threading.Tasks.Task1<!!0>, System.Func2<System.Threading.Tasks.Task1<!!0>,System.Threading.Tasks.Task`1<!!1>>)'.)

Getting this error. I am using a custom bapi (that I know works). Is it possible I have something mapped wrong? Should I be using the bapi from your example for testing purposes?

fw2568 commented 3 years ago

Latest versions of language.ext are incompatible with YaNco < 4.0. Most likely you have upgrade Dbosoft.Functional from 1.0.0 to > 2.0.0 - that has language.ext also referenced and causing the missmatch.

See https://github.com/dbosoft/sap-dmsclient/blob/0b5e97a2111572e7c76a184826f67b53be7ced4c/src/SAPDms.Primitives/SAPDms.Primitives.csproj#L16

I know that's not in the blog, as Dbosoft.Functional 2.0 was released last week.

clint2627 commented 3 years ago

Success! Thanks so much @fw2568 for putting up with me. You were extremely patient and helpful.

fw2568 commented 3 years ago

Thank you for using the lib and going through the example. I will update nuget package for 3.x to ensure that it is not using the incompatible package.