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:

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)
        ;
}
Ma poi mi sono chiesto, esiste un modo più performante?
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();
}
e sono andato avanti testando altri apprici, in questo caso usanddo for:

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();
}
oppure una Regular Expression:

C#: GetSafeName4

private static string GetSafeName4(string name, char replaceChar = '-')
{
    if (string.IsNullOrEmpty(name))
    {
        return string.Empty;
    }

    return Regex.Replace(name.TrimStart('~'), @"[\\/:*?""<>|#%]", replaceChar.ToString());
}
e come alternativa un Regular Expression compilata e statica:

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);
}
un altro approccio è stato copiare i caratteri del nome in un oggetto StringBuilder:

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();
}
in ultimo ho provato a rifare GetSafeName3 usando foreach:

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);
I risultati di alcune esecuzione, sulla mia macchina con .NET 8 su 10 milioni di iterazioni, sono questi:

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.
Tags:
C#241 SharePoint Online79 SharePoint503 Regular Expression11 Esempi226
Potrebbe interessarti anche: