{"id":13629526,"url":"https://github.com/martinothamar/WrapperValueObject","last_synced_at":"2025-04-17T09:34:37.232Z","repository":{"id":54427890,"uuid":"287714637","full_name":"martinothamar/WrapperValueObject","owner":"martinothamar","description":"A .NET source generator for creating simple value objects wrapping primitive types.","archived":false,"fork":false,"pushed_at":"2022-11-17T09:04:06.000Z","size":53,"stargazers_count":52,"open_issues_count":1,"forks_count":3,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-02T22:07:33.812Z","etag":null,"topics":["csharp","csharp-sourcegenerator","dotnet","dotnet-core","dotnet-standard","dotnetcore","source-gen","source-generation","source-generators","sourcegenerator","value-object"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/martinothamar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-08-15T09:15:32.000Z","updated_at":"2024-08-05T18:01:51.000Z","dependencies_parsed_at":"2023-01-23T09:30:07.765Z","dependency_job_id":null,"html_url":"https://github.com/martinothamar/WrapperValueObject","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martinothamar%2FWrapperValueObject","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martinothamar%2FWrapperValueObject/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martinothamar%2FWrapperValueObject/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martinothamar%2FWrapperValueObject/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/martinothamar","download_url":"https://codeload.github.com/martinothamar/WrapperValueObject/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223674438,"owners_count":17183880,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","csharp-sourcegenerator","dotnet","dotnet-core","dotnet-standard","dotnetcore","source-gen","source-generation","source-generators","sourcegenerator","value-object"],"created_at":"2024-08-01T22:01:12.869Z","updated_at":"2024-11-08T20:31:10.607Z","avatar_url":"https://github.com/martinothamar.png","language":"C#","readme":"# WrapperValueObject\n\n![Build](https://github.com/martinothamar/WrapperValueObject/workflows/Build/badge.svg)\n[![NuGet](https://img.shields.io/nuget/v/WrapperValueObject.Generator.svg)](https://www.nuget.org/packages/WrapperValueObject.Generator/)\n\n\u003e **Note**\n\n\u003e This library is not actively maintained at the moment, I recommend looking at [SteveDunn/Vogen](https://github.com/SteveDunn/Vogen)\n\nA .NET source generator for creating\n* Simple value objects wrapping other type(s), without the hassle of manual `Equals`/`GetHashCode`\n* Value objects wrapping math primitives and other types\n  * I.e. `[WrapperValueObject(typeof(int))] readonly partial struct MeterLength { }` - the type is implicitly castable to `int`\n  * Math and comparison operator overloads are automatically generated\n  * `ToString` is generated with formatting options similar to those on the primitive type, i.e. `ToString(string? format, IFormatProvider? provider)` for math types\n* Strongly typed ID's\n  * Similar to F# `type ProductId = ProductId of Guid`, here it becomes `[WrapperValueObject] readonly partial struct ProductId { }` with a `New()` function similar to `Guid.NewGuid()`\n\nThe generator targets .NET Standard 2.0 and has been tested with `netcoreapp3.1` and `net5.0` target frameworks.\n\nNote that record type feature for structs is planned for C# 10, at which point this library might be obsolete.\n\n## Installation\n\nAdd to your project file:\n\n```xml\n\u003cPackageReference Include=\"WrapperValueObject.Generator\" Version=\"0.0.1\"\u003e\n  \u003cPrivateAssets\u003eall\u003c/PrivateAssets\u003e\n  \u003cIncludeAssets\u003eruntime; build; native; contentfiles; analyzers\u003c/IncludeAssets\u003e\n\u003c/PackageReference\u003e\n```\n\nOr install via CLI\n\n```sh\ndotnet add package WrapperValueObject.Generator --version 0.0.1\n```\n\nThis package is a build time dependency only.\n\n## Usage\n\n1. Use the attribute to specify the underlying type.\n2. Declare the struct or class with the `partial` keyword.\n\n### Strongly typed ID\n\n\n```csharp\n[WrapperValueObject] readonly partial struct ProductId { }\n\nvar id = ProductId.New(); // Strongly typed Guid wrapper, i.e. {1658db8c-89a4-46ea-b97e-8cf966cfb3f1}\n\nAssert.NotEqual(ProductId.New(), id);\nAssert.False(ProductId.New() == id);\n```\n\n### Money type\n\n```csharp\n[WrapperValueObject(typeof(decimal))] readonly partial struct Money { }\n\nMoney money = 2m;\n\nvar result = money + 2m; // 4.0\nvar result2 = money + new Money(2m);\n\nAssert.True(result == result2);\nAssert.Equal(4m, (decimal)result);\n```\n\n\n### Metric types\n```csharp\n[WrapperValueObject(typeof(int))]\npublic readonly partial struct MeterLength \n{\n    public static implicit operator CentimeterLength(MeterLength meter) =\u003e meter.Value * 100; // .Value is the inner type, in this case int\n}\n\n[WrapperValueObject(typeof(int))]\npublic readonly partial struct CentimeterLength\n{\n    public static implicit operator MeterLength(CentimeterLength centiMeter) =\u003e centiMeter.Value / 100;\n}\n\nMeterLength meters = 2;\n\nCentimeterLength centiMeters = meters; // 200\n\nAssert.Equal(200, (int)centiMeters);\n```\n\n### Complex types\n\n```csharp\n[WrapperValueObject] // Is Guid ID by default\nreadonly partial struct MatchId { }\n\n[WrapperValueObject(\"HomeGoals\", typeof(byte), \"AwayGoals\", typeof(byte))]\nreadonly partial struct MatchResult { }\n\npartial struct Match\n{\n    public readonly MatchId MatchId { get; }\n\n    public MatchResult Result { get; private set; }\n\n    public void SetResult(MatchResult result) =\u003e Result = result;\n\n    public Match(in MatchId matchId)\n    {\n        MatchId = matchId;\n        Result = default;\n    }\n}\n\nvar match = new Match(MatchId.New());\n\nmatch.SetResult((1, 2)); // Complex types use value tuples underneath, so can be implicitly converted\nmatch.SetResult(new MatchResult(1, 2)); // Or the full constructor\n\nvar otherResult = new MatchResult(2, 1);\n\nDebug.Assert(otherResult != match.Result);\n\nmatch.SetResult((2, 1));\nDebug.Assert(otherResult == match.Result);\n\nDebug.Assert(match.MatchId != default);\nDebug.Assert(match.Result != default);\nDebug.Assert(match.Result.HomeGoals == 2);\nDebug.Assert(match.Result.AwayGoals == 1);\n```\n\n### Validation \n\nTo make sure only valid instances are created.\nThe validate function will be called in the generated constructors.\n\n```csharp\n[WrapperValueObject] // Is Guid ID by default\nreadonly partial struct MatchId\n{ \n    static partial void Validate(Guid id)\n    {\n        if (id == Guid.Empty)\n            throw new ArgumentOutOfRangeException(nameof(id), $\"{nameof(id)} must have value\");\n    }\n}\n\n[WrapperValueObject(\"HomeGoals\", typeof(byte), \"AwayGoals\", typeof(byte))]\nreadonly partial struct MatchResult \n{ \n    static partial void Validate(byte homeGoals, byte awayGoals)\n    {\n        if (homeGoals \u003c 0)\n            throw new ArgumentOutOfRangeException(nameof(homeGoals), $\"{nameof(homeGoals)} value cannot be less than 0\");\n        if (awayGoals \u003c 0)\n            throw new ArgumentOutOfRangeException(nameof(awayGoals), $\"{nameof(awayGoals)} value cannot be less than 0\");\n    }\n}\n```\n\n## Limitations\n\n* Need .NET 5 SDK (I think) due to source generators\n* Does not support nested types\n* Limited configuration options in terms of what code is generated\n\n## Related projects and inspiration\n\n* [StronglyTypedId](https://github.com/andrewlock/StronglyTypedId) by @andrewlock\n\n## TODO/under consideration\n\nFurther development on this PoC was prompted by this discussion: https://github.com/ironcev/awesome-roslyn/issues/17\n\n* Replace one generic attribute (WrapperValueObject) with two (or more) that cleary identify the usecase. E.g. StronglyTypedIdAttribute, ImmutableStructAttribute, ...\n* Support everything that StronglyTypedId supports (e.g. optional generation of JSON converters).\n* Bring the documentation to the same level as in the StronglyTypedId project.\n* Write tests.\n* Create Nuget package.\n","funding_links":[],"categories":["Contributors Welcome for those","Source Generators","Architectural Patterns"],"sub_categories":["1. [ThisAssembly](https://ignatandrei.github.io/RSCG_Examples/v2/docs/ThisAssembly) , in the [EnhancementProject](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#enhancementproject) category","Functional Programming","Domain Driven Design - Domain Centric"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartinothamar%2FWrapperValueObject","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmartinothamar%2FWrapperValueObject","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartinothamar%2FWrapperValueObject/lists"}