EasySoapClient 2.4.0

dotnet add package EasySoapClient --version 2.4.0
                    
NuGet\Install-Package EasySoapClient -Version 2.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EasySoapClient" Version="2.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EasySoapClient" Version="2.4.0" />
                    
Directory.Packages.props
<PackageReference Include="EasySoapClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EasySoapClient --version 2.4.0
                    
#r "nuget: EasySoapClient, 2.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EasySoapClient@2.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EasySoapClient&version=2.4.0
                    
Install as a Cake Addin
#tool nuget:?package=EasySoapClient&version=2.4.0
                    
Install as a Cake Tool

Easy-Soap-Client

Project for talking with SOAP web services, without using a Visual Studio auto-generated proxy

NOTICE!

As of version 2.0, some breaking changes has happened.

I decided to move away from the previous approach, of creating a repository for a specific model. I realized this was very limiting (worked fine, in the start when I was just retrieving data, but as more action became needed, it was more and more limiting).

Reason being, the previous approach forced you to create a new repository for reading. A new repository for creating. A new repository for updating... All regarding the same endpoint.

In the new version you still need to create models which implement the IWebServiceElement and now also the IUpdatableWebServiceElement if you want to use the update method. However, you only need to inject the new service, and use the provided methods. No need to initialize an entire new object just for calling that specific element.

The rest of the documentation has also been updated. If you need the old version, see the commit history on the GitHub repository.

How to use

Setup the library by adding it to the IServiceCollection by doing the following:

builder.Services.AddEasySoapClient(options =>
{
    options.Username = builder.Configuration["Navision:Username"] 
        ?? throw new Exception("Failed to get the 'Username' for Navision from appsettings.json.");

    options.Password = builder.Configuration["Navision:Password"] 
        ?? throw new Exception("Failed to get the 'Password' for Navision from appsettings.json.");

    options.BaseUri = builder.Configuration["Navision:BaseUri"] 
        ?? throw new Exception("Failed to get the 'BaseUri' for Navision from appsettings.json.");
});

Note: Since 2.0, there is no need to manually add the HttpClient, as the libraary also adds this.

BaseUri

You need to provide a BaseUri.
You might have a webservice which is located at the following Url https://<domain>/WEBSERVICE/WS/<company>/Pages/WebServiceNameHere
What we need from this is this piece: https://<domain>/WEBSERVICE/WS/<company>

Create custom models

This project works by creating models which defines namespace, service name, and describes which properties to get data from, from the web service.

Create a class and implement IWebServiceElement, and afterwards use [XmlElement] to target a Web service property.

Example:

public class MachineModel : IWebServiceElement
{
    public string ServiceName => "Machines";
    public string Namespace => "urn:microsoft-dynamics-schemas/page/machines";


    [XmlElement(ElementName = "Key")]
    public string Key { get; set; } = String.Empty;

    [XmlElement(ElementName = "Machine_Name")]
    public string Machine { get; set; } = String.Empty;

    public override string ToString()
    {
        return $"{Machine} [{Key}]";
    }
}

This newly build class, is what is used as type parameter for your repositories.

Filter

Filter the data you get by using the ReadMultipleFilter struct. Example:

ReadMultipleFilter filter = new("ObjectId", "> 100");

Which should filter by webservice property ObjectId.

Note: Filters only specify by what parameters we want to retrieve data, not how much data is retrieved. That is done later when calling the method.

Complete code example:

Following code example is a rough and dirty way of retrieving an element, updating it, and then retrieving the element again to see that it has indeed been updated.

Program.cs

builder.Services.AddEasySoapClient(options =>
{
    options.Username = builder.Configuration["Navision:Username"] 
        ?? throw new Exception("Failed to get the 'Username' for Navision from appsettings.json.");

    options.Password = builder.Configuration["Navision:Password"] 
        ?? throw new Exception("Failed to get the 'Password' for Navision from appsettings.json.");

    options.BaseUri = builder.Configuration["Navision:BaseUri"] 
        ?? throw new Exception("Failed to get the 'BaseUri' for Navision from appsettings.json.");
});

Note: Since version 2.3.0 the Namespace property has no use, and as such can be removed from any classes.

Models/NavisionDocumentModel.cs

public class NavisionDocumentModel : IWebServiceElement
{
    public string ServiceName => "ItemDocuments";

    [XmlElement(ElementName = "Key")]
    public string Key { get; set; } = String.Empty;

    [XmlElement(ElementName = "Tabel_ID")]
    public string TableId { get; set; } = String.Empty;

    [XmlElement(ElementName = "IsControlled")]
    public bool IsControlled { get; set; }
}

Models/UpdateDocumentModel

public class UpdateDocumentModel : IUpdatableWebServiceElement
{
    public string ServiceName => "ItemDocuments";

    [XmlElement(ElementName = "Key")]
    public string Key { get; set; } = String.Empty;

    [XmlElement(ElementName = "IsControlled")]
    public bool IsControlled { get; set; }
}

Note: The key here is used by Navision as the Primary Key, to decide what element to update. The library generates a soap envelope based on the [XmlElement] properties you add to the class.

Worker.cs

public class Worker(ILogger<Worker> logger, IEasySoapService soapService) : BackgroundService
{
    private readonly ILogger<Worker> _logger = logger;
    private readonly IEasySoapService _soapService = soapService;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        ReadMultipleFilter filter = new("Tabel_ID", "27");

        var beforeEdit = await _soapService.GetAsync<NavisionDocumentModel>([filter], 1);
        _logger.LogInformation("{DocumentKey} - has been controlled {IsControlled}", beforeEdit.First().Key, beforeEdit.First().IsControlled);

        // Edit.
        UpdateDocumentModel item = new()
        {
            Key = beforeEdit.First().Key,
            IsControlled = !beforeEdit.First().IsControlled,
        };
        _ = await _soapService.UpdateAsync(item);

        var afterEdit = await _soapService.GetAsync<NavisionDocumentModel>([filter], 1);
        _logger.LogInformation("{DocumentKey} - has been controlled {IsControlled}", afterEdit.First().Key, afterEdit.First().IsControlled);
    }
}

Keyed Services

It is now possible to use keyed services, to make sure one can work with multiple navision instances.

You can register it as a keyed instance using the new AddKeyedEasySoapClient method.

builder.Services.AddKeyedEasySoapClient("CompanyNameA", options =>
{
    options.Username = builder.Configuration["Navision:CompanyNameA:Username"]!;
    options.Password = builder.Configuration["Navision:CompanyNameA:Password"]!;
    options.BaseUri = builder.Configuration["Navision:CompanyNameA:WebServiceLink"]!;
});

builder.Services.AddKeyedEasySoapClient("CompanyNameB", options =>
{
    options.Username = builder.Configuration["Navision:CompanyNameB:Username"]!;
    options.Password = builder.Configuration["Navision:CompanyNameB:Password"]!;
    options.BaseUri = builder.Configuration["Navision:CompanyNameB:WebServiceLink"]!;
});

After that, you can handle the dependency injection like this:

public class Worker(
    [FromKeyedServices("CompanyNameA")] IEasySoapService easySoapCompanyA,
    [FromKeyedServices("CompanyNameB")] IEasySoapService easySoapCompanyB) 
{

}

The library supports using either Keyed services, or non-keyed services. You can also mix and match the two in a project.

Codeunits

As of version 2.2.0 one can now call code units using the library.

To do so, construct a CodeUnitRequest and then call the new CallCodeUnitAsync method on the IEasySoapService interface.

One can create the CodeUnitRequest either using a constructor approach, or using the provided CodeUnitRequestBuilder.

CodeUnitRequest request = CodeUnitRequest.CreateRequest(
    "CodeUnitName",
    "NameOfFunctionToBeInvoked",
    new CodeUnitParameter("ParameterName", "ParameterValue"));
CodeUnitRequest request = CodeUnitRequestBuilder
    .WithCodeUnit("CodeUnitName")
    .WithMethod("NameOfFunctionToBeInvoked")
    .AddParameter("ParameterName", "ParameterValue")
    .Build();

Then call it like this.

CodeUnitResponse response = await _easySoapService.CallCodeUnitAsync(request, stoppingToken);

Configure the HttpClient

As of version 2.3.0 the used HttpClient is configurable, and different from the default http client used in the rest of the application.

builder.Services.AddEasySoapClient(
    options =>
    {
        options.Username = builder.Configuration["Navision:Company:Username"]
            ?? throw new Exception("Failed to get the 'Username' for Navision from appsettings.json.");

        options.Password = builder.Configuration["Navision:Company:Password"]
            ?? throw new Exception("Failed to get the 'Password' for Navision from appsettings.json.");

        options.BaseUri = builder.Configuration["Navision:Company:WebServiceLink"]
            ?? throw new Exception("Failed to get the 'BaseUri' for Navision from appsettings.json.");
    }, 
    http =>
    {
        http.ConfigureHttpClient((provider, client) =>
        {
            client.Timeout = TimeSpan.FromSeconds(10);
        });
    });

Both the AddEasySoapClient and AddKeyedEasySoapClient has been extended to now include the following parameter: Action<IHttpClientBuilder>? configureHttpClientBuilder = null. Meaning you can now use the IHttpClientBuilder to configure everything regarding the HttpClient.

Note: This also includes the BaseUri. However, I'd not recommend to do so, as the library uses it to figure out where to send requests, based on the options.BaseUri and IWebServiceElement.ServiceName.

The idea is to allow the user to combine it with e.g. Polly to add resilience to the requests, if need be.

Get Id from Key

As of version 2.3.0 you can now get the internal Id of an item, based on the key, using the new method GetIdFromKeyAsync<T>(string key, bool longResult = false, CancellationToken cancellationToken = default).

In order to use it, T must implement the ISearchable interface.

public class TestNotification : IWebServiceElement, ISearchable
{
    public string ServiceName => "NotificationEntries";

    [XmlElement(ElementName = "Key")]
    public string Key { get; set; } = String.Empty;
}

Note: ISearchable already implements the IWebServiceElement, meaning you could leave out that part. I let it stay in for clarity.

With that, you can now use it like this:

var result = await _easySoapService.GetIdFromKeyAsync<TestNotification>("12;hsMAAACHBA==9;6075120090;", cancellationToken: stoppingToken);

This would (in my case) result in the value "4".

Navision returns the following syntax: Mail Notification Entry: 4. If you need the full sentence (and not just the value "4"), you can use the boolean argument longResult on the method. Setting it to true, returns the full result.

Get a single specific item

As of version 2.4.0 a new method exists: public async Task<T> GetItemAsync<T>(ReadRequest request, CancellationToken cancellationToken = default) where T : ISearchable, new().

This allows the you to provide a ReadRequest, and based on that return a single specific item that matches the request. Basically, we are implementing the Read endpoint for web services.

Usage, very simple as usual:

_ = await _easySoapService.GetItemAsync<NavisionSalesOrderList>(readRequest, cancellationToken: stoppingToken);

In order to create a ReadRequest, you can either construct it manually, or use the new builder. Here are examples of both scenarios:

var readRequestWithBuilder1 = ReadRequestBuilder.Single("ITEM-NUMBER-HERE"); // Key defaults to "No".
var readRequestWithBuilder2 = ReadRequestBuilder.Single("ITEM-ID-HERE", "Id");
var readRequestWithBuilder3 = ReadRequestBuilder
    .New()
    .With("No", "ITEM-NUMBER-HERE")
    .Build(); // Build is optional, but does make it more clear.
var readRequestWithBuilder4 = ReadRequestBuilder
    .New()
    .With(("Line_No", 111222333), ("Item_No", "ITEM-NUMBER-HERE"));

var readRequestWithoutBuilder1 = new ReadRequest("Line_No", 111222333);
var readRequestWithoutBuilder2 = new ReadRequest(("ProductOrder", "PO-HBA-77283"), ("BatchNumber", 39923));
var readRequestWithoutBuilder3 = new ReadRequest(
    ("ProductOrder", "PO-HBA-77283"),
    ("BatchNumber", 39923),
    ("LineNumber", 71112));
Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.4.0 170 4/29/2025
2.3.0 174 4/24/2025
2.2.3 154 3/13/2025
2.2.2 235 3/6/2025
2.2.1 223 3/6/2025
2.2.0 232 3/6/2025
2.1.0 219 3/5/2025
2.0.2 136 11/27/2024
2.0.1 106 11/25/2024
2.0.0 163 11/8/2024
1.0.6 146 9/30/2024
1.0.5 187 9/9/2024
1.0.4 125 9/9/2024
1.0.3 143 9/9/2024
1.0.2 158 8/8/2024
1.0.1 148 8/8/2024
1.0.0 142 8/8/2024