Connecting SharePoint Online to On-Premise Databases with SharePoint Framework (SPFx)


Introduction

Recently we seem to be getting involved in projects after they have gone awry. One of our partners reached out asking for help with an issue they had whilst migrating a SharePoint 2010 environment to SharePoint Online.

The problem was that the customer made extensive use of Dataview Webparts with their SharePoint 2010 environment. These web parts displayed important information used by the business. The Dataview Webparts connected directly to a SQL server hosted within the customer database.

Of course when the web parts were migrated into the cloud, they did not work as they were unable to connect to the SQL Server. This ended up stopping the migration until a solution was found.

The customer did not want to move the data into the cloud due to several reasons but the most important one being all of the other systems dependent on it.

Several options were discussed which will be discussed in the next session.

The approach

So several options were looked at, and as moving the database was quickly ruled out, we came up with these two:

  • Build a solution with PowerApps and use the On-Premise data gateway.
  • Build a solution with SharePoint Framework using a REST API use Azure Hybrid Connections

Whilst the PowerApps solution would take less time, the licensing cost of the PowerApps solution ended up ruling it out due to its total cost of ownership (TCO).

So, the SPFX solution was chosen. The architecture was to use SPFX webparts which connected to a REST API hosted in Azure App Services. The clever part was using Azure App Service Hybrid Connections which allowed us to connect from Azure back into the customer network without the need to reconfigure complex firewalls.

To help visualise the solution, let’s take a look at the architecture.

We ended up having two Azure Hybrid Connection Services running. One for the active live environment and another for the disaster recovery environment.

The data being accessed was sensitive so the REST Api had to be secure. It was configured so that it was backed by Azure AD using an Azure AD Application, implemented with OAuth authentication using JWT Bearer tokens. The SPFX connected to the REST API and authenticates using Open Id Connect to ensure that only authenticated and authorised users can access the API. Further protection was provided by setting the API behind Azure API Management.

Authentication

For the SharePoint Framework web parts to be able to authenticate with the REST API there are a couple of steps that need to be performed:

  • Configure the SharePoint Framework solution to request permission to access the REST API
  • Authorise the request made by SharePoint Framework to access the REST API.

To configure the SharePoint Framework solution take a look at this Microsoft post which provides a good guide (see section Configure the API Permission Requests).

The second part is performed by going into the SharePoint Admin centre and approving the request. Now the point to make is that the user accepting the request needs to be a SharePoint Administrator and also grant admin consent to Azure AD Applications. Basically a Global Admin has this role, so we worked with the IT team to ensure a privileged user did the authorisation. Be mindful of this when deploying to the customer as that will take some discussion and time to organise!

The screen to authorise SharePoint Framework solutions.

Another point to make here is that the name of the Azure AD Application configured in the SharePoint Framework needs to be map to the name of the Azure AD Application configured in Azure AD. This is configured as mentioned above in the SharePoint Framework solution. When I first looked at this I set the resource to be the resourceid for the Azure AD Application rather than the name of the application.

Hopefully mentioning this will mean you do not waste your time getting this right.

"webApiPermissionRequests": [
      {
        "resource": "HROnline Api",
        "scope": "user_impersonation"
      }
]

Azure Hybrid Connections

The Azure Hybrid Connection is setup in two places.

  • Azure App Service hosted in the cloud
  • Hybrid Connection service – running as a Windows server within the network.

The hybrid connection service establishes a connection to the Azure App Service through Azure Relay which is built on top of Service Bus.

Diagram of Hybrid Connection high-level flow

To setup the Hybrid Connection in Azure App Service you must be running at least the Basic Tier or above.

There are some limitations to the types of connection that the technology supports. The transport mechanism needs to be TCP based and does not support UCP. For this solution a .NET SQL Client was used which is supported and works really well.

For information on setting up the Azure Hybrid Connection see the following Microsoft article.


Performance

One of the areas that we wanted to ensure was the performance of the applicatoin. So we put together a POC was put together to prove the approach and also check performance. The performance has been very good and provided that the REST ApI is developed with some thought, it performed better than expected.

There was plenty of thought that went into the API. A few of the optimisations we made were

  • making sure that we had support for paging and limiting the number of records retrieved at one time.
  • Using Dapper and performing filtering at the SQL layer rather than pulling the data down and filtering in the API

Conclusion

This solution enables SharePoint Online solutions to access data hosted On-Premises and it does work really well. To be honest we were surprised how well the solution performed.

Most importantly, he partner and customer were really happy with the end result too.

I hope that people find this post useful, if there is an aspect that you would like more information on then leave a comment and I’ll see what I can do.

Dev Diary S01E08: Fleshing out the Angular and Azure Web API components


 

Introduction

In the last post we talked about me deleting the Azure Application by mistake! In this post we will flesh out our Azure Web API so that we can start to add, edit and store our Invoices in the API layer. We are not quite ready to start implementing the Azure Document Db side yet, that will be in the nexr post.

Let’s get started!

 

Implement the Invoice API

So I am going to implement the Invoice API using a new Web API Controller, the InvoiceController .

In this initial version, the InvoiceController has implementations for getting all the invoices, get a particular invoice with its reference and add a new invoice and update an existing one.

The guts of the InvoiceController works with an Invoice Repository which has been implemented with the Repository pattern. .

Finally, we need our plain-old CLR objects (POCO), which will represent our Invoices.

 

Invoice Controller

So firstly, I created an InvoiceController in the Controllers folder using the WebAPI item template.

The code for the InvoiceController.cs is shown below:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Itsp365.InvoiceFormApp.Api.Models.Entities;
using Itsp365.InvoiceFormApp.Api.Repositories;
namespace Itsp365.InvoiceFormApp.Api.Controllers
{
[Authorize()]
public class InvoiceController : ApiController
{
private InvoiceRepository _invoiceRepository = InvoiceRepository.GetCurrent();
// GET api/values
public IEnumerable<InvoiceForm> Get()
{
return _invoiceRepository.GetAll();
}
// GET api/values/5
public InvoiceForm Get(int id)
{
var invoiceForm = Get().FirstOrDefault(i => i.Id == id);
if (invoiceForm == null)
{
invoiceForm = new InvoiceNotFoundForm();
}
return invoiceForm;
}
[HttpGet]
// GET api/values/5
public InvoiceForm Get(string reference)
{
var invoiceForm = Get().FirstOrDefault(i => i.Reference == reference);
if (invoiceForm == null)
{
invoiceForm = new InvoiceNotFoundForm();
}
return invoiceForm;
}
[HttpPost]
[Route("api/invoice/add")]
// POST api/values
public void Post([FromBody]InvoiceForm invoice)
{
_invoiceRepository.Add(invoice);
}
[HttpPut]
// PUT api/values/5
public void Put(string reference, [FromBody]InvoiceForm invoice)
{
_invoiceRepository.Update(invoice);
}
[HttpDelete]
// DELETE api/values/5
public void Delete(int id)
{
_invoiceRepository.Remove(id);
}
}
}

 

Invoice Repository

Next, I created the InvoiceRepository class in a Repositories folder. The repository has been implemented so that it returns a list of Invoice objects.

The InvoiceRepository.cs implementation is shown next:

public class InvoiceRepository
{
private static InvoiceRepository _instance = null;
private List<InvoiceForm> _repository;
private InvoiceRepository()
{
_repository = new List<InvoiceForm>();
var invoice = new InvoiceForm();
invoice.Id = 1;
invoice.Reference = "LCCSD01";
invoice.CompanyName = "Leeds City Council";
invoice.AgencyName = "Mondo";
invoice.AgencyContact = "Danny Collins";
invoice.CurrencyType = "GBP";
invoice.InvoiceDate = DateTime.UtcNow;
invoice.VatRate = 0.2;
var invoiceLine = new InvoiceLine();
invoiceLine.Description = "SharePoint Project";
invoiceLine.UnitType = "Day";
invoiceLine.UnitValue = 450;
invoiceLine.UnitQuantity = 22;
invoice.InvoiceLines.Add(invoiceLine);
_repository.Add(invoice);
}
public static InvoiceRepository GetCurrent()
{
if (_instance == null)
{
_instance = new InvoiceRepository();
}
return _instance;
}
public IList<InvoiceForm> GetAll()
{
return _repository;
}
public InvoiceForm Add(InvoiceForm addingForm)
{
addingForm.Id = _repository.Max(i => i.Id) + 1;
_repository.Add(addingForm);
return addingForm;
}
public bool Remove(long Id)
{
bool bSuccess = false;
var form = _repository.FirstOrDefault(f => f.Id == Id);
if (form != null)
{
_repository.Remove(form);
bSuccess = true;
}
return bSuccess;
}
public bool Update(InvoiceForm updatingForm)
{
bool bSuccess = false;
var form = _repository.FirstOrDefault(f => f.Id == updatingForm.Id);
if (form != null)
{
form.ModifiedBy = updatingForm.ModifiedBy;
form.Modified = updatingForm.Modified;
form.Reference = updatingForm.Reference;
form.AddressCity = updatingForm.AddressCity;
form.AddressCountry = updatingForm.AddressCountry;
form.AddressCounty = updatingForm.AddressCounty;
form.AddressLine1 = updatingForm.AddressLine1;
form.AddressLine2 = updatingForm.AddressLine2;
form.AddressLine3 = updatingForm.AddressLine3;
form.AddressLine4 = updatingForm.AddressLine4;
form.AgencyContact = updatingForm.AgencyContact;
form.AgencyName = updatingForm.AgencyName;
form.CompanyName = updatingForm.CompanyName;
form.Contact = updatingForm.Contact;
form.CurrencyType = updatingForm.CurrencyType;
form.InvoiceDate = updatingForm.InvoiceDate;
form.VatRate = updatingForm.VatRate;
form.InvoiceLines = updatingForm.InvoiceLines;
bSuccess = true;
}
return bSuccess;
}
}

 

Invoice Entities and the Special Case Pattern

The Invoice entity is implemented as a new class held within the Models/Entities folder. The Invoice Poco class has all the fields which are used to hold the information related to the Invoice.

The code for the invoice.cs is shown below:

public class InvoiceForm
{
public InvoiceForm()
{
this.InvoiceLines = new List<InvoiceLine>();
}
public long Id { get; set; }
public string Reference { get; set; }
public string CompanyName { get; set; }
public DateTime InvoiceDate { get; set; }
public string Contact { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string AddressLine3 { get; set; }
public string AddressLine4 { get; set; }
public string AddressCity { get; set; }
public string AddressCounty { get; set; }
public string AddressCountry { get; set; }
public string AgencyName { get; set; }
public string AgencyContact { get; set; }
public List<InvoiceLine> InvoiceLines { get; set; }
public double InvoiceTotal
{
get
{
double invoiceTotal = 0;
if (InvoiceLines.Count > 0)
{
invoiceTotal = InvoiceLines.Sum(l => l.LineTotal);
}
return invoiceTotal;
}
}
public string CurrencyType { get; set; }
public double VatRate { get; set; }
public double VatAmount { get; set; }
public double InvoiceTotalWithVat
{
get
{
var totalAmount = VatAmount + InvoiceTotal;
return totalAmount;
}
}
public string Status { get; set; }
public string CreatedBy { get; set; }
public string ModifiedBy { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}
public class InvoiceNotFoundForm : InvoiceForm
{
public InvoiceNotFoundForm()
{
this.Id = -1;
this.AgencyName = "Not Found";
this.CompanyName = "Not Found";
this.Reference = "N/A";
}
}
view raw InvoiceForm.cs hosted with ❤ by GitHub

 

The special case pattern is a approach that I use in most of my applications. The objects have an Exists field which can be used to check whether the object has been found.

This approach helps to avoid using nulls which can be ambiguous when returning data from a particular method. I implemented the InvoiceNotFound  class special case.

 

Json camelCase formatting

One of the things that we need to do is make sure that the WebAPI formats the JSON in the right way. For JavaScript the right format for an objects properties in JSON is camelCase, where the first letter of the object’s property name is lower case and each subsequent word starts with an upper case.

For example:-

CustomerRelationshipNumber becomes customerRelationshipNumber

So how do we set this up with Web API? Well we needed to do a little tweak to the WebAPIConfig.cs file. This is how it looks:

GlobalConfiguration.Configuration.Formatters.Remove(config.Formatters.XmlFormatter);
var jsonFormatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
jsonFormatter.UseDataContractJsonSerializer = false;
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
GlobalConfiguration.Configuration.Formatters.Add(jsonFormatter);

The code sets up the Json Serialisation so that it uses a CamelCaseFormatter to resolve the property names on objects. This is applied globally.

 

 

Update the Angular Application to call the Invoice API

Now that the Invoice Controller is implemented in the WebAPI, next I started on the Angular client so that it calls into the API.

 

invoiceDataService.js – calls into the API directly, the code is shown below:

'use strict';
var invoiceDataServiceModule = angular.module('invoiceDataService', ['AdalAngular', 'configurationServiceModule']);
invoiceDataServiceModule.service('invoiceDataService', ['$http', '$q', 'configurationService', 'adalAuthenticationService', invoiceDataServiceFactory]);
function invoiceDataServiceFactory($http, $q, configurationService, adalService)
{
var invoiceDataServiceDefinition={};
invoiceDataServiceDefinition.getInvoices=function getInvoices(){
var url=configurationService.settings.apiUrl + "/invoice";
console.log(adalService.userInfo);
return $http.get(url);
}
invoiceDataServiceDefinition.getInvoice=function getInvoice(invoiceReference){
var url=configurationService.settings.apiUrl + "/invoice/" + invoiceReference;
return $http.get(url);
}
invoiceDataServiceDefinition.saveNewInvoice=function saveNewInvoice(invoiceNo, invoice) {
var url=configurationService.settings.apiUrl + "/invoice/add";
//update created by, modified by, created and modified
invoice.createdBy=adalService.userInfo.userName;
invoice.created = new Date();
invoice.vatRate = configurationService.vatRate;
var updatedInvoice = invoiceDataServiceDefinition.updateInvoiceMetadata(invoice);
return invoiceDataServiceDefinition.executeSave(url, 'POST', updatedInvoice);
}
invoiceDataServiceDefinition.updateInvoice=function updateInvoice(invoiceNo, invoice){
var url=configurationService.settings.apiUrl + "/invoice/" + invoiceNo;
var updatedInvoice = invoiceDataServiceDefinition.updateInvoiceMetadata(invoice);
return invoiceDataServiceDefinition.executeSave(url, 'PUT', updatedInvoice);
}
invoiceDataServiceDefinition.updateInvoiceMetadata = function updateInvoiceMetadata(invoice){
//update created by, modified by, created and modified
invoice.modifiedBy=adalService.userInfo.userName;
invoice.modified = new Date();
invoice.invoiceDate=new Date(invoice.invoiceDate);
return invoice;
}
invoiceDataServiceDefinition.executeSave=function(url, method, invoice){
var deferred = $q.defer();
var dataPackage=JSON.stringify(invoice);
$http({method:method, url:url, data:dataPackage}).then(function processSuccess(response){
deferred.resolve("Successfully Save Invoice");
},
function processFailure(response){
deferred.reject("Failed to update invoice: " + response.data.message);
});
return deferred.promise;
}
return invoiceDataServiceDefinition;
};

The invoiceDataService calls into a shared function executeSave() which will serialize the data being passed into it. Depending on the type of operation on the data, add/update etc then a different method is used.

POST – used to add a new object to the data through the API

PUT – used to update an existing object through the API

 

InvoiceControllers.js – implements the list invoices and add invoice controllers which call into the invoiceDataService. The code is shown below:

'use strict'
var invoiceControllersModule = angular.module('invoiceControllersModule', ['invoiceDataService']);
invoiceControllersModule.controller('listInvoicesController', ['$scope', '$location', 'adalAuthenticationService', 'invoiceDataService', function ($scope, $location, adalService, invoiceDataService) {
$scope.invoices = [];
$scope.error="";
$scope.showInvoiceList=function(){
return true;
};
$scope.showInvoice=function(referenceNumber){
$location.path("/invoices/" + referenceNumber + "/view");
return true;
}
$scope.load=function load() {
$scope.dataloaded=false;
invoiceDataService.getInvoices().then(function(response){
$scope.invoices = response.data;
$scope.dataloaded=true;
}, function error(response){
$scope.error=response;
});
};
$scope.loggedIn=adalService.userInfo.isAuthenticated;
$scope.login = function loginStub(){
adalService.login().then(function(){
$location('#')
});
}
if(adalService.userInfo.isAuthenticated)
{
$scope.load();
}
function createInvoice(){
var invoiceObject={
reference: '',
companyName: '',
invoiceDate: '',
contact:'',
addressLine1:'',
addressLine2:'',
addressLine3:'',
addressLine4:'',
addressCity:'',
addressCounty:'',
addressPostCode:'',
addressCountry:'',
agencyName:'',
agencyContact: '',
invoiceLines: [
],
invoiceTotal:0,
currencyType:'£',
vatRate:0,
vatAmount:0,
invoiceTotalWithVat:0,
status:'',
createdBy:'',
modifiedBy:'',
created:'',
modified:'',
};
return invoiceObject;
};
}]);
invoiceControllersModule.controller('addInvoiceController', ['$scope', 'dataModelService', '$location', 'invoiceDataService', function($scope, dataModelService, $location, invoiceDataService){
$scope.invoice=new dataModelService.Invoice();
$scope.addInvoiceLine = function addInvoiceLine() {
var newInvoiceLine=new dataModelService.InvoiceLine();
$scope.invoice.invoiceLines.push(newInvoiceLine);
}
$scope.cancel = function cancelStub(){
$location.path('/');
}
$scope.status = "";
$scope.error="";
$scope.saveInvoice = function saveInvoiceStub(){
invoiceDataService.saveNewInvoice($scope.invoiceNo, $scope.invoice).then(function processSuccess(response){
$scope.status="Successfully Saved Invoice";
$location.path("#");
return true;
},
function processFailure(response){
//alert(response.data);
$scope.error="Failed to create invoice: " + response.data.message;
});
};
}]);
invoiceControllersModule.controller('editInvoiceController', ['$scope','$http', '$routeParams', '$filter', 'configurationService', 'adalAuthenticationService', 'invoiceDataService', '$location', 'dataModelService', function ($scope, $http, $routeParams, $filter, configurationServuce, adalService, invoiceDataService, $location, dataModelService) {
$scope.invoice= {};
$scope.mode='edit';
if(adalService.userInfo.isAuthenticated)
{
$scope.invoiceNo=$routeParams.invoiceid;
$scope.load=function load() {
invoiceDataService.getInvoice($scope.invoiceNo).then(
function(response)
{
if(response.data)
{
$scope.invoice = response.data;
$scope.invoice.invoiceDate=new Date($scope.invoice.invoiceDate);
}
else{
$scope.error = "There seem to be more than one invoice with the same reference";
}
},
function(response)
{
$scope.error=response.data;
}
);
};
$scope.config = configurationServuce;
$scope.error="";
$scope.load();
}
$scope.saveInvoice = function saveInvoiceStub(){
invoiceDataService.updateInvoice($scope.invoiceNo, $scope.invoice).then(function processSuccess(response){
$scope.status="Successfully Saved Invoice";
$location.path("#");
},
function processFailure(response){
$scope.error="Failed to update invoice: " + response.data.message;
});
};
$scope.cancel = function cancelFunctionStub(){
$location.path("#");
}
$scope.addInvoiceLine = function addInvoiceLine() {
var newInvoiceLine=new dataModelService.InvoiceLine();
$scope.invoice.invoiceLines.push(newInvoiceLine);
}
$scope.removeInvoiceLine = function removeInvoiceLine(index) {
$scope.invoice.invoiceLines.splice(index, 1);
}
}]);
invoiceControllersModule.controller('viewInvoiceController', ['$scope','$http', '$routeParams', 'configurationService', 'adalAuthenticationService', 'invoiceDataService', '$location', function ($scope, $http, $routeParams, Configuration, adalService, invoiceDataService, $location) {
$scope.invoice= {};
if(adalService.userInfo.isAuthenticated)
{
$scope.invoiceNo=$routeParams.invoiceid;
$scope.load=function load() {
invoiceDataService.getInvoice($scope.invoiceNo).then(
function(response)
{
if(response.data)
{
$scope.invoice = response.data;
$scope.invoice.invoiceDate=new Date($scope.invoice.invoiceDate);
$scope.dataloaded=true;
}
else{
$scope.error = "There seem to be more than one invoice with the same reference";
}
},
function(response)
{
$scope.error=response.data;
}
);
};
$scope.editInvoice=function(){
var urlSnippet="invoices/"+$scope.invoiceNo + "/edit";
$location.path(urlSnippet);
};
$scope.cancel = function cancelStub(){
$location.path('/');
}
$scope.config = Configuration;
$scope.error="";
$scope.load();
}
}]);

 

app.js – changed to add the modules for these new files

'use strict';
var invoiceFormApp = angular.module('itspInvoiceFormApp',
[
'ngRoute', 'invoiceControllersModule', 'dataModelService', 'AdalAngular', 'applicationConstantsModule', 'configurationServiceModule', 'settingsController', 'invoiceDataService'
]);
var appStart = function($routeProvider, $httpProvider, adalProvider, applicationConstants, configurationServiceProvider) {
$routeProvider.when('/invoices/add', {
templateUrl:'/app/views/add-invoice.html',
controller: 'addInvoiceController',
requireADLogin: true
}).when('/invoices/:invoiceid/view', {
templateUrl: '/app/views/invoice-display.html',
controller: 'viewInvoiceController',
requireADLogin: true
}).when('/invoices/:invoiceid/edit', {
templateUrl:'/app/views/invoice-display.html',
controller: 'editInvoiceController',
requireADLogin: true
}).when('/invoices', {
templateUrl:'/app/views/list-invoices.html',
controller: 'listInvoicesController',
requireADLogin: false
}).when('/settings', {
templateUrl:'/app/views/settings.html',
controller: 'settingsController',
requireADLogin: true
}).otherwise({
redirectTo: '/invoices'
});
var instance=applicationConstants.instance;
var clientId=applicationConstants.clientId;
var tenantName=applicationConstants.tenantName;
var endPoints=applicationConstants.endPoints;
var apiUrl = applicationConstants.apiUrl;
configurationServiceProvider.init(apiUrl, tenantName, clientId, endPoints);
adalProvider.init({
instance: instance,
tenant: tenantName,
clientId: clientId,
endpoints: endPoints,
anonymousEndpoints:{},
extraQueryParameter: 'nux=1',
cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
}, $httpProvider);
};
invoiceFormApp.config(['$routeProvider', '$httpProvider', 'adalAuthenticationServiceProvider', 'applicationConstants', 'configurationServiceProvider', appStart]);
view raw app.js hosted with ❤ by GitHub

Also we added new routes for the add, edit, view invoices.

 

Update the List Invoice View to allow editing of the Invoice

So now we have the ability to view a list invoices from the GetAll() function of the InvoiceController.

We should allow a user to view and edit an invoice. This is implemented by the following changes.

list-invoices.html – the code is shown below:

<div class="container">
<div class="row">
<div ng-show="loggedIn && showInvoiceList" class="col-md-10 col-sd-3">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<th>Date</th>
<th>Invoice Reference</th>
<th>Company Name</th>
<th>Total</th>
<th>Status</th>
</thead>
<tbody>
<tr class="clickable-row" ng-repeat="invoice in invoices" ng-click="showInvoice(invoice.reference)">
<td>{{invoice.invoiceDate}}</td>
<td>{{invoice.reference}}</td>
<td>{{invoice.companyName}}</td>
<td>{{invoice.currencyType}}{{invoice.invoiceTotal}}</td>
<td>{{invoice.status}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div ng-hide="loggedIn">
<button class="btn btn-primary" title="Login" ng-click="login()">Login</a>
</div>
</div>
<div class="row">
<div ng-show="error !== ''" class="col-sd-3">
Current Error: {{error}}
</div>
</div>
</div>

The list-invoices.html now has an event to respond to an onclick event. This passes through the current invoice.reference of the record so that we are editing the right Invoice.

This calls into the updated controller, which exposes the function showInvoice()  on the $scope object. The function uses the $location service to change the Angular view to #/invoice/{reference]/view which will in turn direct the user to the view-invoice.html page.

We will discuss that bit of the solution next.

 

Another view was created called invoice-display.html, this is the code:

<div class="container">
<div class="row">
<div class="col-md-4 col-sd-12">
<button class="btn btn-default" type="button" title="Edit" ng-click="editInvoice()">Edit</button>
<span ng-show="error">Error: {{error}}</span>
</div>
</div>
<form>
<div id="invoiceDiv" class="row">
<h2>Invoice: {{invoice.reference}}</h2>
<div class="col-md-4 col-sd-12">
<div class="form-group">
<input type="hidden" ng-model="invoice.id"" id="invoiceid" />
<label for="invoiceReference">Invoice Reference</label>
<input type="text" disabled ng-model="invoice.reference" class="form-control" id="invoiceReference" placeholder="Please enter Invoice Reference">
</div>
<div class="form-group">
<label for="invoiceDate">Date</label>
<input type="date" disabled ng-model="invoice.invoiceDate" class="form-control" id="invoiceDate" placeholder="Please enter Invoice Date">
</div>
<div class="form-group">
<label for="vatRate">Vat Rate</label>
<input type="number" disabled ng-model="config.vatRate" class="form-control" id="vatRate">
</div>
<div class="form-group">
<label for="clientName">Agency</label>
<input type="text" disabled ng-model="invoice.agencyName" class="form-control" id="companyName" placeholder="Please provide the name of the agency.">
<label for="clientName">Agency Contact</label>
<input type="text" disabled ng-model="invoice.agencyContact" class="form-control" id="companyName" placeholder="Please provide the name of the agency contact.">
</div>
</div>
<div class="col-md-4 col-sd-12">
<div class="form-group">
<label for="clientName">Client</label>
<input type="text" disabled ng-model="invoice.companyName" class="form-control" id="companyName" placeholder="Please provide the name of the client.">
</div>
<div class="form-group">
<label for="clientContact">Contact Name</label>
<input type="text" disabled ng-model="invoice.contact" class="form-control" id="clientContact" placeholder="Please provide the name of the contact at the clients.">
</div>
<div class="form-group">
<label for="addressLine1">Address</label>
<input type="text" disabled ng-model="invoice.addressLine1" class="form-control" id="addressLine1">
<input type="text" disabled ng-model="invoice.addressLine2" class="form-control" id="addressLine2">
<input type="text" disabled ng-model="invoice.addressLine3" class="form-control" id="addressLine3">
<input type="text" disabled ng-model="invoice.addressLine4" class="form-control" id="addressLine4">
<label for="addressCity">City</label>
<input type="text" disabled ng-model="invoice.addressCity" class="form-control" id="addressCity" />
<label for="addressPostCode">Postcode</label>
<input type="text" disabled ng-model="invoice.addressPostCode" class="form-control" id="addressPostCode">
<label for="addressCountry">Country</label>
<input type="text" disabled ng-model="invoice.addressCountry" class="form-control" id="addressCountry">
</div>
</div>
</div>
<div class="well">
<table class="table table-striped">
<tr>
<th>
Description
</th>
<th>
Unit Type
</th>
<th>
Quantity
</th>
<th>
Amount
</th>
<th>
Line Total
</th>
</tr>
<tbody>
<tr ng-repeat="invoiceLine in invoice.invoiceLines">
<td>
<input disabled class="form-control" type="text" ng-model="invoiceLine.description" placeholder="Provide description">
</td>
<td>
<select disabled class="form-control"ng-model="invoiceLine.unitType" id="invoiceUnit">
<option ng-repeat="unitOption in config.unitTypes" value="{{unitOption.name}}">{{unitOption.name}}</option>
</select>
</td>
<td>
<input disabled class="form-control" type="number" ng-model="invoiceLine.unitQuantity">
</td>
<td>
<input disabled class="form-control" type="number" ng-model="invoiceLine.unitValue" ng-change="invoiceLine.updateLineTotal()">
</td>
<td>
<p class="form-control-static">{{invoiceLine.lineTotal}</p>
</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>

The code above represents the display invoice view, a bit of a mouthful I know. To be honest I am not sure why I did it this way but hey ho. I mean, there is no need for a view for new, edit and view Invoice. I will come back and refactor this in a later episode.

This view has got an edit button, which will allow us to transition and edit the invoice. This calls into the viewInvoiceController.editInvoice() function.

 

 

Another view was created for edit-invoice.html  this is the code:

<div class="container">
<div class="row">
<div class="col-md-4 col-sd-12">
<button class="btn btn-default" type="button" title="Save" ng-click="saveInvoice()">Save</button>
&nbsp;
<button class="btn" type="button" title="Cancel" ng-click="cancel()">Cancel</button>
</div>
<div class="col-md-8 col-sd-12">
<span ng-show="status !== ''">{{status}}</span>
<span ng-show="error !== ''">{{error}}</span>
</div>
</div>
<form>
<div id="invoiceDiv" class="row">
<h2>Invoice: {{invoice.reference}}</h2>
<div class="col-md-4 col-sd-12">
<div class="form-group">
<input type="hidden" ng-model="invoice.id"" id="invoiceid" />
<label for="invoiceReference">Invoice Reference</label>
<input type="text" ng-model="invoice.reference" class="form-control" id="invoiceReference" placeholder="Please enter Invoice Reference">
</div>
<div class="form-group">
<label for="invoiceDate">Date</label>
<input type="date" ng-model="invoice.invoiceDate" class="form-control" id="invoiceDate" placeholder="Please enter Invoice Date">
</div>
<div class="form-group">
<label for="vatRate">Vat Rate</label>
<input type="number" ng-disabled="true" ng-model="config.vatRate" class="form-control" id="vatRate">
</div>
<div class="form-group">
<label for="clientName">Agency</label>
<input type="text" ng-model="invoice.agencyName" class="form-control" id="companyName" placeholder="Please provide the name of the agency.">
<label for="clientName">Agency Contact</label>
<input type="text" ng-model="invoice.agencyContact" class="form-control" id="companyName" placeholder="Please provide the name of the agency contact.">
</div>
</div>
<div class="col-md-4 col-sd-12">
<div class="form-group">
<label for="clientName">Client</label>
<input type="text" ng-model="invoice.companyName" class="form-control" id="companyName" placeholder="Please provide the name of the client.">
</div>
<div class="form-group">
<label for="clientContact">Contact Name</label>
<input type="text" ng-model="invoice.contact" class="form-control" id="clientContact" placeholder="Please provide the name of the contact at the clients.">
</div>
<div class="form-group">
<label for="addressLine1">Address</label>
<input type="text" ng-model="invoice.addressLine1" class="form-control" id="addressLine1">
<input type="text" ng-model="invoice.addressLine2" class="form-control" id="addressLine2">
<input type="text" ng-model="invoice.addressLine3" class="form-control" id="addressLine3">
<input type="text" ng-model="invoice.addressLine4" class="form-control" id="addressLine4">
<label for="addressCity">City</label>
<input type="text" ng-model="invoice.addressCity" class="form-control" id="addressCity" />
<label for="addressPostCode">Postcode</label>
<input type="text" ng-model="invoice.addressPostCode" class="form-control" id="addressPostCode">
<label for="addressCountry">Country</label>
<input type="text" ng-model="invoice.addressCountry" class="form-control" id="addressCountry">
</div>
</div>
</div>
<div class="well">
<button ng-click="addInvoiceLine()" title="Add new line">Add line to Invoice</button>
<table class="table table-striped">
<tr>
<th>
Description
</th>
<th>
Unit Type
</th>
<th>
Quantity
</th>
<th>
Amount
</th>
<th>
Line Total
</th>
</tr>
<tbody>
<tr ng-repeat="invoiceLine in invoice.invoiceLines">
<td>
<input class="form-control" type="text" ng-model="invoiceLine.description" placeholder="Provide description">
</td>
<td>
<select class="form-control"ng-model="invoiceLine.unitType" id="invoiceUnit">
<option ng-repeat="unitOption in config.unitTypes" value="{{unitOption.name}}">{{unitOption.name}}</option>
</select>
</td>
<td>
<input class="form-control" type="number" ng-model="invoiceLine.unitQuantity">
</td>
<td>
<input class="form-control" type="number" ng-model="invoiceLine.unitValue" ng-change="invoiceLine.updateLineTotal()">
</td>
<td>
<p class="form-control-static">{{invoiceLine.lineTotal}</p>
</td>
</tr>
</tbody>
<tfoot align="right">
<tr>
<td>
<span class="form-control-static">Total: {{invoice.invoiceTotal}}</p>
</td>
</tr>
<tr>
<td>
<span class="form-control-static">VAT: {{invoice.vatAmount}}</p>
</td>
</tr>
<tr>
<td>
<span class="form-control-static">Total including VAT: {{invoice.invoiceTotalWithVat}}</p>
</td>
</tr>
</tfoot>
</table>
</div>
</form>
</div>

The code above represents the edit invoice view. This allows a user to make changes to the invoice and then save those changes back by calling into the Invoice Controller REST API.

The code is structured in a similar way to that of the edit Invoice and add Invoice.

On a successful save then the user is returned back to the list of invoices.

 

Problems

The following problems were experienced whilst building out this portion of the application.

Error when viewing the invoice due to issue with routing

Error: {“message”:”The request is invalid.”,”messageDetail”:”The parameters dictionary contains a null entry for parameter ‘id’ of non-nullable type ‘System.Int32’ for method ‘Itsp365.InvoiceFormApp.Api.Models.Entities.InvoiceForm Get(Int32)’ in ‘Itsp365.InvoiceFormApp.Api.Controllers.InvoiceController’.

The cause of this was my WebApiConfig.cs. Which setup a route:

config.Routes.MapHttpRoute(
name: “DefaultApi”,
routeTemplate: “api/{controller}/{id}”,
defaults: new { id= RouteParameter.Optional }
);

Unfortunately, this did not resolve to the function

InvoiceController.Get(string reference);

bur rather the function

InvoiceController.Get(int id);

The following change was made to fix the routing:

config.Routes.MapHttpRoute(
name: “DefaultApi”,
routeTemplate: “api/{controller}/{reference}”,
defaults: new { reference= RouteParameter.Optional }
);

 

Method HTTP Status Code 405: Method not allowed

This error message was thrown when I added the edit Invoice feature. The edit invoice feature when it saves, calls into the API using an HTTP PUT method. The call fails with a message saying that the Method is not allowed.

In turns out that this issue was very similar to the routing error just mentioned.

The default route was changed to:

config.Routes.MapHttpRoute(
name: “DefaultApi”,
routeTemplate: “api/{controller}/{reference}”,
defaults: new { reference= RouteParameter.Optional }
);

Therefore, the routing was expecting to the PUT method to have method signature such as:

[HttpPut]

Update(string reference, [FromBody] invoice)

but we had:

[HttpPut]

Update(string id, [FromBody] invoice)

Making the change to the function to correct the name of the first parameter from id to reference resolved the issue and the save button worked correctly.

Conclusion

This episode we added support to add/edit and update our Invoices through the MVC Web API. This involved additions to both the server side WebAPI and the Angular client side.

We had a few problems which have been explained.

 

Here are the screenshots showing the changes in action:

image

image

 

Github

The repository for the Invoice Form App Client can be found here:

https://github.com/ithinksharepoint/IT365.InvoiceFormApp.Client/tree/7034429583821c0208c6956ee561c5a9f740a1fe

 

The repository for the Invoice Form App API can be found here:

https://github.com/ithinksharepoint/Invoicing-Application/tree/13f1a71d146cd8afba20d89991324cbcd17272c1

 

Next Episode

In the next episode we will start to actually do some Azure Document DB. The WebAPI will be modified and we will implement the calls into Azure Document Db.

Thanks for reading, I hope you can join us for the next session!