D365Library.Core 1.0.63.3
Create Sales Order
IWarehourseMgrServiceManager.CreateSalesOrder validates and handles
Create Sales Order inside the manager. The method keeps its original name and
parameters, but now returns ApiResponse<D365ServiceOperationData> instead of a
string. This is a breaking API change: rebuild consumers and replace direct
string usage with a success check followed by response.Data.Result.
The additional validated operations listed below also use structured responses.
The current version is D365Library.Core 1.0.63. Consumers keep
registering services.AddD365LibraryServices("Uat") and calling CreateSalesOrder.
Only response handling changes. SQL tracking, connection selection and retry
policy registration are automatic inside the library; no additional appsettings,
connection string, options or AddD365Tracking call is needed in the consuming app.
This candidate has not been published.
var manager = serviceProvider.GetRequiredService<IWarehourseMgrServiceManager>();
var response = manager.CreateSalesOrder(
"TSPM", salesTable.ToArray(), salesLine.ToArray());
if (response.Success) {
Console.WriteLine(response.Data.Result);
} else {
Console.WriteLine($"{response.Data.ErrorCode}: {response.Message}");
Console.WriteLine(response.Error);
Console.WriteLine(response.Data.CorrelationId);
}
The return type is ApiResponse<D365ServiceOperationData> in D365Library.Models,
shared with PostInvoice, PostPackingSlip, CreateCustomerAsync and UpdateCustomer:
Success, Message, Data, UTC Timestamp, original Error, and StatusCode.
StatusCode is metadata; an HTTP controller must set its HTTP response status itself.
Data contains the operation name, order ID, numeric error code, correlation ID, actual SOAP
attempt count, retry exhaustion, outcome uncertainty, logs, attempt failures,
storage failures, and any retry safety check error. Attempt count is zero for
local validation failures. A correlation ID links attempts; it is not an
idempotency key and is not sent to D365 as one.
Other validated operations
All five operations use D365ServiceOperationSoapTransport, the shared response reader, result validators and SQL retry checks in D365Library.Operations. ID365ServiceOperationTransport includes the narrow ICreateSalesOrderTransport contract for compatibility; both resolve to the same scoped transport by default. Custom transport implementations of the common interface also implement Create. Operation-specific validation and retry decisions remain in their own methods.
PostInvoice, PostPackingSlip, CreateCustomerAsync and UpdateCustomer keep
their existing names and parameters and return ApiResponse<D365ServiceOperationData>
(Task<ApiResponse<D365ServiceOperationData>> for CreateCustomerAsync).
Their original successful string is now Data.Result:
var packing = manager.PostPackingSlip(company, salesId, pickingList);
if (!packing.Success) { Console.WriteLine(packing.Error); return; }
var invoice = manager.PostInvoice(company, packing.Data.Result);
if (invoice.Success) Console.WriteLine(invoice.Data.Result);
No new host settings are required. Common response metadata includes ErrorCode, CorrelationId, Attempts, Failures, StorageErrors and AppliedRetryPolicy. Infolog errors override successful-looking IDs. UpdateCustomer also accepts explicit true/ok/success acknowledgements. Other operations require an identifier; CreateCustomerAsync also accepts comma/semicolon-separated customer identifiers. Null optional address/contact lists are sent as empty arrays; null entries are validation errors. Null/empty customer or update lists are validation errors.
When an enabled error rule classifies a CreateCustomerAsync failure as
1003 (CustomerAlreadyExists), it looks up each
requested AccountNum using GetCustomerByCustomerCode in the same company. If all
accounts exist, it returns success with the existing accounts in Data.Result,
even if retries are disabled or the last attempt has been reached. The original
failure remains in Data.Failures. If a single account is not found, normal retry
policy checks apply using code 1003, attempt limits and delays.
Apply D365Library/Tracking/Database/006_AddCustomerAlreadyExistsRule.sql after
scripts 003 and 005 to seed the error rule and retry policy for TOne/Uat (adjust
the script scope for other applications/environments). The rule matches
The record already exists. at priority 5, before the default SQL rules.
Admins can edit its Pattern/MatchType or disable it in D365ErrorRule; the manager
checks the classified code rather than message text. Without an enabled matching
rule, this reconciliation does not run. Its code-1003 retry policy must also be
enabled to resend when the account is not found. Existing admin rows are retained.
Missing accounts, failed lookups and partially committed batches do not authorize
a resend. Custom service transports can implement ID365CustomerLookupTransport
to support this lookup without changing ID365ServiceOperationTransport.
Library owners apply D365Library/Tracking/Database/004_AddServiceRetryPolicies.sql to the central tracking database after script 003. Rules use the public operation names, including CreateCustomerAsync. Existing admin policies are preserved. Missing rules stop resends. Rollback deadlocks can retry; ambiguous SQL errors/timeouts cannot. A single customer insert rejection can retry, but customer batches are never replayed automatically. Each operation has its own CorrelationId.
Error codes
All five validated operations first evaluate enabled rows in
d365tracking.D365ErrorRule. Admins can add new response/error patterns without
republishing NuGet after installing preview.9. Rules use Contains, Equals or
StartsWith (case-insensitive), scoped by application, environment and operation
or *, ordered by Priority then Id ascending. Description becomes Message and
the original text remains in Error; no match keeps the built-in codes below.
Library owners can use Tracking/Database/005_CreateD365ErrorRule.sql to set up
the table. Consumers need no new configuration.
Data.AppliedErrorRule and failure snapshots identify the matched row;
Data.ErrorRuleCheckError reports lookup problems. A lookup problem preserves
the ERP outcome and stops retries. A classification rule does not grant permission
to resend: D365RetryPolicy and operation-specific replay safety still apply.
See SQL tracking documentation
for editing rules and monitoring their effect.
| Code | Meaning | StatusCode |
|---|---|---|
| 0 | Success | 200 |
| 1001 | Invalid input or retry settings | 400 |
| 1002 | D365 business error | 422 |
| 1003 | Customer already exists (configured error rule); verify by account | 422 |
| 1101 | SQL error, cause not established | 500 |
| 1102 | SQL deadlock victim reported | 500 |
| 1201 | Authentication failed | 401 |
| 1202 | Access denied | 403 |
| 1301 | Timeout | 504 |
| 1302 | Connection error | 503 |
| 1401 | Empty response | 500 |
| 1402 | Unrecognized result format | 500 |
| 1501 | Unclassified SOAP fault | 500 |
| 1901 | Error storage failed (separate Data.StorageErrors) |
Does not replace create status |
| 1999 | Unexpected error | 500 |
The reported Cannot create a record in Sales orders (SalesTable)... The SQL database has issued an error is 1101, whether returned in the body, Infolog, or
a typed SOAP fault. SQL classification uses the available D365 text; 1101 does
not prove the failure is transient. Fault request ID and exception type are
preserved in Data.Failures. Business validation rules remain on D365.
A successful body must contain a single sales order ID without error Infolog
entries. The default ID format accepts ASCII letters, digits, dots, underscores,
slashes and hyphens, excluding standalone status words. Confirm the actual D365
success contract in UAT; override CreateSalesOrderOptions.IsSalesOrderId if
your service returns a different ID format. Unknown text is 1402.
Retry configuration
CreateSalesOrder always enables the retry pipeline. Host EnableRetry=false is ignored for this operation. The library supplies its own default safety check. It reads D365RetryPolicy after each failure using the internal application and environment profile. Admins control Enabled, MaxAttempts and DelaySeconds in that table; consumers do not configure retries. A disabled or missing rule stops resending. Database/policy errors preserve the original ERP error and stop retry.
The built-in check supports the explicit SQL rejection
Cannot create a record in Sales orders (SalesTable) and SQL deadlock messages
that identify a victim/rollback. Ambiguous SQL errors, timeouts and missing replies
are not resent automatically. A matching SQL rule alone does not establish
idempotency. Independently repeated calls can still create separate orders.
Policy limits allow 1-10 total sends, including the original call. Delays are
exponential and capped at 300 seconds. Each send uses a fresh SOAP channel.
Data.AppliedRetryPolicy, Attempts, Failures, RetryExhausted and the shared
CorrelationId explain the final result. SQL tracking records each attempt and
whether a later attempt succeeded. See D365Library/Tracking/README.md for admin SQL.
Error storage
Database monitoring is registered automatically by AddD365LibraryServices using
internal UAT/Production connection profiles. The library owner manages these
profiles in D365Library/Tracking/Configuration/DefaultTrackingProfiles.json and
applies the schema once. Package consumers supply no tracking configuration.
See D365Library/Tracking/README.md for administration.
ID365OperationTracker receives lifecycle events independently of the error
store below. A tracking failure appears in Data.StorageErrors with its stage
and never causes another ERP create.
The default ID365OperationErrorStore submits failures to the application's
configured Exceptionless client. Configure Exceptionless in the host. Submission
does not guarantee durable delivery. For database persistence, implement
ID365OperationErrorStore.Save(D365OperationFailure) in the consuming application:
services.AddScoped<ID365OperationErrorStore, MySalesOrderErrorStore>();
services.AddD365LibraryServices(configuration);
Operation tracking is persisted in SQL Server. The library does not include a local text or JSON Lines file store. Exceptionless remains the default additional diagnostic destination.
Each failure is saved before retry, with UTC timestamp, company, user, correlation ID, operation name, attempt number, code, original message, Infolog and SOAP fault details. Full sales payloads and credentials are not added to the error record. If storage throws, the failure remains in the returned response and storage code 1901 is reported separately. Storage failure never causes an additional create.
Tests
D365 tests are included in the existing TOne.Test.Services project in
TOne/TOne.sln:
TOne/Tests/TOne.Test.Services/D365/
Unit/CreateSalesOrderTests.cs
Unit/D365OperationExecutorTests.cs
Unit/D365TrackingTests.cs
Unit/D365TrackingConfigurationTests.cs
Unit/D365ScaffoldCompatibilityTests.cs
Integration/CreateSalesOrderUatTests.cs
Integration/UatFactAttribute.cs
TestDoubles/
In Visual Studio, expand Tests > TOne.Test.Services > D365 > Unit to view
the files. Build the test project and open Test Explorer for discovered cases.
The former standalone D365 test project has been removed. Filter to this test
namespace when running isolated tests; other existing TOne tests can call live
external systems.
dotnet test TOne/Tests/TOne.Test.Services/TOne.Test.Services.csproj -p:GeneratePackageOnBuild=false --filter 'FullyQualifiedName~TOne.Test.Services.D365Tests&Category!=UAT'
The xUnit suite exercises the manager through mocked transport and error store, without D365 calls. For a controlled UAT integration test, retain the existing request fixture and change the call/assertions to:
var response = tools.CreateSalesOrder("TSPM", salesTable.ToArray(), salesLine.ToArray());
Assert.True(response.Success, response.Error);
Assert.Equal(0, response.Data.ErrorCode);
Assert.False(string.IsNullOrWhiteSpace(response.Data.Result));
UAT creates real records and must be run explicitly with appropriate test data. No UAT call or NuGet publication is part of the automated unit test suite.
Explicit UAT tests
Real-data integration tests live under
TOne/Tests/TOne.Test.Services/D365/Integration:
| File | Real UAT operation and prerequisites |
|---|---|
| CreateSalesOrderUatTests.cs | TSPM / SC02508 / 113-120-5008; success and nonexistent-customer rejection |
| PostPackingSlipUatTests.cs | Creates a fresh TSPM / SC00303 / SER-IT3003 order, then posts its packing slip |
| PostInvoiceUatTests.cs | Creates a fresh service-item order and packing slip, then posts its invoice |
| CreateCustomerUatTests.cs | Creates a unique UT-prefixed customer in Go5 using existing DOM/THB/VAT/30D/Transfer master settings |
| UpdateCustomerUatTests.cs | Creates a dedicated Go5 UAT customer, updates its name and reads it back |
Sales fixtures reuse TemplateServiceTests master data, with quantity 1 and price 1.10. Every run generates a unique PO reference or test customer account. Posting tests create their own prerequisite documents and do not depend on another test's execution order. Customer tests use a test company name and no real contact email addresses. Review these master codes if UAT data changes.
All UAT cases are serialized in the D365 UAT test collection. Each step verifies the real SQL operation and attempt records by CorrelationId, including errors and successful retries, before continuing to the next write. Customer tests also read the customer back from D365. Results and diagnostics appear in Test Output; no local result/error files are written. The central SQL schema is checked before the first ERP write, and no schema or policy changes are applied by these tests.
Select an individual test in Visual Studio, or explicitly run, for example:
dotnet test TOne/Tests/TOne.Test.Services/TOne.Test.Services.csproj --no-restore -p:GeneratePackageOnBuild=false --filter 'Category=UAT&D365Operation=PostInvoice'
This command performs real UAT writes when the application environment below is Uat. Created records remain in UAT; tests do not delete or roll back posted documents. A failed later step can leave earlier successful documents in place. Use CorrelationId and Test Output to inspect those records before rerunning.
CreateSalesOrderUatTests uses the same TOneSetting:D365:EnvironmentName in
TOne.API/appsettings.json that configures the application. It runs when that
value is Uat and is skipped for other/missing environments. There is no separate
test enable flag or test settings file. Rebuild to copy settings and refresh
Visual Studio discovery:
{
"TOneSetting": {
"D365": {
"EnvironmentName": "Uat"
}
}
}
The tests use the Uat
configuration profile, rejects any host other than uat-aos.tks.co.th, requires
HTTPS, and uses the library-owned retry pipeline. It uses the supplied TSPM
fixture (customer SC02508 / item 113-120-5008). Review that fixture if UAT master
data changes. The success test creates one real order; the invalid-customer test
expects D365 business validation and verifies SQL Server operation and attempt
records. Both cases require database tracking; neither writes result/error files.
The internal UAT tracking database and all three tracking tables must already exist. Tests check them before sending any ERP create request. No schema is created by tests.
Run the invalid-customer case first to confirm connectivity and error handling:
dotnet test TOne/Tests/TOne.Test.Services/TOne.Test.Services.csproj --no-restore -p:GeneratePackageOnBuild=false --filter 'Category=UAT&D365Operation=CreateSalesOrder&Scenario=InvalidCustomer'
After confirming that response, explicitly run the create case once:
dotnet test TOne/Tests/TOne.Test.Services/TOne.Test.Services.csproj --no-restore -p:GeneratePackageOnBuild=false --filter 'Category=UAT&D365Operation=CreateSalesOrder&Scenario=Success'
Results and attempt diagnostics are stored in SQL Server tables
d365tracking.D365Operation and d365tracking.D365OperationAttempt, and the tests
query them by the returned CorrelationId to verify persistence. The SQL-backed
tracker replaces the former test file destination. Test output includes the
CorrelationId, status, code, attempts and sales order ID for lookup.
Historical file evidence listed in UAT-RESULTS.md remains unchanged; new tests
do not update those files. A timeout or unknown outcome must be reconciled before rerunning the create
case. These tests do not demonstrate SQL rollback or safe automatic retries;
those require server-side evidence and a reliable unique request reference.
Adding another operation
The common execution pipeline lives in D365Library/Operations:
| Component | Responsibility |
|---|---|
D365OperationExecutor |
Validation, attempts, retry safety, delays, response status and failure recording |
D365OperationData |
Shared diagnostics inherited by each operation's result payload |
D365OperationOptions<TRequest,TData> |
Typed options and reconciliation callback per operation |
D365ErrorCodes / D365ErrorClassifier |
Shared numeric codes and SOAP/network exception mapping |
ID365OperationErrorStore |
One storage integration for every operation, identified by Operation |
D365SoapInvoker |
Authentication, a fresh channel per attempt, and safe channel cleanup |
To implement another endpoint in this repository:
- Add a result class inheriting
D365OperationDatacontaining only its business fields, such asPurchaseOrderId. ReturnApiResponse<ThatResult>. - Define its options inheriting
D365OperationOptions<ThatRequest,ThatResult>. The common base retries nothing by default. Choose allowed codes explicitly; never inherit sales-order retry rules implicitly. - Add a transport using
D365SoapInvokerwith the correct client and service group. Register transport and typed options inServiceConfiguration. - Add the manager method in a partial file such as
Tks2D365ServiceManager.PurchaseOrder.cs, and declare it on the relevant public interface. Supply aD365OperationDefinitionand call the manager's sharedoperationExecutor.Execute. - The definition supplies a unique operation name, input validation, transport
call, response interpretation, success message, and existing-result validation
and business-field copying.
ReadResponsereturns null on success or aD365OperationErroron failure. Populate only business fields and current logs; the executor owns attempts, correlation IDs and failure history. - For a different generated SOAP fault type, override the definition's
ClassifyExceptiondelegate. Otherwise it uses the shared classifier, which recognizes the TKS service group's typed fault and common network exceptions. - Add tests for that operation's validation, success contract, error mapping, safe retries and reconciliation. Do not copy the retry loop or error storage.
The numbers in D365ErrorCodes describe shared error categories and retain their
existing values; they are not reserved solely for sales orders. Identify the
operation with Data.Operation / D365OperationFailure.Operation. New business
codes may be allocated separately without changing existing values.
Only CreateSalesOrder currently uses this pipeline. Other existing endpoint implementations are unchanged. Generic executor tests use an in-memory operation with a numeric record ID to verify reuse, independent retry policies, distinct operation identities, reconciliation and service-specific error classification.
Showing the top 20 packages that depend on D365Library.Core.
| Packages | Downloads |
|---|---|
|
D365API
Package Description
|
7 |
.NET Core 3.1
- D365.Auth.Core (>= 1.0.63.3)
- D365.Soap.Core (>= 1.0.63.3)
- AutoMapper (>= 10.1.1)
- Exceptionless (>= 4.6.2)
- Microsoft.EntityFrameworkCore (>= 5.0.17)
- Microsoft.EntityFrameworkCore.SqlServer (>= 5.0.17)
- Microsoft.Extensions.Configuration.Binder (>= 5.0.0)
- Microsoft.Extensions.DependencyInjection (>= 5.0.2)
- System.ComponentModel.Annotations (>= 5.0.0)
- TKSLibrary.Core (>= 1.0.7)
.NET 5.0
- D365.Auth.Core (>= 1.0.63.3)
- D365.Soap.Core (>= 1.0.63.3)
- AutoMapper (>= 10.1.1)
- Exceptionless (>= 4.6.2)
- Microsoft.EntityFrameworkCore (>= 5.0.17)
- Microsoft.EntityFrameworkCore.SqlServer (>= 5.0.17)
- Microsoft.Extensions.Configuration.Binder (>= 5.0.0)
- Microsoft.Extensions.DependencyInjection (>= 5.0.2)
- System.ComponentModel.Annotations (>= 5.0.0)
- TKSLibrary.Core (>= 1.0.7)
.NET Standard 2.1
- D365.Auth.Core (>= 1.0.63.3)
- D365.Soap.Core (>= 1.0.63.3)
- AutoMapper (>= 10.1.1)
- Exceptionless (>= 4.6.2)
- Microsoft.EntityFrameworkCore (>= 5.0.17)
- Microsoft.EntityFrameworkCore.SqlServer (>= 5.0.17)
- Microsoft.Extensions.Configuration.Binder (>= 5.0.0)
- Microsoft.Extensions.DependencyInjection (>= 5.0.2)
- System.ComponentModel.Annotations (>= 5.0.0)
- TKSLibrary.Core (>= 1.0.7)
| Version | Downloads | Last updated |
|---|---|---|
| 2.0.0-preview.9 | 8 | 10/27/2026 |
| 1.0.63.3 | 20 | 10/28/2026 |
| 1.0.63.2 | 4 | 10/27/2026 |
| 1.0.63.1 | 5 | 10/27/2026 |
| 1.0.62 | 37 | 08/25/2026 |
| 1.0.61 | 39 | 07/10/2026 |
| 1.0.60 | 47 | 07/08/2026 |
| 1.0.59 | 44 | 06/04/2026 |
| 1.0.58 | 55 | 04/23/2026 |
| 1.0.57 | 56 | 02/24/2026 |
| 1.0.56 | 3,464 | 02/13/2026 |
| 1.0.55 | 47 | 02/13/2026 |
| 1.0.54 | 42 | 02/12/2026 |
| 1.0.53 | 70 | 01/15/2026 |
| 1.0.43 | 67 | 04/29/2025 |
| 1.0.41 | 53 | 04/29/2025 |
| 1.0.40 | 46 | 04/29/2025 |
| 1.0.39 | 42 | 04/29/2025 |
| 1.0.38 | 53 | 04/29/2025 |
| 1.0.37 | 458 | 04/29/2025 |
| 1.0.36 | 51 | 04/29/2025 |
| 1.0.35 | 51 | 04/29/2025 |
| 1.0.33 | 963 | 04/29/2025 |
| 1.0.32 | 41 | 04/29/2025 |
| 1.0.31 | 54 | 04/29/2025 |
| 1.0.30 | 45 | 04/29/2025 |
| 1.0.29 | 37 | 04/29/2025 |
| 1.0.28 | 158 | 04/29/2025 |
| 1.0.27 | 43 | 04/29/2025 |
| 1.0.25 | 54 | 04/29/2025 |
| 1.0.24 | 53 | 04/29/2025 |
| 1.0.23 | 41 | 04/29/2025 |
| 1.0.22 | 52 | 04/29/2025 |
| 1.0.20 | 43 | 04/29/2025 |
| 1.0.18 | 54 | 04/29/2025 |
| 1.0.17 | 49 | 04/29/2025 |
| 1.0.16 | 40 | 04/29/2025 |
| 1.0.15 | 46 | 04/29/2025 |
| 1.0.14 | 52 | 04/29/2025 |
| 1.0.13 | 41 | 04/29/2025 |
| 1.0.11 | 42 | 04/29/2025 |
| 1.0.10 | 56 | 04/29/2025 |
| 1.0.4 | 45 | 04/29/2025 |
| 1.0.3 | 47 | 04/29/2025 |