Freely setting C # properties and chaining methods

I am using .NET 3.5. We have several complex third-party classes that are automatically generated and go beyond my control, but with which we must work for testing purposes. I see that my team does a lot of deeply nested get / tune properties in our test code, and that gets pretty cumbersome.

To fix this problem, I would like to make a free interface for setting properties for various objects in a hierarchical tree. This third-party library has a large number of properties and classes, and it would be too tedious to display everything manually.

My initial thought was to simply use object initializers. Red , Blue and Green are properties, and Mix() is a method that sets the fourth Color property for the nearest RGB-enabled color with this mixed color. Paints must be homogenized with Stir() before they can be used.

 Bucket b = new Bucket() { Paint = new Paint() { Red = 0.4; Blue = 0.2; Green = 0.1; } }; 

This works to initialize Paint , but I need to associate Mix() and other methods with it. Next attempt:

 Create<Bucket>(Create<Paint>() .SetRed(0.4) .SetBlue(0.2) .SetGreen(0.1) .Mix().Stir() ) 

But this does not scale very well, because I will need to define a method for each property that I want to set, and there are hundreds of different properties in all classes. Also, C # has no way to dynamically define methods before C # 4, so I don’t think I can connect to things to do this automatically in some way.

Third attempt:

 Create<Bucket>(Create<Paint>().Set(p => { p.Red = 0.4; p.Blue = 0.2; p.Green = 0.1; }).Mix().Stir() ) 

It doesn't look so bad, and it seems that it would be possible. Is this an appropriate approach? Is it possible to write a Set method that works this way? Or should I pursue an alternative strategy?

+7
c # xml fluent-interface
source share
4 answers

It works?

 Bucket b = new Bucket() { Paint = new Paint() { Red = 0.4; Blue = 0.2; Green = 0.1; }.Mix().Stir() }; 

Assuming Mix() and Stir() defined to return a Paint object.

To call methods that return void , you can use the extension method, which allows you to further initialize the object you are going through:

 public static T Init<T>(this T @this, Action<T> initAction) { if (initAction != null) initAction(@this); return @this; } 

What can be used in the same way as Set (), as described:

 Bucket b = new Bucket() { Paint = new Paint() { Red = 0.4; Blue = 0.2; Green = 0.1; }.Init(p => { p.Mix().Stir(); }) }; 
+9
source share

I would think of it this way:

Essentially, you want your last method in the chain to return a Bucket. In your case, I think you want this method to be Mix (), since you can Stir () after this bucket

 public class BucketBuilder { private int _red = 0; private int _green = 0; private int _blue = 0; public Bucket Mix() { Bucket bucket = new Bucket(_paint); bucket.Mix(); return bucket; } } 

Thus, before calling Mix () you need to set at least one color. Let strength with some syntax interfaces.

 public interface IStillNeedsMixing : ICanAddColours { Bucket Mix(); } public interface ICanAddColours { IStillNeedsMixing Red(int red); IStillNeedsMixing Green(int green); IStillNeedsMixing Blue(int blue); } 

And apply them to BucketBuilder

 public class BucketBuilder : IStillNeedsMixing, ICanAddColours { private int _red = 0; private int _green = 0; private int _blue = 0; public IStillNeedsMixing Red(int red) { _red += red; return this; } public IStillNeedsMixing Green(int green) { _green += green; return this; } public IStillNeedsMixing Blue(int blue) { _blue += blue; return this; } public Bucket Mix() { Bucket bucket = new Bucket(new Paint(_red, _green, _blue)); bucket.Mix(); return bucket; } } 

Now you need the original static property to start the chain

 public static class CreateBucket { public static ICanAddColours UsingPaint { return new BucketBuilder(); } } 

And to a large extent, now you have a free interface with additional RGB options (if you enter at least one) as a bonus.

 CreateBucket.UsingPaint.Red(0.4).Green(0.2).Mix().Stir(); 

The thing with Fluent Interfaces is that they are not so easy to assemble, but it is easy for developers to write code, and they are very extensible. If you want to add the Matt / Gloss flag to it without changing the entire calling code, this is easy to do.

In addition, if the provider of your API changes everything that is under you, you only need to rewrite this piece of code; all callin code may remain unchanged.

+4
source share

I would use the Init extension method, because U can always play with a delegate. Hell, you can always declare extension methods that deal with expressions, and even play with expressions (store them later, modify, whatever) Thus, you can easily store default commands, for example:

 Create<Paint>(() => new Paint{p.Red = 0.3, p.Blue = 0.2, p.Green = 0.1}). Init(p => p.Mix().Stir()) 

This method. Can you use all actions (or funcs) and standard cache initializers as expression chains for later versions?

0
source share

If you really want to be able to bind property settings without having to write a ton of code, one way to do this is to use code generation (CodeDom). You can use Reflection to get a list of mutable properties, create a free builder class with the final Build() method, which returns the class that you are actually trying to create.

I'm going to skip all the template files on how to register a custom tool - which is pretty easy to find documentation, but still durable, and I don't think I will add it by adding it. I will show you what I think of 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 was just to do it - it is clearly not verified, but here you go from here that you create a codegen (inheriting from BaseCodeGeneratorWithSite ) that compiles CodeCompileUnit populated with a list of types. This list comes from the type of file that you register with the tool - in this case, I will probably just make it a text file with a limited list of types for which you want to create a builder code. Ask the tool to verify this, load the types (maybe load the assemblies first), and generate the bytecode.

It is difficult, but not as difficult as it seems, and when you are done, you can write this code:

 Paint p = new PaintBuilder().Red(0.4).Blue(0.2).Green(0.1).Build().Mix.Stir(); 

I believe that this is almost what you want. All you need to do to trigger the code generation is to register the tool with a custom extension (say .buildertypes ), put a file with this 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 necessary code file, which supports writing statements like the one above.

I used this approach before for a highly messed up system with several hundred different types of messages. Too much time to always compose a message, set a bunch of properties, send it on a channel, receive from a channel, serialize a response, etc. .... using codegen greatly simplified the work, as it allowed me to create one messaging class, which took all the individual properties as arguments and returned the correct type response. This is not something I would recommend to everyone, but when you are dealing with very large projects, sometimes you need to come up with your own syntax!

0
source share

All Articles