Skip to content

Commit 0d15089

Browse files
committed
Drastically simply packing and namespace management
The ability to customize the namespace of the struct id types seemed a bit of a corner case and it was introducing non-trivial complexity in scenarios involving transitive project references (analyzers are transitive, as well as buildTransitive targets, but analyzer options, contentFiles -with or without compile action- are not). This was leading to hack over hack for no serious gain. So even if we keep the CodeTemplate behavior intact should we figure out how to properly handle changing the namespace, this change removes the copying and content updating on the static files, which are now simply included via nuget's contentFiles feature plus buildAction=Compile. This provides the behavior we're looking for: - Interfaces and types required for struct ids are only added to the project referencing the package - using and codegen for struct ids is supported in projects referencing the "core" one: analyzers are already transitive, we just make the templates transitive too via targets (since we can't via contentFiles).
1 parent 513689a commit 0d15089

11 files changed

+52
-76
lines changed

src/StructId.Analyzer/CodeTemplate.cs

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public static SyntaxNode Apply(this SyntaxNode node, INamedTypeSymbol structId)
5353

5454
var tid = iface.TypeArguments.FirstOrDefault()?.ToFullName() ?? "string";
5555
var corens = iface.ContainingNamespace.ToFullName();
56+
if (string.IsNullOrEmpty(corens))
57+
corens = nameof(StructId);
58+
5659
var targetNamespace = structId.ContainingNamespace != null && !structId.ContainingNamespace.IsGlobalNamespace ?
5760
structId.ContainingNamespace.ToDisplayString() : null;
5861

src/StructId.Analyzer/KnownTypes.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ public record KnownTypes(Compilation Compilation)
2020
/// StructId.IStructId
2121
/// </summary>
2222
public INamedTypeSymbol? IStructId { get; } = Compilation
23-
.GetAllTypes(true)
23+
.GetAllTypes(includeReferenced: true)
2424
.FirstOrDefault(x => x.MetadataName == "IStructId" && x.IsGeneratedByStructId());
2525

2626
/// <summary>
2727
/// StructId.IStructId{T}
2828
/// </summary>
2929
public INamedTypeSymbol? IStructIdT { get; } = Compilation
30-
.GetAllTypes(true)
30+
.GetAllTypes(includeReferenced: true)
3131
.FirstOrDefault(x => x.MetadataName == "IStructId`1" && x.IsGeneratedByStructId());
3232
}

src/StructId.FunctionalTests/Functional.cs

+13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ public record User(UserId Id, string Name, Wallet Wallet);
2424

2525
public class FunctionalTests(ITestOutputHelper output)
2626
{
27+
[Fact]
28+
public void TypeConverters()
29+
{
30+
var id = ProductId.New();
31+
var converter = TypeDescriptor.GetConverter(id);
32+
33+
Assert.True(converter.CanConvertTo(typeof(string)));
34+
Assert.True(converter.CanConvertFrom(typeof(string)));
35+
36+
var id2 = (ProductId?)converter.ConvertFromString(converter.ConvertToString(id)!);
37+
Assert.Equal(id, id2);
38+
}
39+
2740
[Fact]
2841
public void EqualityTest()
2942
{

src/StructId.FunctionalTests/NoNs.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using StructId.Functional;
1+
using StructId;
2+
using StructId.Functional;
23

34
// Showcases that types don't need to have a namespace
45
public partial record struct NoNsId : IStructId;

src/StructId.FunctionalTests/StructId.FunctionalTests.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2-
<Import Project="..\StructId.Package\StructId.props" />
32

43
<PropertyGroup>
54
<TargetFramework>net8.0</TargetFramework>

src/StructId.FunctionalTests/UlidEntityFramework.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#nullable enable
33

44
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
5-
using StructId.Functional;
5+
using StructId;
66

77
[TStructId]
88
file partial record struct TSelf(Ulid Value) : INewable<TSelf, Ulid>

src/StructId.FunctionalTests/UlidTests.cs

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Concurrent;
2+
using System.ComponentModel;
23
using System.Data;
34
using Dapper;
45
using Microsoft.Data.Sqlite;
@@ -150,6 +151,19 @@ public void EntityFramework()
150151
Assert.True(UlidToStringConverter.Instantiated, "Custom EF converter for Ulid was not used.");
151152
}
152153

154+
[Fact]
155+
public void TypeConverters()
156+
{
157+
var id = UlidId.New();
158+
var converter = TypeDescriptor.GetConverter(id);
159+
160+
Assert.True(converter.CanConvertTo(typeof(string)));
161+
Assert.True(converter.CanConvertFrom(typeof(string)));
162+
163+
var id2 = (UlidId?)converter.ConvertFromString(converter.ConvertToString(id)!);
164+
Assert.Equal(id, id2);
165+
}
166+
153167
public class UlidContext : DbContext
154168
{
155169
public UlidContext(DbContextOptions<UlidContext> options) : base(options) { }

src/StructId.Package/StructId.Package.msbuildproj

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<DevelopmentDependency>true</DevelopmentDependency>
66
<Description>Stronly typed ids using readonly record structs and modern C# features.</Description>
77
<PackageTags>dotnet record struct typed id</PackageTags>
8-
<PackFolder>build</PackFolder>
8+
<PackFolder>buildTransitive</PackFolder>
9+
<PackNone>true</PackNone>
910
</PropertyGroup>
1011
<ItemGroup>
1112
<PackageReference Include="NuGetizer" Version="1.2.3" />
@@ -15,7 +16,7 @@
1516
<ProjectReference Include="..\StructId.CodeFix\StructId.CodeFix.csproj" />
1617
</ItemGroup>
1718
<ItemGroup>
18-
<None Include="..\StructId\*.cs" Visible="false" />
19+
<Content Include="..\StructId\*.cs" BuildAction="Compile" CodeLanguage="cs" Pack="true" />
1920
<None Include="..\StructId\Templates\*.cs" PackFolder="$(PackFolder)\Templates" Visible="false" />
2021
<None Update="@(None)" CopyToOutputDirectory="PreserveNewest" />
2122
</ItemGroup>

src/StructId.Package/StructId.props

-3
This file was deleted.

src/StructId.Package/StructId.targets

+14-64
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,21 @@
11
<Project>
22

3-
<PropertyGroup>
4-
<StructIdNamespace Condition="$(StructIdNamespace) == ''">$(RootNamespace)</StructIdNamespace>
5-
<StructIdNamespace Condition="$(StructIdNamespace) == ''">StructId</StructIdNamespace>
6-
<StructIdPathHash Condition="$(StructIdNamespace) != ''">$([MSBuild]::StableStringHash($(StructIdNamespace)))\</StructIdPathHash>
7-
</PropertyGroup>
8-
93
<ItemGroup>
10-
<StructId Include="$(MSBuildThisFileDirectory)*.cs" Link="StructId\$(StructIdPathHash)%(Filename)%(Extension)" />
11-
<StructId Include="$(MSBuildThisFileDirectory)Templates\*.cs" Link="StructId\$(StructIdPathHash)Templates\%(Filename)%(Extension)" />
4+
<!-- This is safe to do even if we're in a transitive targets file, since content files providing
5+
these items won't exist in a transitive project. See https://github.com/NuGet/Home/issues/7420 -->
6+
<Compile Update="@(Compile)">
7+
<Visible Condition="'%(NuGetItemType)' == 'Compile' and '%(NuGetPackageId)' == 'StructId'">false</Visible>
8+
<Link Condition="'%(NuGetItemType)' == 'Compile' and '%(NuGetPackageId)' == 'StructId'">StructId\%(Filename)%(Extension)</Link>
9+
</Compile>
10+
11+
<!-- Templates should always be added transitively since they are all file-local and their
12+
syntax tree is required in the compilation for them to apply properly -->
13+
<StructId Include="$(MSBuildThisFileDirectory)Templates\*.cs"
14+
Link="StructId\Templates\%(Filename)%(Extension)"
15+
Visible="false"/>
1216
</ItemGroup>
1317

14-
<Target Name="CollectStructIds">
15-
<ItemGroup>
16-
<StructId>
17-
<TargetPath>$(IntermediateOutputPath)%(Link)</TargetPath>
18-
</StructId>
19-
</ItemGroup>
20-
</Target>
21-
22-
<Target Name="IncludeStructIdAsIs" Condition="$(StructIdNamespace) == 'StructId'" DependsOnTargets="CollectStructIds" Inputs="@(StructId)" Outputs="%(StructId.TargetPath)">
23-
<!-- No copying needed in this case, we'll just include the original files. -->
24-
<ItemGroup>
25-
<StructId>
26-
<TargetPath>%(FullPath)</TargetPath>
27-
</StructId>
28-
</ItemGroup>
29-
</Target>
30-
31-
<Target Name="AddStructIdContent" Condition="$(StructIdNamespace) != 'StructId'" DependsOnTargets="CollectStructIds" Inputs="@(StructId)" Outputs="|%(StructId.Identity)|">
32-
<PropertyGroup>
33-
<StructIdContent>$([System.IO.File]::ReadAllText(%(StructId.FullPath)))</StructIdContent>
34-
<StructIdContent>$(StructIdContent.Replace('using StructId', 'using $(StructIdNamespace)').Replace('namespace StructId', 'namespace $(StructIdNamespace)'))</StructIdContent>
35-
</PropertyGroup>
36-
<ItemGroup>
37-
<StructId>
38-
<Content>$([MSBuild]::Unescape($(StructIdContent)))</Content>
39-
</StructId>
40-
</ItemGroup>
41-
</Target>
42-
43-
<Target Name="CopyStructIdNamespaced" Condition="$(StructIdNamespace) != 'StructId'" DependsOnTargets="AddStructIdContent" Inputs="@(StructId)" Outputs="%(StructId.TargetPath)">
44-
<WriteRaw Content="%(StructId.Content)" SourcePath="%(StructId.FullPath)" TargetPath="%(StructId.TargetPath)" />
45-
</Target>
46-
47-
<Target Name="AddStructId" DependsOnTargets="IncludeStructIdAsIs;CopyStructIdNamespaced;ResolveLockFileReferences" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun">
18+
<Target Name="AddStructId" DependsOnTargets="ResolveLockFileReferences" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun">
4819
<!-- Feature detection -->
4920
<PropertyGroup>
5021
<UseDapper>false</UseDapper>
@@ -62,29 +33,8 @@
6233
</ItemGroup>
6334
<!-- Add final template items to project -->
6435
<ItemGroup>
65-
<Compile Include="%(StructId.TargetPath)" />
36+
<Compile Include="%(StructId.FullPath)" />
6637
</ItemGroup>
6738
</Target>
6839

69-
<UsingTask TaskName="WriteRaw" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
70-
<ParameterGroup>
71-
<Content ParameterType="System.String" Required="true" />
72-
<SourcePath ParameterType="System.String" Required="true" />
73-
<TargetPath ParameterType="System.String" Required="true" />
74-
</ParameterGroup>
75-
<Task>
76-
<Using Namespace="System" />
77-
<Using Namespace="System.IO" />
78-
<Using Namespace="System.Text"/>
79-
<Using Namespace="Microsoft.Build.Framework"/>
80-
<Code Type="Fragment" Language="cs">
81-
<![CDATA[
82-
Directory.CreateDirectory(Path.GetDirectoryName(TargetPath));
83-
File.WriteAllText(TargetPath, Content, Encoding.UTF8);
84-
File.SetLastWriteTimeUtc(TargetPath, File.GetLastWriteTimeUtc(SourcePath));
85-
]]>
86-
</Code>
87-
</Task>
88-
</UsingTask>
89-
9040
</Project>

src/StructId.Tests/StructId.Tests.csproj

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<Import Project="..\StructId.Package\StructId.props" />
4-
53
<PropertyGroup>
64
<TargetFramework>net8.0</TargetFramework>
75
<ImplicitUsings>enable</ImplicitUsings>

0 commit comments

Comments
 (0)