???At time of writing, the GP2018 module in satellite is structured as such:
Clarity.Satellite.GP2018/ - Contains the code generated from the WSDL, as well as interface definitions for gRPC. Must be referencable from both .NET Core (Connect) and .NET Framework (Satellite).
Clarity.Satellite.GP2018.Impl/ - Contains the actual .NET Framework implementation that talks to GP
Polymorphism
Satellite handles GP’s polymorphism in serialization by passing all GP types in a JSONWrapped<T> wrapper with “$type” properties to tell Newtonsoft.Json the proper types to deserialize as. This also involves a translation layer, as not all types between .NET Framework and .NET Core are located in the same assemblies.
Interface Structure
As a general rule, most methods will take an integer Company ID to use in the organization key.
All listing interfaces return IAsyncEnumerables, which translate to a stream of backpressured data in gRPC.
Currently, interfaces are structured as such:
public async IAsyncEnumerable<JSONWrapped<Customer>> ListFull(JSONWrapped<CustomerCriteria> crit, int companyID)
{
using var client = Init();
var items = await client.GetCustomerListAsync(crit, Context(companyID));
foreach (var item in items.Where(i => !string.IsNullOrWhiteSpace(i.Key.Id)))
yield return await client.GetCustomerByKeyAsync(item.Key, Context(companyID));
await sat.Config.Configure(_gpOptions);
var gpCustomers = sat.Customer.ListFull(new CustomerCriteria()
{
LastModifiedDate = new() { GreaterThan = modDate },
//Name = new() { Like = "%Claritian" },
}, GPCompanyID);
foreach (var addr in gpCust.Addresses)
// Note how an addr itself has no relation to a customer.
if ((await sat.Customer.UpsertAddress(addr, GPCompanyID)).NullIfWhitespace() is string err)
throw new Exception($"Failed while creating address {addr.Key.Id} with error from satellite {err}");
else
LogInfo($"\tUpserted an address {addr.Key.Id}.");
if ((await sat.Customer.Create(gpCust, GPCompanyID)).NullIfWhitespace() is string err)
throw new Exception($"Failed on creating customer {cust.Email} in GP with error from satellite {err}");
var gCust = await sat.Customer.Get(new CustomerKey { Id = id, }, GPCompanyID);
var allProducts = sat
.Item
.ListFull(new ItemCriteria
{
LastModifiedDate = new()
{
GreaterThan = compareDate,
},
IsDiscontinued = new() { EqualValue = false, },
Type = new()
{
Items = new()
{
ItemType.Kit,
ItemType.SalesItem,
},
}
}, GPCompanyID)
var price = (await sat.Item.GetItemListPrice(prod.Key, "USD", GPCompanyID)).Val
// Recursively descends into the kit items and takes the minimum inventory from all items in kit
var availQuant = await sat.Item.AvailableCount(prod.Key);
Orders
// Getting invoices for an order
var invoices = _spDB.Sop10100s
.AsQueryable()
.Where(s => s.Orignumb == order.Key.Id)
.AsAsyncEnumerable()
.SelectAwait(async sop => (await sat.SalesInvoice.Get(new SalesDocumentKey { Id = sop.Sopnumbe, CompanyKey = GPCompanyKey }, GPCompanyID)).Val);
Every customer needs a customer key which involves an ID which is set manually and the GP company key
Key = new CustomerKey
{
Id = hsCompany.CustomerID,
CompanyKey = new CompanyKey { Id = 6 }
}
Every customer will also need a customer class key
ClassKey = new CustomerClassKey
{
Id = hsCompany.ClassID != null ? hsCompany.ClassID.ToUpper() : "DEFAULT",
CompanyKey = new CompanyKey { Id = 6 }
}
Addresses are added as a list of Customer addresses You will need to set the default Customer address key
DefaultAddressKey = new CustomerAddressKey
{
Id = defaultAddressKey
},
}
###Creating Addresses
Every Customer needs a Customer Address Key set as a default. This key should match the key set on the account.
```csharp
Key = new CustomerAddressKey
{
Id = contact.CustomerID,
CustomerKey = new CustomerKey
{
Id = hsCompany.CustomerID,
CompanyKey = new CompanyKey { Id = 6 }
}
}
All other fields should be very clear except for phone and email. Phone has its own phone PhoneNumber object and email has its own InternetAddress object
Phone1 = contact.Phone != null ? new PhoneNumber { Value = contact.Phone } : null,
InternetAddresses = contact.Email != null ? new InternetAddresses { EmailToAddress = contact.Email } : null
There may be other keys that the customer will want you to set
There are Tax Schedule keys
newAddress.TaxScheduleKey = new TaxScheduleKey
{
Id = contact.TaxSchedule.ToUpper()
};
Shipping method Key
newAddress.ShippingMethodKey = new ShippingMethodKey
{
Id = contact.ShippingMethod.ToUpper()
};
Sales territory Key
newAddress.SalesTerritoryKey = new SalesTerritoryKey
{
Id = contact.TerritoryID.ToUpper()
};
Every customer requires a CustomerKey a ShipToAddressKey as a CustomerAddressKey object, a batch key, and a list of SalesOrderLine
Optionally you can include the PaymentAmount as a MoneyAmount object
var gpSalesOrder = new SalesOrder
{
CustomerKey = new CustomerKey { Id = GPAccountID },
ShipToAddressKey = new CustomerAddressKey { Id = contact.CustomerID },
BatchKey = new BatchKey { Id = "TESTBATCH" },
PaymentAmount = new MoneyAmount
{
Value = decimal.Parse(deal.Amount)
},
Lines = lines,
};
Line Items only require an Item Key and a Quantity
var line = new SalesOrderLine
{
ItemKey = new ItemKey
{
Id = SKU,
},
Quantity = new Quantity
{
Value = decimal.Parse(item.Quantity),
},
};
The Product name is pulled from the product description field. the product description is often pulled from ShortDescription. The price is often pulled from the Value field of the CurrentCost object. The SKU is pulled from the ID field of the Key.
var product = new Product
{ Name = item.Description,
Description = item.ShortDescription,
UnitPrice = item.CurrentCost.Value,
SKU = item.Key.Id
};
GP 2018 doesn’t come with Web Services and eConnect installed/enabled. They must both be installed via GP’s original installation media for the rest of this guide to work. Also ensure that an appropriate security group and integration user have been set up on the server’s AD, with any developer accounts added to the security group. T
he security group is needed to allow multiple people access to the web exceptions console (which is crucial to integrating with GP), and the integration account will be the account that Connect uses to log in.
At some point in the Web Services setup, you should obtain a URL that looks something like http://server:48620/Dynamics/GPService (hostname and port number subject to change). The developer uses this URL to generate the appropriate networking code.
It is recommended to download this WSDL manually, then use Visual Studio’s Add WCF Reference functionality to add it to the appropriate project:
Note that this may require adding a plugin to Visual Studio, and the reference should be added to a .NET Standard 2.0 (important, explained later) project.
GP2018, at least in the default configuration, requires WCF encryption methods which are not presently supported by .NET Core. Clarity Connect, however, requires .NET Core, so the options for integrating are:
Despite lesser indirection, the first was deemed to be more difficult due to superior levels of documentation and tooling available for Web Services, as well as the difficulty of making a COM interface work in .NET Core. Thus, we structure our code to use a .NET Framework based proxy called the Connect Server Agent (Satellite, internally) which talks back to the .NET Core Connect.
This server agent is typically run as a separate Windows Service on the same server as Connect, but can also be run on the same server as GP or even another arbitrary server, if needed. If the latter is required, then a TCP port (default 2663; configurable) must be opened between the agent’s server and Connect’s server.
General GP Observations (Dev/IT)
Portions marked
This section shall deal with general observations made from within the server agent itself. Most developers should use the agent instead, but these will be useful to any who must implement new/fix methods inside the agent.
Phase 2