
SharePoint GetSafeName in C#
Oggi mi è capitato di dover creare una funzione C# per SharePoint Online per "sanificare" il nome del file che dovevo caricare.
Questo perché nei nomi file che mi venivano passati c'erano caratteri non validi come ad esempio "?" (punto di domanda, vedi anche https://support.microsoft.com/en-us/...a66588505b4e ).
I caatteri non validi sono: \, /, :, *, ?, ", <, >, | , # , %
La funzione che ho realizzato sostituisce i caratteri non validi con un altro carattere:
Ma poi mi sono chiesto, esiste un modo più performante?
Così ho realizzato una versione con StringBuilder e Replace:
e sono andato avanti testando altri apprici, in questo caso usanddo for:
oppure una Regular Expression:
e come alternativa un Regular Expression compilata e statica:
un altro approccio è stato copiare i caratteri del nome in un oggetto StringBuilder:
in ultimo ho provato a rifare GetSafeName3 usando foreach:
A questo punto mi sono chiesto, qual'è la più performante?
Così ho scritto una funzione di test:
I risultati di alcune esecuzione, sulla mia macchina con .NET 8 su 10 milioni di iterazioni, sono questi:
Possiamo anche notare che il peggiore è GetSafeName4 che usa una oggetto Regular Expression istanziato ogni volta.
La memoria non è un dato preciso, in quanto è influenzata, molto probabilmente, dall'intervento del garbage collector, infatti alcuni valori sono negativi.
Però ci da comunque un indicazione di massima su qual'è il metodo peggiore in termini di consumo di memoria, ovvero GetSafeName1, almeno 10 volte in più degli altri metodi.
Questo perche le stringhe sono immutabili e quindi ad ogni replace viene copiata la stringa in un nuovo spazio di memoria con il carattere sostituito, questo per il numero di replace e iterazioni impostate.
Morale, usate GetSafeName2.
Questo perché nei nomi file che mi venivano passati c'erano caratteri non validi come ad esempio "?" (punto di domanda, vedi anche https://support.microsoft.com/en-us/...a66588505b4e ).
I caatteri non validi sono: \, /, :, *, ?, ", <, >, | , # , %
La funzione che ho realizzato sostituisce i caratteri non validi con un altro carattere:
C#: GetSafeName1
private static string GetSafeName1(string name, char replaceChar = '-')
{
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
return name
.TrimStart('~')
.Replace('\\', replaceChar)
.Replace('/', replaceChar)
.Replace(':', replaceChar)
.Replace('*', replaceChar)
.Replace('?', replaceChar)
.Replace('"', replaceChar)
.Replace('<', replaceChar)
.Replace('>', replaceChar)
.Replace('|', replaceChar)
.Replace('#', replaceChar)
.Replace('%', replaceChar)
;
}
Così ho realizzato una versione con StringBuilder e Replace:
C#: GetSafeName2
private static string GetSafeName2(string name, char replaceChar = '-')
{
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
StringBuilder sb = new StringBuilder(name.TrimStart('~'));
sb.Replace('\\', replaceChar);
sb.Replace('/', replaceChar);
sb.Replace(':', replaceChar);
sb.Replace('*', replaceChar);
sb.Replace('?', replaceChar);
sb.Replace('"', replaceChar);
sb.Replace('<', replaceChar);
sb.Replace('>', replaceChar);
sb.Replace('|', replaceChar);
sb.Replace('#', replaceChar);
sb.Replace('%', replaceChar);
return sb.ToString();
}
C#: GetSafeName3
private static string GetSafeName3(string name, char replaceChar = '-')
{
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
string nameTrimmed = name.TrimStart('~');
StringBuilder sb = new StringBuilder(nameTrimmed.Length);
int m = nameTrimmed.Length;
for (int i = 0; i < m; i++)
{
char c = nameTrimmed[i];
if (c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' ||
c == '<' || c == '>' || c == '|' || c == '#' || c == '%')
{
sb.Append(replaceChar);
}
else
{
sb.Append(c);
}
}
return sb.ToString();
}
C#: GetSafeName4
private static string GetSafeName4(string name, char replaceChar = '-')
{
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
return Regex.Replace(name.TrimStart('~'), @"[\\/:*?""<>|#%]", replaceChar.ToString());
}
C#: GetSafeName5
static Regex re = new Regex(@"[\\/:*?""<>|#%]", RegexOptions.Compiled | RegexOptions.Singleline);
/// Note: RegexOptions.NonBacktracking stranamente peggiora le performance 00:00:05.7917534 ms
private static string GetSafeName5(string name, string replaceChar = "-")
{
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
return re.Replace(name.TrimStart('~'), replaceChar);
}
C#: GetSafeName6
private static string GetSafeName6(string name, char replaceChar = '-')
{
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
StringBuilder sb = new StringBuilder(name);
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if (c == '~' && i == 0)
{
continue; // Skip leading tilde
}
// if (@"\\/:\*?\""<>|#%".IndexOf(c) >= 0) peggiora le performance
// if (@"\\/:\*?\""<>|#%".Any(x => x == c)) almeno 10 volte più lento
// if (@"\\/:\*?\""<>|#%".Contains(c)) peggiora le performance
if (c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' ||
c == '<' || c == '>' || c == '|' || c == '#' || c == '%')
{
sb[i] = replaceChar;
}
}
return sb.ToString();
}
C#: GetSafeName7
private static string GetSafeName7(string name, char replaceChar = '-')
{
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
StringBuilder sb = new StringBuilder(name.Length);
string nameTrimmed = name.TrimStart('~');
foreach (var c in nameTrimmed)
{
if (c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' ||
c == '<' || c == '>' || c == '|' || c == '#' || c == '%')
{
sb.Append(replaceChar);
}
else
{
sb.Append(c);
}
}
return sb.ToString();
}
A questo punto mi sono chiesto, qual'è la più performante?
Così ho scritto una funzione di test:
C#: ExecuteAllTests
private static void ExecuteAllTests(int iterationCount)
{
Console.WriteLine("TEST START");
const string name = "~Test/Name:with*invalid?characters\"<>'|#%.extension";
for (int i = 1; i <= 7; i++)
{
TestN(name, iterationCount, i);
}
Console.WriteLine("TEST STOP");
}
private static void TestN(string name, int iterationCount, int testNumber)
{
Console.Write($"Test {testNumber} iteration {iterationCount:N0} ... ");
long totalBytesOfMemoryUsedBegin = Process.GetCurrentProcess().WorkingSet64;
long startTime = Stopwatch.GetTimestamp();
for (int i = 0; i < iterationCount; i++)
{
string safeName = string.Empty;
switch (testNumber)
{
case 1:
safeName = GetSafeName1(name, '-');
break;
case 2:
safeName = GetSafeName2(name, '-');
break;
case 3:
safeName = GetSafeName3(name, '-');
break;
case 4:
safeName = GetSafeName4(name, '-');
break;
case 5:
safeName = GetSafeName5(name, '-'.ToString());
break;
case 6:
safeName = GetSafeName6(name, '-');
break;
case 7:
safeName = GetSafeName7(name, '-');
break;
default:
throw new ArgumentException("Invalid test number");
}
}
TimeSpan elapsedTime = Stopwatch.GetElapsedTime(startTime);
long memoryUsed = Process.GetCurrentProcess().WorkingSet64 - totalBytesOfMemoryUsedBegin;
Console.Write($"execution time {testNumber}: {elapsedTime} ms, memory used: {memoryUsed:N0} B \r\n");
}
ExecuteAllTests(10_000_000);
Text: Test con 10.000.000 di iterazioni
TEST START
Test 1 iteration 10.000.000 ... execution time 1: 00:00:02.1408643 ms, memory used: 10.383.360 B
Test 2 iteration 10.000.000 ... execution time 2: 00:00:01.6515556 ms, memory used: 278.528 B
Test 3 iteration 10.000.000 ... execution time 3: 00:00:02.4569806 ms, memory used: 286.720 B
Test 4 iteration 10.000.000 ... execution time 4: 00:00:04.9803456 ms, memory used: 4.759.552 B
Test 5 iteration 10.000.000 ... execution time 5: 00:00:03.1838946 ms, memory used: 1.626.112 B
Test 6 iteration 10.000.000 ... execution time 6: 00:00:02.5126495 ms, memory used: 16.384 B
Test 7 iteration 10.000.000 ... execution time 7: 00:00:02.6086522 ms, memory used: -3.477.504 B
TEST STOP
TEST START
Test 1 iteration 10.000.000 ... execution time 1: 00:00:02.5518582 ms, memory used: 10.190.848 B
Test 2 iteration 10.000.000 ... execution time 2: 00:00:01.8242579 ms, memory used: 372.736 B
Test 3 iteration 10.000.000 ... execution time 3: 00:00:02.9517277 ms, memory used: 73.728 B
Test 4 iteration 10.000.000 ... execution time 4: 00:00:05.5024028 ms, memory used: 4.751.360 B
Test 5 iteration 10.000.000 ... execution time 5: 00:00:03.1969920 ms, memory used: 1.671.168 B
Test 6 iteration 10.000.000 ... execution time 6: 00:00:02.3667378 ms, memory used: 4.096 B
Test 7 iteration 10.000.000 ... execution time 7: 00:00:02.8004957 ms, memory used: -3.272.704 B
TEST STOP
TEST START
Test 1 iteration 10.000.000 ... execution time 1: 00:00:02.4953029 ms, memory used: 9.932.800 B
Test 2 iteration 10.000.000 ... execution time 2: 00:00:01.7563700 ms, memory used: 393.216 B
Test 3 iteration 10.000.000 ... execution time 3: 00:00:02.5473535 ms, memory used: 65.536 B
Test 4 iteration 10.000.000 ... execution time 4: 00:00:05.2047286 ms, memory used: 4.898.816 B
Test 5 iteration 10.000.000 ... execution time 5: 00:00:03.2518021 ms, memory used: 1.679.360 B
Test 6 iteration 10.000.000 ... execution time 6: 00:00:02.5365150 ms, memory used: 0 B
Test 7 iteration 10.000.000 ... execution time 7: 00:00:02.7418346 ms, memory used: -3.489.792 B
TEST STOP
Text: Test con 100.000.000 di iterazioni (cento milioni)
TEST START
Test 1 iteration 100.000.000 ... execution time 1: 00:00:19.6499726 ms, memory used: 13.185.024 B
Test 2 iteration 100.000.000 ... execution time 2: 00:00:16.3922513 ms, memory used: 3.715.072 B
Test 3 iteration 100.000.000 ... execution time 3: 00:00:25.3381688 ms, memory used: 98.304 B
Test 4 iteration 100.000.000 ... execution time 4: 00:00:50.4454881 ms, memory used: 1.224.704 B
Test 5 iteration 100.000.000 ... execution time 5: 00:00:30.7098790 ms, memory used: 1.921.024 B
Test 6 iteration 100.000.000 ... execution time 6: 00:00:24.2956478 ms, memory used: -20.480 B
Test 7 iteration 100.000.000 ... execution time 7: 00:00:28.0570462 ms, memory used: -49.152 B
TEST STOP
Conclusioni
Dal test emerge chiaramente come la più performante, in termini di tempo di esecuzione, è il metodo GetSafeName2 che usa StringBuilder e Replace.Possiamo anche notare che il peggiore è GetSafeName4 che usa una oggetto Regular Expression istanziato ogni volta.
La memoria non è un dato preciso, in quanto è influenzata, molto probabilmente, dall'intervento del garbage collector, infatti alcuni valori sono negativi.
Però ci da comunque un indicazione di massima su qual'è il metodo peggiore in termini di consumo di memoria, ovvero GetSafeName1, almeno 10 volte in più degli altri metodi.
Questo perche le stringhe sono immutabili e quindi ad ogni replace viene copiata la stringa in un nuovo spazio di memoria con il carattere sostituito, questo per il numero di replace e iterazioni impostate.
Morale, usate GetSafeName2.