Find all possible words in a board of characters
If I were you, I  would stop having the tests mixed up with the implementation. It is somehow "smart", and for small projects like this, you can say it doesn't matter. But IMO it does matter because it prevents you from thinking properly about the design of you code.

Your implementation is a nearly one-to-one implementation of the C++ from Geeks and it appears rather C++-ish.



When it comes to naming, you should call your members after what they do or represent, - not after how they do it or is: DFS() as well as _list tell me nothing.




        for (int row = i - 1; row &lt;= i + 1 &amp;&amp; row &lt; boggle.GetLength(0); row++)
        {
            for (int col = j - 1; col &lt;= j + 1 &amp;&amp; col &lt; boggle.GetLength(1); col++)
            {
                if (col &gt;= 0 &amp;&amp; row &gt;= 0 &amp;&amp; !visited[row, col])
                {
                    DFS(row, col, boggle, dictionary, str, visited);
                }
            }
        }



IMO this way to determine the boundaries of the loops is hard to follow. The stop conditions are calculated in the for-statements while the start conditions are determined half in the for-statements and half in the if-statement.

A more clean and C#-ish way could be something like:

  int minRow = Math.Max(0, row - 1);
  int maxRow = Math.Min(rows, row + 2);
  int minCol = Math.Max(0, col - 1);
  int maxCol = Math.Min(cols, col + 2);

  for (int r = minRow; r &lt; maxRow; r++)
  {
    for (int c = minCol; c &lt; maxCol; c++)
    {
      if (!visited[r, c])
        Search(r, c);
    }
  }


The conditions are calculated once before the loops and hence separated nicely from the loops,  and the loops are immediately easy to read and understand. For rows and cols see below.




    private bool IsWord(string str, string[] dictionary)
    {
        for (int i = 0; i &lt; dictionary.Length; i++)
        {
            if (string.CompareOrdinal(str, dictionary[i]) == 0)
            {
                return true;
            }
        }
        return false;
    }



In C# this is a one-liner:

    private bool IsWord(string str, string[] dictionary)
    {
        return dictionary.Contains(str);
    }





        str.Remove(str.Length - 1, 1);



On the StringBuilder it is possible to set the length, which might be faster than removing a sequence of the one last character:

str.Length -= 1;




About the overall design:

On the one hand you let _list be a class member, while all other variables are arguments to the methods. I think I would make a statefull object having as many of the variables as possible as members:

  class WordFinder
  {
    private readonly char[,] boggle;
    private readonly bool[,] visited;
    private readonly int rows;
    private readonly int cols;
    private readonly List&lt;string&gt; words = new List&lt;string&gt;();
    private readonly StringBuilder word = new StringBuilder();
    private string[] dictionary;

    public WordFinder(char[,] boggle)
    {
      this.boggle = boggle;
      rows = boggle.GetLength(0);
      cols = boggle.GetLength(1);
      visited = new bool[rows, cols];
    }
    ...


Other than a reduced set of arguments for the methods one benefit is that the calculation of the boggle dimensions is done once here.

The FindWords method could then look like:

public IList&lt;string&gt; FindWords(string[] dictionary)
{
  this.dictionary = dictionary;
  words.Clear();
  word.Clear();

  for (int row = 0; row &lt; rows; row++)
  {
    for (int col = 0; col &lt; cols; col++)
    {
      Search(row, col);
    }
  }

  return words;
}


where the Search() method is:

private void Search(int row, int col)
{
  visited[row, col] = true;

  word.Append(boggle[row, col]);
  AddIfWord(word.ToString());

  int minRow = Math.Max(0, row - 1);
  int maxRow = Math.Min(rows, row + 2);
  int minCol = Math.Max(0, col - 1);
  int maxCol = Math.Min(cols, col + 2);

  for (int r = minRow; r &lt; maxRow; r++)
  {
    for (int c = minCol; c &lt; maxCol; c++)
    {
      if (!visited[r, c])
        Search(r, c);
    }
  }

  word.Length -= 1;
  visited[row, col] = false;
}


and AddIfWord() is

private void AddIfWord(string candidate)
{
  if (dictionary.Contains(candidate))
    words.Add(candidate);
}


Maybe word should be an argument rather than a member, because it is somehow a "local" variable, but on the other hand there is only one word at the time while the process is running, so it's fairly safe to let it be a member.



For convenience you could provide a static member as:

public static IList&lt;string&gt; FindWords(char[,] boggle, string[] dictionary)
{
  WordFinder finder = new WordFinder(boggle);
  return finder.FindWords(dictionary);
}


We could argue about if boggle and dictionary should be arguments to the constructor or FindWords() - that may be a matter of - if the process is about finding words from dictionary in boggle or visa versa and/or a matter of habit and taste.



When it comes to performance, there is not much to say, because the algorithm is "optimized" by its definition - I can't see any other ways to implement this implementation so to speak.