Undirected graph data structure in C# Description
A class representing an undirected graph. At the moment, it supports integer values as vertices. An example of the type of graph represented is shown in the following diagram:

It is represented internally as an array of adjacency lists. Each index of this array corresponds to a vertex number, and contains a List&lt;int&gt; of vertex numbers to which it is connected.
Purpose
Purely as an exercise in implementing a data structure. Implemented in C# after studying a Java-based implementation.
Code
The data structure class definition
public class UndirectedGraph
{
    // number of vertices
    private int _V;

    // number of edges
    private int _E = 0;

    // array of adjacency lists
    private List&lt;int&gt;[] _adj;

    /// &lt;summary&gt;
    /// Constructor to create a graph.
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;v&quot;&gt;Number of vertices&lt;/param&gt;
    public UndirectedGraph(int v)
    {
        this._V = v;

        // create array of lists
        // initialise all lists to empty
        this._adj = new List&lt;int&gt;[v];
        for (int i = 0; i &lt; this._adj.Length; i++)
            this._adj[i] = new List&lt;int&gt;();
    }

    // number of vertices
    public int V
    {
        get { return this._V; }
    }

    // number of edges
    public int E
    {
       get { return this._E; }
    }

    /// &lt;summary&gt;
    /// Add an edge to the graph.
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;v&quot;&gt;One vertex of the new edge.&lt;/param&gt;
    /// &lt;param name=&quot;w&quot;&gt;The other vertex of the new edge.&lt;/param&gt;
    public void addEdge(int v, int w)
    {
        // validate given node numbers
        if ((v &gt; this._adj.Length) || (w &gt; this._adj.Length))
            throw new ArgumentException(&quot;Invalid node number specified.&quot;);

        // add to adjacency lists
        this._adj[v].Add(w);
        this._adj[w].Add(v);
        // increment edge count
        this._E++;
    }

    /// &lt;summary&gt;
    /// Get an adjacency list for a vertex.
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;v&quot;&gt;The vertex.&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    public List&lt;int&gt; getAdjacency(int v)
    {
        return this._adj[v];
    }

    /// &lt;summary&gt;
    /// Get a string representation of the graph's adjacency lists.
    /// &lt;/summary&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    public override string ToString()
    {
        StringBuilder builder = new StringBuilder();
        builder.AppendLine(this._V + &quot; vertices, &quot; + this._E + &quot; edges&quot;);
        for (int v = 0; v &lt; this._V; v++)
        {
            builder.Append(v + &quot;: &quot;);
            foreach (var w in this._adj[v])
            {
                builder.Append(w + &quot; &quot;);
            }
            builder.AppendLine(string.Empty);
        }

        return builder.ToString();
   }
}

Sample usage. Prints out a string representation of the example graph in the diagram above to the console:
UndirectedGraph myGraph = new UndirectedGraph(7);
myGraph.addEdge(0,1);
myGraph.addEdge(0,2);
myGraph.addEdge(2,3);
myGraph.addEdge(2,4);
myGraph.addEdge(2,5);
myGraph.addEdge(5,6);
Console.Write(myGraph.ToString());

// RESULT:
7 vertices, 6 edges
0: 1 2
1: 0
2: 0 3 4 5
3: 2
4: 2
5: 2 6
6: 5

Questions

Any feedback about variable names, commenting and implementation details.
Changes, if any, to make it unit-testable.
Ways to include C# 7.0 features, if they haven't been already.
How to extend this to other data types, such as string.
Performance improvements might be interesting.
Naming
Renaming your instance variable _V and _E to nbVertices and nbEdges respectively allows you get rid of two unnecessary comments and also makes the rest of the code much clearer, especially to someone who's not too familiar with graph notation.
I'm also not a big fan of prefixing private instance variables with underscores, but that's just my personal opinion.
Similarly, you could give most of your methods' arguments much clearer names.
The standard naming convention for C# is to use PascalCase for methods &amp; properties, and camelCase for fields, so I would rename getAdjacency() and addEdge() to GetAdjacency() and AddEdge().
Properties
    // number of vertices
    public int V
    {
        get { return this._V; }
    }

    // number of edges
    public int E
    {
       get { return this._E; }
    }

can be changed to
    // number of vertices
    public int V
    {
        get =&gt; this._V;
    }

    // number of edges
    public int E
    {
       get =&gt; this._E;
    }

which can further be simplified to
    // number of vertices
    public int V =&gt; this._V;

    // number of edges
    public int E =&gt; this._E;

You could go one step further and merge V and _V into a single property, like this :
public int V { get; private set; }

public int E { get; private set; }

and since V doesn't change after being set in the constructor, you can go even further beyond and make V into a read-only property :
public int V { get; }

This makes it so that V can only be set once in the constructor.
Comments
Personal opinion again, but I don't like comments, especially when they can be made useless by simply changing a variable or a method's name, as was the case with your declarations of _V or _E.
I also really don't like comments that just say what the code is doing, like in your addEdge() method :
public void addEdge(int v, int w)
{
    // validate given node numbers
    if ((v &gt; this._adj.Length) || (w &gt; this._adj.Length))
        throw new ArgumentException(&quot;Invalid node number specified.&quot;);

    // add to adjacency lists
    this._adj[v].Add(w);
    this._adj[w].Add(v);
    // increment edge count
    this._E++;
}

These comments add nothing more than redundant, distracting text that makes the code harder to read. If you see a line of code that says NbEdges++;, do you really need a comment to say // increment edge count ?
Exceptions
I'd recommend always throwing the exception that most closely fits the issue, which in this case would be an ArgumentOutOfRangeException (which is derived from ArgumentException).
Additionally, I would make the exception message more clear and give more information about what actually went wrong. Imagine being a user and seeing &quot;Invalid node number specified.&quot;; which argument was wrong? Was it v or w? Why was the value wrong?
You could go even further and create your own exception class that inherits said exception, in order to reduce code repetition and give your exceptions meaningful names which would help your users.
if (v &lt; 0 || v &gt; this._adj.Length)
{
    throw new ArgumentOutOfRangeException($&quot;Error, the value provided for 'v' should be&quot; + 
        &quot;between 0 and {this._adj.Length - 1}\n&quot; + 
        &quot;\tv = {v}\n&quot;);
}

if (w &lt; 0 || w &gt; this._adj.Length)
{
    throw new ArgumentOutOfRangeException($&quot;Error, the value provided for 'v' should be&quot; + 
        &quot;between 0 and {this._adj.Length - 1}\n&quot; + 
        &quot;\tv = {v}\n&quot;);
}