Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions ThisAssembly.sln
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ them as `ProjectProperty` MSBuild items in project file, such as:

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

## ThisAssembly.Resources

[![Version](https://img.shields.io/nuget/vpre/ThisAssembly.Resources.svg?color=royalblue)](https://www.nuget.org/packages/ThisAssembly.Resources)
[![Downloads](https://img.shields.io/nuget/dt/ThisAssembly.Resources.svg?color=green)](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
<ItemGroup>
<EmbeddedResource Include="Content/Docs/License.md" />
</ItemGroup>
```

![](img/ThisAssembly.Resources.png)

## ThisAssembly.Strings

[![Version](https://img.shields.io/nuget/vpre/ThisAssembly.Strings.svg?color=royalblue)](https://www.nuget.org/packages/ThisAssembly.Strings)
Expand Down
24 changes: 19 additions & 5 deletions src/EmbeddedResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
50 changes: 50 additions & 0 deletions src/ThisAssembly.Resources/CSharp.sbntxt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
{{ func resource }}
/// <summary>
{{~ if $0.Comment ~}}
/// {{ $0.Comment }}
{{~ else ~}}
/// => @"{{ $0.Path }}"
{{~ end ~}}
/// </summary>
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 }}
}
42 changes: 42 additions & 0 deletions src/ThisAssembly.Resources/Model.cs
Original file line number Diff line number Diff line change
@@ -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; }
};
8 changes: 8 additions & 0 deletions src/ThisAssembly.Resources/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"profiles": {
"ThisAssembly.Resources": {
"commandName": "DebugRoslynComponent",
"targetProject": "..\\ThisAssembly.Tests\\ThisAssembly.Tests.csproj"
}
}
}
73 changes: 73 additions & 0 deletions src/ThisAssembly.Resources/ResourcesGenerator.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
43 changes: 43 additions & 0 deletions src/ThisAssembly.Resources/ThisAssembly.Resources.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<PackageId>ThisAssembly.Resources</PackageId>
<Description>
** C# 9.0+ ONLY **
This package generates a static `ThisAssembly.Resources` class with public
properties exposing `string` and `Stream` shortcuts to access embedded resources.
</Description>
</PropertyGroup>

<ItemGroup>
<None Remove="ThisAssembly.Resources.targets" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NuGetizer" Version="0.9.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />

<PackageReference Include="Scriban" Version="5.5.0" Pack="false" IncludeAssets="build" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />

<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" Pack="false" />
<PackageReference Include="PolySharp" Version="1.7.1" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ThisAssembly.Prerequisites\ThisAssembly.Prerequisites.csproj" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="..\EmbeddedResource.cs" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions src/ThisAssembly.Resources/ThisAssembly.Resources.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>

<PropertyGroup>
<EmbeddedResourceStringExtensions>.txt;.cs;.sql;.json;.md;</EmbeddedResourceStringExtensions>
</PropertyGroup>

</Project>
39 changes: 39 additions & 0 deletions src/ThisAssembly.Resources/ThisAssembly.Resources.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project>

<ItemGroup>
<CompilerVisibleProperty Include="EmbeddedResourceStringExtensions" />

<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemType" />
<CompilerVisibleItemMetadata Include="EmbeddedResource" MetadataName="Kind" />
<CompilerVisibleItemMetadata Include="EmbeddedResource" MetadataName="Comment" />
<CompilerVisibleItemMetadata Include="EmbeddedResource" MetadataName="Value" />
</ItemGroup>

<ItemDefinitionGroup>
<EmbeddedResource>
<Link />
<AreaPath />
<Area />
<Value />
</EmbeddedResource>
</ItemDefinitionGroup>

<Target Name="_InjectResourcesAdditionalFiles"
BeforeTargets="PrepareForBuild;CompileDesignTime;GenerateMSBuildEditorConfigFileShouldRun"
DependsOnTargets="PrepareResourceNames">
<ItemGroup>
<EmbeddedResource Condition="!$([System.IO.Path]::IsPathRooted('%(RelativeDir)')) OR '%(Link)' != ''">
<AreaPath Condition="!$([System.IO.Path]::IsPathRooted('%(RelativeDir)'))">%(RelativeDir)%(Filename)</AreaPath>
<AreaPath Condition="'%(Link)' != ''">$([System.IO.Path]::GetDirectoryName('%(Link)'))$([System.IO.Path]::DirectorySeparatorChar)$([System.IO.Path]::GetFileNameWithoutExtension('%(Link)'))</AreaPath>
<FileExtension Condition="!$([System.IO.Path]::IsPathRooted('%(RelativeDir)'))">%(Extension)</FileExtension>
<FileExtension Condition="'%(Link)' != ''">$([System.IO.Path]::GetExtension('%(Link)'))</FileExtension>
</EmbeddedResource>
<EmbeddedResource Condition="'%(AreaPath)' != ''">
<Area>$([MSBuild]::ValueOrDefault('%(AreaPath)', '').Replace('\', '.').Replace('/', '.'))</Area>
<Value>%(AreaPath)%(FileExtension)</Value>
</EmbeddedResource>
<AdditionalFiles Include="@(EmbeddedResource)" SourceItemType="EmbeddedResource" />
</ItemGroup>
</Target>

</Project>
2 changes: 1 addition & 1 deletion src/ThisAssembly.Strings/StringsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
{
Expand Down
2 changes: 1 addition & 1 deletion src/ThisAssembly.Strings/ThisAssembly.Strings.targets
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<ItemGroup>
<ResxCode Include="@(EmbeddedResource)"
Condition="'%(EmbeddedResource.Type)' == 'Resx' And '%(EmbeddedResource.GenerateResource)' != 'false' And '%(EmbeddedResource.ManifestResourceName)' != ''" />
<AdditionalFiles Include="@(ResxCode -> '%(FullPath)')" SourceItemType="EmbeddedResource" />
<AdditionalFiles Include="@(ResxCode -> '%(FullPath)')" SourceItemType="ResourceString" />
</ItemGroup>
</Target>

Expand Down
Loading