I'm starting a project in which I'll generate random graphs and use algorithms to solve them. The first necessary step is obviously to build a graph. My graph has the following characteristics : It can be directional or not, implying that the Edge between NodeA and NodeB can be traversed from B to A if the Graph is NonDirectional.The Edge can be weighted, meaning that there's an int value linked to an edge, which will be used for some graph algorithms.At the moment, I have mostly tested the non directional weighted graph, so it's the one I'll put for review. I hav removed the header comments from the class/methods as I have multiple classes to show.public class Node : IEquatable<Node>{    public string Name { get; }    public Node(string name)    {        if (String.IsNullOrWhiteSpace(name)) throw new ArgumentException(Name cannot be null or empty, nameof(name));        Name = name;    }    public override bool Equals(object obj)    {        return Equals(obj as Node);    }    public bool Equals(Node other)    {        return Name == other.Name;    }    public override int GetHashCode()    {        return Name.GetHashCode() * 23;     }    public override string ToString()    {        return $({this.GetType().Name}) - Name : {Name};    }} Note : The PriorityQueueNode base class is used in the algorithms, but it is not used anywhere in this review. The AssociationId is used in case of a non directional graph, it is used to see if two edges are from the same... association (A to B and B to A).public class Edge : PriorityQueueNode, IEquatable<Edge>{    private readonly Lazy<int> computedHashCode;    public Guid AssociationId { get; }    public Node From { get; }    public Node To { get; }    public Edge(Node from, Node to, Guid associationId)    {        if (to == null) throw new ArgumentNullException(nameof(to));        if (from == null) throw new ArgumentNullException(nameof(from));        From = from;        To = to;        AssociationId = associationId;        //Since Edge is immutable, hash can be computed once.        computedHashCode = new Lazy<int>(() => (From.GetHashCode() * 17) ^ (To.GetHashCode() * 17) ^ (AssociationId.GetHashCode() * 17));    }    public bool ConnectsSameNode(Edge edge)    {        if (edge == null) throw new ArgumentNullException(nameof(edge));        if (AssociationId == edge.AssociationId) return true;        bool connectsSameFrom = edge.From.Equals(From) || edge.To.Equals(From);        return connectsSameFrom && (edge.From.Equals(To) || edge.To.Equals(To));    }    public override bool Equals(object obj)    {        return Equals(obj as Edge);    }    public bool Equals(Edge edge)    {        if (edge == null) return false;        return AssociationId == edge.AssociationId && From.Equals(edge.From) && To.Equals(edge.To);    }    public override int GetHashCode()    {        return computedHashCode.Value;    }    public override string ToString()    {        return $({this.GetType().Name}) - From : {From.Name}, To : {To.Name}, AssociationId : {AssociationId};    }}public class WeightedEdge : Edge, IEquatable<WeightedEdge>, IComparable<WeightedEdge>{    public int Weight { get; }    public WeightedEdge(Node from, Node to, int weight, Guid associationId)        : base(from, to, associationId)    {        if (weight <= 0) throw new ArgumentException(${nameof(weight)} must be higher than zero, nameof(weight));        Weight = weight;    }    public int CompareTo(WeightedEdge other)    {        if (other == null) throw new ArgumentNullException(other);        int compareValue = Math.Sign(Weight - other.Weight);        //Compare AssociationId because something needs to decide the priority and it can't be random.        return compareValue == 0 ? AssociationId.CompareTo(other.AssociationId) : compareValue;    }    public override bool Equals(object obj)    {        return Equals(obj as WeightedEdge);    }    public bool Equals(WeightedEdge edge)    {        return base.Equals(edge) && edge.Weight == Weight;    }    public override int GetHashCode()    {        return base.GetHashCode() ^ (Weight.GetHashCode() * 13);    }    public override string ToString()    {        return ${base.ToString()}, Weight : {Weight};    }}That is it for the structure of a Graph. Now, the Graph class. I use a ICollectionDictionary<Node,T> to hold my graph, the interface is defined as this : public interface ICollectionDictionary<T,TK> : IDictionary<T, ICollection<TK>>{    void AddToCollection(T key, TK value);    /// <summary>    /// Creates a new empty ICollection for the T key.    /// </summary>    /// <param name=key>Key.</param>    void InitializeKeyCollection(T key);}I'll leave the implementation out of the review as the question gets long already, but I'll add a link to the GitHub repo at the end.public interface IGraph<T> where T : Edge{    bool IsDirectional { get; }    int NodesCount { get; }    int EdgesCount { get; }    IEnumerable<Node> Nodes { get; }    IEnumerable<T> GetEdges(Node node);    IEnumerable<T> GetAllUniquesEdges();}public class Graph<T> : IGraph<T> where T : Edge{    private readonly ICollectionDictionary<Node, T> _map;    private bool _isDirectional;    public int NodesCount => _map.Keys.Count;    public int EdgesCount => _map.Values.Count;    public bool IsDirectional => _isDirectional;    public IEnumerable<Node> Nodes => _map.Keys.ToList();    internal Graph(ICollectionDictionary<Node, T> map, bool isDirectional)    {        if (map == null) throw new ArgumentNullException(nameof(map));        _map = map;        _isDirectional = isDirectional;    }    public IEnumerable<T> GetEdges(Node node)    {        if (node == null) throw new ArgumentNullException(node);        _map.InitializeKeyCollection(node);        return _map[node];    }    public IEnumerable<T> GetAllUniquesEdges()    {        var associationIds = new HashSet<Guid>();        foreach (var item in _map.Values.SelectMany(v => v))         {            //If graph is non directional, we don't want the two edges A->B & B->A.            if (!associationIds.Add(item.AssociationId)) continue;            yield return item;        }    }}The creation of a Graph is somehow complex, so I created a IGraphBuilder<T>.public interface IGraphBuilder<T> where T : Edge{    IEnumerable<Node> NodesInGraph { get; }    IGraph<T> Build();}public interface IWeightedGraphBuilder : IGraphBuilder<WeightedEdge>{    IWeightedGraphBuilder AddEdge(Node a, Node b, int weight);}public class WeightedGraphBuilder : IWeightedGraphBuilder{    private ICollectionDictionary<Node, WeightedEdge> _map;    private bool _hasBeenBuilt;    private bool _isDirectional;    public IEnumerable<Node> NodesInGraph => _map.Keys.ToList();    public bool GeneratedGraphIsDirectional => _isDirectional;    public WeightedGraphBuilder(bool isDirectional)    {        _isDirectional = isDirectional;        _map = new CollectionDictionary<Node, WeightedEdge>();        _hasBeenBuilt = false;    }    public IWeightedGraphBuilder AddEdge(Node nodeA, Node nodeB, int weight)    {        if (nodeA == null) throw new ArgumentNullException(a);        _map.InitializeKeyCollection(nodeA);        if (nodeB == null) { return this; }        _map.InitializeKeyCollection(nodeB);        ICollection<WeightedEdge> edgesA = _map[nodeA];        var associationId = Guid.NewGuid();        var edgeAToB = new WeightedEdge(nodeA, nodeB, weight, associationId);        edgesA.Add(edgeAToB);        if (!_isDirectional)        {            var edgesB = _map[nodeB];            var edgeBToA = new WeightedEdge(nodeB, nodeA, weight, associationId);            edgesB.Add(edgeBToA);        }        return this;    }    public IGraph<WeightedEdge> Build()    {        if (_hasBeenBuilt) throw new InvalidOperationException(Graph has already been built using this builder);        _hasBeenBuilt = true;        return new Graph<WeightedEdge>(_map, _isDirectional);    }}An example use would be : class Example{    void Test()    {        IWeightedGraphBuilder bob = new WeightedGraphBuilder(false);        bob.AddEdge(new Node(a), new Node(b), 3);        bob.AddEdge(new Node(a), new Node(c), 2);        bob.AddEdge(new Node(b), new Node(d), 10);        IGraph<WeightedEdge> graph = bob.Build();    }}I've unit tested this code (except the builder, but it works according to the unit test that uses it, tests will come later, oops). I want to know if there's anything smelly in there or things that could be optimized.The full code can be found on https://github.com/topinfrassi01/GraphTheories
Bob the (Graph) Builder
c#;graph