Bulk update user work hours for the Schedule Board

If you have lots of users which need their work hours added into CRM, it can take a long time to manually set up each user through the user interface.

Below is the code to update user’s work hours in bulk for a regular 9am-5pm schedule and will also reflect on the Universal Resource Scheduling board.

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Crm.Sdk.Messages;
using System.Net;
using System.ServiceModel.Description;
using System;
using Microsoft.Xrm.Sdk.Query;
using System.Collections.Generic;

namespace BulkCreateWorkHours
{
    class Program
    {
        private static Guid Systemusers;
        private static string Names;

        static void Main(string[] args)
        {
            IOrganizationService organizationService = null;

            try
            {
                //Login details
                ClientCredentials clientCredentials = new ClientCredentials();
                clientCredentials.UserName.UserName = "email";
                clientCredentials.UserName.Password = "password";

                ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

                //CRM Organisation URL details
                organizationService = (IOrganizationService)new OrganizationServiceProxy(new Uri("https://XXXXXXX.api.XX.dynamics.com/XRMServices/2011/Organization.svc"),
                 null, clientCredentials, null);

                if (organizationService != null)
                {
                    Guid userid = ((WhoAmIResponse)organizationService.Execute(new WhoAmIRequest())).UserId;

                    Console.WriteLine("Connection Established Successfully...");

                    //Fetches all Users which have a Bookable Resource
                    string fetchxml = "<?xml version='1.0'?>" +
                                    "<fetch distinct='true' mapping='logical' output-format='xml-platform' version='1.0'>" +
                                    "<entity name='systemuser'>" +
                                    "<attribute name='systemuserid'/>" +
                                    "<attribute name='firstname'/>" +
                                    "<attribute name='lastname'/>" +
                                    "<order descending='false' attribute='firstname'/>" +
                                    "<link-entity name='bookableresource' alias='ac' link-type='inner' to='systemuserid' from='userid'/>" +
                                    "</entity>" +
                                    "</fetch>";

                    EntityCollection result = organizationService.RetrieveMultiple(new FetchExpression(fetchxml));

                    Console.WriteLine("There are {0} entities found", result.Entities.Count);

                    foreach (var c in result.Entities)
                    {
                        Systemusers = ((Guid)c.Attributes["systemuserid"]);
                        Names = ((string)c.Attributes["firstname"]) + " " + ((string)c.Attributes["lastname"]);

                        //Clears original Calander Rules for all of the Users retrieved from the fetch
                        ClearCalenderRules(organizationService, Systemusers);

                        //Add new Calander Rules for all of the Users retrieved from the fetch
                        AddCalenderRules(organizationService, Systemusers);                       
                    }

                    Console.WriteLine("COMPLETE");
                }
                else
                {
                    Console.WriteLine("Failed to Established Connection!!!");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception caught - " + ex.Message);
            }
            Console.ReadKey();
        }

        public static void AddCalenderRules(IOrganizationService organizationService, Guid userid)
        {
            if (userid != Guid.Empty)
            {

                    Entity systemUserEntity = organizationService.Retrieve("systemuser", Systemusers, new ColumnSet(new String[] { "calendarid" }));
                    Entity userCalendarEntity = organizationService.Retrieve("calendar", ((Microsoft.Xrm.Sdk.EntityReference)(systemUserEntity.Attributes["calendarid"])).Id, new ColumnSet(true));
                    EntityCollection calendarRules = (EntityCollection)userCalendarEntity.Attributes["calendarrules"];

                    Entity newInnerCalendar = new Entity("calendar");
                    newInnerCalendar.Attributes["businessunitid"] = new EntityReference("businessunit", ((Microsoft.Xrm.Sdk.EntityReference)(userCalendarEntity["businessunitid"])).Id);
                    Guid innerCalendarId = organizationService.Create(newInnerCalendar);

                    //Create a new calendar rule and assign the inner calendar id to it
                    Entity calendarRule = new Entity("calendarrule");
                    calendarRule.Attributes["duration"] = 1440;
                    calendarRule.Attributes["extentcode"] = 1;
                    //Create a pattern of Mon-Fri
                    calendarRule.Attributes["pattern"] = "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR";
                    calendarRule.Attributes["rank"] = 0;
                    //85 = UK Time Code
                    calendarRule.Attributes["timezonecode"] = 85;
                    calendarRule.Attributes["innercalendarid"] = new EntityReference("calendar", innerCalendarId);
                    //Starting at 12:00 on 1 January 2018
                    calendarRule.Attributes["starttime"] = new DateTime(2018, 01, 01, 0, 0, 0, DateTimeKind.Utc);
                    calendarRules.Entities.Add(calendarRule);

                    //Assign all the calendar rule back to the user calendar
                    userCalendarEntity.Attributes["calendarrules"] = calendarRules;
                    //Please refer to here for Calander Types https://msdn.microsoft.com/en-us/library/dn689038.aspx
                    userCalendarEntity.Attributes["type"] = new OptionSetValue(-1);
                    organizationService.Update(userCalendarEntity);

                    //Creates a new Calendar Rule 
                    Entity calendarRule1 = new Entity("calendarrule");
                    //Duration of 9 hours (540 mins)
                    calendarRule1.Attributes["duration"] = 540;
                    calendarRule1.Attributes["effort"] = 1.0;
                    calendarRule1.Attributes["issimple"] = true;
                    //Offset of 8 hours (480 mins) - Start Time is 8am
                    calendarRule1.Attributes["offset"] = 480;
                    calendarRule1.Attributes["rank"] = 0;
                    calendarRule1.Attributes["subcode"] = 1;
                    calendarRule1.Attributes["timecode"] = 0;
                    calendarRule1.Attributes["timezonecode"] = -1;
                    calendarRule1.Attributes["calendarid"] = new EntityReference("calendar", innerCalendarId);

                    EntityCollection innerCalendarRules = new EntityCollection();
                    innerCalendarRules.EntityName = "calendarrule";
                    innerCalendarRules.Entities.Add(calendarRule1);

                    newInnerCalendar.Attributes["calendarrules"] = innerCalendarRules;
                    newInnerCalendar.Attributes["calendarid"] = innerCalendarId;
                    organizationService.Update(newInnerCalendar);

                    Console.WriteLine("Work Hours Added to " + Names);
                
            }
        }

public static void ClearCalenderRules(IOrganizationService organizationService, Guid userId)
        {
            if (userId != null)
            {
                //Retrieves all CalanderId's for all the users
                string fetchxml = "<?xml version='1.0'?>" +
                                "<fetch distinct='true' mapping='logical' output-format='xml-platform' version='1.0'>" +
                                "<entity name='calendar' >" +
                                "    <attribute name='calendarid' />" +
                                "    <filter type='and' >" +
                                "      <condition attribute='primaryuserid' operator='eq' value='" + userId + "' />" +
                                "    </filter>" +
                                "  </entity>" +
                                "</fetch>";

                EntityCollection result = organizationService.RetrieveMultiple(new FetchExpression(fetchxml));

                Console.WriteLine("There are {0} entities found", result.Entities.Count);

                foreach (var c in result.Entities)
                {
                    Entity Calendar = organizationService.Retrieve("calendar", ((Guid)c.Attributes["calendarid"]), new ColumnSet(true));
                    EntityCollection calendarRules = (EntityCollection)Calendar.Attributes["calendarrules"];

                    int num = 0;
                    List<int> list = new List<int>();

                    foreach (Entity current in calendarRules.Entities)
                    {
                        list.Add(num);
                        num++;
                    }

                    list.Sort();
                    list.Reverse();

                    for (int i = 0; i < list.Count; i++)
                    {
                        //Remove all Calander Rules from Collection
                        calendarRules.Entities.Remove(calendarRules.Entities[list[i]]);
                    }

                    //Assign Calander Rules to empty Entity Collection
                    Calendar.Attributes["calendarrules"] = calendarRules;
                    organizationService.Update(Calendar);

                    Console.WriteLine("Work Hours Deleted for " + Names);
                }
            }
        }
    }
}

This code works in 2 steps:

  1. I created a Fetch to retrieve all users which have a Bookable Resource.
  2. I run a for each loop through each user and pass that UserId into two methods:
ClearCalenderRules(organizationService, Systemusers);

This method clears the current Calendar of the users.

AddCalenderRules(organizationService, Systemusers);

This method adds the Calendar rule specified for the users.

Important parts of the code that needs expanding:

userCalendarEntity.Attributes["type"] = new OptionSetValue(-1);

You need to specify the ‘type’ attribute as -1 because this will then reflect on the Schedule Board. Here is a link for more information https://msdn.microsoft.com/en-us/library/dn689038.aspx

calendarRule.Attributes["pattern"] = "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR";

This pattern string specifies a schedule frequency for Monday to Friday. You can see more examples of different patterns here https://www.syncfusion.com/kb/3719/what-is-recurrencerule-in-the-schedule-control

Hopefully this post helps 🙂

5 thoughts on “Bulk update user work hours for the Schedule Board

  1. Hi Thomas,
    I`m using msdyn_SaveCalendar with flow, I`m passing this JSON to the unbound action:
    {
    “CalendarEventInfo”: “{\”CalendarId\”:\”71199c77-5875-ed11-81aa-000d3abad624\”,\”EntityLogicalName\”:\”bookableresource\”,\”TimeZoneCode\”:5,\”RulesAndRecurrences\”:[{\”Rules\”:[{\”StartTime\”:\”2023-07-01T00:00:00.000Z\”,\”EndTime\”:\”2023-07-22T00:00:00.000Z\”,\”Effort\”:1,\”WorkHourType\”:0}]}]}”
    }

    And I`m getting the following error:
    The data contract type ‘Microsoft.Dynamics.UCICalendar.Plugins.SaveCalendarContract+CalendarEventInfo’ cannot be deserialized because the required data member ‘CalendarId’ was not found.

    What could be the issue?

    1. Looks correct to me. It seems to be complaining about not being able to find the property ‘CalendarId’ in your JSON, which I can see that it is indeed inside of your copied JSON. Have you validated you JSON to see if it is correct syntax?

    2. The JSON string should be:
      {“CalendarId”:”CalendarGUID”,”EntityLogicalName”:”bookableresource”,”TimeZoneCode”:201,”RulesAndRecurrences”:[{“Rules”:[{“StartTime”:”2024-01-20T00:00:00.000Z”,”EndTime”:”2028-01-22T00:00:00.000Z”,”Effort”:1,”WorkHourType”:0}]}]}

Leave a Reply

Your email address will not be published. Required fields are marked *