If you really want to be able to chain property settings without having to write a ton of code, one way to do this would be to use code generation (CodeDom). You can use Reflection to get a list of the mutable properties, the generate a fluent builder class with a final Build()
method that returns the class you're actually trying to create.
I'm going to skip over all the boilerplate stuff about how to register the custom tool - that's fairly easy to find documentation on but still long-winded and I don't think I'd be adding much by including it. I will show you what I'm thinking of for the codegen though.
public static class PropertyBuilderGenerator
{
public static CodeTypeDeclaration GenerateBuilder(Type destType)
{
if (destType == null)
throw new ArgumentNullException("destType");
CodeTypeDeclaration builderType = new
CodeTypeDeclaration(destType.Name + "Builder");
builderType.TypeAttributes = TypeAttributes.Public;
CodeTypeReference destTypeRef = new CodeTypeReference(destType);
CodeExpression resultExpr = AddResultField(builderType, destTypeRef);
PropertyInfo[] builderProps = destType.GetProperties(
BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo prop in builderProps)
{
AddPropertyBuilder(builderType, resultExpr, prop);
}
AddBuildMethod(builderType, resultExpr, destTypeRef);
return builderType;
}
private static void AddBuildMethod(CodeTypeDeclaration builderType,
CodeExpression resultExpr, CodeTypeReference destTypeRef)
{
CodeMemberMethod method = new CodeMemberMethod();
method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
method.Name = "Build";
method.ReturnType = destTypeRef;
method.Statements.Add(new MethodReturnStatement(resultExpr));
builderType.Members.Add(method);
}
private static void AddPropertyBuilder(CodeTypeDeclaration builderType,
CodeExpression resultExpr, PropertyInfo prop)
{
CodeMemberMethod method = new CodeMemberMethod();
method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
method.Name = prop.Name;
method.ReturnType = new CodeTypeReference(builderType.Name);
method.Parameters.Add(new CodeParameterDeclarationExpression(prop.Type,
"value"));
method.Statements.Add(new CodeAssignStatement(
new CodePropertyReferenceExpression(resultExpr, prop.Name),
new CodeArgumentReferenceExpression("value")));
method.Statements.Add(new MethodReturnStatement(
new CodeThisExpression()));
builderType.Members.Add(method);
}
private static CodeFieldReferenceExpression AddResultField(
CodeTypeDeclaration builderType, CodeTypeReference destTypeRef)
{
const string fieldName = "_result";
CodeMemberField resultField = new CodeMemberField(destTypeRef, fieldName);
resultField.Attributes = MemberAttributes.Private;
builderType.Members.Add(resultField);
return new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), fieldName);
}
}
I think this should just about do it - it's obviously untested, but where you go from here is that you create a codegen (inheriting from BaseCodeGeneratorWithSite
) that compiles a CodeCompileUnit
populated with a list of types. That list comes from the file type you register with the tool - in this case I'd probably just make it a text file with a line-delimited list of types that you want to generate builder code for. Have the tool scan this, load the types (might have to load the assemblies first), and generate bytecode.
It's tough, but not as tough as it sounds, and when you're done you'll be able to write code like this:
Paint p = new PaintBuilder().Red(0.4).Blue(0.2).Green(0.1).Build().Mix.Stir();
Which I believe is almost exactly what you want. All you have to do to invoke the code generation is register the tool with a custom extension (let's say .buildertypes
), put a file with that extension in your project, and put a list of types in it:
MyCompany.MyProject.Paint
MyCompany.MyProject.Foo
MyCompany.MyLibrary.Bar
And so on. When you save, it will automatically generate the code file you need that supports writing statements like the one above.
I've used this approach before for a highly convoluted messaging system with several hundred different message types. It was taking too long to always construct the message, set a bunch of properties, send it through the channel, receive from the channel, serialize the response, etc... using a codegen greatly simplified the work as it enabled me to generate a single messaging class that took all of the individual properties as arguments and spit back a response of the correct type. It's not something I would recommend to everyone, but when you're dealing with very large projects, sometimes you need to start inventing your own syntax!