Meziantou.Framework.TaggedValues
1.0.1
Prefix Reserved
dotnet add package Meziantou.Framework.TaggedValues --version 1.0.1
NuGet\Install-Package Meziantou.Framework.TaggedValues -Version 1.0.1
<PackageReference Include="Meziantou.Framework.TaggedValues" Version="1.0.1" />
<PackageVersion Include="Meziantou.Framework.TaggedValues" Version="1.0.1" />
<PackageReference Include="Meziantou.Framework.TaggedValues" />
paket add Meziantou.Framework.TaggedValues --version 1.0.1
#r "nuget: Meziantou.Framework.TaggedValues, 1.0.1"
#:package Meziantou.Framework.TaggedValues@1.0.1
#addin nuget:?package=Meziantou.Framework.TaggedValues&version=1.0.1
#tool nuget:?package=Meziantou.Framework.TaggedValues&version=1.0.1
Meziantou.Framework.TaggedValues
Tag primitive values with [ValueTag], and the bundled Roslyn analyzer reports when values with different tags are mixed. The value stays a Guid, an int or a string, so there is nothing to change in serializers, ORMs, or APIs.
using Meziantou.Framework.TaggedValues;
class Order
{
[ValueTag("OrderId")] public Guid Id { get; set; }
[ValueTag("ProjectId")] public Guid ProjectId { get; set; }
}
class OrderService
{
public Order Load([ValueTag("OrderId")] Guid orderId) => ...;
public void M(Order order)
{
_ = order.Id == order.ProjectId; // MFTV0001: values with different tags are compared
Load(order.ProjectId); // MFTV0002: a ProjectId flows to a parameter that expects an OrderId
}
}
Values that are not tagged are never reported, so you can adopt the package one declaration at a time.
Tagging values
- Fields, properties, and parameters:
[ValueTag("OrderId")] - Return values:
[return: ValueTag("OrderId")]. This also works for async methods and iterators, where the tag describes the result or the elements. - Local variables cannot have attributes, so use a comment:
Guid /* ValueTag=OrderId */ id = ...;. See Tagging local variables. - Unions:
[ValueTag("OrderId", "ProjectId")]accepts both. Two values are compatible when they share at least one tag. - Casing does not matter: tags are compared ignoring case, so
"OrderId"and"orderId"are the same tag. - Members of types you do not own:
[assembly: ValueTag(typeof(Process), nameof(Process.Id), "ProcessId")]. The tags also apply when the member is accessed through a derived type. Assembly attributes are read from referenced assemblies too, so a shared project can declare them once.
Tags are inherited from overridden members, from implemented interface members, and from the parameters of a record primary constructor. An explicit cast through object drops the tag: (Guid)(object)value.
Tagging local variables
C# does not allow attributes on local variables, so a local is tagged with a comment, either /* ... */ or // .... The comment accepts three forms:
| Comment | Equivalent attribute |
|---|---|
/* ValueTag=OrderId */ or // ValueTag=OrderId |
[ValueTag("OrderId")] |
/* ValueTag=OrderId, ProjectId */ or // ValueTag=OrderId, ProjectId |
[ValueTag("OrderId", "ProjectId")] |
/* ValueTag Key=OrderId Value=ProjectId */ or // ValueTag Key=OrderId Value=ProjectId |
[ValueTag(Key = "OrderId", Value = "ProjectId")] |
Spaces around = and , are allowed. A tag cannot contain spaces, =, or ,, so a // comment cannot continue with other text after the tags. In the dictionary form, Key and Value can be separated by a space or a comma, either one can be omitted, and each accepts a single tag.
A /* ... */ comment must be part of the declaration: before the type, after the type, or next to the name. A comment before or after the type tags every variable of the declaration, and a comment next to a name tags only that variable:
/* ValueTag=OrderId */ Guid id1 = ...;
Guid /* ValueTag=OrderId */ id2 = ...;
var id3 /* ValueTag=OrderId */ = ...;
using var /* ValueTag=OrderId */ id4 = ...;
Guid /* ValueTag=OrderId */ a = ..., b = ...; // a and b are OrderIds
Guid c = ..., d /* ValueTag=ProjectId */ = ...; // only d is a ProjectId
var /* ValueTag Key=OrderId Value=ProjectId */ projectIdByOrderId = new Dictionary<Guid, Guid>();
A // comment must be on a line before the statement, and tags every variable the statement declares. Other comments can sit between the // ValueTag comment and the statement, but a comment at the end of the previous line does not count. When a variable also has a /* ... */ comment, that comment wins:
// ValueTag=OrderId
Guid id1 = ...;
// ValueTag=OrderId
// The order being processed
Guid a = ..., b /* ValueTag=ProjectId */ = ...; // a is an OrderId, b is a ProjectId
// ValueTag Key=OrderId Value=ProjectId
var projectIdByOrderId = new Dictionary<Guid, Guid>();
It also works for the variables of foreach, for, out var, and patterns. A // comment works for foreach and for, but not for out var and patterns, whose statement is not a declaration:
// ValueTag=OrderId
foreach (var id in ids) { }
foreach (Guid /* ValueTag=OrderId */ id in ids) { }
for (int /* ValueTag=OrderIndex */ i = 0; i < count; i++) { }
if (TryGetId(out var /* ValueTag=OrderId */ id)) { }
if (value is Guid /* ValueTag=OrderId */ id) { }
A local without a comment takes the tag of its initializer, of the collection of a foreach, of the parameter of an out var, or of the value matched by a pattern. In a property pattern, a list pattern, or a deconstruction, it takes the tag of the part it matches: order is { Id: var id } is an OrderId, and so is id in foreach (var (id, projectId) in projectIdByOrderId) or in var (id, projectId) = (order.Id, order.ProjectId):
var id = order.Id; // id is an OrderId
Load(id); // ok
id = order.ProjectId; // MFTV0002
When the comment disagrees with the initializer, the collection of a foreach, the parameter of an out var, or the value matched by a pattern, the comment wins and the value is reported (MFTV0002). The code fix changes the tag in the comment.
Tagging type arguments
A /* ValueTag=... */ comment can also be written in a type argument, to tag the elements of a collection or the keys and the values of a dictionary without naming them:
var projectIdByOrderId = new Dictionary</* ValueTag=OrderId */ Guid, /* ValueTag=ProjectId */ Guid>();
var orderIds = new List</* ValueTag=OrderId */ Guid>();
Dictionary</* ValueTag=OrderId */ Guid, /* ValueTag=ProjectId */ Guid> map = GetMap();
- The comment can be placed before or after the type argument:
</* ValueTag=OrderId */ Guid, ...>and<Guid /* ValueTag=OrderId */, ...>are equivalent. - The type argument of a collection, a
Nullable<T>, aTask<T>, aValueTask<T>, or aLazy<T>tags its elements or its value. The type arguments of a dictionary or aKeyValuePair<TKey, TValue>tag its keys and its values. Type arguments can be nested:new Dictionary</* ValueTag=OrderId */ Guid, List</* ValueTag=ProjectId */ Guid>>(). - Only the single-tag and union forms are accepted, as the position of the argument already selects the key or the value.
/* ValueTag Key=... */is reported (MFTV0005). - The comment applies to the type of a
newexpression, and to the type of a local variable, includingforeach,for,using,outand pattern variables. The tag of anewexpression flows to avarlocal like any initializer. - The elements of a collection initializer and the arguments of the constructor are checked:
new List</* ValueTag=OrderId */ Guid> { projectId }is reported (MFTV0002). - A comment on the variable itself wins over the comments in the type arguments of its declared type.
Comments that do not start with ValueTag, prose that starts with ValueTag but contains several words and no =, such as // ValueTag is not used here, and /// documentation comments, are ignored. A ValueTag comment that cannot be parsed, or that does not tag a local variable, is reported (MFTV0005) and does not tag anything. This includes a comment on a field, inside an initializer, at the end of a line, before a statement that declares no variable, in the type arguments of a field, a parameter, or a generic method, and in the type arguments of a type that is neither a collection, a dictionary, nor a wrapper, such as Tuple<Guid, Guid>. The code fix removes it.
Collections, dictionaries, and wrappers
On a collection, a Nullable<T>, a Task<T>, a ValueTask<T>, a Lazy<T>, a span, or a memory, the tag describes the element or the value. It flows through indexers, foreach, LINQ, lambda parameters (including IQueryable expression trees), and any generic method whose signature preserves the element type:
[ValueTag("OrderId")] public List<Guid> OrderIds { get; }
OrderIds.Add(order.ProjectId); // MFTV0002
OrderIds.Where(id => id == order.ProjectId); // MFTV0001
Load(OrderIds.Where(id => id != Guid.Empty).First()); // ok
Use Key and Value for dictionaries:
[ValueTag(Key = "OrderId", Value = "ProjectId")]
public Dictionary<Guid, Guid> ProjectIdByOrderId { get; }
Anonymous type properties take the tag of their initializer, so projections keep their tags. A collection initializer takes the tags of the elements it adds: new List<Guid> { order.Id } holds OrderIds, and new Dictionary<Guid, Guid> { [order.Id] = order.ProjectId } maps OrderIds to ProjectIds.
Arithmetic and operators
Built-in arithmetic on numbers keeps the tag, so orderId + 1 == projectId is still reported:
+and-keep the tag of their operands. Adding values with different tags is reported (MFTV0003):orderId + projectId.*and/keep the tag when the other operand is not tagged:price * 2is a price, butmeters / secondsis not tagged.- Unary
+and-,++,--,+=, and-=keep the tag. %, bitwise operators, shifts, and string concatenations create another kind of value, so the result is not tagged.
A user-defined operator or conversion creates a new value, so it does not keep the tag of its operands: dueDate - startDate is a TimeSpan, not a due date. Tag the return value of the operator to tag its result, and its parameters to check its operands:
[return: ValueTag("Meters")]
public static Distance operator +([ValueTag("Meters")] Distance left, [ValueTag("Meters")] Distance right) => ...;
Naming conventions (opt-in)
When enabled, the analyzer infers tags from names, so most ids need no attribute:
[*.cs]
taggedvalues.infer_tags_from_names = true
- A property or a field named
Idtakes the name of its declaring type:IdonOrderis an"OrderId". When the member is inherited, it is also an id of every type between the receiver and the declaring type. - A property, a field, or a parameter named
xxxIdtakes its name:projectIdis a"projectId", which is the same tag as"ProjectId"as tags are compared ignoring case. Thes_prefix and leading underscores of fields are ignored:s_projectIdand_projectIdare both"projectId". - A parameter named
idtakes the name of its type:void Load(Order id)is an"OrderId", andvoid Load(OrderId id)is an"OrderId"too. A parameter namedidwhose type is a primitive, astring, aGuid, or an enum is not tagged, as its type does not say what it identifies. - Explicit tags always win. Collections and indexers are not tagged by convention.
Strict mode (opt-in)
By default, untagged values are never reported. Strict mode also reports a tagged value mixed with an untagged value (MFTV0009), so every value that interacts with a tagged value must be tagged too:
[*.cs]
taggedvalues.strict = true
[ValueTag("OrderId")] public Guid Id { get; set; } = Guid.NewGuid(); // ok, a new value
void Load(Guid id) { }
Guid _other;
_ = order.Id == _other; // MFTV0009, compared with an untagged value
order.Id = _other; // MFTV0009, an untagged value flows to a tagged declaration
Load(order.Id); // MFTV0009, a tagged value flows to an untagged declaration
_ = order.Id == Guid.Empty; // ok
- Default values,
null, constants, andGuid.Emptyare always allowed. - New values, such as
Guid.NewGuid(),new Guid(bytes), orGuid.Parse(text), can flow to a tagged declaration. Values read from an untagged field, property, parameter, local, or array element, or returned by an untagged method of your code, cannot. A?:,??, or switch expression is a new value only when each of its branches is a new value or an allowed value. - A tagged value can flow to a declaration of a referenced assembly, or typed
object,dynamic, or a type parameter, as they cannot be tagged. A local initialized with a tagged value takes its tag, so it is not reported. - The code fix adds the tag to the untagged declaration.
What is reported
- Comparisons:
==,!=,<,<=,>,>=, tuple equality,Equals,CompareTo,EqualityComparer<T>.Equals,Comparer<T>.Compare,string.Equals(a, b, comparison) - Flows: assignments, object initializers,
withexpressions, field and property initializers, arguments (includingref,out, and generic arguments that must share a type),returnandyield return - Combined values: operands of
+and-, branches of?:,??, and switch expressions, and the elements of arrays, collection expressions, and collection initializers - Overrides and interface implementations whose tags differ from the base member
- In strict mode, tagged values mixed with untagged values
- Suggestions to tag a return value: when every value returned by a method, a local function, or a property getter has the same tag, but the return value is not tagged, so the callers lose the tag (only for tags written by the user, not inferred from a naming convention)
- Invalid annotations: empty tags, malformed comments, comments on something other than a local variable,
KeyandValueon something other than a dictionary,[field: ValueTag]on a property, whose backing field is not analyzed, and assembly attributes that name a missing member
Every message names both declarations and their tags, so a build log is enough to act on. Code fixes change the tag of the target of a flow or of an override, add the suggested tag to a return value, and remove invalid or redundant annotations.
Analyzer rules
| Id | Category | Description | Severity | Enabled |
|---|---|---|---|---|
MFTV0001 |
TaggedValues | Do not compare values with different tags | Warning | ✔️ |
MFTV0002 |
TaggedValues | Use a value with the tag the target expects | Warning | ✔️ |
MFTV0003 |
TaggedValues | Combine only values with the same tag | Warning | ✔️ |
MFTV0004 |
TaggedValues | Use the tags of the overridden or implemented member | Warning | ✔️ |
MFTV0005 |
TaggedValues | Fix or remove the invalid value tag annotation | Warning | ✔️ |
MFTV0006 |
TaggedValues | Add an explicit tag to disambiguate the conventional tag | Warning | ✔️ |
MFTV0007 |
TaggedValues | Remove the redundant value tag | Info | ✔️ |
MFTV0008 |
TaggedValues | Tag the return value with the tag of the returned values | Info | ✔️ |
MFTV0009 |
TaggedValues | Do not mix tagged values with untagged values | Warning | ✔️ |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net10.0
- No dependencies.
-
net11.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.