Skip to content

Commit 785c99d

Browse files
committed
Document diagnostics and additional features
1 parent 8bfbf9d commit 785c99d

File tree

8 files changed

+243
-7
lines changed

8 files changed

+243
-7
lines changed

docs/SID002.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## SID002: Struct Id custom constructor must provide a single Value parameter
2+
3+
When providing a custom constructor for a struct id, it must provide a single
4+
parameter named `Value`. This guarantees that other generated code can rely on
5+
this parameter to construct new instances of the struct id.
6+
7+
|Item|Value|
8+
|-|-|
9+
|Category|Build|
10+
|Enabled|True|
11+
|Severity|Error|
12+
|CodeFix|True|
13+
---
14+
15+
### How to fix violations
16+
17+
Change the primary constructor parameter name to `Value` and remove all other
18+
parameters.
19+
20+
A codefix is provided to do this automatically.
21+
22+
Example:
23+
24+
```csharp
25+
using System.ComponentModel;
26+
27+
public readonly partial record struct ProductId([property: Browsable(false)] Guid Value) : IStructId<Guid>;
28+
```
29+
30+
In this case the custom constructor parameter is used to annotate the record property
31+
with a `Browsable` attribute for application-specific reasons (i.e. hide the property
32+
from a generic object browsing/display UI).

docs/SID003.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
## SID003: Struct Id templates must be file-local partial record structs
2+
3+
Compiled templates annotated with `[TStructId]` must be declared as
4+
`file partial record struct` structs since they become partial definitions
5+
for each user-defined struct id. They must be declared as file-local
6+
to avoid polluting the containing project's visible API surface.
7+
8+
|Item|Value|
9+
|-|-|
10+
|Category|Build|
11+
|Enabled|True|
12+
|Severity|Error|
13+
|CodeFix|True|
14+
---
15+
16+
### How to fix violations
17+
18+
Change the type declaration to be a `file partial record struct` struct.
19+
20+
A codefix is provided to do this automatically.
21+
22+
Example:
23+
24+
```csharp
25+
using System;
26+
using StructId;
27+
28+
[TStructId]
29+
file partial record struct TSelf(Ulid Value)
30+
{
31+
public static TSelf New() => new(Ulid.NewUlid());
32+
}
33+
34+
// This will be removed when applying the template to each user-defined struct id.
35+
file partial record struct TSelf : INewable<TSelf, Ulid>
36+
{
37+
public static TSelf New(Ulid value) => throw new NotImplementedException();
38+
}
39+
```
40+

docs/SID004.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## SID004: Struct Id template constructor must provide a single Value parameter
2+
3+
Custom template constructor must have a single Value parameter, if present.
4+
This allows using the `Value` property in the implementation of the templated
5+
code, in a manner that meshes well with the generated code for the struct id.
6+
7+
|Item|Value|
8+
|-|-|
9+
|Category|Build|
10+
|Enabled|True|
11+
|Severity|Error|
12+
|CodeFix|True|
13+
---
14+
15+
### How to fix violations
16+
17+
Make sure the primary constructor has a single parameter named `Value`, or
18+
remove it entirely if not used.
19+
20+
A codefix is provided to do this automatically.

docs/SID005.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## SID005: Struct Id template declaration must use the reserved name 'TSelf'
2+
3+
The template struct record must be named `TSelf` to ensure proper code generation
4+
when applied to user-defined struct ids.
5+
6+
|Item|Value|
7+
|-|-|
8+
|Category|Build|
9+
|Enabled|True|
10+
|Severity|Error|
11+
|CodeFix|True|
12+
---
13+
14+
### How to fix violations
15+
16+
Rename the template record struct declaration to be named `TSelf`.
17+
18+
A codefix is provided to do this automatically.
19+
20+
Example:
21+
22+
```csharp
23+
using System;
24+
using StructId;
25+
26+
[TStructId]
27+
file partial record struct TSelf(Ulid Value)
28+
{
29+
public static TSelf New() => new(Ulid.NewUlid());
30+
}
31+
32+
// This will be removed when applying the template to each user-defined struct id.
33+
file partial record struct TSelf : INewable<TSelf, Ulid>
34+
{
35+
public static TSelf New(Ulid value) => throw new NotImplementedException();
36+
}
37+
```

readme.md

+108-3
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,20 @@ file partial record struct TSelf(Guid Value) : IId
206206
}
207207
```
208208

209+
Another example of a built-in template that applies to a single type of `TValue` is
210+
the following:
211+
212+
```csharp
213+
using StructId;
214+
215+
[TStructId]
216+
file partial record struct TSelf(string Value)
217+
{
218+
public static implicit operator string(TSelf id) => id.Value;
219+
public static explicit operator TSelf(string value) => new(value);
220+
}
221+
```
222+
209223
This template is a proper C# compilation unit, so you can use any C# feature that
210224
your project supports, since its output will also be emitted via a source generator
211225
in the same project for matching struct ids.
@@ -228,10 +242,101 @@ partial record struct PersonId : IId
228242

229243
Things to note at template expansion time:
230244
1. The `[TStructId]` attribute is removed from the generated type automatically.
231-
1. The `TSelf` type is replaced with the actual name of the struct id.
232-
1. The primary constructor on the template is removed since it is already provided
233-
by anoother generator.
245+
2. The `TSelf` type is replaced with the actual name of the struct id.
246+
3. The primary constructor on the template is removed since it is already provided
247+
by another generator.
248+
249+
You can also constrain the type of `TValue` the template applies to by using using
250+
the special name `TValue` for the primary constructor parameter type, as in the following
251+
example from the implicit conversion template:
252+
253+
```csharp
254+
using StructId;
255+
256+
[TStructId]
257+
file partial record struct TSelf(TValue Value)
258+
{
259+
public static implicit operator TValue(TSelf id) => id.Value;
260+
public static explicit operator TSelf(TValue value) => new(value);
261+
}
262+
263+
file record struct TValue;
264+
```
265+
266+
The `TValue` is subsequently defined as a file-local type where you can
267+
specify whether it's a struct or a class and any interfaces it implements.
268+
These are used to constrain the template expansion to only apply to struct ids,
269+
such as those whose `TValue` is a struct above.
270+
271+
Here's another example from the built-in templates that uses this technique to
272+
apply to all struct ids whose `TValue` implements `IComparable<TValue>`:
273+
274+
```csharp
275+
using System;
276+
using StructId;
277+
278+
[TStructId]
279+
file partial record struct TSelf(TValue Value) : IComparable<TSelf>
280+
{
281+
/// <inheritdoc/>
282+
public int CompareTo(TSelf other) => ((IComparable<TValue>)Value).CompareTo(other.Value);
283+
284+
/// <inheritdoc/>
285+
public static bool operator <(TSelf left, TSelf right) => left.Value.CompareTo(right.Value) < 0;
286+
287+
/// <inheritdoc/>
288+
public static bool operator <=(TSelf left, TSelf right) => left.Value.CompareTo(right.Value) <= 0;
289+
290+
/// <inheritdoc/>
291+
public static bool operator >(TSelf left, TSelf right) => left.Value.CompareTo(right.Value) > 0;
292+
293+
/// <inheritdoc/>
294+
public static bool operator >=(TSelf left, TSelf right) => left.Value.CompareTo(right.Value) >= 0;
295+
}
296+
297+
file record struct TValue : IComparable<TValue>
298+
{
299+
public int CompareTo(TValue other) => throw new NotImplementedException();
300+
}
301+
```
302+
303+
This automatically covers not only all built-in value types, but also any custom
304+
types that implement the interface, making the code generation much more flexible
305+
and powerful.
306+
307+
In addition to constraining on the `TValue` type, you can also constrain on the
308+
the struct id/`TSelf` itself by declaring the inheritance requirements in a partial
309+
class of `TSelf` in the template. For example, the following (built-in) template
310+
ensures it's only applied/expanded for struct ids whose `TValue` is [Ulid](https://github.com/Cysharp/Ulid)
311+
and implement `INewable<TSelf, Ulid>`. Its usefulness in this case is that
312+
the given interface constraint allows us to use the `TSelf.New(Ulid)` static interface
313+
factory method and have it recognized by the C# compiler as valid code as part of the
314+
implementation of the parameterless `New()` factory method:
315+
316+
```csharp
317+
[TStructId]
318+
file partial record struct TSelf(Ulid Value)
319+
{
320+
public static TSelf New() => new(Ulid.NewUlid());
321+
}
322+
323+
// This will be removed when applying the template to each user-defined struct id.
324+
file partial record struct TSelf : INewable<TSelf, Ulid>
325+
{
326+
public static TSelf New(Ulid value) => throw new NotImplementedException();
327+
}
328+
```
234329

330+
> NOTE: the built-in templates will always provide an implementation of
331+
> `INewable<TSelf, TValue>`.
332+
333+
Here you can see that the constraint that the value type must be `Ulid` is enforced by
334+
the `TValue` constructor parameter type, while the interface constraint in the partial
335+
declaration enforces inheritance from `INewable<TSelf, Ulid>`. Since this part of
336+
the partial declaration is removed, there is no need to provide an actual implementation
337+
for the constrain interface(s), just the signature is enough. But the partial declaration
338+
providing the interface constraint is necessary for the C# compiler to recognize the
339+
line with `public static TSelf New() => new(Ulid.NewUlid());` as valid.
235340

236341
<!-- #content -->
237342
<!-- #ci -->

src/StructId.CodeFix/TemplateCodeFix.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
4040

4141
public class FixerAction(Document document, SyntaxNode root, TypeDeclarationSyntax original) : CodeAction
4242
{
43-
public override string Title => "Change to file-local partial record struct";
43+
public override string Title => "Change to file-local partial record struct TSelf";
4444
public override string EquivalenceKey => Title;
4545

4646
protected override Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)

src/StructId.FunctionalTests/Functional.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Diagnostics.CodeAnalysis;
1+
using System.ComponentModel;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Text;
34
using Dapper;
45
using Microsoft.Data.Sqlite;
@@ -10,8 +11,8 @@
1011

1112
namespace StructId.Functional;
1213

13-
// Showcases providing your own ctor
14-
public readonly partial record struct ProductId(Guid Value) : IStructId<Guid>;
14+
// Showcases providing your own ctor for additional annotations or attributes
15+
public readonly partial record struct ProductId([property: Browsable(false)] Guid Value) : IStructId<Guid>;
1516

1617
public readonly partial record struct UserId : IStructId<long>;
1718
// Showcases string-based id

src/StructId/Templates/NewableUlid.cs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ file partial record struct TSelf(Ulid Value)
77
public static TSelf New() => new(Ulid.NewUlid());
88
}
99

10+
// This will be removed when applying the template to each user-defined struct id.
1011
file partial record struct TSelf : INewable<TSelf, Ulid>
1112
{
1213
public static TSelf New(Ulid value) => throw new NotImplementedException();

0 commit comments

Comments
 (0)