First of all I would separate the packet parser from the data stream reader (so that I could write tests without dealing with the stream). Then consider a base class which provides a method to read in a packet and one to write a packet.
Additionally I would build a dictionary (one time only then reuse it for future calls) like the following:
class Program {
static void Main(string[] args) {
var assembly = Assembly.GetExecutingAssembly();
IDictionary<byte, Func<Message>> messages = assembly
.GetTypes()
.Where(t => typeof(Message).IsAssignableFrom(t) && !t.IsAbstract)
.Select(t => new {
Keys = t.GetCustomAttributes(typeof(AcceptsAttribute), true)
.Cast<AcceptsAttribute>().Select(attr => attr.MessageId),
Value = (Func<Message>)Expression.Lambda(
Expression.Convert(Expression.New(t), typeof(Message)))
.Compile()
})
.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))
.ToDictionary(o => o.Key, v => v.Value);
//will give you a runtime error when created if more
//than one class accepts the same message id, <= useful test case?
var m = messages[5](); // consider a TryGetValue here instead
m.Accept(new Packet());
Console.ReadKey();
}
}
[Accepts(5)]
public class FooMessage : Message {
public override void Accept(Packet packet) {
Console.WriteLine("here");
}
}
//turned off for the moment by not accepting any message ids
public class BarMessage : Message {
public override void Accept(Packet packet) {
Console.WriteLine("here2");
}
}
public class Packet {}
public class AcceptsAttribute : Attribute {
public AcceptsAttribute(byte messageId) { MessageId = messageId; }
public byte MessageId { get; private set; }
}
public abstract class Message {
public abstract void Accept(Packet packet);
public virtual Packet Create() { return new Packet(); }
}
Edit: Some explanations of what is going on here:
First:
[Accepts(5)]
This line is a C# attribute (defined by AcceptsAttribute
) says the the FooMessage
class accepts the message id of 5.
Second:
Yes the dictionary is being built at runtime via reflection. You need only to do this once (I would put it into a singleton class that you can put a test case on it that can be run to ensure that the dictionary builds correctly).
Third:
var m = messages[5]();
This line gets the following compiled lambda expression out of the dictionary and executes it:
()=>(Message)new FooMessage();
(The cast is necessary in .NET 3.5 but not in 4.0 due to the covariant changes in how delagates work, in 4.0 an object of type Func<FooMessage>
can be assigned to an object of the type Func<Message>
.)
This lambda expression is built by the Value assignment line during dictionary creation:
Value = (Func<Message>)Expression.Lambda(Expression.Convert(Expression.New(t), typeof(Message))).Compile()
(The cast here is necessary to cast the compiled lambda expression to Func<Message>
.)
I did that this way because I happen to already have the type available to me at that point. You could also use:
Value = ()=>(Message)Activator.CreateInstance(t)
But I believe that would be slower (and the cast here is necessary to change Func<object>
into Func<Message>
).
Fourth:
.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))
This was done because I felt that you might have value in placing the AcceptsAttribute
more than once on a class(to accept more than one message id per class). This also has the nice side affect of ignoring message classes that do not have a message id attribute (otherwise the Where method would need to have the complexity of determining if the attribute is present).