Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"rollForward": false
},
"purview.build": {
"version": "0.5.4",
"version": "0.7.1",
"commands": [
"purview-build"
],
Expand Down
11 changes: 7 additions & 4 deletions .config/purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
"Solution": "src/ValueObjects.slnx",
"TestRoot": "src/tests",
"TestPatterns": "*Tests.csproj",
"TestProjects": "*UnitTests.csproj",
"TestFilter": "/*/*/*/*[Category=Unit]"
"TestProjects": "*Tests.csproj",
"TestFilter": "/*/*/*/*"
},
"PackValidation": {
// Explicit because Purview.Build 0.5.3 defaults RequiredCompilerFlags to empty. Earlier
// versions forced this check on and could not be configured off, so these packages have
// always been validated against it; declaring it keeps that behaviour.
"RequiredCompilerFlags": ["optimization=release"],
"RequiredCompilerFlags": [
"optimization=release"
],
"RequireSymbolPackage": true,
"RequireSymbolFiles": true,
"RequiredContent": {
"purview.valueobjects": [
"buildTransitive/Purview.ValueObjects.props",
"buildTransitive/Purview.ValueObjects.targets",
"analyzers/dotnet/cs/Purview.ValueObjects.SourceGenerator.dll",
"analyzers/dotnet/cs/Purview.ValueObjects.SourceGenerator.Refactorings.dll",
Expand All @@ -28,4 +31,4 @@
"Release": {
"Mode": "None"
}
}
}
2 changes: 1 addition & 1 deletion .csharpierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Generated code produced by the repository's own source generator tests
src/tests/SourceGenerator.UnitTests/**/Generated/*
src/tests/SourceGenerator.UnitTests/**/Generated/*
26 changes: 21 additions & 5 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# Purview.BuildSdk 1.1.0 - managed file, do not edit
#
# This file is written by Purview.BuildSdk and is replaced whenever the SDK version changes. The version
# on the first line is how the SDK recognises a file it owns: a .editorconfig without that marker is
# yours, and is never touched.
#
# To add your own rules, create a second .editorconfig in a subdirectory - `src/.editorconfig` is the
# usual choice - without `root = true`. EditorConfig applies the nearest file last, so your settings win
# over these, and nothing you write is lost when this file is updated.
#
# To stop the SDK updating this file, set one of:
# <PurviewEditorConfigAutoUpdate>false</PurviewEditorConfigAutoUpdate> keep this file as it is
# <PurviewRepoBootstrapMode>Never</PurviewRepoBootstrapMode> opt out of all bootstrapping
# <BootstrapEditorConfigToRepoRoot>false</BootstrapEditorConfigToRepoRoot>
#
# Deleting the first line also works: without the marker the SDK treats the file as yours. You then own
# every rule in it, including the ones a future SDK version would have fixed.
#
# See https://github.com/purview-dev/build-sdk#editorconfig for the full description.

root = true

# Base Configuration - General Settings, File Formatting, and Naming Conventions
Expand Down Expand Up @@ -360,12 +380,8 @@ dotnet_style_require_accessibility_modifiers = omit_if_default:warning
# Code-block preferences
csharp_prefer_braces = when_multiline:suggestion
csharp_prefer_simple_using_statement = false:silent
csharp_prefer_system_threading_lock = true
dotnet_style_prefer_collection_expression = true:suggestion

# Enable foreach explicit cast preference
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning

# Prefer System.Threading.Lock
csharp_prefer_system_threading_lock = true:warning

Expand Down Expand Up @@ -497,7 +513,7 @@ dotnet_style_prefer_auto_properties = true:warning
dotnet_style_prefer_compound_assignment = true:warning
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_simplified_boolean_expressions = false:silent
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ on:
push:
branches: [main]

permissions:
contents: write
# The shared release workflow pushes the same packages to nuget.org and, when
# PURVIEW_PUBLISH_GITHUB_PACKAGES=true, to GitHub Packages. A reusable workflow cannot grant
# more than its caller, so the permission is declared here.
packages: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ Released versions correspond to `v<version>` GitHub releases. `package.json` is
newest heading below is the version in development. Earlier releases are listed under
[releases](https://github.com/purview-dev/value-objects/releases).

## Unreleased

### Added

- **`VO1025`: `[StringNormalize]` placement is enforced.** The attribute is only read for a string member of a
complex `[ValueObject]`. On a `[Scalar]` (where `Trim`/`Casing` on the `[Scalar]` attribute is the supported
mechanism), on any type that is neither `[Scalar]` nor `[ValueObject]`, or on a property that is not a value
member, it previously compiled and did nothing; it is now the error `VO1025`, reported at the attribute.
- **MSBuild-configurable assembly defaults.** Every `[ValueObjectDefaults]` option now has a matching
`ValueObjectsDefault<Property>` MSBuild property (for example `ValueObjectsDefaultTrim`,
`ValueObjectsDefaultCasing`, `ValueObjectsDefaultGenerateConstructor`). The packaged targets turn the values
into a build-generated assembly-level `[ValueObjectDefaults]` under `obj/`, and the build props declares each
property (conditionally, so an earlier value is never overwritten) for IDE completion. The generator and
analyzer read the result through the compilation, so a referenced assembly keeps the defaults it was built
with. Precedence is built-in default → MSBuild property → an option named on your own `[ValueObjectDefaults]`
→ per-type or per-member option. Unrecognised values fail the build with the allowed values in the message.
- **Redundant defaults are reported.** An author-declared `[assembly: ValueObjectDefaults]` argument that
repeats a value supplied by a `ValueObjectsDefault*` MSBuild property is the error `VO1026`; a
`[Scalar]`/`[ValueObject]` option that repeats the effective assembly default is the error `VO1027`.
`[StringNormalize]` remains a per-member override and is not part of that check. The rules keep each
option in one place instead of silently accepting two sources of truth. `[ValueObjectDefaults]` still
allows multiple applications so the build-generated attribute and an author-declared attribute can
coexist for the options the author deliberately overrides.

## 1.1.0

### Added
Expand Down
8 changes: 4 additions & 4 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<RoslynVersion>5.9.0</RoslynVersion>
<TUnitVersion>1.75.0</TUnitVersion>
<PurviewSGFVersion>1.1.1</PurviewSGFVersion>
<PurviewZodSharpVersion>2.1.0</PurviewZodSharpVersion>
<TUnitVersion>1.76.0</TUnitVersion>
<PurviewSGFVersion>1.2.0</PurviewSGFVersion>
<PurviewZodSharpVersion>2.3.0</PurviewZodSharpVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Purview.SourceGeneratorFramework" Version="$(PurviewSGFVersion)" />
Expand All @@ -25,7 +25,7 @@
<PackageVersion Include="TUnit.Core" Version="$(TUnitVersion)" />
<PackageVersion Include="TUnit.Mocks" Version="$(TUnitVersion)" />
<PackageVersion Include="TUnit.Assertions" Version="$(TUnitVersion)" />
<PackageVersion Include="Purview.Skills.Testing" Version="1.0.6" />
<PackageVersion Include="Purview.Skills.Testing" Version="1.1.0" />
<PackageVersion Include="Bogus" Version="35.6.5" />
</ItemGroup>
<ItemGroup Label="Entity Framework (test/sample only)">
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,24 @@ the `src/src/ZodSharp.AspNetCoreSample` project (ASP.NET Core Problem Details fo
- Contextual value objects (`IContextualValueObject<TSelf, TValue, TOwner>`) validate against the owning instance
through `ValueObjectContext<TOwner>`.

## Assembly-wide defaults from MSBuild

Every `[ValueObjectDefaults]` option has a matching `ValueObjectsDefault<Property>` MSBuild property, so the
defaults can live once in `Directory.Build.props` instead of an assembly attribute in every project:

```xml
<PropertyGroup>
<ValueObjectsDefaultTrim>true</ValueObjectsDefaultTrim>
<ValueObjectsDefaultCasing>LowerInvariant</ValueObjectsDefaultCasing>
</PropertyGroup>
```

The packaged targets emit the values as a build-generated assembly-level `[ValueObjectDefaults]`, which the
generator and analyzer read like any other attribute. Precedence is: built-in default, then the MSBuild property,
then a named argument on your own `[ValueObjectDefaults]`, then a per-type or per-member option. Unknown values
fail the build. See [Assembly-level defaults](docs/Getting-Started.md#6-assembly-level-defaults) for the full
property list.

## Disabling the generator

Set `DisableValueObjectsSourceGenerator` to `true` in your project:
Expand Down
8 changes: 8 additions & 0 deletions docs/Diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ See [Value object design](Value-Object-Design.md) for the built-in string normal
| --- | --- | --- | --- |
| `VO1023` | Warning | Built-in string normalization is configured but the value object implements `OnNormalize` or declares its own `Create`. | Remove the option, or move the normalization into the hook/`Create`. |
| `VO1024` | Warning | Built-in string normalization is configured on a non-string scalar or member. | Remove the option; it only applies to `string` values. |
| `VO1025` | Error | `[StringNormalize]` is applied to a member of a `[Scalar]`, or of a type that is neither `[Scalar]` nor `[ValueObject]`, or to a property that is not a value member. | Move the attribute to a string member of a complex `[ValueObject]`. On a scalar, set `Trim`/`Casing` on `[Scalar]` instead. |
| `VO1026` | Error | An author-declared `[assembly: ValueObjectDefaults]` sets an option to the value the packaged targets already supplied from a `ValueObjectsDefault*` MSBuild property. | Remove the argument from the assembly attribute, or set a different value. |
| `VO1027` | Error | A `[Scalar]` or `[ValueObject]` sets an option to the effective assembly default. | Remove the option from the attribute; it repeats the inherited value. |

Restating an option that the assembly defaults (or `ValueObjectsDefault*` MSBuild properties) already set is a
no-op. `VO1026`/`VO1027` report that redundancy for assembly, `[Scalar]` and `[ValueObject]` options, so the
configuration has a single source of truth. `[StringNormalize]` is a per-member override and is not included in
that check.

## Entity Framework rules

Expand Down
44 changes: 44 additions & 0 deletions docs/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,50 @@ Use `[ValueObjectDefaults]` to set generic defaults for the whole assembly. Ever
Assembly defaults apply to every value object in the assembly; an option explicitly set on an individual
`[Scalar]`/`[ValueObject]` attribute always overrides it.

### Setting defaults from MSBuild

Every `[ValueObjectDefaults]` option also has a matching `ValueObjectsDefault<Property>` MSBuild property, so a
repository can configure the defaults once in `Directory.Build.props` without adding an attribute to each
project. Unset properties fall through to the attribute and then to the built-in default.

```xml
<PropertyGroup>
<ValueObjectsDefaultTrim>true</ValueObjectsDefaultTrim>
<ValueObjectsDefaultCasing>LowerInvariant</ValueObjectsDefaultCasing>
<ValueObjectsDefaultGenerateJsonConverter>false</ValueObjectsDefaultGenerateJsonConverter>
</PropertyGroup>
```

| Property | Allowed values |
| --- | --- |
| `ValueObjectsDefaultGenerateJsonConverter` | `true` / `false` |
| `ValueObjectsDefaultGenerateComparable` | `true` / `false` |
| `ValueObjectsDefaultGenerateComparisonOperators` | `true` / `false` |
| `ValueObjectsDefaultGenerateEnumProperties` | `true` / `false` |
| `ValueObjectsDefaultGenerateImplicitFromPrimitive` | `true` / `false` |
| `ValueObjectsDefaultGenerateImplicitToPrimitive` | `true` / `false` |
| `ValueObjectsDefaultGenerateEmpty` | `true` / `false` |
| `ValueObjectsDefaultGenerateConstructor` | `true` / `false` |
| `ValueObjectsDefaultEFMapping` | `ComplexType`, `Json`, `None` |
| `ValueObjectsDefaultGenerateEFConverter` | `true` / `false` |
| `ValueObjectsDefaultGenerateEFComparer` | `true` / `false` |
| `ValueObjectsDefaultGenerateEFValueGenerator` | `true` / `false` |
| `ValueObjectsDefaultTrim` | `true` / `false` |
| `ValueObjectsDefaultCasing` | `None`, `LowerInvariant`, `UpperInvariant` |
| `ValueObjectsDefaultDeserializationMode` | `Hydrate`, `Strict` |
| `ValueObjectsDefaultZodSchemaMode` | `InAdditionToHooks`, `InsteadOfHooks` |

The packaged targets turn the properties into a build-generated assembly-level `[ValueObjectDefaults]` under
`obj/`, so the generator and analyzer read them exactly like an attribute you declared yourself. The package
declares the properties in its build props, so IDEs offer them while you edit the project file; the declarations
are conditional and never overwrite a value set earlier. An option named explicitly on your own
`[ValueObjectDefaults]` wins over the MSBuild value, and a per-type or per-member option wins over both. An
unrecognised value fails the build instead of silently falling back to the built-in default.

Because the attribute is compiled into the assembly, the defaults a project generates with are part of its
public contract: a value object declared in a referenced assembly keeps the defaults its own project built it
with.

## 7. Validate with ZodSharp

[Purview.ZodSharp](https://www.nuget.org/packages/Purview.ZodSharp) is a C# port of Zod that can validate
Expand Down
14 changes: 14 additions & 0 deletions docs/Value-Object-Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,27 @@ on any other scalar or member is reported (`VO1024`) and ignored. The option is
`Create`, so a hand-written `OnNormalize` hook or a hand-written `Create` takes precedence: a configured
option alongside either is reported (`VO1023`) and ignored.

Setting an option to the value it already has is a no-op and is reported as redundant: with an assembly
default of `Trim = true`, `[Scalar(Trim = true)]` is reported as `VO1027`, and an author-declared
`[assembly: ValueObjectDefaults(Trim = true)]` that repeats a `ValueObjectsDefaultTrim` value is reported as
`VO1026`. `[StringNormalize(Trim = true)]` is a per-member override and is not part of that check.

`[StringNormalize]` belongs on a string member of a complex `[ValueObject]`. On a `[Scalar]` — where
`Trim`/`Casing` on the `[Scalar]` attribute is the supported mechanism — or on any type that is neither
`[Scalar]` nor `[ValueObject]`, the attribute can never take effect and is reported as the error `VO1025`.

An assembly-level default applies to every string-backed scalar and every string member of a
`[ValueObject]`, and can be overridden per type or per member:

```csharp
[assembly: ValueObjectDefaults(Trim = true, Casing = StringCasing.LowerInvariant)]
```

The same defaults can be supplied from MSBuild with the `ValueObjectsDefault<Property>` properties
(`ValueObjectsDefaultTrim`, `ValueObjectsDefaultCasing`, and one for every other `[ValueObjectDefaults]`
option). An option named explicitly on the attribute wins over the MSBuild value. See
[Getting Started](Getting-Started.md#setting-defaults-from-msbuild) for the full list.

Casing uses the invariant culture only, because a value object's canonical form must not depend on the
current thread culture.

Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"allowPrerelease": true
},
"msbuild-sdks": {
"Purview.BuildSdk": "1.0.6"
"Purview.BuildSdk": "1.1.0"
},
"test": {
"runner": "Microsoft.Testing.Platform"
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "value-objects",
"version": "1.1.0",
"version": "1.2.0",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
Expand All @@ -14,4 +14,4 @@
"type": "git",
"url": "git+https://github.com/purview-dev/value-objects.git"
}
}
}
7 changes: 7 additions & 0 deletions skills/purview-value-objects-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ failure to the missing member.
| --- | --- | --- | --- |
| `VO1023` | Warning | Built-in string normalization is configured but the type implements `OnNormalize` or declares its own `Create`. | Remove the option, or move the normalization into the hook/`Create`. The hand-written path wins either way. |
| `VO1024` | Warning | Built-in string normalization is configured on a non-string scalar or member. | Remove the option; `Trim`/`Casing` apply only to `string`. |
| `VO1025` | Error | `[StringNormalize]` is on a member of a `[Scalar]`, on a type that is neither `[Scalar]` nor `[ValueObject]`, or on a property that is not a value member. | Move it to a string member of a complex `[ValueObject]`; on a scalar set `Trim`/`Casing` on `[Scalar]`. |
| `VO1026` | Error | An author-declared `[assembly: ValueObjectDefaults]` repeats a value already supplied by the packaged targets from `ValueObjectsDefault*` MSBuild properties. | Remove the argument, or set a different value. |
| `VO1027` | Error | A `[Scalar]`/`[ValueObject]` option repeats the effective assembly default. | Remove the option; it repeats the inherited value. |

Restating an assembly or type default is reported as `VO1026`/`VO1027` so each option has a single source of
truth. Only `[StringNormalize]` remains a per-member override outside that check; a value that changes the
effective configuration still raises `VO1023`/`VO1024` where those rules apply.

## ZodSharp validation rules

Expand Down
4 changes: 3 additions & 1 deletion skills/purview-value-objects-efcore/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ EF dependency, so a domain assembly stays free of Entity Framework while the per
gets the converters, comparers and conventions.

Everything is opt-in per feature with a three-level opt-out: the `DisableValueObjectsEFGeneration`
MSBuild property, then assembly defaults, then per-type options.
MSBuild property, then assembly defaults (the attribute, or the `ValueObjectsDefaultEFMapping`,
`ValueObjectsDefaultGenerateEFConverter`, `ValueObjectsDefaultGenerateEFComparer` and
`ValueObjectsDefaultGenerateEFValueGenerator` properties), then per-type options.

## Wiring it up

Expand Down
5 changes: 4 additions & 1 deletion skills/purview-value-objects/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ public readonly partial record struct Contact
Trim runs first, then casing, then validation — and only inside the generated `Create`. Casing is
invariant-culture only, deliberately: a canonical form that depends on the ambient thread culture is
not canonical. A hand-written `OnNormalize` or `Create` wins and the option is ignored (`VO1023`);
the option on a non-string is ignored (`VO1024`).
the option on a non-string is ignored (`VO1024`); restating a value the assembly default already sets
is reported as redundant (`VO1026`/`VO1027`). `[StringNormalize]` itself only belongs on a complex value
object member — on a `[Scalar]` or any other type it is the error `VO1025`, and MSBuild can supply the
assembly defaults with `ValueObjectsDefaultTrim` / `ValueObjectsDefaultCasing`.

## Complex value objects

Expand Down
8 changes: 8 additions & 0 deletions src/src/SourceGenerator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@
;
; Every rule is recorded in AnalyzerReleases.Shipped.md. Add a new or changed diagnostic here, and move
; it into a new `## Release <version>` block when that version ships.

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
VO1025 | ValueObjects | Error | [StringNormalize] is only supported on a string member of a complex [ValueObject]; on a [Scalar] or any other type it has no effect
VO1026 | ValueObjects | Error | [ValueObjectDefaults] repeats a default already supplied by the project's MSBuild configuration
VO1027 | ValueObjects | Error | A [Scalar]/[ValueObject] option repeats the effective assembly default
Loading
Loading