Skip to content

Commit e26779f

Browse files
Add ThisAssembly.Resources
Closes #45
1 parent 74e49e8 commit e26779f

File tree

15 files changed

+323
-9
lines changed

15 files changed

+323
-9
lines changed

ThisAssembly.sln

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 16
4-
VisualStudioVersion = 16.0.30509.190
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.4.33103.184
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Metadata", "src\ThisAssembly.Metadata\ThisAssembly.Metadata.csproj", "{B5007099-8BE7-490B-9E9A-18CC50C92C29}"
77
EndProject
@@ -35,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Constants", "s
3535
EndProject
3636
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Tests", "src\ThisAssembly.Tests\ThisAssembly.Tests.csproj", "{AD25424F-7DE0-4515-AE9F-B95414218292}"
3737
EndProject
38+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThisAssembly.Resources", "src\ThisAssembly.Resources\ThisAssembly.Resources.csproj", "{14D0C5BA-8410-4454-87A2-7BF5993E1EA2}"
39+
EndProject
3840
Global
3941
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4042
Debug|Any CPU = Debug|Any CPU
@@ -73,6 +75,10 @@ Global
7375
{AD25424F-7DE0-4515-AE9F-B95414218292}.Debug|Any CPU.Build.0 = Debug|Any CPU
7476
{AD25424F-7DE0-4515-AE9F-B95414218292}.Release|Any CPU.ActiveCfg = Release|Any CPU
7577
{AD25424F-7DE0-4515-AE9F-B95414218292}.Release|Any CPU.Build.0 = Release|Any CPU
78+
{14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79+
{14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
80+
{14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
81+
{14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Release|Any CPU.Build.0 = Release|Any CPU
7682
EndGlobalSection
7783
GlobalSection(SolutionProperties) = preSolution
7884
HideSolutionNode = FALSE

readme.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,23 @@ them as `ProjectProperty` MSBuild items in project file, such as:
114114

115115
![](img/ThisAssembly.Project.png)
116116

117+
## ThisAssembly.Resources
118+
119+
[![Version](https://img.shields.io/nuget/vpre/ThisAssembly.Resources.svg?color=royalblue)](https://www.nuget.org/packages/ThisAssembly.Resources)
120+
[![Downloads](https://img.shields.io/nuget/dt/ThisAssembly.Resources.svg?color=green)](https://www.nuget.org/packages/ThisAssembly.Resources)
121+
122+
This package generates a static `ThisAssembly.Resources` class with public
123+
properties exposing shortcuts to retrieve the contents of embedded resources.
124+
125+
126+
```xml
127+
<ItemGroup>
128+
<EmbeddedResource Include="Content/Docs/License.md" />
129+
</ItemGroup>
130+
```
131+
132+
![](img/ThisAssembly.Resources.png)
133+
117134
## ThisAssembly.Strings
118135

119136
[![Version](https://img.shields.io/nuget/vpre/ThisAssembly.Strings.svg?color=royalblue)](https://www.nuget.org/packages/ThisAssembly.Strings)

src/EmbeddedResource.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,28 @@
55

66
static class EmbeddedResource
77
{
8-
static readonly string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
8+
static readonly string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
99

1010
public static string GetContent(string relativePath)
11+
{
12+
using var stream = GetStream(relativePath);
13+
using var reader = new StreamReader(stream);
14+
return reader.ReadToEnd();
15+
}
16+
17+
public static byte[] GetBytes(string relativePath)
18+
{
19+
using var stream = GetStream(relativePath);
20+
var bytes = new byte[stream.Length];
21+
stream.Read(bytes, 0, bytes.Length);
22+
return bytes;
23+
}
24+
25+
public static Stream GetStream(string relativePath)
1126
{
1227
var filePath = Path.Combine(baseDir, Path.GetFileName(relativePath));
1328
if (File.Exists(filePath))
14-
return File.ReadAllText(filePath);
29+
return File.OpenRead(filePath);
1530

1631
var baseName = Assembly.GetExecutingAssembly().GetName().Name;
1732
var resourceName = relativePath
@@ -25,13 +40,12 @@ public static string GetContent(string relativePath)
2540
if (string.IsNullOrEmpty(manifestResourceName))
2641
throw new InvalidOperationException($"Did not find required resource ending in '{resourceName}' in assembly '{baseName}'.");
2742

28-
using var stream = Assembly.GetExecutingAssembly()
43+
var stream = Assembly.GetExecutingAssembly()
2944
.GetManifestResourceStream(manifestResourceName);
3045

3146
if (stream == null)
3247
throw new InvalidOperationException($"Did not find required resource '{manifestResourceName}' in assembly '{baseName}'.");
3348

34-
using var reader = new StreamReader(stream);
35-
return reader.ReadToEnd();
49+
return stream;
3650
}
3751
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by a tool.
4+
//
5+
// ThisAssembly.Resources: {{ Version }}
6+
//
7+
// Changes to this file may cause incorrect behavior and will be lost if
8+
// the code is regenerated.
9+
// </auto-generated>
10+
//------------------------------------------------------------------------------
11+
{{ func resource }}
12+
/// <summary>
13+
{{~ if $0.Comment ~}}
14+
/// {{ $0.Comment }}
15+
{{~ else ~}}
16+
/// => @"{{ $0.Path }}"
17+
{{~ end ~}}
18+
/// </summary>
19+
public static partial class {{ $0.Name }}
20+
{
21+
{{~ if $0.IsText ~}}
22+
private static string text;
23+
public static string Text =>
24+
text ??= EmbeddedResource.GetContent(@"{{ $0.Path }}");
25+
{{~ end ~}}
26+
27+
public static byte[] GetBytes() =>
28+
EmbeddedResource.GetBytes(@"{{ $0.Path }}");
29+
public static Stream GetStream() =>
30+
EmbeddedResource.GetStream(@"{{ $0.Path }}");
31+
}
32+
{{ end }}
33+
{{ func render }}
34+
public static partial class {{ $0.Name }}
35+
{
36+
{{~ if $0.Resource ~}}
37+
{{- resource $0.Resource ~}}
38+
{{~ else ~}}
39+
{{ render $0.NestedArea }}
40+
{{~ end ~}}
41+
}
42+
{{ end }}
43+
44+
using System;
45+
using System.IO;
46+
47+
partial class ThisAssembly
48+
{
49+
{{ render RootArea }}
50+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Reflection;
7+
8+
[DebuggerDisplay("Values = {RootArea.Values.Count}")]
9+
record Model(Area RootArea)
10+
{
11+
public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3);
12+
}
13+
14+
[DebuggerDisplay("Name = {Name}")]
15+
record Area(string Name)
16+
{
17+
public Area? NestedArea { get; private set; }
18+
public Resource? Resource { get; private set; }
19+
20+
public static Area Load(Resource resource, string rootArea = "Resources")
21+
{
22+
var root = new Area(rootArea);
23+
24+
// Splits: ([area].)*[name]
25+
var area = root;
26+
var parts = resource.Name.Split(new[] { "\\", "/" }, StringSplitOptions.RemoveEmptyEntries);
27+
foreach (var part in parts.AsSpan()[..^1])
28+
{
29+
area.NestedArea = new Area(part);
30+
area = area.NestedArea;
31+
}
32+
33+
area.Resource = resource with { Name = Path.GetFileNameWithoutExtension(parts[^1]), Path = resource.Name, };
34+
return root;
35+
}
36+
}
37+
38+
[DebuggerDisplay("{Name}")]
39+
record Resource(string Name, string? Comment, bool IsText)
40+
{
41+
public string? Path { get; set; }
42+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"profiles": {
3+
"ThisAssembly.Resources": {
4+
"commandName": "DebugRoslynComponent",
5+
"targetProject": "..\\ThisAssembly.Tests\\ThisAssembly.Tests.csproj"
6+
}
7+
}
8+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Text;
8+
using Scriban;
9+
10+
namespace ThisAssembly
11+
{
12+
[Generator(LanguageNames.CSharp)]
13+
public class ResourcesGenerator : IIncrementalGenerator
14+
{
15+
public void Initialize(IncrementalGeneratorInitializationContext context)
16+
{
17+
context.RegisterPostInitializationOutput(
18+
spc => spc.AddSource(
19+
"ThisAssembly.Resources.EmbeddedResource.cs",
20+
SourceText.From(EmbeddedResource.GetContent("EmbeddedResource.cs"), Encoding.UTF8)));
21+
22+
var files = context.AdditionalTextsProvider
23+
.Combine(context.AnalyzerConfigOptionsProvider)
24+
.Where(x =>
25+
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.AdditionalFiles.SourceItemType", out var itemType)
26+
&& itemType == "EmbeddedResource")
27+
.Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var value) && value != null)
28+
.Select((x, ct) =>
29+
{
30+
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var resourceName);
31+
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Kind", out var kind);
32+
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Comment", out var comment);
33+
return (resourceName!, kind, comment: string.IsNullOrWhiteSpace(comment) ? null : comment);
34+
})
35+
.Combine(context.AnalyzerConfigOptionsProvider
36+
.Select((p, _) =>
37+
{
38+
p.GlobalOptions.TryGetValue("build_property.EmbeddedResourceStringExtensions", out var extensions);
39+
return extensions!;
40+
}));
41+
42+
context.RegisterSourceOutput(
43+
files,
44+
GenerateSource);
45+
}
46+
47+
static void GenerateSource(SourceProductionContext spc, ((string resourceName, string? kind, string? comment), string extensions) arg2)
48+
{
49+
var ((resourceName, kind, comment), extensions) = arg2;
50+
51+
var file = "CSharp.sbntxt";
52+
var template = Template.Parse(EmbeddedResource.GetContent(file), file);
53+
54+
var isText = kind != null && kind.Equals("text", StringComparison.OrdinalIgnoreCase)
55+
|| extensions.Split(';').Contains(Path.GetFileName(resourceName));
56+
var root = Area.Load(new Resource(resourceName, comment, isText));
57+
var model = new Model(root);
58+
59+
var output = template.Render(model, member => member.Name);
60+
61+
// Apply formatting since indenting isn't that nice in Scriban when rendering nested
62+
// structures via functions.
63+
output = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseCompilationUnit(output)
64+
.NormalizeWhitespace()
65+
.GetText()
66+
.ToString();
67+
68+
spc.AddSource(
69+
$"{resourceName.Replace('\\', '.').Replace('/', '.')}.g.cs",
70+
SourceText.From(output, Encoding.UTF8));
71+
}
72+
}
73+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<IsRoslynComponent>true</IsRoslynComponent>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<PropertyGroup>
11+
<PackageId>ThisAssembly.Resources</PackageId>
12+
<Description>
13+
** C# 9.0+ ONLY **
14+
This package generates a static `ThisAssembly.Resources` class with public
15+
properties exposing `string` and `Stream` shortcuts to access embedded resources.
16+
</Description>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<None Remove="ThisAssembly.Resources.targets" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<PackageReference Include="NuGetizer" Version="0.9.0" />
25+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
26+
27+
<PackageReference Include="Scriban" Version="5.5.0" Pack="false" IncludeAssets="build" />
28+
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
29+
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
30+
31+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" Pack="false" />
32+
<PackageReference Include="PolySharp" Version="1.7.1" PrivateAssets="All" />
33+
</ItemGroup>
34+
35+
<ItemGroup>
36+
<ProjectReference Include="..\ThisAssembly.Prerequisites\ThisAssembly.Prerequisites.csproj" />
37+
</ItemGroup>
38+
39+
<ItemGroup>
40+
<EmbeddedResource Include="..\EmbeddedResource.cs" />
41+
</ItemGroup>
42+
43+
</Project>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Project>
2+
3+
<PropertyGroup>
4+
<EmbeddedResourceStringExtensions>.txt;.cs;.sql;.json;.md;</EmbeddedResourceStringExtensions>
5+
</PropertyGroup>
6+
7+
</Project>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<Project>
2+
3+
<ItemGroup>
4+
<CompilerVisibleProperty Include="EmbeddedResourceStringExtensions" />
5+
6+
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemType" />
7+
<CompilerVisibleItemMetadata Include="EmbeddedResource" MetadataName="Kind" />
8+
<CompilerVisibleItemMetadata Include="EmbeddedResource" MetadataName="Comment" />
9+
<CompilerVisibleItemMetadata Include="EmbeddedResource" MetadataName="Value" />
10+
</ItemGroup>
11+
12+
<ItemDefinitionGroup>
13+
<EmbeddedResource>
14+
<Link />
15+
<AreaPath />
16+
<Area />
17+
<Value />
18+
</EmbeddedResource>
19+
</ItemDefinitionGroup>
20+
21+
<Target Name="_InjectResourcesAdditionalFiles"
22+
BeforeTargets="PrepareForBuild;CompileDesignTime;GenerateMSBuildEditorConfigFileShouldRun"
23+
DependsOnTargets="PrepareResourceNames">
24+
<ItemGroup>
25+
<EmbeddedResource Condition="!$([System.IO.Path]::IsPathRooted('%(RelativeDir)')) OR '%(Link)' != ''">
26+
<AreaPath Condition="!$([System.IO.Path]::IsPathRooted('%(RelativeDir)'))">%(RelativeDir)%(Filename)</AreaPath>
27+
<AreaPath Condition="'%(Link)' != ''">$([System.IO.Path]::GetDirectoryName('%(Link)'))$([System.IO.Path]::DirectorySeparatorChar)$([System.IO.Path]::GetFileNameWithoutExtension('%(Link)'))</AreaPath>
28+
<FileExtension Condition="!$([System.IO.Path]::IsPathRooted('%(RelativeDir)'))">%(Extension)</FileExtension>
29+
<FileExtension Condition="'%(Link)' != ''">$([System.IO.Path]::GetExtension('%(Link)'))</FileExtension>
30+
</EmbeddedResource>
31+
<EmbeddedResource Condition="'%(AreaPath)' != ''">
32+
<Area>$([MSBuild]::ValueOrDefault('%(AreaPath)', '').Replace('\', '.').Replace('/', '.'))</Area>
33+
<Value>%(AreaPath)%(FileExtension)</Value>
34+
</EmbeddedResource>
35+
<AdditionalFiles Include="@(EmbeddedResource)" SourceItemType="EmbeddedResource" />
36+
</ItemGroup>
37+
</Target>
38+
39+
</Project>

0 commit comments

Comments
 (0)