Skip to content

Commit 99ae817

Browse files
committed
Refactor SharePoint synchronization tool: Enhanced logging, improved security with IDisposable for SecureString, modernized data quality operations, and eliminated code duplication through a new base class. Updated configuration handling for SharePoint URLs and improved error handling across the application.
1 parent 95f463e commit 99ae817

11 files changed

Lines changed: 936 additions & 495 deletions

File tree

SPOtoSQL-Snapshots/ConsoleApp1/ConsoleLogger/Logger.cs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,74 @@
22

33
namespace Bring.SPODataQuality
44
{
5+
/// <summary>
6+
/// Static logger for consistent, formatted console output with verbosity control.
7+
/// Supports levels 0-3: 0=silent, 1=errors, 2=warnings, 3=debug.
8+
/// </summary>
59
public static class Logger
610
{
11+
private const string TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.fff";
12+
private enum LogLevel { ERROR = 1, WARNING = 2, DEBUG = 3 }
13+
14+
/// <summary>
15+
/// Verbosity level (0-3). Messages logged at or below this level will display.
16+
/// </summary>
717
public static int VerboseLevel { get; set; } = 0;
818

19+
/// <summary>
20+
/// Logs a message with the specified verbosity level.
21+
/// </summary>
22+
/// <param name="level">1=ERROR, 2=WARNING, 3=DEBUG. Only messages at or below VerboseLevel display.</param>
23+
/// <param name="message">The message to log.</param>
924
public static void Log(int level, string message)
1025
{
11-
if (VerboseLevel >= level)
12-
Console.WriteLine(message);
26+
if (level < 1 || level > 3) return;
27+
if (VerboseLevel < level) return;
28+
29+
string timestamp = DateTime.Now.ToString(TIMESTAMP_FORMAT);
30+
string levelName = GetLevelName(level);
31+
string formattedMessage = $"[{timestamp}] [{levelName}] {message}";
32+
33+
Console.WriteLine(formattedMessage);
1334
}
35+
36+
/// <summary>
37+
/// Logs an error message (level 1).
38+
/// </summary>
39+
public static void LogError(string message, Exception ex = null)
40+
{
41+
if (VerboseLevel >= 1)
42+
{
43+
Log(1, message);
44+
if (ex != null && VerboseLevel >= 3)
45+
{
46+
Log(3, $"Exception Details: {ex.GetType().Name}: {ex.Message}");
47+
}
48+
}
49+
}
50+
51+
/// <summary>
52+
/// Logs a warning message (level 2).
53+
/// </summary>
54+
public static void LogWarning(string message)
55+
{
56+
Log(2, message);
57+
}
58+
59+
/// <summary>
60+
/// Logs a debug message (level 3).
61+
/// </summary>
62+
public static void LogDebug(string message)
63+
{
64+
Log(3, message);
65+
}
66+
67+
private static string GetLevelName(int level) => level switch
68+
{
69+
1 => "ERROR",
70+
2 => "WARN",
71+
3 => "DEBUG",
72+
_ => "INFO"
73+
};
1474
}
1575
}

SPOtoSQL-Snapshots/ConsoleApp1/SPODataQuality/RefreshSPOLists.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,4 +567,4 @@ private static List<Field> GetFields(SPOList list)
567567
return fieldList; // Return the list of fields
568568
}
569569
}
570-
}
570+
}
Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,76 @@
11
using Microsoft.SharePoint.Client;
2+
using System;
3+
using Bring.SPODataQuality;
24

35
namespace Bring.Sharepoint
46
{
57
/// <summary>
68
/// Handles backfilling the '_OpportunityID' field on 'activities' items
79
/// when it is currently null, by copying the value from 'OpportunityID'.
810
/// </summary>
9-
internal class ActivitiesDQ
11+
internal class ActivitiesDQ : DataQualityBase
1012
{
13+
private const string LIST_NAME = "activities";
14+
private const string SITE = "wolf";
15+
private const string TARGET_FIELD = "_OpportunityID";
16+
private const string SOURCE_FIELD = "OpportunityID";
17+
1118
/// <summary>
12-
/// The current SharePoint user context, used for list operations.
19+
/// Initializes a new ActivitiesDQ instance with a SharePoint user context.
1320
/// </summary>
14-
public SPOUser Me { get; set; }
21+
/// <param name="user">The authenticated SharePoint user.</param>
22+
public ActivitiesDQ(SPOUser user) : base(user)
23+
{
24+
}
1525

1626
/// <summary>
27+
/// Executes the data quality operation.
1728
/// Queries the 'activities' list for items where '_OpportunityID' is null,
1829
/// then updates each item's '_OpportunityID' field to match 'OpportunityID'.
1930
/// </summary>
20-
/// <returns>Always returns true if execution completes without exception.</returns>
21-
public bool UpdateIDs()
31+
/// <returns>True if processing completed successfully.</returns>
32+
public override bool Execute()
2233
{
23-
// Define a CAML query to find items lacking the custom _OpportunityID field
24-
string camlQuery =
25-
"<View>"
26-
+ "<Query><Where><IsNull>"
27-
+ "<FieldRef Name='_OpportunityID' />"
28-
+ "</IsNull></Where></Query>"
29-
+ "</View>";
30-
31-
// Initialize the SPOList wrapper for the 'activities' list
32-
var activitiesList = new SPOList
34+
try
3335
{
34-
Name = "activities",
35-
Site = "wolf",
36-
SPOUser = this.Me,
37-
CAMLQuery = camlQuery
38-
};
36+
Logger.LogWarning($"Starting {LIST_NAME} update: backfilling {TARGET_FIELD} from {SOURCE_FIELD}");
3937

40-
// Retrieve matching items from SharePoint
41-
activitiesList.Build();
38+
// Define a CAML query to find items lacking the custom _OpportunityID field
39+
string camlQuery =
40+
"<View>" +
41+
"<Query><Where><IsNull>" +
42+
$"<FieldRef Name='{TARGET_FIELD}' />" +
43+
"</IsNull></Where></Query>" +
44+
"</View>";
4245

43-
// Iterate through each item, copying OpportunityID into _OpportunityID
44-
foreach (ListItem item in activitiesList.ItemCollection)
45-
{
46-
// Copy the existing 'OpportunityID' value into the '_OpportunityID' field
47-
item["_OpportunityID"] = item["OpportunityID"];
48-
item.Update();
49-
}
46+
// Create and build the activities list
47+
var activitiesList = CreateAndBuildList(LIST_NAME, SITE, camlQuery);
5048

51-
// Commit all pending updates to SharePoint in a single batch
52-
activitiesList.Ctx.ExecuteQuery();
49+
if (activitiesList.ItemCollection.Count == 0)
50+
{
51+
Logger.LogWarning($"No items found with null {TARGET_FIELD} in {LIST_NAME}");
52+
return true;
53+
}
5354

54-
return true;
55+
// Process each item in batches
56+
ProcessListItemsInBatches(activitiesList, item =>
57+
{
58+
var sourceValue = GetFieldValue<object>(item, SOURCE_FIELD);
59+
if (sourceValue != null)
60+
{
61+
SetFieldValue(item, TARGET_FIELD, sourceValue);
62+
item.Update();
63+
}
64+
});
65+
66+
Logger.LogWarning($"Completed {LIST_NAME} update: {activitiesList.ItemCollection.Count} items processed");
67+
return true;
68+
}
69+
catch (Exception ex)
70+
{
71+
Logger.LogError($"Failed to update activities data quality", ex);
72+
return false;
73+
}
5574
}
5675
}
5776
}

SPOtoSQL-Snapshots/ConsoleApp1/Sharepoint/Context.cs

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System;
33
using System.Linq.Expressions;
44
using System.Net;
5+
using Bring.SPODataQuality;
6+
using Bring.XmlConfig;
57

68
namespace Bring.Sharepoint
79
{
@@ -37,21 +39,36 @@ public class Context
3739
/// </summary>
3840
public void BuildContext()
3941
{
40-
// Combine base URL with relative site path
41-
var url = "https://bringglobal.sharepoint.com/" + this.Site;
42-
43-
// Create a new ClientContext and assign credentials
44-
var clientContext = new ClientContext(url)
42+
try
4543
{
46-
Credentials = this.SPOUser.Credentials
47-
};
44+
// Get base URL from configuration, with fallback to default
45+
string baseUrl = ConfigurationReader.GetSharePointBaseUrl()
46+
?? "https://bringglobal.sharepoint.com";
47+
48+
string url = $"{baseUrl.TrimEnd('/')}/{Site.TrimStart('/')}";
49+
Logger.LogDebug($"Building SharePoint context for: {url}");
50+
51+
// Create a new ClientContext and assign credentials
52+
var clientContext = new ClientContext(url)
53+
{
54+
Credentials = SPOUser.Credentials
55+
};
56+
57+
// Store references for later use
58+
Ctx = clientContext;
59+
web = Ctx.Web;
4860

49-
// Store references for later use
50-
this.Ctx = clientContext;
51-
this.web = this.Ctx.Web;
61+
// Load minimal Web object metadata
62+
Ctx.Load(web, w => w.Title, w => w.Url);
63+
Ctx.ExecuteQuery();
5264

53-
// Load minimal Web object metadata (e.g., Title, Url)
54-
this.Ctx.Load<Web>(this.web, Array.Empty<Expression<Func<Web, object>>>());
65+
Logger.LogDebug($"SharePoint context established successfully for site: {web.Title}");
66+
}
67+
catch (Exception ex)
68+
{
69+
Logger.LogError($"Failed to build SharePoint context for site '{Site}'", ex);
70+
throw;
71+
}
5572
}
5673

5774
/// <summary>
@@ -60,18 +77,32 @@ public void BuildContext()
6077
/// <returns>ListCollection representing all lists on the site.</returns>
6178
public ListCollection GetAllLists()
6279
{
63-
// Ensure context is built or rebuilt if Site has changed
64-
var expectedUrl = "https://bringglobal.sharepoint.com/" + this.Site;
65-
if (this.web == null || this.Ctx.Site.Context.Url != expectedUrl)
80+
try
6681
{
67-
this.BuildContext();
68-
}
82+
// Ensure context is built or rebuilt if Site has changed
83+
string baseUrl = ConfigurationReader.GetSharePointBaseUrl()
84+
?? "https://bringglobal.sharepoint.com";
85+
string expectedUrl = $"{baseUrl.TrimEnd('/')}/{Site.TrimStart('/')}";
86+
87+
if (web == null || Ctx?.Site?.Url != expectedUrl)
88+
{
89+
Logger.LogDebug("Rebuilding context for GetAllLists");
90+
BuildContext();
91+
}
6992

70-
// Load and execute query to get all lists
71-
var lists = this.web.Lists;
72-
this.Ctx.Load<ListCollection>(lists, Array.Empty<Expression<Func<ListCollection, object>>>());
73-
this.Ctx.ExecuteQuery();
74-
return lists;
93+
// Load and execute query to get all lists
94+
var lists = web.Lists;
95+
Ctx.Load(lists);
96+
Ctx.ExecuteQuery();
97+
98+
Logger.LogDebug($"Retrieved {lists.Count} lists from site");
99+
return lists;
100+
}
101+
catch (Exception ex)
102+
{
103+
Logger.LogError($"Failed to retrieve lists from site '{Site}'", ex);
104+
throw;
105+
}
75106
}
76107
}
77108
}

0 commit comments

Comments
 (0)