Here is an example:
edge1 = Property[1 -> 2, EdgeStyle -> Red]; edge2 = Property[1 -> 2, EdgeStyle -> Blue]; Graph[{edge1, edge2}] 
This does not work the way I want it. How can I make it so that I get two edges, one blue and one red?
Another trick you can do:
Graph[Join[Table[1 -> 2, {10}], Table[2 -> 3, {5}], Table[3 -> 1, {5}]], EdgeShapeFunction -> {1 \[DirectedEdge] 2 -> (a = 0; {a++; ColorData[35, "ColorList"][[a]], Arrow[#]} &), 2 \[DirectedEdge] 3 -> (b = 0; {b++; ColorData[55, "ColorList"][[b]], Arrow[#]} &), 3 \[DirectedEdge] 1 -> (c = 0; {c++; ColorData[5, "ColorList"][[c]], Arrow[#]} &)}] VertexInDegree[g] yields $\{5, 10, 5\}$. +1. $\endgroup$ The trick is to render both a directed and an undirected edge with the same arrow EdgeShapeFunction. Alas, the full graph representation will retain the different classes of edge, so functions such as FindKClan, VertexOutDegree, VertexInDegree, and others that distinguish between different classes of edge will give incorrect answers.
Graph[{1, 2}, {Property[1 \[DirectedEdge] 2, EdgeStyle -> Red], Property[1 <-> 2, EdgeStyle -> Blue]}, EdgeShapeFunction -> (Arrow[#] &)] Because there are only two classes of edge (directed and undirected), following this approach also implies that one can have at most two edges between a given pair of vertexes.
You can kludge together a graph representation that appears as if three (or more) edges join two vertexes by forcing different vertexes to lie in the same position, through VertexCoordinates:
Graph[{1, 2, 3}, {Property[1 \[DirectedEdge] 2, EdgeStyle -> Red], Property[1 <-> 2, EdgeStyle -> Blue], Property[1 <-> 3, EdgeStyle -> Green]}, EdgeShapeFunction -> (Arrow[#] &), VertexCoordinates -> {{0, 0}, {1, 0}, {1, 0}}] (Arrow[#]&) works, I think that more properly you would want (Arrow[#1]&) for your EdgeShapeFunction, to reming the reader that a custom edge function is passed a sequence of two arguments: 1) a list of points describing the edge; 2) the edge denomination itself. Arrow needs only the first one. I think this is also the reason why simply using Arrow doesn't work: the edge definition would be interpreted as an arrow setback amount, which is expected to be a number. $\endgroup$ EdgeShapeFunction of the form ef[#, ___] and called it within Graph, as the two-argument approach would seem to require. I found indeed that both (Arrow[#]&) and (Arrow[#1]&) worked, but don't know why the former does. $\endgroup$ (Arrow[#]&) works because # always represents only the first argument provided to the function, i.e. it is automatically equivalent to #1, so that call discards the second argument passed to Arrow by EdgeShapeFunction. In other words, (Arrow[#]&) is equivalent to Arrow[#1]&; both are different from (Arrow) alone, which would behave as (Arrow[__]&), i.e. SlotSequence[]. That's why I suggested using #1 rather than #: although they are perfectly equivalent, the latter makes the intended behavior more explicit. $\endgroup$
Graph[{1, 2}, {Property[1 \[DirectedEdge] 2, EdgeStyle -> Red], Property[1 <-> 2, EdgeStyle -> Blue]}]$\endgroup$