Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Integration/MessageProcessorConsoleApp.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36408.4 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessageProcessorConsoleApp", "MessageProcessorConsoleApp\MessageProcessorConsoleApp.csproj", "{04476EAC-E2DE-4D93-B5EE-995EF477D854}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{04476EAC-E2DE-4D93-B5EE-995EF477D854}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04476EAC-E2DE-4D93-B5EE-995EF477D854}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04476EAC-E2DE-4D93-B5EE-995EF477D854}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04476EAC-E2DE-4D93-B5EE-995EF477D854}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {66531EC9-4EA5-4839-84BC-74553326B5BA}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* SAMPLE CODE NOTICE
*
* THIS SAMPLE CODE IS MADE AVAILABLE AS IS. MICROSOFT MAKES NO WARRANTIES, WHETHER EXPRESS OR IMPLIED,
* OF FITNESS FOR A PARTICULAR PURPOSE, OF ACCURACY OR COMPLETENESS OF RESPONSES, OF RESULTS, OR CONDITIONS OF MERCHANTABILITY.
* THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS SAMPLE CODE REMAINS WITH THE USER.
* NO TECHNICAL SUPPORT IS PROVIDED. YOU MAY NOT DISTRIBUTE THIS CODE UNLESS YOU HAVE A LICENSE AGREEMENT WITH MICROSOFT THAT ALLOWS YOU TO DO SO.
*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

namespace MessageProcessorConsoleApp
{
/*
"BaseUri": "https://aladamuvirtualentity765eccb33923d24ddevaos.axcloud.dynamics.com",
"AzureKeyVault": {
"VaultUri": "https://aladamuvirtualentitykv.vault.azure.net/",
"AppIdentifier": "aladamuVirtualEntity",
"KeyVaultTenantId": "979fd422-22c4-4a36-bea6-1cf87b6502dd"
},
"AzureAd": {
"ClientId": "2fd29989-4f6b-4fbf-8e09-c86c703255bd",
"Authority": "https://login.microsoftonline.com/{0}",
"TenantId": "979fd422-22c4-4a36-bea6-1cf87b6502dd",
"Audience": "https://aladamuvirtualentity765eccb33923d24ddevaos.axcloud.dynamics.com"
}
*/

public class AuthenticationConfiguration
{
public string BaseUri { get; set; }
public AzureKeyVaultConfig AzureKeyVault { get; set; }
public AzureAdConfig AzureAd { get; set; }
public string Instance { get; set; } = "https://login.microsoftonline.com/{0}";
public string Authority
{
get
{
return String.Format(CultureInfo.InvariantCulture, Instance, AzureAd.TenantId);
}
}

public static AuthenticationConfiguration ReadFromJsonFile(string path)
{
// build a config object, using the appsettings.json file as the default source
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(path, optional: false, reloadOnChange: true)
.Build();
// bind the config object to the AppSettings class
var result = new AuthenticationConfiguration();
config.Bind(result);
// return the populated AppSettings object
return result;
}

}

public class AzureKeyVaultConfig
{
public string VaultUri { get; set; }
public string AppIdentifier { get; set; }
public string KeyVaultTenantId { get; set; }
}

public class AzureAdConfig
{
public string ClientId { get; set; }
public string TenantId { get; set; }
public string Audience { get; set; }
}
}
39 changes: 39 additions & 0 deletions Integration/MessageProcessorConsoleApp/AuthenticationHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* SAMPLE CODE NOTICE
*
* THIS SAMPLE CODE IS MADE AVAILABLE AS IS. MICROSOFT MAKES NO WARRANTIES, WHETHER EXPRESS OR IMPLIED,
* OF FITNESS FOR A PARTICULAR PURPOSE, OF ACCURACY OR COMPLETENESS OF RESPONSES, OF RESULTS, OR CONDITIONS OF MERCHANTABILITY.
* THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS SAMPLE CODE REMAINS WITH THE USER.
* NO TECHNICAL SUPPORT IS PROVIDED. YOU MAY NOT DISTRIBUTE THIS CODE UNLESS YOU HAVE A LICENSE AGREEMENT WITH MICROSOFT THAT ALLOWS YOU TO DO SO.
*/
using Microsoft.Identity.Client;

namespace MessageProcessorConsoleApp
{
public class AuthenticationHelper
{
/// <summary>
/// Get the access token to call CSU API
/// </summary>
/// <param name="clientId"></param>
/// <param name="authority"></param>
/// <param name="clientSecret"></param>
/// <param name="tenantId"></param>
/// <param name="audience"></param>
/// <returns></returns>
public static async Task<string> GetAuthenticationResult(string clientId, string authority, string clientSecret, string tenantId, string audience)
{
var confidentialClientApplication = ConfidentialClientApplicationBuilder.
Create(clientId)
.WithAuthority(authority + tenantId)
.WithClientSecret(clientSecret);
string[] scopes = [$"{audience}/.default"];
AuthenticationResult authResult = await confidentialClientApplication
.Build()
.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
return authResult.AccessToken;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.16.0" />
<PackageReference Include="Azure.Identity.Broker" Version="1.3.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.76.0" />
</ItemGroup>

<ItemGroup>
<None Update="appsettingsPrivate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
166 changes: 166 additions & 0 deletions Integration/MessageProcessorConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* SAMPLE CODE NOTICE
*
* THIS SAMPLE CODE IS MADE AVAILABLE AS IS. MICROSOFT MAKES NO WARRANTIES, WHETHER EXPRESS OR IMPLIED,
* OF FITNESS FOR A PARTICULAR PURPOSE, OF ACCURACY OR COMPLETENESS OF RESPONSES, OF RESULTS, OR CONDITIONS OF MERCHANTABILITY.
* THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS SAMPLE CODE REMAINS WITH THE USER.
* NO TECHNICAL SUPPORT IS PROVIDED. YOU MAY NOT DISTRIBUTE THIS CODE UNLESS YOU HAVE A LICENSE AGREEMENT WITH MICROSOFT THAT ALLOWS YOU TO DO SO.
*/
// This example solution requires NuGet packages System.Text.Json, Microsoft.Identity.Client,
// Microsoft.Extensions.Configuration, Microsoft.Extensions.Configuration.Binder, Microsoft.Extensions.Configuration.Json

using Azure.Security.KeyVault.Secrets;
using Azure.Identity;
using MessageProcessorConsoleApp;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client;
using System;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;




namespace ODataCoreConsoleApp
{
class Program
{
private const string Endpoint = "/api/services/SysMessageServices/SysMessageService/SendMessage";
private static async Task<string> GetAuthenticationHeader(AuthenticationConfiguration config)
{

var interactiveCredential = new InteractiveBrowserCredential(
new InteractiveBrowserCredentialOptions
{
TenantId = config.AzureKeyVault.KeyVaultTenantId,
RedirectUri = new Uri("http://localhost")
});

var client = new SecretClient(new Uri(config.AzureKeyVault.VaultUri), interactiveCredential);
KeyVaultSecret secret = await client.GetSecretAsync(config.AzureKeyVault.AppIdentifier);
string clientSecret = secret.Value;


IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(config.AzureAd.ClientId)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri(config.Authority))
.Build();
string[] scopes = new string[] { $"{config.BaseUri}/.default" };


AuthenticationResult result = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
return result.CreateAuthorizationHeader();
}

private static void ReportError(Exception ex)
{
while (ex != null)
{
Console.WriteLine(ex.Message);
ex = ex.InnerException;
}

}

private static async Task MainAsync()
{
string? orderPrefix = "IMP";
int initialOrderNumber = 1;
int numberOfOrders = 2;

//Get user input for order details
Console.WriteLine("Please provide the number of orders to generate (default=2, limit 2000)...");
string? input = Console.ReadLine();
if (!string.IsNullOrEmpty(input))
{
numberOfOrders = int.Parse(input);
}
numberOfOrders = numberOfOrders > 0 ? numberOfOrders : 2;
numberOfOrders = numberOfOrders > 2000 ? 2000 : numberOfOrders;

Console.WriteLine("Please provide the OrederPrefix (default IMP)...");
orderPrefix = Console.ReadLine();
orderPrefix = string.IsNullOrEmpty(orderPrefix) ? "IMP" : orderPrefix;

Console.WriteLine("Please provide the initial order number (default=1)...");
input = Console.ReadLine();
if (!string.IsNullOrEmpty(input))
{
initialOrderNumber = int.Parse(input);
}
initialOrderNumber = initialOrderNumber > 0 ? initialOrderNumber : 1;
try
{
//Authenticate with Entra ID
Console.WriteLine("Authenticating with EntraId...");
AuthenticationConfiguration config = AuthenticationConfiguration.ReadFromJsonFile("appsettings.json");
string bearerToken = await GetAuthenticationHeader(config);
bearerToken = bearerToken.Split(' ')[1];


string fullUrl = $"{config.BaseUri.TrimEnd('/')}{Endpoint}";
Console.WriteLine(fullUrl);

//Prepare oreders and send them to the Message Procesor endpoint
for (int i = 0; i < numberOfOrders; i++)
{
string nextOrderNumber = orderPrefix + (initialOrderNumber + i).ToString("D4");
Console.Write($"Sending order {nextOrderNumber}...");

var jsonBody = @"
{
""_companyId"": ""USMF"",
""_messageQueue"": ""SalesOrderQuickQueue"",
""_messageType"": ""SalesOrderQuickMessage"",
""_messageContent"": ""{\""CustomerAccount\"": \""US-001\"", \""SalesOrderNumber\"": \""" + nextOrderNumber + @"\"", \""SalesOrderLines\"": [{\""ItemNumber\"": \""D0001\"", \""Qty\"": 23},{\""ItemNumber\"": \""D0003\"", \""Qty\"": 17}]}""
}";


using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);

var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");

var response = await httpClient.PostAsync(fullUrl, content);
string responseContent = await response.Content.ReadAsStringAsync();

Console.WriteLine($"Status Code: {response.StatusCode}");
if (!String.IsNullOrEmpty(responseContent))
{
Console.WriteLine("Response:");
Console.WriteLine(responseContent);
}
}
}
catch (Exception ex)
{
ReportError(ex);
}

Console.WriteLine("All done!");

}

public static int Main(string[] args)
{
try
{
MainAsync().Wait();
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return -1;
}
}
}

}
Binary file not shown.
45 changes: 45 additions & 0 deletions Integration/MessageProcessorConsoleApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!--
---
page_type: sample
languages:
- csharp
products:
- dynamics-finance-operations

description: "Dynamics 365 for Finance and Operations sample Message Processor console application"
urlFragment: "d365-fo-message-processor-console"
---
-->
# Dynamics 365 for Finance and Operations sample Message Processor console application

This asset contains a sample console application that demonstrates how to use the the Message Processor to import simple sales orders in Dynamics 365 for Finance and Operations (F&O).
The sample consists of

## How to instal

This sample requires Dynamics 365 for Finance and Operations version 10.0.31 or later, Visual Studio 2022, and the .NET 8 SDK.
Instructions to set up the sample are provided below.
1. Download the sample code from the [GitHub repository](https://github.com/microsoft/Dynamics-365-FastTrack-Implementation-Assets).
2. Import and compile the Dynamics 365 for Finance and Operations project into your F&O environment. The project has been following the [official documentation page](https://learn.microsoft.com/en-us/dynamics365/supply-chain/message-processor/developer/message-processor-develop).
3. Configure the message processor in your environment by following the instructions in the [Message Processor documentation](https://learn.microsoft.com/en-us/dynamics365/supply-chain/message-processor/message-processor).
* Navigate to _System administration > Message processor > Message queue_ setup and create a new message processor queue selecting the new type "Quick sales orders". Choose teh number of processors (e.g. 4)
* Navigate to _System administration > Message processor > Message processor_ to set upo the processing batch job. In the batch setupo page select e new queue type "Quick sales orders".
* From now you can navigate to _System administration > Message processor > Message precessor messages_ to check the incoming messages.
4. Open the solution in Visual Studio 2022.
5. Edit the `appsettings.json` file to include the correct settings for your F&O environment.
6. Build the solution.
7. Run the console application.

# Contents
| File/folder | Description |
|-------------|-------------|
| `README.md` | This README file. |
| `MessageProcessorConsoleApp.csproj` | Visual Studio 2022 project definition. |
| `AuthenticationConfig.cs` | Utility class for EntraId authentication using the web application (EntraId client ID + secret) flow. |
| `Program.cs` | Main console application performing the import of sales orders via the Message Processor. |
| `appsettings.json` | Settings template file to be edited for target F&O environment. Must be present in the same folder as the executable console application. |
| `QuickOrderProcessor.axpp` | F&O project containing the Message Processor configuration and the data entity used for the import. |




Loading