Skip to content

Commit 9d36808

Browse files
authored
feat: FluentLambda and InitialStep (#17)
* fix(Readme): add media from githubusercontent * feat: add FluentLambdaAttribute * test: add test class FluentLambdaClass * feat: explicit interface implementations for the generated builder * refactor(MethodSignature): rename isStandAlone to isSignatureForInterface * fix(MethodSignature): no default parameter values for explicit interface implementations * test: rerun tests and write results with explicit interface implementations * refactor(MethodSignature): don't erase values in copy constructor * test: EmptyClass * feat: implement InitialStep method and test with OneMemberClass, TwoMemberClass and ThreeMemberClass * fix(TestDataProvider): remove single test filter method * fix: GenericClassWithConstraints test * test: rerun tests and write generated code as expected code * fix: names of test classes * fix(Readme): typo * feat(DuplicateMethodsChecker): check for reserved method names * chore: add M31FA021 (reserved method name) to shipped analyzer releases * test: ThreePrivateMembersClass * test: add more code generation execution tests * test: add CanExecutePrivateConstructorClass * feat(FluentLambda): make FluentLambdaClass test work * feat(FluentLambda): FluentLambdaMemberWithoutFluentApi diagnostic * test: CanDetectFluentLambdaMemberWithoutFluentApiClass * test: CanExecuteFluentLambdaClass * test: FluentLambdaSingleStepClass and CanExecuteFluentLambdaSingleStepClass * test: FluentLambdaNullablePropertyClass * test: add failing test FluentLambdaRecursiveClass * test(FluentLambdaRecursiveClass): add name property * refactor(BuilderClassFields): single GetNewFieldName method * refactor(BuilderClassFields): rename to ReservedVariableNames * feat: make test FluentLambdaRecursiveClass work * test: CanExecuteFluentLambdaRecursiveClass * test: add failing test TryBreakFluentApiClass1 * fix: make test TryBreakFluentApiClass1 work * test: TryBreakFluentApiClass2 * test(FluentLambdaRecursiveClass): add name property * fix: don't use builder type prefix for info fields in the constructor * test: rerun tests and write results as expected results * test: FluentLambdaClassInDifferentNamespace * fix: address resharper warnings * fix: address more resharper warnings * fix: failing tests due to resharper annotations * fix: remove obsolete comment * chore: optimize description and package tags * improve(ExampleProject): add person examples to Program.cs * fix: nullable private method property * improve(ExampleProject): add HashCode class * improve(ExampleProject): add DockerFile example * fix(ExampleProject): rename file * docs(Readme): add FluentLambda * fix: InitialStep for empty class * fix: remove obsolete NoMember class * improve: add city to FluentLambda tests * fix(TestDataProvider): remove NoMemberClass * fix(CodeGenerationTests): adjust for new Address model * improve(FluentLambda): also generate normal member builder method * fix(FluentLambdaClassInDifferentNamespace): take normal builder method into account * improve(Storybook): add FluentLambda * docs(Readme): minor changes * improve(Storybook): revise texts * chore: increase nuget versions to 1.4.0
1 parent f5f7808 commit 9d36808

File tree

278 files changed

+7576
-988
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

278 files changed

+7576
-988
lines changed

README.md

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Fluent APIs in C#
22

3-
![M31.FluentApi logo](media/fluent-api-logo-256.jpg)
3+
![M31.FluentApi logo](https://raw.githubusercontent.com/m31coding/M31.FluentAPI/main/media/fluent-api-logo-256.jpg)
44

55
Everybody wants to use fluent APIs but writing them is tedious. With this library providing fluent APIs for your classes becomes a breeze. Simply annotate them with attributes and the source code for the fluent API will be generated. The fluent API library leverages incremental source code generation at development time and your IDE will offer you the corresponding code completion immediately.
66

@@ -25,7 +25,7 @@ PM> Install-Package M31.FluentApi
2525
A package reference will be added to your `csproj` file. Moreover, since this library provides code via source code generation, consumers of your project don't need the reference to `M31.FluentApi`. Therefore, it is recommended to use the `PrivateAssets` metadata tag:
2626

2727
```xml
28-
<PackageReference Include="M31.FluentApi" Version="1.3.0" PrivateAssets="all"/>
28+
<PackageReference Include="M31.FluentApi" Version="1.4.0" PrivateAssets="all"/>
2929
```
3030

3131
If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file:
@@ -46,8 +46,16 @@ If you use this library for the first time I recommend that you read the storybo
4646
- [01 Basics](src/M31.FluentApi.Storybook/01_Basics.cs)
4747
- [02 Control attributes](src/M31.FluentApi.Storybook/02_ControlAttributes.cs)
4848

49+
Moreover, you may find several Fluent API examples and their [usage](src/ExampleProject/Program.cs) in the example project:
4950

50-
Here is the full example from the introduction to the basics:
51+
- [Student](src/ExampleProject/Student.cs)
52+
- [Person](src/ExampleProject/Person.cs)
53+
- [HashCode](src/ExampleProject/HashCode.cs)
54+
- [Node](src/ExampleProject/Node.cs)
55+
- [DockerFile](src/ExampleProject/DockerFile.cs)
56+
- ...
57+
58+
Here is an example from the introduction to the basics:
5159

5260
```cs
5361
[FluentApi]
@@ -89,15 +97,15 @@ public class Student
8997
}
9098
```
9199

92-
![fluent-api-usage](media/fluent-api.gif)
100+
![fluent-api-usage](https://raw.githubusercontent.com/m31coding/M31.FluentAPI/main/media/fluent-api.gif)
93101

94102
You may have a look at the generated code for this example: [CreateStudent.g.cs](src/M31.FluentApi.Tests/CodeGeneration/TestClasses/StudentClass/CreateStudent.g.cs). Note that if you use private members or properties with a private set accessor, as it is the case in this example, the generated code will use reflection to set the properties.
95103

96104
## Attributes
97105

98106
The attributes `FluentApi` and `FluentMember` are all you need in order to get started.
99107

100-
The attributes `FluentPredicate` and `FluentCollection` can be used instead of a `FluentMember` attribute if the decorated member is a boolean or a collection, respectively.
108+
The attributes `FluentPredicate`, `FluentCollection`, and `FluentLambda` can be used instead of the `FluentMember` attribute if the decorated member is a boolean, a collection, or has its own Fluent API, respectively.
101109

102110
`FluentDefault` and `FluentNullable` can be used in combination with these attributes to set a default value or null, respectively.
103111

@@ -118,8 +126,17 @@ Use this attribute for your class / struct / record. The optional parameter allo
118126
public class Student
119127
```
120128

129+
You can create instances by statically accessing the generated `CreateStudent` class:
130+
131+
```cs
132+
Student alice = CreateStudent.WithFirstName("Alice")...
133+
```
134+
135+
Alternatively, you can call `InitialStep` to get a new builder instance:
136+
121137
```cs
122-
Student alice = CreateStudent...
138+
ICreateStudent createStudent = CreateStudent.InitialStep();
139+
Student alice = createStudent.WithFirstName("Alice")...
123140
```
124141

125142
### FluentMember
@@ -160,7 +177,7 @@ public string LastName { get; private set; }
160177
FluentPredicate(int builderStep, string method = "{Name}", string negatedMethod = "Not{Name}")
161178
```
162179

163-
Can be used instead of a `FluentMember` attribute if the decorated member is of type `bool`. This attribute generates three methods, one for setting the value of the member to `true`, one for setting it to `false`, and one for passing the boolean value.
180+
Can be used instead of the `FluentMember` attribute if the decorated member is of type `bool`. This attribute generates three methods, one for setting the value of the member to `true`, one for setting it to `false`, and one for passing the boolean value.
164181

165182
```cs
166183
[FluentPredicate(4, "WhoIsHappy", "WhoIsSad")]
@@ -185,21 +202,40 @@ FluentCollection(
185202
string withZeroItems = "WithZero{Name}")
186203
```
187204

188-
Can be used instead of a `FluentMember` attribute if the decorated member is a collection. This attribute generates methods for setting multiple items, one item and zero items. The supported collection types can be seen in the source file [CollectionInference.cs](src/M31.FluentApi.Generator/SourceGenerators/Collections/CollectionInference.cs).
205+
Can be used instead of the `FluentMember` attribute if the decorated member is a collection. This attribute generates methods for setting multiple items, one item and zero items. The supported collection types can be seen in the source file [CollectionInference.cs](src/M31.FluentApi.Generator/SourceGenerators/Collections/CollectionInference.cs).
189206

190207
```cs
191208
[FluentCollection(5, "Friend", "WhoseFriendsAre", "WhoseFriendIs", "WhoHasNoFriends")]
192209
public IReadOnlyCollection<string> Friends { get; private set; }
193210
```
194211

195212
```cs
196-
....WhoseFriendsAre(new string[] { "Bob", "Carol", "Eve" })...
213+
...WhoseFriendsAre(new string[] { "Bob", "Carol", "Eve" })...
197214
...WhoseFriendsAre("Bob", "Carol", "Eve")...
198215
...WhoseFriendIs("Alice")...
199216
...WhoHasNoFriends()...
200217
```
201218

202219

220+
### FluentLambda
221+
222+
```cs
223+
FluentLambda(int builderStep, string method = "With{Name}")
224+
```
225+
226+
Can be used instead of the `FluentMember` attribute if the decorated member has its own Fluent API. Generates an additional builder method that accepts a lambda expression for creating the target field or property.
227+
228+
```cs
229+
[FluentLambda(1)]
230+
public Address Address { get; private set; }
231+
```
232+
233+
```cs
234+
...WithAddress(new Address("23", "Market Street", "San Francisco"))...
235+
...WithAddress(a => a.WithHouseNumber("23").WithStreet("Market Street").InCity("San Francisco"))...
236+
```
237+
238+
203239
### FluentDefault
204240

205241
```cs
@@ -328,6 +364,9 @@ string serialized = ...ToJson();
328364
```
329365

330366

367+
## Miscellaneous
368+
369+
331370
### Forks
332371

333372
To create forks specify builder methods at the same builder step. The resulting API offers all specified methods at this step but only one can be called:
@@ -352,9 +391,28 @@ private void BornOn(DateOnly dateOfBirth)
352391
```
353392

354393

394+
### Lambda pattern
395+
396+
Instances of Fluent API classes can be created and passed into methods of other classes using the lambda pattern. For example, given a `University` class that needs to be augmented with an `AddStudent` method, the following code demonstrates the lambda pattern:
397+
398+
```cs
399+
public void AddStudent(Func<CreateStudent.ICreateStudent, Student> createStudent)
400+
{
401+
Student student = createStudent(CreateStudent.InitialStep());
402+
students.Add(student);
403+
}
404+
```
405+
406+
```cs
407+
university.AddStudent(s => s.Named("Alice", "King").OfAge(22)...);
408+
```
409+
410+
Note that if you want to set a single field or property on a Fluent API class, you can instead use the `FluentLambda` attribute.
411+
412+
355413
## Problems with the IDE
356414

357-
As of 2023 code generation with Roslyn is still a relatively new feature but is already supported quite well in Visual Studio and Rider. Since code generation is potentially triggered with every single key stroke, there are sometimes situations where the code completion index of the IDE does not keep up with all the changes.
415+
Since code generation is potentially triggered with every single key stroke, there are sometimes situations where the code completion index of the IDE does not keep up with all the changes.
358416

359417
In particular, if your IDE visually indicates that there are errors in your code but it compiles and runs just fine, try the following things:
360418

src/ExampleProject/DockerFile.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Non-nullable member is uninitialized
2+
#pragma warning disable CS8618
3+
// ReSharper disable All
4+
5+
// Example from https://mitesh1612.github.io/blog/2021/08/11/how-to-design-fluent-api.
6+
7+
using System.Text;
8+
using M31.FluentApi.Attributes;
9+
10+
namespace ExampleProject;
11+
12+
[FluentApi]
13+
public class DockerFile
14+
{
15+
private readonly StringBuilder content;
16+
17+
private DockerFile()
18+
{
19+
content = new StringBuilder();
20+
}
21+
22+
[FluentMethod(0)]
23+
private void FromImage(string imageName)
24+
{
25+
content.AppendLine($"FROM {imageName}");
26+
}
27+
28+
[FluentMethod(1)]
29+
[FluentContinueWith(1)]
30+
private void CopyFiles(string source, string destination)
31+
{
32+
content.AppendLine($"COPY {source} {destination}");
33+
}
34+
35+
[FluentMethod(1)]
36+
[FluentContinueWith(1)]
37+
private void RunCommand(string command)
38+
{
39+
content.AppendLine($"RUN {command}");
40+
}
41+
42+
[FluentMethod(1)]
43+
[FluentContinueWith(1)]
44+
private void ExposePort(int port)
45+
{
46+
content.AppendLine($"EXPOSE {port}");
47+
}
48+
49+
[FluentMethod(1)]
50+
[FluentContinueWith(1)]
51+
private void WithEnvironmentVariable(string variableName, string variableValue)
52+
{
53+
content.AppendLine($"ENV {variableName}={variableValue}");
54+
}
55+
56+
[FluentMethod(1)]
57+
private void WithCommand(string command)
58+
{
59+
var commandList = command.Split(" ");
60+
content.Append("CMD [ ");
61+
content.Append(string.Join(", ", commandList.Select(c => $"\"{c}\"")));
62+
content.Append(']');
63+
}
64+
65+
public override string ToString()
66+
{
67+
return content.ToString();
68+
}
69+
}

src/ExampleProject/HashCode.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using M31.FluentApi.Attributes;
2+
3+
namespace ExampleProject;
4+
5+
[FluentApi]
6+
public struct HashCode
7+
{
8+
private int hash;
9+
10+
public HashCode()
11+
{
12+
hash = 17;
13+
}
14+
15+
[FluentMethod(0)]
16+
[FluentContinueWith(0)]
17+
public void Add<T>(T value)
18+
{
19+
unchecked
20+
{
21+
hash = hash * 23 + value?.GetHashCode() ?? 0;
22+
}
23+
}
24+
25+
[FluentMethod(0)]
26+
[FluentContinueWith(0)]
27+
public void Add<T1, T2>(T1 value1, T2 value2)
28+
{
29+
unchecked
30+
{
31+
hash = hash * 23 + value1?.GetHashCode() ?? 0;
32+
hash = hash * 23 + value2?.GetHashCode() ?? 0;
33+
}
34+
}
35+
36+
[FluentMethod(0)]
37+
[FluentContinueWith(0)]
38+
public void Add<T1, T2, T3>(T1 value1, T2 value2, T3 value3)
39+
{
40+
unchecked
41+
{
42+
hash = hash * 23 + value1?.GetHashCode() ?? 0;
43+
hash = hash * 23 + value2?.GetHashCode() ?? 0;
44+
hash = hash * 23 + value3?.GetHashCode() ?? 0;
45+
}
46+
}
47+
48+
[FluentMethod(0)]
49+
[FluentContinueWith(0)]
50+
public void Add<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4)
51+
{
52+
unchecked
53+
{
54+
hash = hash * 23 + value1?.GetHashCode() ?? 0;
55+
hash = hash * 23 + value2?.GetHashCode() ?? 0;
56+
hash = hash * 23 + value3?.GetHashCode() ?? 0;
57+
hash = hash * 23 + value4?.GetHashCode() ?? 0;
58+
}
59+
}
60+
61+
[FluentMethod(0)]
62+
[FluentContinueWith(0)]
63+
public void Add<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5)
64+
{
65+
unchecked
66+
{
67+
hash = hash * 23 + value1?.GetHashCode() ?? 0;
68+
hash = hash * 23 + value2?.GetHashCode() ?? 0;
69+
hash = hash * 23 + value3?.GetHashCode() ?? 0;
70+
hash = hash * 23 + value4?.GetHashCode() ?? 0;
71+
hash = hash * 23 + value5?.GetHashCode() ?? 0;
72+
}
73+
}
74+
75+
[FluentMethod(0)]
76+
[FluentContinueWith(0)]
77+
public void AddSequence<T>(IEnumerable<T> items)
78+
{
79+
unchecked
80+
{
81+
foreach (T item in items)
82+
{
83+
hash = hash * 23 + item?.GetHashCode() ?? 0;
84+
}
85+
86+
}
87+
}
88+
89+
[FluentMethod(0)]
90+
[FluentReturn]
91+
public int Value()
92+
{
93+
return hash;
94+
}
95+
}

0 commit comments

Comments
 (0)