Check out my new blog at https://shibumiware.blogspot.com

Monday, July 10, 2017

Microsoft Dynamics 365 & A Developer's Adventure Spelunking the SDK

Version

Microsoft Dynamics 365

Don't get blinded by the skirmishes between Microsoft and SalesForce.com over the hearts and dollars of salespeople across the world: Microsoft Dynamics 365 offers a tightly integrated, full-featured CRM and ERP platform that goes beyond just customer relationship management.  Visit https://explore.dynamic forces.com/ and take a tour of some of the fantastic capabilities the platform provides. I gave up writing like a marketer in about 1997, so I won't’ try to restart here.

Microsoft Dynamics 365 Solution Areas  
Microsoft Dynamics 365 Solution Areas

Fortunately, there are a great many technical topics that applicable to my daily work. Recently I was given access to an instance of Dynamics 365 with Project Services Automation and Field Management. It bears repeating “Project Services Automation (PSA),” which is an acronymic collision with “Professional Services Automation.” Having two PSA’s to contend with is regrettable because I think Gartner, Forrester, and IDC spent a prodigious amount of time and money explaining the original PSA to the world, and now we have a second. Project Services Automation fulfills a different business requirement than simply CRM and differently than Microsoft Project  Online, which is a traditional enterprise project management system.  Project Server Online has its place, but Project Services Automation is "end-to-end", as you can see from the quote below:

Project Service Automation in Microsoft Dynamics 365 (online) provides an end-to-end solution that helps sales and delivery teams engage customers and deliver billable projects on time and within budget. Microsoft Dynamics 365 for Project Service Automation Automation helps you: • Estimate, quote, and contract work • Plan and assign resources • Enable team collaboration • Capture time, expense, and progress data for real-time insights and accurate invoicing

- Manage Project Based Sales Project Service Automation

General Resources

Here are links for those interested in the solution generally and for those that would like to dive deep into the offering:

Developer Resources

One of the most exciting aspects of being involved with the company I work for is the changing technological landscape and variety of technologies used to accomplish a project’s goals.  Working with the Dynamics Platform differs from what I have been doing for the last decade or so. I have focused on enterprise project management systems, notably Microsoft Project, Project Server, and a touch of Primavera, with a focus on earned value management, API design, synchronization technologies, performance, domain-specific algorithms, cryptography, just to name a few. Dynamics is a big product, its cloud-based, and it has an extensive set of integration points to fulfill a broad range of structural challenges in building and integrating large-scale enterprise resource management systems (ERP). ERP rarely stands alone. Often, line-of-business off-the-shelf products augment and integrate with the ERP system. Even more often, internal solutions are built to satisfy a specific challenge a customer feels the market can not meet it. Putting all of these data flows, compute units, and human interfaces together is an incredible challenge.

Here are a few links to developer resources. If you are a Microsoft Partner, you may have access to a certain number of Dynamics instances, depending on your partnership level.  If you don't have this resource, you can follow the instructions here to start a 30 day trail.

The Beginning Is a Great Place to Start

After a little warm up, a lot of reading, and getting my hands on a Dynamics 365 instance, I was ready to jump right in. I had success with Project Server’s PSI (Project Server Interface) years ago by starting at the bottom and working my way towards more complicated integration scenarios. So successful, in fact, I ended up working for Microsoft, forProject Technology, and now DeltaBahn largely because of my experience making my way through the PSI.

I have an unnatural patience in dealing with poorly, incomplete, or flatly wrong documentation. I also won’t let go of a problem until I have resolved it, learned it, digested it, and usually written my own version of whatever extensibility “product (API)" a product group provides—or at the very least, encapsulated it to increase usability, decrease complexity, and improve performance wherever possible. If API design is exposing a system’s internal machinery and capability to the outside world in a reasonable, consistent, useful, extendable, and understandable fashion, then writing an API on top another API shares the same goals except done by people who have consumed the original API and found it lacking.

In other words, the API’s API is the result of field experience. Furthermore, encapsulation protects the consumer to future changes in the base API and is decoupled from the product group’s release cycle so, as encapsulation API designers, we are free to extend, bend, and often (legally) reverse engineer how the first API works to provide better documentation, samples,  and depth of knowledge.  In my experience dealing with APIs, particularly with Microsoft, there lies within most APIs a grand vision the developers wanted to share with the world but one of the first things to get cut to deliver on time and budget is extensibility.

Connecting to Dynamics 365 as an Office 365 User

I can't do much with my shiny new instance of Office 365 With PSA & Field Management until I authenticate. In early days of working with the PSI I had relied primarily on Active Directory integration, but I did write a Forms Authentication extension that enables the Forms scenario easily. Recently I have done quite a bit with Google's Blogger API, which is employed Open Authentication (OAuth) 2.0, and I had done certificate-based custom transport and user-level authentication on a massive WCF project at forProject Technology, Inc., so I felt comfortable about what I had read about Dynamics' authentication.

I went to the QuickStart and the "Simplified connection quick start using Microsoft Dynamics 365" samples and started in with gusto, only to be disappointed to find that neither sample worked as advertised.  The QuickStart demonstrator has plenty of ells and whistles, whereas the simplified version does not.  Problem number 1 when writing samples.  Have each sample do one thing and one thing only as you start, treat each sample as a teaching opportunity, ensure the developer has a firm grasp before moving on and never include extraneous code to support "other scenarios".    This applies to each method of the example.  In the "Simplified" version of the sample, the first call does what appears reasonable:

ServerConnection.Configuration config = serverConnect.GetServerConfiguration();

GetConfigurations(I show listing and images throughout this portion of the post because I provide the link above to the sample and it is available in the SDK--I do this for conciseness and to be able to illustrate points quicker.)

The point here is that I am first shown how to read previous connection profiles from an XML file and later to write this server configuration back to the file.   Even worse, the call to ReadConfiguration has global side-effects because it modifies a class property Configurations with the configuration information in the XML file and its return value is simply the count of the Configurations property.

Global side-effects are bad, plus any C# developer worth their weight in, say, barbershop floor detritus can figure out how to read and write an XML file. Plus, there are much better ways to do THAT than demonstrated in the sample. I would know from personal experience that many if not all samples were created by somebody other than the developers responsible for the code—one of my first tasks at Microsoft was to write sample code for the Solutions Development Kit back in the mid ‘90’s. Anway, end of rant.

In the addConfig section, the code attempts to choose the discovery URL and successfully chooses a URL but it is the wrong URL and results in an error.  I spend some time in the forums, particularly on StackOverflow and find the sample code is out of sync with the latest packages available from NuGet so I back everything out and use the binaries that ship with SDK.  From the URL built up by the sample, I get a "file not found" error back from the server so I go LOOK UP the correct URL in Dynamics-->Project Services Automation-->Settings--Customizations-->Developer Resources and here is the punch line:  the URL is nothing fancy like the sample code is building up, it is simply  https://disco.crm.dynamics.com/XRMServices/2011/Discovery.svc.

I look at this 1498 lines of code and think "forget it, it cannot possibly be that difficult to connect to Dynamics through my Office 365 account."  There are to many if\then\switch constructs depending if you are on prem, using a Live account (???), using federated AD, or Office 365.

Then What Happened?!

Login ScreenI look at a tool that ships with the SDK called the Plugin Registration Tool.  To the left is the login screen.  That's more like it.  

II mentioned earlier there are may ways to connect to Dynamics. I like OAuth as it is usable in a web app, a Windows app, or on a mobile device. I start focusing in on the parts of the various samples that deal with OAuth. I strip away the saving of configurations, the server connection constructor that is SUPPOSED to take an Organization name to work but does not (another hot topic on the forums), and do a little decompiling using dotPeek to see how it works under all the layers of “helpful” sample code.

I get the feeling the authors are embarrassed to ship something simple—as if doing just what is needed doesn’t show off how smart they are. I think the ability to simplify or distil something potentially complex down to its simplest form shows both intelligence and dedication to their audience.

I worked for about twenty minutes pulling out the pieces needed to connect, authenticate, and retrieve all of the groups I belonged to in our Dynamics implementation, including my newly created developer instance.  Here is a screen shot of the resulting widget I create that shows the groups, and upon selection, the property grid is populated with basic information about the group:

The code to accomplish this is so unbelievably simple I will provde the complete listing here.  First, I created a class called OAuthDynamicsOnlineConnector:


public class OAuthDynamicsOnlineConnector
{
    private readonly string _DiscoveryUrl;
 
    public OAuthDynamicsOnlineConnector(string discoveryUrl)
    {
        if (string.IsNullOrEmpty(discoveryUrl))
        {
            throw new ArgumentException(FoundationResources.ERROR_ARG_NULL_OR_EMPTYnameof(discoveryUrl));
        }
 
        _DiscoveryUrl = discoveryUrl;
 
        Organizations = new List<OrganizationDetail>();
    }
 
    public List<OrganizationDetailOrganizations { getset; }
 
    public void Connect(string userName, string password)
    {
        if (string.IsNullOrEmpty(userName))
        {
            throw new ArgumentException(FoundationResources.ERROR_ARG_NULL_OR_EMPTYnameof(userName));
        }
        if (string.IsNullOrEmpty(password))
        {
            throw new ArgumentException(FoundationResources.ERROR_ARG_NULL_OR_EMPTYnameof(password));
        }
 
        // Create Microsoft.Xrm.Sdk.Client.AuthenticationCredentials from the userName and password
        AuthenticationCredentials authenticationCredentials = CreateAuthenticationCredentials(userName, password);
 
        DiscoveryServiceProxy discoveryProxy = null;
 
        try
        {
            // Use the Microsoft.Xrm.Sdk.Client.ServiceConfigurationFactory to create the 
            // Microsoft.Xrm.Sdk.Client.IServiceManagement<IDiscoveryService> object from our discovery url
            IServiceManagement<IDiscoveryService> discoveryService = 
                    ServiceConfigurationFactory.CreateManagement<IDiscoveryService>(new Uri(_DiscoveryUrl));
 
            // Authenticate, which will populate the SecurityTokenResource object with a valid SecurityTokenResponse 
            // or it will throw a SecurityAccessDenied exception
            authenticationCredentials = discoveryService.Authenticate(authenticationCredentials);
 
            // Create the discovery proxy, which is combining the discoveryService object with the security metadata
            // to give you an authenticated, callable proxy for issuing calls to Dynamics
            discoveryProxy = new DiscoveryServiceProxy(discoveryService, authenticationCredentials.SecurityTokenResponse);
 
            // From here on down it is self-explanatory, resulting in a collection of OrganizationDetail objects
            RetrieveOrganizationsRequest orgRequest = new RetrieveOrganizationsRequest();
            RetrieveOrganizationsResponse response = (RetrieveOrganizationsResponsediscoveryProxy.Execute(orgRequest);
 
            OrganizationDetailCollection organizations = response.Details;
 
            if (organizations == null)
            {
                return;
            }
 
            foreach (OrganizationDetail organization in organizations)
            {
                Organizations.Add(organization);
            }
        }
        catch (SecurityAccessDeniedException accessDeniedException)
        {
            Debug.WriteLine(accessDeniedException.Message);
            throw;
        }
        catch (Exception exception)
        {
            Debug.WriteLine(exception.GetType().FullName);
            Debug.WriteLine(exception.Message);
            throw;
        }
        finally
        {
            discoveryProxy?.Dispose();
        }
    }
 
    private static AuthenticationCredentials CreateAuthenticationCredentials(string userName, string password)
    {
        AuthenticationCredentials authenticationCredentials = new AuthenticationCredentials();
        ClientCredentials clientCredentials = new ClientCredentials();
 
        clientCredentials.UserName.UserName = userName;
        clientCredentials.UserName.Password = password;
 
        authenticationCredentials.ClientCredentials = clientCredentials;
 
        return authenticationCredentials;
    }
} 

The code behind the form is equally simple:

public partial class DynamicsWorkBenchForm : Form
{
    private readonly string _DiscoveryUrl;
    private readonly string _Password;
    private readonly string _UserName;
 
    public DynamicsWorkBenchForm()
    {
        InitializeComponent();
    }
 
    public DynamicsWorkBenchForm(string userName, string password, string discoveryUrl)
    {
        if (string.IsNullOrEmpty(userName))
        {
            throw new ArgumentException(FoundationResources.ERROR_ARG_NULL_OR_EMPTYnameof(userName));
        }
        if (string.IsNullOrEmpty(password))
        {
            throw new ArgumentException(FoundationResources.ERROR_ARG_NULL_OR_EMPTYnameof(password));
        }
 
        _UserName = userName;
        _Password = password;
        _DiscoveryUrl = discoveryUrl;
        InitializeComponent();
    }
 
    private void DynamicsWorkBenchForm_Shown(object sender, EventArgs e)
    {
        OAuthDynamicsOnlineConnector dynamicsOnlineConnector = new OAuthDynamicsOnlineConnector(_DiscoveryUrl);
 
        ((Action)(() => dynamicsOnlineConnector.Connect(_UserName_Password))).TryCatchMethod(this);
 
        SortableBindingList<OrganizationDetail> organizationDetails = 
            ObjectExtensions.ToSortableBindingList(dynamicsOnlineConnector.Organizations);
 
        organizationsListBox.DisplayMember = @"FriendlyName";
        organizationsListBox.DataSource = organizationDetails;
    }
 
    public void OrganizationsListBox_SelectedIndexChanged(object sender, EventArgs e)
    {
        propertyGrid.SelectedObject = organizationsListBox.SelectedItem;
    }
}

Wrap-Up

I will continue writing about Dynamics (setup, cryptography, extension methods, and anything else that strikes my fancy too) over time. I am going to be working quite a bit in this area, and I find the documentation lacking and as a colleague or two of mine said “underwhelming.”  Please do leave a comment if you would like to see a particular topic covered and I will see what I can do to help.

I was going to explain in some detail what a SecurityResponseToken is, how it works, and where the OAuth specifications provides detailed information about it—it is central to how the mechanism works plus… it involves cryptography, specifically 192 bit Triple DES!


Colby-Tait

No comments :

Disclaimer

Content on this site is provided "AS IS" with no warranties and confers no rights. Additionally, all content on this site is my own personal opinion and does not represent my employer's view in any way.