Kleinere Robustheitsveränderungen
logging im program.cs gefixt - hier wurde der fälschlicherweise implementierte singleton statt der Wrapper-Klasse aufgerufen. Retry eingebaut: Nach anlegen eines Kontakts wird jetzt 4x Geprüft ob der Kontakt angelegt wurde. Wenn nicht wird 1x das Anlegen wiederholt. Methode checkt jetzt zwar den LastRun timestamp, hat aber eine Grateful period von 4h, in der sie die Kontakte nochmals prüft.
This commit is contained in:
@@ -21,6 +21,12 @@ namespace Epi2O365
|
|||||||
private string tenantID;
|
private string tenantID;
|
||||||
private string BaseUrl;
|
private string BaseUrl;
|
||||||
|
|
||||||
|
private const int CreateVerifyDelayMs = 2500;
|
||||||
|
private const int CreateVerifyMaxRetries = 4;
|
||||||
|
private const int CreateHttpRetryMax = 1; // 1 zusätzlicher Versuch bei 429/5xx
|
||||||
|
private const int CreateRetryCount = 1; // Anzahl zusätzlicher Versuche
|
||||||
|
|
||||||
|
|
||||||
public O365Connector(string clientID, string clientSecret, string tenantID, string BaseUrl)
|
public O365Connector(string clientID, string clientSecret, string tenantID, string BaseUrl)
|
||||||
{
|
{
|
||||||
this.clientID = clientID;
|
this.clientID = clientID;
|
||||||
@@ -29,14 +35,18 @@ namespace Epi2O365
|
|||||||
this.BaseUrl = BaseUrl;
|
this.BaseUrl = BaseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SyncContact(Contact contact, string destinationAccountPrimaryAddress)
|
// OData-Escape für Filterwerte
|
||||||
|
private static string O(string s) => (s ?? string.Empty).Replace("'", "''");
|
||||||
|
|
||||||
|
public async Task SyncContact(Contact contact, string destinationAccountPrimaryAddress, int depth = 0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var graphClient = await GetGraphClient();
|
var graphClient = await GetGraphClient();
|
||||||
|
|
||||||
if (await DoesContactExist(contact, destinationAccountPrimaryAddress, graphClient))
|
if (await DoesContactExist(contact, destinationAccountPrimaryAddress, graphClient))
|
||||||
{
|
{
|
||||||
Logger.Info("Contact " + contact.DisplayName + " found in O365");
|
Logger.Info(destinationAccountPrimaryAddress + " | Contact " + contact.DisplayName + " found in O365");
|
||||||
string O365ContactID = await GetContactIdByNickName(graphClient, destinationAccountPrimaryAddress, contact.NickName);
|
string O365ContactID = await GetContactIdByNickName(graphClient, destinationAccountPrimaryAddress, contact.NickName);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(O365ContactID))
|
if (!string.IsNullOrEmpty(O365ContactID))
|
||||||
@@ -44,23 +54,98 @@ namespace Epi2O365
|
|||||||
await graphClient.Users[destinationAccountPrimaryAddress]
|
await graphClient.Users[destinationAccountPrimaryAddress]
|
||||||
.Contacts[O365ContactID]
|
.Contacts[O365ContactID]
|
||||||
.PatchAsync(contact);
|
.PatchAsync(contact);
|
||||||
Logger.Info($"Contact with ID '{O365ContactID}' successfully updated.");
|
Logger.Info(destinationAccountPrimaryAddress + $" | Contact with ID '{O365ContactID}' successfully updated");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Warn("No valid contact ID found for update.");
|
Logger.Warn(destinationAccountPrimaryAddress + " | No valid contact ID found for update.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Info("Contact " + contact.DisplayName + " NOT found in O365");
|
// --- Create-Flow mit NickName-basierter Verifikation ---
|
||||||
Logger.Info("Creating new Contact");
|
Logger.Info(destinationAccountPrimaryAddress + " | Contact " + contact.DisplayName + " NOT found in O365");
|
||||||
await graphClient.Users[destinationAccountPrimaryAddress].Contacts.PostAsync(contact);
|
Logger.Info(destinationAccountPrimaryAddress + " | Creating new Contact (Nick='" + contact.NickName + "')");
|
||||||
|
|
||||||
|
// kleiner HTTP-Retry nur für den POST (429/5xx)
|
||||||
|
int httpAttempts = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Prefer: return=representation – liefert dir direkt eine ID (nur fürs Log)
|
||||||
|
var created = await graphClient.Users[destinationAccountPrimaryAddress]
|
||||||
|
.Contacts
|
||||||
|
.PostAsync(contact, rc => rc.Headers.Add("Prefer", "return=representation"));
|
||||||
|
Logger.Info(destinationAccountPrimaryAddress + " | Created contact: Id=" + (created?.Id ?? "n/a") + " Nick='" + (created?.NickName ?? contact.NickName) + "'");
|
||||||
|
// NickName sicherstellen (einmaliges PATCH – idempotent)
|
||||||
|
if (!string.IsNullOrEmpty(created?.Id) && !string.IsNullOrWhiteSpace(contact.NickName))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await graphClient.Users[destinationAccountPrimaryAddress]
|
||||||
|
.Contacts[created.Id]
|
||||||
|
.PatchAsync(new Contact { NickName = contact.NickName });
|
||||||
|
|
||||||
|
Logger.Debug(destinationAccountPrimaryAddress + $" | Ensured nickName='{contact.NickName}' on Id={created.Id}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warn(destinationAccountPrimaryAddress + $" | Unable to ensure nickName on Id={created.Id}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
break; // POST erfolgreich
|
||||||
|
}
|
||||||
|
catch (Microsoft.Kiota.Abstractions.ApiException apiEx) when (
|
||||||
|
(apiEx.ResponseStatusCode >= 500 || apiEx.ResponseStatusCode == 429)
|
||||||
|
&& httpAttempts < CreateHttpRetryMax)
|
||||||
|
{
|
||||||
|
httpAttempts++;
|
||||||
|
Logger.Warn(destinationAccountPrimaryAddress + $" | Create POST failed with {apiEx.ResponseStatusCode}. Retry {httpAttempts}/{CreateHttpRetryMax} in 1s …");
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifizieren mit bis zu N Wiederholungen – immer dieselbe NickName-Checkmethode
|
||||||
|
for (int attempt = 0; attempt <= CreateVerifyMaxRetries; attempt++)
|
||||||
|
{
|
||||||
|
await Task.Delay(CreateVerifyDelayMs);
|
||||||
|
Logger.Info(destinationAccountPrimaryAddress + $" | Starting to Verify: contact '{contact.NickName}'. (attempt {attempt}).");
|
||||||
|
bool existsNow = await DoesContactExist(contact, destinationAccountPrimaryAddress, graphClient);
|
||||||
|
if (existsNow)
|
||||||
|
{
|
||||||
|
Logger.Info(destinationAccountPrimaryAddress + $" | Verified: contact '{contact.NickName}' is present after create (attempt {attempt}).");
|
||||||
|
// Optional direkt noch die ID loggen:
|
||||||
|
var id = await GetContactIdByNickName(graphClient, destinationAccountPrimaryAddress, contact.NickName);
|
||||||
|
if (!string.IsNullOrEmpty(id))
|
||||||
|
Logger.Debug(destinationAccountPrimaryAddress + $" | Verified ContactId: {id}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt == CreateVerifyMaxRetries)
|
||||||
|
{
|
||||||
|
Logger.Warn(destinationAccountPrimaryAddress + $" | Contact '{contact.NickName}' still not visible after create (attempts: {CreateVerifyMaxRetries + 1}).");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.Info(destinationAccountPrimaryAddress + $" | Not visible yet, re-checking (attempt {attempt + 1}/{CreateVerifyMaxRetries}) …");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional: eine vorsichtige Wiederholung mit gleicher Logik (NickName-only)
|
||||||
|
if (depth < CreateRetryCount)
|
||||||
|
{
|
||||||
|
Logger.Warn(destinationAccountPrimaryAddress + $" | Retrying SyncContact once for Nick='{contact.NickName}' (depth {depth + 1}) …");
|
||||||
|
await Task.Delay(2000);
|
||||||
|
await SyncContact(contact, destinationAccountPrimaryAddress, depth + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error("Error while synchronizing contact: " + ex.Message);
|
Logger.Error(destinationAccountPrimaryAddress + " | Error while synchronizing contact: " + ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,11 +153,15 @@ namespace Epi2O365
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var safe = O(nickName);
|
||||||
|
|
||||||
var contacts = await graphClient.Users[userPrimaryMail]
|
var contacts = await graphClient.Users[userPrimaryMail]
|
||||||
.Contacts
|
.Contacts
|
||||||
.GetAsync(requestConfiguration =>
|
.GetAsync(rc =>
|
||||||
{
|
{
|
||||||
requestConfiguration.QueryParameters.Filter = $"NickName eq '{nickName}'";
|
rc.QueryParameters.Filter = $"nickName eq '{safe}'";
|
||||||
|
rc.QueryParameters.Select = new[] { "id", "nickName", "displayName" };
|
||||||
|
rc.QueryParameters.Top = 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
var contact = contacts?.Value?.FirstOrDefault();
|
var contact = contacts?.Value?.FirstOrDefault();
|
||||||
@@ -80,11 +169,44 @@ namespace Epi2O365
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error($"Error retrieving contact with NickName '{nickName}': {ex.Message}");
|
Logger.Error(userPrimaryMail + $" | Error retrieving contact with NickName '{nickName}': {ex.Message}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> DoesContactExist(Contact contact, string userPrimaryMail, GraphServiceClient graphClient)
|
||||||
|
{
|
||||||
|
string NickName = contact.NickName;
|
||||||
|
if (string.IsNullOrWhiteSpace(NickName))
|
||||||
|
{
|
||||||
|
Logger.Warn(userPrimaryMail + " | No valid NickName found for the given contact.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var safe = O(NickName);
|
||||||
|
|
||||||
|
var contacts = await graphClient.Users[userPrimaryMail]
|
||||||
|
.Contacts
|
||||||
|
.GetAsync(rc =>
|
||||||
|
{
|
||||||
|
rc.QueryParameters.Filter = $"nickName eq '{safe}'";
|
||||||
|
rc.QueryParameters.Select = new[] { "id" };
|
||||||
|
rc.QueryParameters.Top = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
bool exists = contacts?.Value != null && contacts.Value.Any();
|
||||||
|
Logger.Info(userPrimaryMail + $" | Contact with NickName {NickName} exists: {exists}");
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(userPrimaryMail + $" | Error checking contacts: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<GraphServiceClient> GetGraphClient()
|
private async Task<GraphServiceClient> GetGraphClient()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -105,35 +227,6 @@ namespace Epi2O365
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> DoesContactExist(Contact contact, string userPrimaryMail, GraphServiceClient graphClient)
|
|
||||||
{
|
|
||||||
string NickName = contact.NickName;
|
|
||||||
if (string.IsNullOrWhiteSpace(NickName))
|
|
||||||
{
|
|
||||||
Logger.Warn("No valid NickName found for the given contact.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var contacts = await graphClient.Users[userPrimaryMail]
|
|
||||||
.Contacts
|
|
||||||
.GetAsync(requestConfiguration =>
|
|
||||||
{
|
|
||||||
requestConfiguration.QueryParameters.Filter = $"NickName eq '{NickName}'";
|
|
||||||
});
|
|
||||||
|
|
||||||
bool exists = contacts?.Value != null && contacts.Value.Any();
|
|
||||||
Logger.Info($"Contact with NickName '{NickName}' exists: {exists}");
|
|
||||||
return exists;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.Error($"Error checking contacts: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TokenAcquisitionProvider : IAccessTokenProvider
|
public class TokenAcquisitionProvider : IAccessTokenProvider
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Epi2O365
|
|||||||
{
|
{
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
private static readonly ILog Logger = LogManager.GetLogger(typeof(Program));
|
//private static readonly ILog Logger = LogManager.GetLogger(typeof(Program));
|
||||||
private static ConfigLoader Configuration;
|
private static ConfigLoader Configuration;
|
||||||
private static Model.ContactList ContactList;
|
private static Model.ContactList ContactList;
|
||||||
private static O365Connector O365;
|
private static O365Connector O365;
|
||||||
@@ -22,7 +22,7 @@ namespace Epi2O365
|
|||||||
{
|
{
|
||||||
string pathLastRuntime = "LastRun.txt";
|
string pathLastRuntime = "LastRun.txt";
|
||||||
DateTime LastRun = DateTime.Parse("01.01.1970 10:00:00");
|
DateTime LastRun = DateTime.Parse("01.01.1970 10:00:00");
|
||||||
|
Logger.Info("Started2");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(pathLastRuntime))
|
if (File.Exists(pathLastRuntime))
|
||||||
@@ -31,7 +31,7 @@ namespace Epi2O365
|
|||||||
string datetimeFromText = File.ReadAllText(pathLastRuntime);
|
string datetimeFromText = File.ReadAllText(pathLastRuntime);
|
||||||
Logger.Debug("Found datetime string: " + datetimeFromText);
|
Logger.Debug("Found datetime string: " + datetimeFromText);
|
||||||
|
|
||||||
LastRun = DateTime.Parse(datetimeFromText);
|
LastRun = DateTime.Parse(datetimeFromText).AddHours(-4);
|
||||||
Logger.Info("Last run timestamp: " + LastRun.ToString());
|
Logger.Info("Last run timestamp: " + LastRun.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +105,8 @@ namespace Epi2O365
|
|||||||
Contact o365CompanyContact = new Contact
|
Contact o365CompanyContact = new Contact
|
||||||
{
|
{
|
||||||
DisplayName = contactDetail.Payload[0].Name,
|
DisplayName = contactDetail.Payload[0].Name,
|
||||||
|
GivenName = contactDetail.Payload[0].Name,
|
||||||
|
Surname= contactDetail.Payload[0].Name1,
|
||||||
CompanyName = contactDetail.Payload[0].Name,
|
CompanyName = contactDetail.Payload[0].Name,
|
||||||
EmailAddresses = CompanyMailAddresses,
|
EmailAddresses = CompanyMailAddresses,
|
||||||
BusinessPhones = CompanyPhoneNumbers,
|
BusinessPhones = CompanyPhoneNumbers,
|
||||||
@@ -167,7 +169,7 @@ namespace Epi2O365
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
O365.SyncContact(o365contact, addressBookHolder);
|
await O365.SyncContact(o365contact, addressBookHolder);
|
||||||
Logger.Info("Contact person synchronized for user: " + addressBookHolder);
|
Logger.Info("Contact person synchronized for user: " + addressBookHolder);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user