使用 C# 可执行文件通过 Microsoft Exchange Web Services Managed API 2.2 创建 Exchange 日历事件





5.00/5 (1投票)
C# .Net 命令行应用程序,可通过模拟身份在 Exchange 日历中创建日历事件
下载 EWSCalendarUpdater.zip
下载 Nuget_packages.zip
引言
在某些情况下,系统服务或管理员可能需要在不同的 Exchange 帐户之间创建日历事件,例如学校课表或任何类型的组织课表。该应用程序通过模拟 Exchange 帐户,并在 Exchange 帐户的日历中创建事件。
提供的代码是一个 C#.Net 命令行应用程序,具有几个参数,例如 CSV 输入文件、线程数、最大尝试次数、租户管理员凭据。它利用 Microsoft Exchange Web Services Managed API 2.2 (EWS),并已在 Office 365 的 Exchange 服务(显然是 2013 版本)上进行了测试。当然,EWS 作为一项 Web 服务,有时会超时并引发异常。代码会重新尝试推送失败的保存,直到成功或达到设定的最大尝试次数。
此项目的最终目的是在数百个帐户之间创建相当数量的事件。到目前为止,已经进行了几次测试,在 599 个用户帐户中处理了 800,000 条记录。在测试期间,有一定比例(10%)的事件创建失败,应用程序进行了 5 次重试以成功推送所有内容。总共花费了近 12 个小时。可能还可以进一步优化,但目前已满足其目的。
此代码使用了 Command Line Parser Library、CsvHelper 库以及一个名为 "PagingCollection" 的类。感谢各位作者!
使用代码
该命令行应用程序消耗一个包含事件记录的 CSV 文件。记录由 5 个字段组成。虽然这对于本项目来说已经足够,但当然可以添加更多字段来创建更详细的事件。待创建的事件会打包成每批 100 个事件的请求。EWS 服务请求按用户帐户进行多线程处理,并采用指定的线程数。
该应用程序需要以下输入参数。
--user:必需,此应用程序已针对作为 Office 365 服务一部分的 Exchange Server 开发并进行了测试。对于“user”,使用了租户管理员的电子邮件。此代码尚未在独立 Exchange 上进行测试。
--method:创建事件的方法。目前只有一种真正有效的方法:“DeleteFolderAndCreateEventsFromScratch”。这显然是一种非常原始的“更新”事件的方式,通过删除目标文件夹来重新创建一切。然而,这在开发时符合目的。EWS 公开了某些可能通过比较和更新(如果需要)来提供真正更新的方法。
--read: 必需,输入 CSV 文件和路径,示例 CSV 文件内容如下
StartDateTime,EndDateTime,Subject,Location,Username
2017-09-09 13:45:00.000,2017-09-09 14:10:00.000,Meeting1,Location1,<enter user's email>
2018-09-16 13:45:00.000,2018-09-16 14:10:00.000,Meeting2,Location2,<enter user's email>
2014-09-29 09:55:00.000,2014-09-29 10:55:00.000,Meeting3,Location3,<enter user's email>
--folder: 必需,Exchange 日历文件夹。如果文件夹不存在,将被创建。如果文件夹存在,将被删除并重新创建。这可能取决于“method”参数,但目前只有一个选项:“DeleteFolderAndCreateEventsFromScratch”。
--threads:(默认为 10),在创建大量线程时,此参数可能有助于优化整体性能。请求被分批到指定的线程数。过多的线程数也可能降低性能,因此可能需要单独测试。
--attempts:(默认为 10),总尝试次数。代码会重新尝试创建失败的保存,直到完全成功和/或达到指定的次数。
--verbose: 是否显示所有消息。
从命令行执行已编译程序的示例
EWSCalendarUpdater.exe --user <tenant admin email> -p <password> --method DeleteFolderAndCreateEventsFromScratch --folder "My Meetings" --read c:\Test1.csv --threads 10 -v true
执行后的控制台窗口。在此示例中,将在 599 个用户之间创建 800000 个事件。
大约 5 小时后,控制台窗口。它在 2 次总尝试内成功创建了所有事件。
下面提供的代码是应用程序的核心方法,对应于同名的唯一选项:“DeleteFolderAndCreateEventsFromScratch”。该方法可以分解成功能块,但在此示例中保留为一个方法。它当然可以分解成至少以下更小的功能:
- 冒充身份
- 如果 Exchange 文件夹已存在,则删除
- 创建文件夹
- 创建事件。
private static EventsCreationResult
DeleteFolderAndCreateEventsFromScratch
(ExchangeService exchangeService,
GroupedAppointmentEntries appointments,
string folderName, Options options)
{
var result = new EventsCreationResult()
{ Responses = new List<ServiceResponseCollection<ServiceResponse>>(),
Key = appointments.Username };
var logResult = new StringBuilder();
var folderView = new FolderView(int.MaxValue, 0, OffsetBasePoint.Beginning);
folderView.PropertySet = new PropertySet(BasePropertySet.FirstClassProperties);
folderView.PropertySet.Add(FolderSchema.DisplayName);
folderView.PropertySet.Add(FolderSchema.EffectiveRights);
folderView.Traversal = FolderTraversal.Deep;
bool folderCreated = false;
var searchFilter = new SearchFilter.IsEqualTo(FolderSchema.DisplayName,
folderName);
try
{
// Impersonation
logResult.AppendLine(string.Format("Impersonating {0}...",
appointments.Username));
exchangeService.ImpersonatedUserId =
new ImpersonatedUserId(ConnectingIdType.SmtpAddress,
appointments.Username.Trim());
logResult.AppendLine("OK");
//Delete folder if it exists
var destinationFolder = exchangeService.FindFolders
(WellKnownFolderName.MsgFolderRoot, searchFilter,
folderView).FirstOrDefault();
if (destinationFolder == null)
{
logResult.Append("Destination folder not found. ");
}
else
{
logResult.Append("Destination folder exists. ");
logResult.Append("Deleting destination folder...");
// Delete the folder.
// EWS was throwing an exception when there
// was 2000+ items in the folder.
// (Microsoft.Exchange.WebServices.Data.ServiceResponseException
// or ServiceRequestException)
try
{
exchangeService.FindFolders(WellKnownFolderName.MsgFolderRoot,
searchFilter, folderView).FirstOrDefault().Delete(DeleteMode.HardDelete);
}
catch (Exception ex)
{
if (ex is ServiceRequestException || ex is ServiceResponseException)
{
var pauseAfterDeletingFolderInSeconds = 5;
logResult.Append(
string.Format(" known ServiceResponseException was thrwon, waiting for the folder do disapear..."
, pauseAfterDeletingFolderInSeconds));
var folderStillExist = true;
var numberOfAttempts = 0;
// Wiat upto 5 minutes
while (folderStillExist && numberOfAttempts <= 30)
{
System.Threading.Thread.Sleep(pauseAfterDeletingFolderInSeconds
* 1000);
logResult.Append(".");
folderStillExist =
exchangeService.FindFolders(WellKnownFolderName.MsgFolderRoot,
searchFilter, folderView).FirstOrDefault() != null;
numberOfAttempts++;
}
}
else
{
throw ex;
}
}
destinationFolder = exchangeService.FindFolders(WellKnownFolderName.MsgFolderRoot,
searchFilter, folderView).FirstOrDefault();
if (destinationFolder == null)
{
logResult.AppendLine(" - deleted OK");
}
else
{
throw new EWSCalendarUpdaterException(string.Format
(" Error. Destination folder {0} could not be deleted from user {1}."
, folderName, appointments.Username));
}
}
//Create folder
logResult.Append(String.Format("Creating destination folder..."));
var folder = new CalendarFolder(exchangeService)
{ DisplayName = folderName };
folder.Save(WellKnownFolderName.Calendar);
logResult.AppendLine("OK");
folderCreated = true;
// Create events
var meetings = new Collection<Appointment>();
foreach (var item in appointments.Entries)
{
meetings.Add(item.ToAppointment(exchangeService));
}
destinationFolder = exchangeService.FindFolders
(WellKnownFolderName.MsgFolderRoot, searchFilter,
folderView).FirstOrDefault();
var paginatedMeetings
= new PagingCollection<Appointment>(meetings);
ServiceResponseCollection<ServiceResponse> responses = null;
logResult.Append(String.Format("Creating calendar item(s) ({0}) in batches of {1}...:",
meetings.Count(),
paginatedMeetings.PageSize));
for (int i = 1; i <= paginatedMeetings.PagesCount; i++)
{
logResult.Append(string.Format("{0}/{1}...", i, paginatedMeetings.PagesCount));
var items = paginatedMeetings.GetData(i);
var saveResult = exchangeService.CreateItems(items,
destinationFolder.Id, MessageDisposition.SaveOnly,
SendInvitationsMode.SendToNone);
result.Responses.Add(saveResult);
if (saveResult.OverallResult == ServiceResult.Success)
{
logResult.Append(string.Format("OK, "));
}
else if (responses.OverallResult == ServiceResult.Warning)
{
logResult.AppendLine("there were warnings when saving items");
foreach (ServiceResponse response in responses)
{
if (response.Result == ServiceResult.Error)
{
logResult.AppendLine("Error code: " + response.ErrorCode.ToString());
logResult.AppendLine("Error message: " + response.ErrorMessage);
}
}
}
else if (responses.OverallResult == ServiceResult.Error)
{
Console.WriteLine("there were errors when saving items");
foreach (ServiceResponse response in responses)
{
if (response.Result == ServiceResult.Error)
{
logResult.AppendLine("Error code: " + response.ErrorCode.ToString());
logResult.AppendLine("Error message: " + response.ErrorMessage);
}
}
}
else
{
throw new NotImplementedException();
}
}
logResult.AppendLine("");
}
catch (Exception e)
{
logResult.AppendLine("Error: " + e.Message);
result.Error = e;
if(folderCreated)
{
logResult.AppendLine("Deleting the forlder...(without waiting for results)");
try
{
exchangeService.FindFolders(WellKnownFolderName.MsgFolderRoot,
searchFilter, folderView).FirstOrDefault().Delete(DeleteMode.HardDelete);
}
catch
{
// Likely (Microsoft.Exchange.WebServices.Data.ServiceResponseException
// or ServiceRequestException)
}
}
}
logResult.AppendLine("-----------------------------------------------------------------------");
result.EventsCreationLog = logResult.ToString();
return result;
}
关注点
在开发过程中,我查阅了一些 MSDN 文章,这些文章对读者可能也很有用:
http://cloudfinder.com/user-impersonation-settings-office-365/
如何:使用 Exchange 模拟添加约会
http://msdn.microsoft.com/en-us/library/office/dn722379(v=exchg.150).aspx
如何:使用 EWS 在 Exchange 2013 中创建约会和会议
http://msdn.microsoft.com/en-us/library/office/dn495611(v=exchg.150).aspx
Microsoft Exchange Web Services Managed API 2.2
http://www.microsoft.com/en-us/download/confirmation.aspx?id=42951
如何:引用 EWS Managed API 程序集
http://msdn.microsoft.com/en-US/library/dn528373(v=exchg.150).aspx
Exchange 2013:以编程方式创建会议
http://code.msdn.microsoft.com/exchange/Exchange-2013-Create-79148637
开始使用 EWS Managed API 客户端应用程序
http://msdn.microsoft.com/library/dn567668(v=exchg.150).aspx
如何:使用 EWS 在 Exchange 中获取约会和会议
http://msdn.microsoft.com/en-us/library/office/dn495614(v=exchg.150).aspx