Consume Business Central APIs in SharePoint web part

This post can help the developers in a scenario where consuming Business Central APIs secured with Azure AD and OAuth 2.0 from within a SharePoint client-side web part. This post explains how to create a SharePoint web part that uses Business Central data.


Scenario

Creating a web part that fetch and display customers data from Business Central on selecting a Company in the dropdown list. In this scenario, the web part needs data from Customers and Companies entities from Business Central using APIs.


Create a web part project

Follow the steps in Build your first SharePoint client-side web part page to create a new web part project. Name the web part as Customers instead of HelloWorld. Otherwise, the complete Customers web part project's source code can be downloaded from GitHub.

API permissions

To consume Business Central APIs, it should be authenticated with Azure AD OAuth 2.0 authentication. SharePoint can acquire permissions for the web part from Azure AD, OAuth 2.0 for the resources configured in package-solution.json / solution / webApiPermissionRequests.


package-solution.json

The Following package-solution.json file contains the required permissions to access Business Central APIs.

{
  "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
  "solution": {
    "name": "customers-webpart",
    "id": "ff91b46e-3292-4100-9b77-c38d91fc6ed2",
    "version": "1.0.0.14",
    "includeClientSideAssets": true,
    "skipFeatureDeployment": false,
    "isDomainIsolated": false,
    "developer": {
      "name": "",
      "websiteUrl": "",
      "privacyUrl": "",
      "termsOfUseUrl": "",
      "mpnId": "Undefined-1.13.1"
    },
    "webApiPermissionRequests": [
      {
        "resource": "Dynamics 365 Business Central",
        "scope": "Financials.ReadWrite.All"
      }
    ]
  },
  "paths": {
    "zippedPackage": "solution/customers-webpart.sppkg"
  }
}


Services to consume Business Central APIs

Web part context has factory methods (aadHttpClientFactory) that can create HTTP client (AadHttpClient) for an endpoint. The same HTTP client can be used to consume secured APIs. SharePoint framework will take care of OAuth 2.0, sending access token in HTTP headers etc.


The following two services fetch data from Companies and Customers using AadHttpClient from Business Central.


Companies.service.ts

The following CompaniesService class gets companies data from Business Central API.

import { WebPartContext } from "@microsoft/sp-webpart-base";
import { AadHttpClient, AadHttpClientResponse } from '@microsoft/sp-http';
import { ClientUrl } from "../config/WebAPIs.constants";
import { APIResponse } from "../models/APIResponse.model";
import { Company } from "../models/Company.model";

export default class CompaniesService {
    constructor(private context: WebPartContext, private environment: string) {
        this.environment = environment || "Production";
    }

    public getCompanies(): Promise<Company[]> {
        return new Promise((resolve, reject) => {
            const apiUrl = `${ClientUrl}/v2.0/${this.environment}/api/v2.0/companies`;

            this.context.aadHttpClientFactory
                .getClient(ClientUrl)
                .then((client: AadHttpClient): void => {
                    client
                        .get(apiUrl, AadHttpClient.configurations.v1)
                        .then((response: AadHttpClientResponse) => {
                            if (response.ok) {
                                return response.json()
                                    .then((response: APIResponse<Company>): void => {
                                        resolve(response.value);
                                    });
                            } else {
                                response.text().then(text => {
                                    reject(JSON.parse(text).error);
                                });
                            }
                        })
                        .catch(error => {
                            reject(error);
                        });
                }).catch(error => {
                    reject(error);
                });
        });
    }
}

Customers.service.ts

The following CustomerService class gets customers data from Business Central API.

import { WebPartContext } from "@microsoft/sp-webpart-base";
import { AadHttpClient, AadHttpClientResponse } from '@microsoft/sp-http';
import { ClientUrl } from "../config/WebAPIs.constants";
import { Customer } from "../models/Customer.model";
import { APIResponse } from "../models/APIResponse.model";

export default class CustomerService {
  constructor(private context: WebPartContext, private environment: string, private companyId: string) {
    this.environment = environment || "Production";
  }

  public getCustomers(): Promise<Customer[]> {
    return new Promise((resolve, reject) => {
      if (!this.companyId) {
        reject(new Error('Company ID should not be blank.'));
        return;
      }

      const apiUrl = `${ClientUrl}/v2.0/${this.environment}/api/v2.0/companies(${this.companyId})/customers`;

      this.context.aadHttpClientFactory
        .getClient(ClientUrl)
        .then((client: AadHttpClient): void => {
          client
            .get(apiUrl, AadHttpClient.configurations.v1)
            .then((response: AadHttpClientResponse) => {
              if (response.ok) {
                response.json()
                  .then((apiResponse: APIResponse<Customer>) => {
                    resolve(apiResponse.value);
                  });
              } else {
                response.text().then(text => {
                  reject(JSON.parse(text).error);
                });
              }
            })
            .catch(error => {
              reject(error);
            });
        })
        .catch(error => {
          reject(error);
        });
    });
  }
}

React components

The following CompaniesDropdown and CompaniesList React components renders data using the above services. These code samples are using Fluent UI react components.


CompaniesDropdown.tsx

The following CompaniesDropdown class is a React component to render a dropdown using Companies service.

import * as React from 'react';
import { ICompaniesDropdownProps } from './ICompaniesDropdownProps';
import CompaniesService from '../../../../services/Companies.service';
import { Company } from '../../../../models/Company.model';
import { Dropdown } from '@fluentui/react-northstar';

interface ICompaniesDropdownState {
  companies: Company[];
  loaded: boolean;
  hasError: boolean;
  error?: Error;
}

export default class CompaniesDropdown extends React.Component<ICompaniesDropdownProps, ICompaniesDropdownState, {}> {
  constructor(props: ICompaniesDropdownProps) {
    super(props);
    this.state = { companies: [], loaded: false, hasError: false };
  }

  public componentDidMount() {
    const service = new CompaniesService(this.props.context, this.props.environment);

    service.getCompanies().then(companies => {
      this.setState({ companies: companies, loaded: true });
    }).catch(error => {
      this.setState({ companies: [], loaded: true, hasError: true, error: error });
    });
  }

  public render(): React.ReactElement<ICompaniesDropdownProps> {
    const items = !this.state.loaded ? [] : this.state.companies.map((item) => {
      return { key: item.id, header: item.name };
    });

    const onChange = (_: any, event: any) => {
      this.props.onChange(event.value.key);
    };

    return (
      <Dropdown items={items} placeholder="Select company" onChange={onChange.bind(this)} />
    );
  }
}

CustomerList.tsx

The following CustomerList class is a React component to render a list using Customers service.

import * as React from 'react';
import { Loader, Segment, Text, List, Card, CardHeader, CardBody } from '@fluentui/react-northstar';
import { ICustomerListProps } from './ICustomerListProps';
import { Customer } from '../../../../models/Customer.model';
import CustomerService from '../../../../services/Customers.service';

interface ICustomerListState {
  customers: Customer[];
  loaded: boolean;
  hasError: boolean;
  error?: Error;
}

export default class CustomerList extends React.Component<ICustomerListProps, ICustomerListState, {}> {
  constructor(props: ICustomerListProps) {
    super(props);
    this.state = { customers: [], loaded: false, hasError: false };
  }

  public componentDidMount() {
    this.getCustomers();
  }

  public componentDidUpdate(prevProps: ICustomerListProps) {
    if (prevProps.companyId != this.props.companyId) {
      this.getCustomers();
    }
  }

  public render(): React.ReactElement<ICustomerListProps> {
    return (<Card fluid ghost>
      <CardHeader><Text weight="bold" content="Customers" /></CardHeader>
      <CardBody>{this.renderBody()}</CardBody>
    </Card>);
  }

  public renderBody() {
    if (!this.props.companyId) {
      return (<Text content="You must select a Company." />);
    }

    if (!this.state.loaded) {
      return (<Loader label="Loading..." />);
    }

    if (this.state.hasError)
      return (
        <Segment inverted color="red">
          <Text style={{ whiteSpace: "pre-wrap" }} as="pre" content={`${this.state.error.message}`} />
        </Segment>);

    const listItems = this.state.customers.map(customer => {
      return {
        key: customer.id,
        header: customer.displayName,
        content: `${customer.addressLine1} ${customer.addressLine2} ${customer.city} ${customer.postalCode}`
      };
    });

    return (<List navigable items={listItems} />);
  }

  private getCustomers() {
    const { context, environment, companyId } = this.props;

    if (!companyId) {
      this.setState({ customers: [], loaded: false, hasError: false, error: null });
    }

    const service = new CustomerService(context, environment, companyId);
    service.getCustomers()
      .then(customers => {
        this.setState({ customers: customers, loaded: true, hasError: false, error: null });
      })
      .catch(error => {
        this.setState({ customers: [], loaded: true, hasError: true, error: error });
      });
  }
}


Customers.tsx

The following Customers class is a React component to render CompaniesDropdown and CustomerList components.

import * as React from 'react';
import { Provider, teamsTheme } from '@fluentui/react-northstar';
import CompaniesDropdown from './companies-dropdown/CompaniesDropdown';
import CustomerList from './customer-list/CustomerList';
import { ICustomersProps } from './ICustomersProps';

interface ICustomersState {
  companyId: string;
}

export default class Customers extends React.Component<ICustomersProps, ICustomersState, {}> {
  constructor(props: ICustomersProps) {
    super(props);
    this.state = { companyId: null };
  }

  private onChange(value: string) {
    this.setState({ companyId: value });
  }

  public render(): React.ReactElement<ICustomersProps> {
    return (
      <Provider theme={teamsTheme}>
        <CompaniesDropdown {... this.props} onChange={this.onChange.bind(this)} />
        <br />
        <CustomerList {... this.props} companyId={this.state.companyId} ></CustomerList>
      </Provider>
    );
  }
}


CustomersWebPart.ts

The following CustomersWebPart class is a SharePoint web part that renders Customers React component.

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'CustomersWebPartStrings';
import Customers from './components/Customers';
import { ICustomersProps } from './components/ICustomersProps';

export interface ICustomersWebPartProps {
  environment: string;
}

export default class CustomersWebPart extends BaseClientSideWebPart<ICustomersWebPartProps> {

  public render(): void {
    const element: React.ReactElement<ICustomersProps> = React.createElement(
      Customers,
      { context: this.context, environment: this.properties.environment }
    );

    ReactDom.render(element, this.domElement);
  }

  protected onDispose(): void {
    ReactDom.unmountComponentAtNode(this.domElement);
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('environment', {
                  label: strings.EnvironmentFieldLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

Build & Deploy the Customers web part

Run the following command at the project root folder to build and package the solution:

gulp build && gulp bundle --ship && gulp package-solution --ship

This will generate a solution file in \sharepoint\solution folder with .sppkg extension that can be deployed in SharePoint's App Catalog site.



After deploying the web part solution in App Catalog site, the SharePoint administrator has to Accept API access in the SharePoint admin center.



Web part output

The following is the output of the Customers web part:



Conclusion

The above explanation is good enough to understand the concept of using Business Central APIs in SharePoint. The same approach can be used to consume Dataverse, Dynamics CRM, Graph APIs etc. To access multiple resources, an array of resource and scope should be updated in webApiPermissionRequests in package-soulution.json file.


Happy Coding!!!


Complete source code is available at GitHub.


#MSDyn365 #MSDyn365BC #BusinessCentral #SharePoint #NodeJS #WebAPIs

668 views0 comments

Recent Posts

See All