In this post, part 2 of 3, we’re going to take a look at using 2 different APIs to create calendar items. The goal will be to create a web service in Visual Studio that can accept an event, and use that to create calendar events in Google, or in Outlook 365.
In the third and final installment, we will take a look at what we’ve done in parts 1&2, and use Blackbaud CRM™ as the trigger for adding events to Google and Outlook 365.
“Why do we need to have a separate web service?” you may wonder, as the Blackbaud Infinity platform allows developers to create rich customizations, and it’s certainly capable of calling external APIs. This may be true, but in reality, the APIs for Google and Outlook rely on some very specific dependencies, which will load a lot of extra components into CRM’s bin\custom directory, and may not play well either with CRM, or other customizations you will be writing. Additionally, separating the API calls into their own service allows some extra flexibility around testing, as well as the ability to be consumed by applications other than CRM.
Finally, as the Google API uses a proprietary security file, it gives administrators a little more leeway in maintaining credentials, and not have to shoehorn them into a CRM customization.
In this exercise what we’ll do is create a web service that can take a simple event with a collection of attendees, and add it to either a Google Calendar or an Outlook 365 calendar. In doing so we’ll revisit some of the Google API code we saw in Part 1, and also look at some code to work with the Outlook API.
Before you start this exercise, it’s assumed you went through part 1 and created a Google Developer’s account and project, and downloaded a .json security file to your local system. As long as you went up to the point where Part 1 discussed creating a Visual Studio project, you’ll be ok to start here. In this post, we’ll be creating a brand new VS project.
First thing is to create a new web service project:
As we saw in Part one, in order to use the Google API, we need to import some references:
Tools>NuGet Package Manager>Manage NuGet Packages for Solution
Google APIs Auth Client Library
Then do the same thing, searching for “Google.Apis.Calendar.v3 Client Library”
Rename IService1.vb and Service1.svc to ICalendarSvc.vb and CalendarSvc.scv, respectively. Then open up our interface, ICalendarSvc.vb
Now replace the default operation contracts with the following:
<OperationContract()>
Sub AddCalendarEvent(Details As EventDetails)
Next, remove the default Data Contracts and create the following, which is a new composite type. This type will be a relatively simple version of a calendar event, and is sent into our web service as the request:
<DataContract()>
Public Class EventDetails
<DataMember()>
Public Property Mode() As enumCalendarMode
<DataMember()>
Public Property EventTitle() As String
<DataMember()>
Public Property EventBody() As String
<DataMember()>
Public Property EmailAddresses() As List(Of String)
<DataMember()>
Public Property dtStart() As Date
<DataMember()>
Public Property dtEnd() As Date
End Class
The last thing we will do in this file is add an enum to tell the service what mode to use. In our example we will build in two modes – one for Google and another for Outlook365:
Public Enum enumCalendarMode As Integer
CALENDAR_MODE_GOOGLE = 0
CALENDAR_MODE_OUTLOOK = 1
End Enum
The entire file should now look like this:
<ServiceContract()>
Public Interface ICalendarSvc
<OperationContract()>
Sub AddCalendarEvent(Details As EventDetails)
End Interface
<DataContract()>
Public Class EventDetails
<DataMember()>
Public Property Mode() As enumCalendarMode
<DataMember()>
Public Property EventTitle() As String
<DataMember()>
Public Property EventBody() As String
<DataMember()>
Public Property EmailAddresses() As List(Of String)
<DataMember()>
Public Property dtStart() As Date
<DataMember()>
Public Property dtEnd() As Date
End Class
Public Enum enumCalendarMode As Integer
CALENDAR_MODE_GOOGLE = 0
CALENDAR_MODE_OUTLOOK = 1
End Enum
Open CalendarSvc.svc, remove what’s there and paste the following:
Public Class CalendarSvc
Implements ICalendarSvc
Public Sub New()
End Sub
Sub AddCalendarEvent(Details As EventDetails) Implements ICalendarSvc.AddCalendarEvent
Dim cal As ICalendarApi
If Details.Mode = enumCalendarMode.CALENDAR_MODE_GOOGLE Then
cal = New GoogleCalendarSvc
cal.AddEvent(Details)
ElseIf Details.Mode = enumCalendarMode.CALENDAR_MODE_OUTLOOK Then
cal = New Outlook365CalendarSvc
cal.AddEvent(Details)
End If
End Sub
End Class
What we’ll do next is fill in the actual code that does the work for adding the Google and Outlook events. Since the implementation is similar for both, it makes some sense to create an interface. Looking at the code above, we can infer that we’ll be making an interface called “ICalendarApi” and two classes which implement the interface: “GoogleCalendarSvc” and “Outlook365CalendarSvc”. So go ahead and create ICalendarApi, which should be very simple and look like this:
Public Interface ICalendarApi
Sub AddEvent(Details As EventDetails)
End Interface
Next, create a new class called “GoogleCalendarSvc.vb” and paste in the following:
Imports System.IO
Imports System.Threading
Imports Google.Apis.Calendar.v3
Imports Google.Apis.Calendar.v3.Data
Imports Google.Apis.Services
Imports Google.Apis.Auth.OAuth2
Imports Google.Apis.Util.Store
Imports System.Net.Http
Public Class GoogleCalendarSvc
Implements ICalendarApi
Dim _scopes As IList(Of String) = New List(Of String)()
Dim _service As Google.Apis.Calendar.v3.CalendarService
Dim _AllEvents As New List(Of Data.Event)
Dim _Details As EventDetails
Public Sub AddEvent(Details As EventDetails) Implements ICalendarApi.AddEvent
_Details = Details
InitializeService()
CreateEvent()
End Sub
Private Sub InitializeService()
_scopes.Add(Google.Apis.Calendar.v3.CalendarService.Scope.Calendar)
Dim jsonFile As String = "C:\Credentials\client_secret_791792756839-c1ev8q9gjgvep2qm1v2cl872joh2pd98.apps.googleusercontent.com.json"
Dim credential As UserCredential
Dim secrets As ClientSecrets
Dim fds As New FileDataStore("C:\Credentials", True)
Dim googleAccount As String = "brightvinesolutions@gmail.com"
Try
Using stream As New FileStream(jsonFile, FileMode.Open, FileAccess.Read)
secrets = GoogleClientSecrets.Load(stream).Secrets
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(secrets, _scopes, googleAccount, CancellationToken.None, fds).Result
End Using
Catch ex As Exception
Throw ex
End Try
' Create the calendar service using an initializer instance
Dim initializer As New BaseClientService.Initializer()
initializer.HttpClientInitializer = credential
initializer.ApplicationName = "VB.NET Calendar Sample"
_service = New Google.Apis.Calendar.v3.CalendarService(initializer)
End Sub
Private Sub CreateEvent()
Try
Dim calEvent As New Google.Apis.Calendar.v3.Data.Event
calEvent.Summary = _Details.EventTitle
calEvent.Description = _Details.EventBody
calEvent.Location = ""
Dim attendees As New List(Of EventAttendee)
Dim evAttendee As EventAttendee
For Each curAttendee As String In _Details.EmailAddresses
evAttendee = New EventAttendee
evAttendee.Email = curAttendee
attendees.Add(evAttendee)
Next
calEvent.Attendees = attendees
Dim evStart As New EventDateTime
evStart.DateTime = _Details.dtStart
Dim evEnd As New Data.EventDateTime
evEnd.DateTime = _Details.dtEnd
calEvent.Start = evStart
calEvent.End = evEnd
Dim createdEvent As [Event] = _service.Events.Insert(calEvent, "primary").Execute
Catch ex As HttpRequestException
Throw ex
Catch ex As Exception
Throw ex
End Try
End Sub
End Class
Note that in InitializeService, you’ll need to change the “dim jsonFile…” to reflect the real location of the json file you downloaded from Google. This location, of course, will need to be accessible by IIS, so make sure the AppPool you’re running under has proper rights.
At this point if we were to go back into CalendarSvc.svc and comment out the following lines:
cal = New Outlook365CalendarSvc
cal.AddEvent(Details)
…we should be able to use this service to create new Google events. To those who read through all of part 1, the “InitializeService” section should look familiar, and the CreateEvent sub should look very straightforward, its purpose mostly being to create the correct Google Event object, set some properties (we iterate the Attendees ArrayList), and finally, execute the request.
This can be easily tested by creating a Win Forms test project in the same solution, creating a service reference to CalendarSvc named “ProjSvc” and running the following code either on form load or in a button click event:
Dim gRef As New ProjSvc.CalendarSvcClient
Dim ed As New ProjSvc.EventDetails
Dim emailAddresses As New ArrayList
Try
emailAddresses.Add("my.account@gmail.com")
ed.Mode = ProjSvc.enumCalendarMode.CALENDAR_MODE_GOOGLE
ed.EmailAddresses = emailAddresses.ToArray(GetType(String))
ed.EventTitle = "Yet another conference call"
ed.EventBody = "Make sure you're not on mute when speaking!"
ed.dtStart = Now().AddHours(1)
ed.dtEnd = Now().AddHours(2)
gRef.AddCalendarEvent(ed)
Catch ex As Exception
MsgBox(ex.Message)
End Try
In the downloadable code, we’ll have a richer form for testing, and will include Outlook testing, as well, but the above snippet should let you try this service out now.
Ok, let’s get back to the Calendar Service. If you commented out the following:
cal = New Outlook365CalendarSvc
cal.AddEvent(Details)
…uncomment them, and add a new class called CallbackMethods.vb with the following code:
' This source is subject to the Microsoft Public License.
' See http://www.microsoft.com/en-us/openness/licenses.aspx#MPL.
' All other rights reserved.
'
' THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
' EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
' WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
'**************************************************************************/
Imports System.Security.Cryptography.X509Certificates
Public NotInheritable Class CallbackMethods
Private Sub New()
End Sub
Public Shared Function RedirectionUrlValidationCallback(ByVal redirectionUrl As String) As Boolean
' The default for the validation callback is to reject the URL.
Dim result As Boolean = False
Dim redirectionUri As New Uri(redirectionUrl)
' Validate the contents of the redirection URL. In this simple validation
' callback, the redirection URL is considered valid if it is using HTTPS
' to encrypt the authentication credentials.
If redirectionUri.Scheme = "https" Then
result = True
End If
Return result
End Function
Public Shared Function CertificateValidationCallBack(ByVal sender As Object,
ByVal certificate As System.Security.Cryptography.X509Certificates.X509Certificate,
ByVal chain As System.Security.Cryptography.X509Certificates.X509Chain,
ByVal sslPolicyErrors As System.Net.Security.SslPolicyErrors) As Boolean
'return true;
' If the certificate is a valid, signed certificate, return true.
If sslPolicyErrors = System.Net.Security.SslPolicyErrors.None Then
Return True
End If
' If there are errors in the certificate chain, look at each error to determine the cause.
If (sslPolicyErrors And System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) <> 0 Then
If chain IsNot Nothing AndAlso chain.ChainStatus IsNot Nothing Then
For Each status As System.Security.Cryptography.X509Certificates.X509ChainStatus In chain.ChainStatus
If (certificate.Subject = certificate.Issuer) AndAlso
(status.Status =
System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.UntrustedRoot) Then
' Self-signed certificates with an untrusted root are valid.
Continue For
Else
If status.Status <>
System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.NoError Then
' If there are any other errors in the certificate chain, the certificate is invalid,
' so the method returns false.
Return False
End If
End If
Next status
End If
' When processing reaches this line, the only errors in the certificate chain are
' untrusted root errors for self-signed certificates. These certificates are valid
' for default Exchange server installations, so return true.
Return True
Else
' In all other cases, return false.
Return False
End If
End Function
End Class
This code snippet came from this project:
https://code.msdn.microsoft.com/office365/How-to-Search-Calendar-11437894
You can go here to get the full context, but the gist is that when authenticating with Outlook, using AutodiscoverUrl may throw a redirect to https, which could throw an error. This class, used as a callback, will override that.
The last file in the project is the Outlook365CalendarSvc.vb class. Go ahead and add that, then past in the following:
Imports Microsoft.Exchange.WebServices.Data
Imports System.Net
Public Class Outlook365CalendarSvc
Implements ICalendarApi
Public Sub AddEvent(Details As EventDetails) Implements ICalendarApi.AddEvent
'ServicePointManager.ServerCertificateValidationCallback = AddressOf CallbackMethods.CertificateValidationCallBack
Dim service As New ExchangeService(ExchangeVersion.Exchange2010_SP2)
' Get the information of the account.
Dim user As New User
Try
user.Account = "my.account@brightvinesolutions.com"
User.Pwd = "MyPassword"
service.Credentials = New WebCredentials(user.Account, user.Pwd)
' Set the url of server.
If AutodiscoverUrl(service, User) Then
Dim appt As New Appointment(service)
'Set the properties on the appointment object to create the appointment.
appt.Subject = Details.EventTitle
appt.Body = Details.EventBody
appt.Start = Details.dtStart
appt.End = Details.dtEnd
appt.Location = ""
appt.ReminderDueBy = DateTime.Now
For Each curAttendee As String In Details.EmailAddresses
appt.RequiredAttendees.Add(curAttendee)
Next
'Save the appointment to your calendar.
appt.Save(SendInvitationsMode.SendToNone)
End If
Catch ex As Exception
Throw ex
Exit Sub
End Try
End Sub
Private Shared Function AutodiscoverUrl(ByVal service As ExchangeService, ByVal user As User) As Boolean
Dim isSuccess As Boolean = False
Try
service.AutodiscoverUrl(user.Account, AddressOf CallbackMethods.RedirectionUrlValidationCallback)
isSuccess = True
Catch ex As Exception
Throw ex
End Try
Return isSuccess
End Function
End Class
Friend Class User
Private accountInfo As String = Nothing
Private pwdInfo As String = Nothing
Public Sub New()
End Sub
Public Property Account() As String
Get
Return accountInfo
End Get
Set(value As String)
accountInfo = value
End Set
End Property
Public Property Pwd() As String
Get
Return pwdInfo
End Get
Set(value As String)
pwdInfo = value
End Set
End Property
End Class
Of course, you’ll want to make sure to change the user name and password to be something more appropriate. In a production situation, we’d probably want to set up some service account in Outlook, and at the very least, store the credentials in a config file.
At this point you can build your project and using the same code snippet as above, set the mode to “OUTLOOK” and you should be getting calendar items added to your Outlook 365 Calendar.
Next week, we'll look at part 3 of this series, how add these objects to Blackbaud CRM.