Skip to content

Instantly share code, notes, and snippets.

@codejockie
Created January 19, 2024 13:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codejockie/81c0075f64f17b237a43cdb744a772eb to your computer and use it in GitHub Desktop.
Save codejockie/81c0075f64f17b237a43cdb744a772eb to your computer and use it in GitHub Desktop.
Using ResourceDescription to embed resource information
// https://github.com/dotnet/roslyn/issues/7791#issuecomment-394133511
var resourceDescription = new Microsoft.CodeAnalysis.ResourceDescription(
resourceFullName,
() => ProcessFile(resourceFilePath),
isPublic: true);
private static MemoryStream ProcessFile(string inFile)
{
var readers = new List<ReaderInfo>();
var resources = ReadResources(inFile);
using (var outStream = WriteResources(resources))
{
//outstream is closed, so we create a new memory stream based on its buffer.
var openStream = new MemoryStream(outStream.GetBuffer());
return openStream;
}
}
private static MemoryStream WriteResources(ReaderInfo resources)
{
var memoryStream = new MemoryStream();
using (var resourceWriter = new ResourceWriter(memoryStream))
{
WriteResources(resources, resourceWriter);
}
return memoryStream;
}
private static void WriteResources(ReaderInfo readerInfo, ResourceWriter resourceWriter)
{
foreach (ResourceEntry entry in readerInfo.resources)
{
string key = entry.Name;
object value = entry.Value;
resourceWriter.AddResource(key, value);
}
}
private static ReaderInfo ReadResources(string fileName)
{
ReaderInfo readerInfo = new ReaderInfo();
var path = Path.GetDirectoryName(fileName);
var resXReader = new ResXResourceReader(fileName);
resXReader.BasePath = path;
using (resXReader)
{
IDictionaryEnumerator resEnum = resXReader.GetEnumerator();
while (resEnum.MoveNext())
{
string name = (string)resEnum.Key;
object value = resEnum.Value;
AddResource(readerInfo, name, value, fileName);
}
}
return readerInfo;
}
private static void AddResource(ReaderInfo readerInfo, string name, object value, string fileName)
{
ResourceEntry entry = new ResourceEntry(name, value);
if (readerInfo.resourcesHashTable.ContainsKey(name))
{
// Duplicate resource name. We'll ignore and continue.
return;
}
readerInfo.resources.Add(entry);
readerInfo.resourcesHashTable.Add(name, value);
}
internal sealed class ReaderInfo
{
// We use a list to preserve the resource ordering (primarily for easier testing),
// but also use a hash table to check for duplicate names.
public ArrayList resources { get; }
public Hashtable resourcesHashTable { get; }
public ReaderInfo()
{
resources = new ArrayList();
resourcesHashTable = new Hashtable(StringComparer.OrdinalIgnoreCase);
}
}
/// <summary>
/// Name value resource pair to go in resources list
/// </summary>
private class ResourceEntry(string name, object value)
{
public string Name { get; } = name;
public object Value { get; } = value;
}
// https://github.com/dotnet/roslyn/issues/7791#issuecomment-394243401
/// <summary>
/// Method to analyze the .csproj file and determine if there are embedded resource files and
/// to convert them into a collection of ResourceDescription objects.
///
/// Embedded resource files are typically .resx files, but there are many other possibilities:
/// .txt, .xml, graphics files, etc.
///
/// Parts of this code based on this Stack Overflow answer:
/// https://stackoverflow.com/a/44142655/253938
/// </summary>
/// <returns>collection of ResourceDescription objects,
/// or null if no embedded resource files or error encountered</returns>
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
private static IEnumerable<ResourceDescription> GetManifestResources(string csprojFileName)
{
try
{
XDocument csprojAsXml = XDocument.Load(csprojFileName);
XNamespace xmlNamespace = "http://schemas.microsoft.com/developer/msbuild/2003";
IEnumerable<XElement> projectNamespaceElements =
csprojAsXml.Descendants(xmlNamespace + "RootNamespace");
XElement projectNamespaceElement = projectNamespaceElements.First();
if (projectNamespaceElement == null)
{
DisplayErrorOrInfo(
"Unable to determine default namespace for project " + csprojFileName);
return null;
}
string projectNamespace = projectNamespaceElement.Value;
List<ResourceDescription> resourceDescriptions = new List<ResourceDescription>();
foreach (XElement embeddedResourceElement in
csprojAsXml.Descendants(xmlNamespace + "EmbeddedResource"))
{
XAttribute includeAttribute = embeddedResourceElement.Attribute("Include");
if (includeAttribute == null)
continue; // Shouldn't be possible
string resourceFilename = includeAttribute.Value;
// ReSharper disable once AssignNullToNotNullAttribute
string resourceFullFilename =
Path.Combine(Path.GetDirectoryName(csprojFileName), resourceFilename);
string resourceName =
resourceFilename.EndsWith(".resx", StringComparison.OrdinalIgnoreCase) ?
resourceFilename.Remove(resourceFilename.Length - 5) + ".resources" :
resourceFilename;
resourceDescriptions.Add(
new ResourceDescription(projectNamespace + "." + resourceName,
() => ProvideResourceData(resourceFullFilename), true));
}
return resourceDescriptions.Count == 0 ? null : resourceDescriptions;
}
catch (Exception e)
{
DisplayErrorOrInfo("Exception while processing embedded resource files: " + e.Message);
return null;
}
}
/// <summary>
/// Method that gets called by ManagedResource.WriteData() in project CodeAnalysis during code
/// emitting to get the data for an embedded resource file. Caller guarantees that the
/// returned Stream object gets disposed.
/// </summary>
/// <param name="resourceFullFilename">full path and filename for resource file to embed</param>
/// <returns>MemoryStream containing .resource file data for a .resx file,
/// or FileStream providing image of a non-.resx file</returns>
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
private static Stream ProvideResourceData(string resourceFullFilename)
{
// For non-.resx files just create a FileStream object to read the file as binary data
if (!resourceFullFilename.EndsWith(".resx", StringComparison.OrdinalIgnoreCase))
return new FileStream(resourceFullFilename, FileMode.Open);
// Remainder of this method converts a .resx file into .resource file data and returns it
// as a MemoryStream
MemoryStream shortLivedBackingStream = new MemoryStream();
using (ResourceWriter resourceWriter = new ResourceWriter(shortLivedBackingStream))
{
resourceWriter.TypeNameConverter = TypeNameConverter;
using (ResXResourceReader resourceReader = new ResXResourceReader(resourceFullFilename))
{
IDictionaryEnumerator dictionaryEnumerator = resourceReader.GetEnumerator();
while (dictionaryEnumerator.MoveNext())
{
string resourceKey = dictionaryEnumerator.Key as string;
if (resourceKey != null) // Should not be possible
resourceWriter.AddResource(resourceKey, dictionaryEnumerator.Value);
}
}
}
// This needed because shortLivedBackingStream is now closed
return new MemoryStream(shortLivedBackingStream.GetBuffer());
}
/// <summary>
/// This is needed to fix a "Could not load file or assembly 'System.Drawing, Version=4.0.0.0"
/// exception, although I'm not sure why that exception was occurring.
///
/// See also here: https://github.com/dotnet/corefx/issues/11083 - it says it doesn't work,
/// but it did save the day for me.
/// </summary>
private static string TypeNameConverter(Type objectType)
{
// ReSharper disable once PossibleNullReferenceException
return objectType.AssemblyQualifiedName.Replace("4.0.0.0", "2.0.0.0");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment