diff --git a/ThisAssembly.sln b/ThisAssembly.sln
index 7d966b3c..58695a4b 100644
--- a/ThisAssembly.sln
+++ b/ThisAssembly.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30509.190
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33103.184
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Metadata", "src\ThisAssembly.Metadata\ThisAssembly.Metadata.csproj", "{B5007099-8BE7-490B-9E9A-18CC50C92C29}"
EndProject
@@ -35,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Constants", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Tests", "src\ThisAssembly.Tests\ThisAssembly.Tests.csproj", "{AD25424F-7DE0-4515-AE9F-B95414218292}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThisAssembly.Resources", "src\ThisAssembly.Resources\ThisAssembly.Resources.csproj", "{14D0C5BA-8410-4454-87A2-7BF5993E1EA2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -73,6 +75,10 @@ Global
{AD25424F-7DE0-4515-AE9F-B95414218292}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD25424F-7DE0-4515-AE9F-B95414218292}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD25424F-7DE0-4515-AE9F-B95414218292}.Release|Any CPU.Build.0 = Release|Any CPU
+ {14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/readme.md b/readme.md
index 911ddaa3..61378bf7 100644
--- a/readme.md
+++ b/readme.md
@@ -114,6 +114,23 @@ them as `ProjectProperty` MSBuild items in project file, such as:

+## ThisAssembly.Resources
+
+[](https://www.nuget.org/packages/ThisAssembly.Resources)
+[](https://www.nuget.org/packages/ThisAssembly.Resources)
+
+This package generates a static `ThisAssembly.Resources` class with public
+properties exposing shortcuts to retrieve the contents of embedded resources.
+
+
+```xml
+
+
+
+```
+
+
+
## ThisAssembly.Strings
[](https://www.nuget.org/packages/ThisAssembly.Strings)
diff --git a/src/EmbeddedResource.cs b/src/EmbeddedResource.cs
index a5075a92..c738ef09 100644
--- a/src/EmbeddedResource.cs
+++ b/src/EmbeddedResource.cs
@@ -5,13 +5,28 @@
static class EmbeddedResource
{
- static readonly string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ static readonly string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
public static string GetContent(string relativePath)
+ {
+ using var stream = GetStream(relativePath);
+ using var reader = new StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+
+ public static byte[] GetBytes(string relativePath)
+ {
+ using var stream = GetStream(relativePath);
+ var bytes = new byte[stream.Length];
+ stream.Read(bytes, 0, bytes.Length);
+ return bytes;
+ }
+
+ public static Stream GetStream(string relativePath)
{
var filePath = Path.Combine(baseDir, Path.GetFileName(relativePath));
if (File.Exists(filePath))
- return File.ReadAllText(filePath);
+ return File.OpenRead(filePath);
var baseName = Assembly.GetExecutingAssembly().GetName().Name;
var resourceName = relativePath
@@ -25,13 +40,12 @@ public static string GetContent(string relativePath)
if (string.IsNullOrEmpty(manifestResourceName))
throw new InvalidOperationException($"Did not find required resource ending in '{resourceName}' in assembly '{baseName}'.");
- using var stream = Assembly.GetExecutingAssembly()
+ var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream(manifestResourceName);
if (stream == null)
throw new InvalidOperationException($"Did not find required resource '{manifestResourceName}' in assembly '{baseName}'.");
- using var reader = new StreamReader(stream);
- return reader.ReadToEnd();
+ return stream;
}
}
\ No newline at end of file
diff --git a/src/ThisAssembly.Resources/CSharp.sbntxt b/src/ThisAssembly.Resources/CSharp.sbntxt
new file mode 100644
index 00000000..407c1c4e
--- /dev/null
+++ b/src/ThisAssembly.Resources/CSharp.sbntxt
@@ -0,0 +1,50 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// ThisAssembly.Resources: {{ Version }}
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+{{ func resource }}
+ ///
+ {{~ if $0.Comment ~}}
+ /// {{ $0.Comment }}
+ {{~ else ~}}
+ /// => @"{{ $0.Path }}"
+ {{~ end ~}}
+ ///
+ public static partial class {{ $0.Name }}
+ {
+ {{~ if $0.IsText ~}}
+ private static string text;
+ public static string Text =>
+ text ??= EmbeddedResource.GetContent(@"{{ $0.Path }}");
+ {{~ end ~}}
+
+ public static byte[] GetBytes() =>
+ EmbeddedResource.GetBytes(@"{{ $0.Path }}");
+ public static Stream GetStream() =>
+ EmbeddedResource.GetStream(@"{{ $0.Path }}");
+ }
+{{ end }}
+{{ func render }}
+ public static partial class {{ $0.Name }}
+ {
+ {{~ if $0.Resource ~}}
+ {{- resource $0.Resource ~}}
+ {{~ else ~}}
+ {{ render $0.NestedArea }}
+ {{~ end ~}}
+ }
+{{ end }}
+
+using System;
+using System.IO;
+
+partial class ThisAssembly
+{
+ {{ render RootArea }}
+}
diff --git a/src/ThisAssembly.Resources/Model.cs b/src/ThisAssembly.Resources/Model.cs
new file mode 100644
index 00000000..245efccb
--- /dev/null
+++ b/src/ThisAssembly.Resources/Model.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+[DebuggerDisplay("Values = {RootArea.Values.Count}")]
+record Model(Area RootArea)
+{
+ public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3);
+}
+
+[DebuggerDisplay("Name = {Name}")]
+record Area(string Name)
+{
+ public Area? NestedArea { get; private set; }
+ public Resource? Resource { get; private set; }
+
+ public static Area Load(Resource resource, string rootArea = "Resources")
+ {
+ var root = new Area(rootArea);
+
+ // Splits: ([area].)*[name]
+ var area = root;
+ var parts = resource.Name.Split(new[] { "\\", "/" }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var part in parts.AsSpan()[..^1])
+ {
+ area.NestedArea = new Area(part);
+ area = area.NestedArea;
+ }
+
+ area.Resource = resource with { Name = Path.GetFileNameWithoutExtension(parts[^1]), Path = resource.Name, };
+ return root;
+ }
+}
+
+[DebuggerDisplay("{Name}")]
+record Resource(string Name, string? Comment, bool IsText)
+{
+ public string? Path { get; set; }
+};
\ No newline at end of file
diff --git a/src/ThisAssembly.Resources/Properties/launchSettings.json b/src/ThisAssembly.Resources/Properties/launchSettings.json
new file mode 100644
index 00000000..fb947eaa
--- /dev/null
+++ b/src/ThisAssembly.Resources/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "ThisAssembly.Resources": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "..\\ThisAssembly.Tests\\ThisAssembly.Tests.csproj"
+ }
+ }
+}
diff --git a/src/ThisAssembly.Resources/ResourcesGenerator.cs b/src/ThisAssembly.Resources/ResourcesGenerator.cs
new file mode 100644
index 00000000..ea1e3d60
--- /dev/null
+++ b/src/ThisAssembly.Resources/ResourcesGenerator.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+using Scriban;
+
+namespace ThisAssembly
+{
+ [Generator(LanguageNames.CSharp)]
+ public class ResourcesGenerator : IIncrementalGenerator
+ {
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ context.RegisterPostInitializationOutput(
+ spc => spc.AddSource(
+ "ThisAssembly.Resources.EmbeddedResource.cs",
+ SourceText.From(EmbeddedResource.GetContent("EmbeddedResource.cs"), Encoding.UTF8)));
+
+ var files = context.AdditionalTextsProvider
+ .Combine(context.AnalyzerConfigOptionsProvider)
+ .Where(x =>
+ x.Right.GetOptions(x.Left).TryGetValue("build_metadata.AdditionalFiles.SourceItemType", out var itemType)
+ && itemType == "EmbeddedResource")
+ .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var value) && value != null)
+ .Select((x, ct) =>
+ {
+ x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var resourceName);
+ x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Kind", out var kind);
+ x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Comment", out var comment);
+ return (resourceName!, kind, comment: string.IsNullOrWhiteSpace(comment) ? null : comment);
+ })
+ .Combine(context.AnalyzerConfigOptionsProvider
+ .Select((p, _) =>
+ {
+ p.GlobalOptions.TryGetValue("build_property.EmbeddedResourceStringExtensions", out var extensions);
+ return extensions!;
+ }));
+
+ context.RegisterSourceOutput(
+ files,
+ GenerateSource);
+ }
+
+ static void GenerateSource(SourceProductionContext spc, ((string resourceName, string? kind, string? comment), string extensions) arg2)
+ {
+ var ((resourceName, kind, comment), extensions) = arg2;
+
+ var file = "CSharp.sbntxt";
+ var template = Template.Parse(EmbeddedResource.GetContent(file), file);
+
+ var isText = kind != null && kind.Equals("text", StringComparison.OrdinalIgnoreCase)
+ || extensions.Split(';').Contains(Path.GetFileName(resourceName));
+ var root = Area.Load(new Resource(resourceName, comment, isText));
+ var model = new Model(root);
+
+ var output = template.Render(model, member => member.Name);
+
+ // Apply formatting since indenting isn't that nice in Scriban when rendering nested
+ // structures via functions.
+ output = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseCompilationUnit(output)
+ .NormalizeWhitespace()
+ .GetText()
+ .ToString();
+
+ spc.AddSource(
+ $"{resourceName.Replace('\\', '.').Replace('/', '.')}.g.cs",
+ SourceText.From(output, Encoding.UTF8));
+ }
+ }
+}
diff --git a/src/ThisAssembly.Resources/ThisAssembly.Resources.csproj b/src/ThisAssembly.Resources/ThisAssembly.Resources.csproj
new file mode 100644
index 00000000..a41f1dae
--- /dev/null
+++ b/src/ThisAssembly.Resources/ThisAssembly.Resources.csproj
@@ -0,0 +1,43 @@
+
+
+
+ netstandard2.0
+ latest
+ true
+ enable
+
+
+
+ ThisAssembly.Resources
+
+ ** C# 9.0+ ONLY **
+ This package generates a static `ThisAssembly.Resources` class with public
+ properties exposing `string` and `Stream` shortcuts to access embedded resources.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ThisAssembly.Resources/ThisAssembly.Resources.props b/src/ThisAssembly.Resources/ThisAssembly.Resources.props
new file mode 100644
index 00000000..d4a06db1
--- /dev/null
+++ b/src/ThisAssembly.Resources/ThisAssembly.Resources.props
@@ -0,0 +1,7 @@
+
+
+
+ .txt;.cs;.sql;.json;.md;
+
+
+
diff --git a/src/ThisAssembly.Resources/ThisAssembly.Resources.targets b/src/ThisAssembly.Resources/ThisAssembly.Resources.targets
new file mode 100644
index 00000000..5823e988
--- /dev/null
+++ b/src/ThisAssembly.Resources/ThisAssembly.Resources.targets
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %(RelativeDir)%(Filename)
+ $([System.IO.Path]::GetDirectoryName('%(Link)'))$([System.IO.Path]::DirectorySeparatorChar)$([System.IO.Path]::GetFileNameWithoutExtension('%(Link)'))
+ %(Extension)
+ $([System.IO.Path]::GetExtension('%(Link)'))
+
+
+ $([MSBuild]::ValueOrDefault('%(AreaPath)', '').Replace('\', '.').Replace('/', '.'))
+ %(AreaPath)%(FileExtension)
+
+
+
+
+
+
diff --git a/src/ThisAssembly.Strings/StringsGenerator.cs b/src/ThisAssembly.Strings/StringsGenerator.cs
index 8aafdd70..76e39de6 100644
--- a/src/ThisAssembly.Strings/StringsGenerator.cs
+++ b/src/ThisAssembly.Strings/StringsGenerator.cs
@@ -34,7 +34,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Combine(context.AnalyzerConfigOptionsProvider)
.Where(x =>
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.AdditionalFiles.SourceItemType", out var itemType)
- && itemType == "EmbeddedResource")
+ && itemType == "ResourceString")
.Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.AdditionalFiles.ManifestResourceName", out var value) && value != null)
.Select((x, ct) =>
{
diff --git a/src/ThisAssembly.Strings/ThisAssembly.Strings.targets b/src/ThisAssembly.Strings/ThisAssembly.Strings.targets
index 0676ae7e..914f8195 100644
--- a/src/ThisAssembly.Strings/ThisAssembly.Strings.targets
+++ b/src/ThisAssembly.Strings/ThisAssembly.Strings.targets
@@ -6,7 +6,7 @@
-
+
diff --git a/src/ThisAssembly.Tests/Tests.cs b/src/ThisAssembly.Tests/Tests.cs
index 2c260287..073f0a2b 100644
--- a/src/ThisAssembly.Tests/Tests.cs
+++ b/src/ThisAssembly.Tests/Tests.cs
@@ -44,5 +44,14 @@ public void CanUseStringsIndexedArguments()
[Fact]
public void CanUseStringResource()
=> Assert.Equal("Value", ThisAssembly.Strings.Foo.Bar.Baz);
+
+ [Fact]
+ public void CanUseTextResource()
+ => Assert.NotNull(ThisAssembly.Resources.Content.Styles.Custom.Text);
+
+ [Fact]
+ public void CanUseByteResource()
+ => Assert.NotNull(ThisAssembly.Resources.Content.Styles.Main.GetBytes());
+
}
}
diff --git a/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj b/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj
index ab9c3be6..f453ff01 100644
--- a/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj
+++ b/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj
@@ -23,6 +23,7 @@
+
@@ -39,6 +40,10 @@
PreserveNewest
+
+
+
+
diff --git a/src/ThisAssembly/ThisAssembly.csproj b/src/ThisAssembly/ThisAssembly.csproj
index e6148c51..72c95a53 100644
--- a/src/ThisAssembly/ThisAssembly.csproj
+++ b/src/ThisAssembly/ThisAssembly.csproj
@@ -28,6 +28,7 @@
+