stefh / fluentbuilder Goto Github PK
View Code? Open in Web Editor NEWA project which uses Source Generation to create a FluentBuilder for a specified model or DTO
License: MIT License
A project which uses Source Generation to create a FluentBuilder for a specified model or DTO
License: MIT License
For protobuf RepeatedField<T>
collections (which are read-only and implements ICollection
and IList
among others) there are no With*
methods generated.
If more than 1 AutoGenerateBuilder
class is defined, no classes are generated
[AutoGenerateBuilder(typeof(A))]
public partial class ABuilder {
}
[AutoGenerateBuilder(typeof(B))]
public partial class BBuilder {
}
A
and B
are third-party DTOs
My project :
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentBuilder" Version="0.9.0" PrivateAssets="All"/>
<PackageReference Include="ABNuget" Version="xxx" />
</ItemGroup>
</Project>
dotnet sdk 7.0.401
if we have a model like the following:
public class Model
{
public string Name { get; private set; }
public void DoSomething(string newName)
{
// magic done here...
}
then a following builder could be generated to handle this:
public partial class ModelBuilderCliff : Builder<RedDragon.BeastMode.Domain.Exercises.Model>
{
private Lazy<string>? _name=null;
public ModelBuilderCliff WithName(string value) => WithName(() => value);
partial void SetName(Model instance, string value);
public ModelBuilderCliff WithName(Func<string> func)
{
_name = new Lazy<string>(func);
return this;
}
public ModelBuilderCliff WithoutName()
{
_name = null;
return this;
}
public override Model Build(bool useObjectInitializer = true)
{
if (Object?.IsValueCreated != true)
{
Object = new Lazy<Model>(
() =>
{
if (useObjectInitializer)
{
var instance1 = new Model
{
};
SetName(instance1, _name == null?Default().Name:_name.Value);
return instance1;
}
var instance = new Model();
if (_name !=null )
{
SetName(instance, _name == null ? Default().Name : _name.Value);
}
return instance;
});
}
PostBuild(Object.Value);
return Object.Value;
}
public static Model Default() => new Model();
}
then a developer could provide an implementation of SetName
that does something meaningful for the object:
partial void SetName(Model instance, string value)
{
instance.DoSomething(value);
}
this doesn't seem to work or generate any errors:
namespace namespace1
{
using System.Diagnostics;
using FluentBuilder;
[AutoGenerateBuilder(typeof(MyModel))]
public partial class MyModelBuilder
{
}
public record MyModel
{
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; } = "Unknown";
}
}
namespace namespace2
{
using FluentBuilder;
using namespace1;
// doesn't work
[AutoGenerateBuilder(typeof(MyModel))]
public partial class MyModelBuilder2
{
}
}
Coming from WireMock-Net/WireMock.Net#621, I suggested that the builder API could create overloads that took other builders if the property type is also annotated with the AutoGenerateBuilder attribute. I was thinking of the following API:
public UserDtoBuilder WithPrimaryEmail(Action<FluentBuilder.EmailDtoBuilder> action) => WithPrimaryEmail(() =>
{
var builder = new FluentBuilder.EmailDtoBuilder();
action(builder);
return builder.Build();
});
public UserDtoBuilder WithPrimaryEmailBuilder(FluentBuilder.EmailDtoBuilder builder) => WithPrimaryEmail(() =>
{
return builder.Build();
});
I was looking into the code and was able to implement it with no problem. If you're interested, I could open a pull request right now, this is my fork
I'm trying to use this library to create my objects, but I've encountered a problem. My library uses callbacks (System.Func
and System.Action
) pretty heavily, so I have classes like:
public class Operation
{
public Func<int, string> ToStringCallback { get; set; }
public Action<int> IntCallback { get; set; }
}
The resulting code is:
public partial class OperationBuilder : Builder<Operation>
{
private bool _toStringCallbackIsSet;
private Lazy<System.Func<int, string>> _toStringCallback = new Lazy<System.Func<int, string>>(() => new System.Func<int, string>(new object(), default(System.IntPtr)));
public OperationBuilder WithToStringCallback(System.Func<int, string> value) => WithToStringCallback(() => value);
public OperationBuilder WithToStringCallback(Func<System.Func<int, string>> func)
{
_toStringCallback = new Lazy<System.Func<int, string>>(func);
_toStringCallbackIsSet = true;
return this;
}
public OperationBuilder WithoutToStringCallback()
{
WithToStringCallback(() => new System.Func<int, string>(new object(), default(System.IntPtr)));
_toStringCallbackIsSet = false;
return this;
}
private bool _intCallbackIsSet;
private Lazy<System.Action<int>> _intCallback = new Lazy<System.Action<int>>(() => new System.Action<int>(new object(), default(System.IntPtr)));
public OperationBuilder WithIntCallback(System.Action<int> value) => WithIntCallback(() => value);
public OperationBuilder WithIntCallback(Func<System.Action<int>> func)
{
_intCallback = new Lazy<System.Action<int>>(func);
_intCallbackIsSet = true;
return this;
}
public OperationBuilder WithoutIntCallback()
{
WithIntCallback(() => new System.Action<int>(new object(), default(System.IntPtr)));
_intCallbackIsSet = false;
return this;
}
This produces errors when compling, mainly on these two lines:
WithToStringCallback(() => new System.Func<int, string>(new object(), default(System.IntPtr)));
WithIntCallback(() => new System.Action<int>(new object(), default(System.IntPtr)));
The constructors are the failure since those aren't allowed. A possibly alternative would be, but that might require some additional reflection/investigating. Sadly, it would add a lot more overhead to create an interface to handle these since the object being built is basically just a collection of callbacks.
new System.Action<int>((v1) => { });
new System.Func<int, string>((v1) => default(string));
Thank you.
Using latest VS22.
It doesn't seem to work, neither with your provided samples, nor with the example projects.
Installation of the nuget runs through, but it seems like there is no FluentBuilder namespace available.
In the build logs there seems to exist the generated code tho...
1>\TestBuilder\FluentBuilderGenerator\FluentBuilderGenerator.FluentBuilderSourceGenerator\.UserBuilder.g.cs(38,65,38,80): warning CS8603: Possible null reference return.
My code:
using System;
namespace Test
{
class Program
{
static void Main(string[] args)
{
var user = new FluentBuilder.UserBuilder()
.WithFirstName("Test")
.WithLastName("User")
.Build();
Console.WriteLine($"{user.FirstName} {user.LastName}");
}
}
}
[FluentBuilder.AutoGenerateBuilder]
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime? Date { get; set; }
}
public class MyEntity : ITableEntity
{
#region ITableEntity properties
public string PartitionKey { get; set; } = null!;
public string RowKey { get; set; } = null!;
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
#endregion
public string Test { get; set; } = null!;
}
The ETag
is defined in Azure.ETag
.
Also there is a struct named ETag
in the Stef.Azure.ETag
.
The builder uses the full namespace:
private Lazy<Azure.ETag> _eTag = new(() => default(Azure.ETag));
However this fails.
FluentBuilderGenerator.Extensions.TypeSymbolExtensions.GetNewConstructor
always selects the ctor with the least parameters and fills in a fitting default by calling GetDefault
for the expected ctor parameters.
For the following class, an infinite nesting of new Person(new Person(/*...*/))
is generated, leading to stack overflow.
public class Person
{
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public Person(Person person)
{
FirstName = person.FirstName;
LastName = person.LastName;
}
public string FirstName { get; set; }
public string LastName { get; set; }
}
[AutoGenerateBuilder]
public partial class SomeDTO
{
public Person Person { get; set; }
}
When generating a builder for a class containing a ReadOnlyCollection<>
property, the generated code initializes the property with a new List<>()
.
Consider the following class:
[AutoGenerateBuilder]
public partial class SomeDTO
{
public ReadOnlyCollection<string> Strings { get; set; }
}
The following builder is being generated, where the _strings
initialization and the WithStrings(() => new List<string>())
call cause the compilation to fail:
public partial class SomeDTOBuilder : Builder<Library.SomeDTO>
{
private bool _stringsIsSet;
private Lazy<System.Collections.ObjectModel.ReadOnlyCollection<string>> _strings = new Lazy<System.Collections.ObjectModel.ReadOnlyCollection<string>>(() => new List<string>());
public SomeDTOBuilder WithStrings(System.Collections.ObjectModel.ReadOnlyCollection<string> value) => WithStrings(() => value);
public SomeDTOBuilder WithStrings(Func<System.Collections.ObjectModel.ReadOnlyCollection<string>> func)
{
_strings = new Lazy<System.Collections.ObjectModel.ReadOnlyCollection<string>>(func);
_stringsIsSet = true;
return this;
}
public SomeDTOBuilder WithStrings(Action<ClassLibrary1.FluentBuilder.IListBuilder<string>> action, bool useObjectInitializer = true) => WithStrings(() =>
{
var builder = new ClassLibrary1.FluentBuilder.IListBuilder<string>();
action(builder);
return (System.Collections.ObjectModel.ReadOnlyCollection<string>) builder.Build(useObjectInitializer);
});
public SomeDTOBuilder WithoutStrings()
{
WithStrings(() => new List<string>());
_stringsIsSet = false;
return this;
}
public override SomeDTO Build(bool useObjectInitializer = true) { /*...*/ }
public static SomeDTO Default() => new SomeDTO();
}
Looking at the FluentBuilder source, the issue seems to be that FluentBuilderGenerator.Extensions.TypeSymbolExtensions.GetDefault
calls FluentBuilderGenerator.Extensions.TypeSymbolExtensions.GetFluentTypeKind
to check for an adequate default initializer, but GetFluentTypeKind
returns interface types found for implementing types, meaning that ReadOnlyCollection : System.Collections.Generic.IList<T> /*, ...*/
causes FluentTypeKind.IList
to be returned from GetFluentTypeKind
which then leads to new List<>()
to be chosen as default initializer.
I think the 4 interface implementation checks in GetFluentTypeKind
should only happen if typeSymbol.TypeKind == TypeKind.Interface
like so:
public static FluentTypeKind GetFluentTypeKind(this ITypeSymbol typeSymbol)
{
if (typeSymbol.SpecialType == SpecialType.System_String)
{
return FluentTypeKind.String;
}
if (typeSymbol.TypeKind == TypeKind.Array)
{
return FluentTypeKind.Array;
}
if (typeSymbol.TypeKind == TypeKind.Interface)
{
if (typeSymbol.ImplementsInterfaceOrBaseClass(typeof(IDictionary<,>)) || typeSymbol.ImplementsInterfaceOrBaseClass(typeof(IDictionary)))
{
return FluentTypeKind.IDictionary;
}
if (typeSymbol.ImplementsInterfaceOrBaseClass(typeof(IList<>)) || typeSymbol.ImplementsInterfaceOrBaseClass(typeof(IList)))
{
return FluentTypeKind.IList;
}
if (typeSymbol.ImplementsInterfaceOrBaseClass(typeof(ICollection<>)) || typeSymbol.ImplementsInterfaceOrBaseClass(typeof(ICollection)))
{
return FluentTypeKind.ICollection;
}
if (typeSymbol.AllInterfaces.Any(i => i.SpecialType == SpecialType.System_Collections_IEnumerable))
{
return FluentTypeKind.IEnumerable;
}
}
return FluentTypeKind.Other;
}
if a property is an array, a With
method is generated like so:
public Model WithData(string[] value) => WithData(() => value);
It would nice syntactic sugar to have:
public Model WithData(params string[] value) => WithData(() => value);
just so code could be builder.WithData("bob")
instead of builder.WithData(new[]{"bob"})
I'm having a few issues generating builders on a child List property.
public class User
{
public List<Option> Options { get; set; }
}
public class Option
{
public string Name { get; set; }
}
[AutoGenerateBuilder(typeof(User))]
public partial class MyUserBuilder {}
[AutoGenerateBuilder(typeof(Option))]
public partial class MyOptionBuilder {}
Build error:
...\FluentBuilderGenerator\FluentBuilderGenerator.FluentBuilderSourceGenerator\FluentBuilderSandbox.Option_IListBuilder.g.cs(28,46,28,59): error CS0246: The type or namespace name 'OptionBuilder' could not be found (are you missing a using directive or an assembly reference?)
Main project
public class User
{
public List<Option> Options { get; set; }
}
public class Option
{
public string Name { get; set; }
}
Test project
[AutoGenerateBuilder(typeof(User))]
public partial class UserBuilder { }
[AutoGenerateBuilder(typeof(Option))]
public partial class OptionBuilder { }
Build error:
...\FluentBuilderGenerator\FluentBuilderGenerator.FluentBuilderSourceGenerator\FluentBuilderSandboxTests.UserBuilder.g.cs(31,68,31,86): error CS0234: The type or namespace name 'IListOptionBuilder' does not exist in the namespace 'FluentBuilderSandbox' (are you missing an assembly reference?)
Given the following classes:
public class Simple
{
public string Name { get; private set;} // or public string Name { get; init;}
}
[AutoGenerateBuilder(typeof(Simple))]
public partial class SimpleBuilder
{
}
should not generate this code in the Build
method:
public override Simple Build(bool useObjectInitializer = true)
{
if (Object?.IsValueCreated != true)
{
Object = new Lazy<Simple>(() =>
{
if (useObjectInitializer)
{
return new Simple
{
Name = _name.Value // this could be here if property has init but shouldn't be otherwise
};
}
var instance = new Simple();
if (_nameIsSet) { instance.Name = _name.Value; } // this shouldn't be here since it won't compile
return instance;
});
}
PostBuild(Object.Value);
return Object.Value;
}
Instead, for situations like this, generate the following:
// add this method to the builder. A user can choose to implement and add custom behavior
partial void SetName(Simple instance, string value); //NOTE: private could be added to the front of this to force the user to implement
public override Simple2 Build(bool useObjectInitializer = true)
{
if (Object?.IsValueCreated != true)
{
Object = new Lazy<Simple2>(() =>
{
if (useObjectInitializer)
{
var result = new Simple2
{
// other settable props here
};
if (_nameIsSet) { SetName( result, _name.Value); } // or try to find a method on the class that starts with Set<PropName>
return result;
}
var instance = new Simple2();
if (_nameIsSet) { SetName( instance, _name.Value); } // or try to find a method on the class that starts with Set<PropName>
return instance;
});
}
PostBuild(Object.Value);
return Object.Value;
}
Also, this technique could be used to overcome the default constructor requirement.
// add this method to the builder. A user can choose to implement and add custom behavior
partial void SetName(Simple instance, string value);
// Developer could provide implementation like "instance = new Simple(_name);"
// only added if no default constructor found or if you don't want to find an appropriate constructor to use
private partial void CreateInstance( out Simple instance);
public override Simple2 Build()
{
if (Object?.IsValueCreated != true)
{
Object = new Lazy<Simple2>(() =>
{
Simple instance;
CreateInstance(out instance);
if (_nameIsSet) { SetName( instance, _name.Value); }
return instance;
});
}
PostBuild(Object.Value);
return Object.Value;
}
Due to the following check, an exception is thrown if the target DTO has at least one ctor with parameters, even if a parameterless one is available.
The condition should be changed to
classSymbol.NamedTypeSymbol.Constructors.IsEmpty || classSymbol.NamedTypeSymbol.Constructors.All(c => !c.Parameters.IsEmpty)
Writing out the thrown exception into the Error.g.cs
file is great to allow errors to be analyzed - wouldn't it be good to also emit a compiler warning/error via GeneratorExecutionContext.ReportDiagnostic(...)
for such an error? I think this would make the development experience even better.
Anyway, thank you for your work on this project!
if we have a class like the following:
public class Model
{
public Model(string id, string name)
{
Id = id;
Name = name;
}
public string Name {get; set;}
}
then the following builder could be generated:
// Dev's code
public partial class ModelBuilderCliff
{
static private partial void CreateInstance(ModelBuilderCliff builder,out Model instance)
{
instance = new Model(builder._id.Value, builder._name.Value);
}
}
public partial class ModelBuilderCliff : Builder<RedDragon.BeastMode.Domain.Exercises.Model>
{
static private partial void CreateInstance(ModelBuilderCliff builder, out Model instance);
private Lazy<string>? _id=null;
public ModelBuilderCliff WithId(string value) => WithId(() => value);
partial void SetId(Model instance, string value); // since we know this is a private field, "private" could be added to the beginning of this to force the developer to provide an implementation. In order to remove any ambiguity, could add prop to AutoGenerateBuilder so user can specify if they want forced partials
public ModelBuilderCliff WithId(Func<string> func)
{
_id = new Lazy<string>(func);
return this;
}
public ModelBuilderCliff WithoutId()
{
_id = null;
return this;
}
private Lazy<string>? _name=null;
public ModelBuilderCliff WithName(string value) => WithName(() => value);
partial void SetName(Model instance, string value);
public ModelBuilderCliff WithName(Func<string> func)
{
_name = new Lazy<string>(func);
return this;
}
public ModelBuilderCliff WithoutName()
{
_name = null;
return this;
}
public override Model Build(bool useObjectInitializer = true)
{
if (Object?.IsValueCreated != true)
{
Object = new Lazy<Model>(
() =>
{
if (useObjectInitializer)
{
CreateInstance(this,out var instance1);
if (_id != null)
{
SetId(instance1, _id == null ? string.Empty : _id.Value);
}
if (_name != null)
{
instance1.Name= _name == null ? string.Empty : _name.Value;
}
return instance1;
}
// these next lines are dupes of the first if useObjectInitializer is kept
CreateInstance(this,out var instance);
if (_id != null)
{
SetId(instance, _id == null ? string.Empty : _id.Value);
}
if (_name !=null )
{
instance.Name = _name == null ? string.Empty : _name.Value;
}
return instance;
});
}
PostBuild(Object.Value);
return Object.Value;
}
}
I'm loving the library, but I think it would be very helpful to have the source generator automatically create extension methods for the updating of existing class instances. This would allow classes to become more "record-like", allowing updates in-place.
I'm proposing here that the "lifting" method to be called BuildNew()
, but I dont think this is ideal, and I think a better name should be thought of. I would call it With()
, but that conflicts with the current WithX
naming convention of the existing builders.
[AutoGenerateBuilder(generateSetters: true)]
public class User
{
public string Name { get; set; }
public string Gender { get; set; }
}
...
var user = await dbContext.Users.FirstAsync();
user.BuildNew() // `BuildNew` originates from an auto-generated extension method class in UserBuilder.g.cs. Lifts user into a UserBuilder
.WithName("New Name") // usual updates happen here
.Build(); // Updates the object in-place.
// OPTIONALLY if generateSetter == True
user.SetName("Newer Name"); // Sets the name
await dbContext.SaveChangesAsync(); // user's Name property is updated.
// also allow for operations on items in a collection
var users = dbContext.Users.Where(x => x.Gender == "Male").ToListAsync();
users.SetGender("Female") // Updates all elements in-place. Could also use `BuildNew()` syntax.
dbContext.SaveChangesAsync();
I feel like this is a natural continuation of the current library. It appears like you're supposed to be able to use UsingInstance
for this purpose to update existing instances in-place, but I can't get it to work. This would also bring existing non-record classes to be more "record like".
Hi,
Thanks a lot for all the work done on this library it has been a real help!
I found a bug while working with a class that holds a CultureInfo member.
This is my class:
public class MenuItemDisplayModel
{
// other members removed since the error is based on the one below
public CultureInfo Locale { get; set; } = CultureInfo.CurrentCulture;
}
The builder created this line of code:
private Lazy<System.Globalization.CultureInfo> _locale = new Lazy<System.Globalization.CultureInfo>(() => new System.Globalization.CultureInfo(default(int)));
which fails because default(int) is 0 and there's no culture with that number, so I get the exception System.Globalization.CultureNotFoundException: Culture is not supported. (Parameter 'culture') 0 (0x0000) is an invalid culture identifier.
Is there any attribute to tell the builder to ignore the property?
Thanks!
C# 12 has primary constructors in classes, but they don't generate properties automatically (it's a really bad design choice, but it's the chosen one).
It'd be interesting to generate properties (you just have to convert the first letter to uppercase).
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.