MelGrubb / BuilderGenerator

A source-generator-based implementation of the Builder pattern
https://melgrubb.github.io/BuilderGenerator/
MIT License
36 stars 8 forks source link

Need guidance for building entities without parameterless constructor #42

Closed MelGrubb closed 8 months ago

MelGrubb commented 8 months ago

Create examples for how to use builders when the object has no parameterless constructor. The Build method probably needs a hook that can be used in a hand written partial class to allow for custom construction of the object in order to handle any additional non-standard cases. The builder should probably be more intelligent about this if possible, though. How would it choose which constructor to use? Greediest? Stingiest? I think a partial Build method called from the main Build method might be the only real solution, but this will take some thought.

asmolarczyk commented 8 months ago

Hi, I'm using your great project to create builders for DevExpress XPO entities. These entities must be created by an ObjectSpace.CreateObject<{{TargetClassFullName}}>() factory method. Unfortunately this is not supported of the box (yet) and unfortunately https://github.com/MelGrubb/BuilderGenerator/issues/32 is also not implemented yet. Therefore I cloned you repo and modified the templates.

Really would like to see out of the box support for such scenarios! Thank you!

gburkecw commented 8 months ago

image

In the above, I was hoping that "WithObject" would allow me to call the constructor myself and therefore work around the fact that my object has no parameterless constructor. However, in this example "var test" is a generic Builder\<Company> instead of CompanyBuilder.

To workaround this, I am currently inheriting from Company and creating a class with a parameterless constructor that passes some defaults to the base constructor, but it would be great if WithObject worked, or if there was some kind of WithInstance which took an instance of the object I'm attempting to build.

image

MelGrubb commented 8 months ago

What you've posted is what WithObject was meant to enable. It lets you start from a pre-existing object, or that was the idea at least. I think you're getting the Builder<Company> because of the WithObject call. Since that's defined down on the base class, it's going to return what it knows, and it can't know about the existence of CompanyBuilder. I'm curious why this is a problem, though. You should still be able to call Build on it and get back the company you wanted, right?

Sorry, it's been a little while. I was developing on this pretty hard and then the day job got busy on me. I really need to find my way back to my own projects.

MelGrubb commented 8 months ago

After thinking about it for a while, I think I see the problem you're running into. After calling WithObject, you're ending up with an instance of the generic base class, and can't use additional .With methods, is that it? If that's the problem, then just cast what you get back from WithObject back to CompanyBuilder and you should be all set. I suppose moving the WithObject from the base class to the generated class would make this a non-issue, so I'm going to leave this one open with a note to myself.

Self, move WithObject to the generated class. Anyone already casting to a derived type should just get some helpful grayed-out text telling them that the cast isn't necessary, but otherwise it should all be 100% compatible since CompanyBuilder would still derive from Builder.

Consider giving the generated builders a constructor that just takes the starting object in the first place. That might make for some nice syntax.

var builder = new CompanyBuilder(myCompany).WithName("Foo");

MelGrubb commented 8 months ago

I wrapped up some work I'd left outstanding, and created a new PR this evening (https://github.com/MelGrubb/BuilderGenerator/pull/43)

There was a lot of reorganization and general cleanup, but the main additions are 1) Moved WithObject to the generated file to get rid of the type confusion 2) Added a FromTemplate method, which I might rename to FromExample or something. I'm open to suggestions.

I need to do some other cleanup still before I make a build out of this. Maybe tomorrow.

MelGrubb commented 8 months ago

I've just released what I'm calling v2.4.0-alpha. Among other things, it introduces a parameter to the generated builders' constructors so that you can skip calling WithObject altogether and just pass your "starter" object straight to the new builder.

Internally, it sets the underlying object, but also does a shallow clone of that object's properties into the builder's backing fields so that when you call .Build(), it will only change values you've explicitly changed on that builder instance. I don't have a good local test case for this, so you'll have to tell me if it works for you.

There was quite a bit of refactoring in this one, so if you forked the repo, it's going to change a lot if you merge my changes in. I've laid some of the groundwork for custom templates, but there are some conceptual problems to deal with. Mainly, it's where to specify what templates to use. If you specify them in the attribute, then there's the possibility of mismatches. Mixing and matching the templates for the generated code is fine, but the base class and attribute also come from templates, so which version would win? My current thought is to create another attribute that would be applied at the assembly level, maybe. That way, you'd have just one place to say which templates you're using.

Anyway. Progress was made this weekend. Please test it out and see if it works better for you.