Creating a SAP DMS library with YaNco (Part 1)

This is the first post in a series of posts on how to connect to SAP Document Management with dbosoft YaNco.

by Frank Wagner on 1/10/2021

This is the first post in a series of posts on how to connect to SAP Document Management with dbosoft YaNco.

Within this post series we will implement step by step a library that can be used to read documents, transfer files and to update documents.

tl;dr  You can find the code of this post on github: https://github.com/dbosoft/sap-dmsclient/tree/blog_series/part-1.

   

Getting started

   

Ok, let's get started. But wait - before we dive into coding, first have a look at the YaNco library and the requirements.  

What is YaNco?

YaNco is Yet another .NET Connector. It has been created to solve problems we had with the official and alternative libraries to call SAP RFCs from .NET:

  • YaNco supports ABAP callbacks, these are required to start SAPRFC and SAPHTTP on the frontend. SAPRFC and SAPHTTP used in all KPRO applications (like SAP DMS) to transfer files.
  • Functional programming friendly API - we use functional programming in many of our projects and see many of benefits when it comes to stability and maintainability.
  • DI container friendly API
  • use of the modern sapnwrfc library instead of the outdated saprfc library

YaNco is fully open source (MIT license) and available on github.

 

Download the SAP Netweaver RFC library

YaNco uses the SAP Netweaver RFC library to connect to the SAP system.  

However, to access this library, you or your work / client must have the appropriate license. The download authorization is granted to all SAP customers with a corresponding license (Netweaver,ERP, S/4).  

If you have access to the Support Lauchpad you can download it from Support Packages & Patches → ADDITIONAL COMPONENTS → SAP NW RFC SDK → SAP NW RFC SDK 7.50  

If you don't have access please ask a colleague from your SAP team for assistance.

 

 

Implementing the library

   

Okay - now really - let's start coding.

First we create three projects in Visual Studio:

  • a C# .NET Standard 2.0 library called DmsClient.Primitives
  • a C# .NET standard 2.0 library called DmsClient.Core
  • a C# .NET Core 3.1 app called Apps.ExportDocument

The first library will contain simple types, such as the document data structure. The second library will contain the function calls for accessing the SAP system. The last project is the actual application to export the document data.

 

Defining primitive types

First add the following nuget package to the DmsClient.Primitives project:

Dbosoft.Functional

Then we first create a class for the document key fields (class name DocumentId).

We will use a record type from Language.Ext (this package is included with Dbosoft.Functional). RecordType<A> is a nice helper from Language.Ext to create immutable types. You can also use POCO objects but we will continue to use immutables types everywhere in this library.


    public class DocumentId : Record<DocumentId>
    {
        public readonly string Type;
        public readonly string Number;
        public readonly string Part;
        public readonly string Version;

        public DocumentId(string documentType, string documentNumber, string documentPart, 
				string documentVersion)
        {
            Type = documentType;
            Number = documentNumber;
            Part = documentPart;
            Version = documentVersion;
        }
    }
		

Now we add a additional type for the document data:


    public class DocumentData : Record<DocumentData>
    {
        public readonly DocumentId Id;
        public readonly string Description;
        public readonly string Status;

        public DocumentData(DocumentId id, string description, string status)
        {
            Id = id;
            Description = description;
            Status = status;
        }
    }
		

For the moment these types are sufficient for us. Lets go on with the core library implementation.

 

SAP function call implementation

For the functions calls we will use extension methods on the IRfcContext (you will learn later what a IRFcContext is). All functions created within this blog series will be added to a static class DmsRfcFunctionExtensions. To avoid one giant code file with all functions the DmsRfcFunctionExtensions class will be splitted into multiple files.

Please add the following nuget package to the SAPDms.Core project:

Dbosoft.YaNco.Core

and a project reference to the SAPDms.Primitives project.

Then create a folder "Functions" in the project SAPDms.Core and add a empty file ReadDocument.cs

Add the following code to the file:


using System.Linq;
using System.Threading.Tasks;
using Dbosoft.YaNco;
using LanguageExt;

namespace Dbosoft.SAPDms.Functions
{
    public static partial class DmsRfcFunctionExtensions
    {
        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<string>("STATUSEXTERN")
                        from description in docData.GetField<string>("DESCRIPTION")
                        select new DocumentData(documentId, description, status)
                    )
                    );
        }

    }
}


Now let us have a deeper look at the code.


            return context.CallFunction("BAPI_DOCUMENT_GETDETAIL2",
                Input: func => func
                    .SetField("DOCUMENTTYPE", documentId.Type)
                    .SetField("DOCUMENTNUMBER", documentId.Number)

You may notice that the syntax of the method CallFunction is similar to the ABAP command CALL FUNCTION. The YaNco library was intentionally designed in a way that the function calls are similar to the SAP command.

The first argument of CallFunction is the name of the ABAP function. Then you pass a function to the input argument. This function is used to set the input data of the RFC function call.


                Output: func => func
                    .HandleReturn()
                    .MapStructure("DOCUMENTDATA", docData =>
                        from status in docData.GetField<string>("STATUSEXTERN")
                        from description in docData.GetField<string>("DESCRIPTION")
                        select new DocumentData(documentId, description, status)

The last argument is the output function that maps the data returned from the RFC call back the DocumentData class.

 

Application implementation

Now our library is sufficiently implemented. Let us now create a working application to read the document data from the SAP system.

Open the project file definition of project Apps.ExportDocuments and add the following nuget packages:

  <ItemGroup>
    <PackageReference Include="Dbosoft.YaNco" Version="3.1.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.7" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.7" />
    <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="3.1.7" />
    <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.1.7" />
  </ItemGroup>

Now we can implement the program class of the app. Here the full code (will be explained below in detail):

    class Program
    {
        static async Task Main(string[] args)
        {
            var configurationBuilder =
                new ConfigurationBuilder();

            configurationBuilder.AddUserSecrets<Program>();
            configurationBuilder.AddCommandLine(args);
            var config = configurationBuilder.Build();

            var rfcSettings = new Dictionary<string, string>();
            config.GetSection("saprfc").Bind(rfcSettings);
           
            var documentId = ParseDocumentIdSettings(config);

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

            var documentResult = await context.DocumentGetDetail(documentId);

            documentResult
                .Match(r =>
                    {
                        Console.WriteLine($"Document : {r.Id.Type}/{r.Id.Number}/{r.Id.Part}/{r.Id.Version}, Status: {r.Status}, Description: {r.Description}");
                    },
                    l =>
                    {
                        Console.Error.WriteLine(l.Message);
                    });

        }

        private static DocumentId ParseDocumentIdSettings(IConfiguration config)
        {
            var documentSettings = config.GetSection("doc");
            var documentType = documentSettings["type"] ?? throw new ArgumentException("Missing argument /doc:type");
            var documentNumber = documentSettings["number"] ?? throw new ArgumentException("Missing argument /doc:number");
            var documentVersion = documentSettings["version"] ?? "00";
            var documentPart = documentSettings["part"] ?? "000";
            
            return new DocumentId(documentType, documentNumber, documentPart, documentVersion);
        }
    }

So what we are doing here? The first part of the application only handles the configuration, like SAP RFC connection arguments and the number and type of the document you would like to read. To keep the things simple we use a ConfigurationBuilder from the Microsoft.Extensions.Configuration package.

            var configurationBuilder =
                new ConfigurationBuilder();

            configurationBuilder.AddUserSecrets<Program>();
            configurationBuilder.AddCommandLine(args);
            var config = configurationBuilder.Build();

            var rfcSettings = new Dictionary<string, string>();
            config.GetSection("saprfc").Bind(rfcSettings);
           
            var documentId = ParseDocumentIdSettings(config);

The dictionary will now contain the connection settings and the document id the key of the document. The configuration could also be set from a user secrets json file (right click on the project and choose Manage user secrets to edit the secrets). For example:

{
  "saprfc": {
    "ashost": "some_sap_host",
    "sysnr": "01",
    "client": "800",
    "user": "<your username>",
    "passwd": "<your password>",
    "lang":  "EN" 
  } 
}

The next part of the Program class creates a YaNco RfcRuntime and the RFC context:

            var runtime = new RfcRuntime();
            var connectionFunc = fun( () =>Connection.Create(rfcSettings, runtime));

            using var context = new RfcContext(connectionFunc);

So, what is the RFC runtime and the RFC context? The runtime is a abstraction of the implementation to access the SAP system. You have to pass it to the connection to make it possible to replace the actual runtime implementation (for example for unit testing).

The IRfcContext is used to make sure that a created connection is closed after using it. So everytime you create a RFC context a new connection to the SAP system will be used for this context. It also makes sure that only one request on the connection is send to the SAP system at the same time. Therefore the IRfcContext is completly thread-safe.

And finally we call our extension method to read the document data and some logic to print the result:

            var documentResult = await context.DocumentGetDetail(documentId);

            documentResult
                .Match(r =>
                    {
                        Console.WriteLine($"Document : {r.Id.Type}/{r.Id.Number}/{r.Id.Part}/{r.Id.Version}, Status: {r.Status}, Description: {r.Description}");
                    },
                    l =>
                    {
                        Console.Error.WriteLine(l.Message);
                    });

The documentResult returned is a Either<RfcErrorInfo,DocumentData> type. The Either<L,R> type is another great helper from Language.Ext. It contains either a left value (indicating failure) or a right value (indicating sucess). With the Match method you can extract the Right or the Left value from the Either. So in this case we write a error message when the Either is in left state and the document data if it is in right state.

 

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

and set in project the option "Copy to Output Directory" to "Copy if never" for each file.

Now you could start the application. To set the connection data and document key from the command line use following arguments:

ExportDocument.exe /doc:type=ANY /doc:number=0000000000000010000004711 _
     /saprfc:ashost=<your SAP hostname> /saprfc:sysnr=01 _  
		 /saprfc:client=800 /saprfc:<further arguments>

The document type and number are just samples from our IDES system. You will have to replace it with a existing document number and type for your SAP system.

   

Conclusion

 

So now we have a created an application that can read the status and description of a given document id. You have learned how to setup a project with dbosoft YaNco and how to configure the connection settings.

Of course, most times you will not only have to read the metadata of a document but also the files within the document. Therefore in the next part of this series will add this feature to the application.

code for this post: https://github.com/dbosoft/sap-dmsclient/tree/blog_series/part-1

  • Author:   Frank Wagner