Ben Ford
Ben Ford

Reputation: 1394

Include source code for library in that library

I've a plugin based web app that allows an administrator to assign various small pieces of functionality (the actual functionality is unimportant here) to users. This functionality is configurable and an administrator having an understanding of the plugins is important. The administrator is technical enough to be able to read the very simple source code for these plugins. (Mostly just arithmetic).

I'm aware there are a couple of questions already about accessing source code from within a DLL built from that source:

How to include source code in dll? for example.

I've played with getting the .cs files into a /Resources folder. However doing this with a pre-build event obviously don't include these files in the project. So VS never copies them and I'm unable to access them on the Resources object.

Is there a way to reference the source for a particular class... from the same assembly that I'm missing? Or alternatively a way to extract COMPLETE source from the pdb for that assembly? I'm quite happy to deploy detailed PDB files. There's no security risk for this portion of the solution.

As I've access to the source code, I don't want to go about decompiling it to display it. This seems wasteful and overly complicated.

Upvotes: 0

Views: 524

Answers (2)

Ben Ford
Ben Ford

Reputation: 1394

Taking from @Luaan's suggestion of using a pre-build step to create a single Zipped folder I created a basic console app to package the source files into a zip file at a specific location.

static void Main(string[] args)
        {
            Console.WriteLine("Takes a folder of .cs files and flattens and compacts them into a .zip." +
                          "Arg 1 : Source Folder to be resursively searched" +
                          "Arg 2 : Destination zip file" +
                          "Arg 3 : Semicolon List of folders to ignore");

            if (args[0] == null || args[1] == null)
            {
                Console.Write("Args 1 or 2 missing");
                return;
            };

            string SourcePath = args[0];
            string ZipDestination = args[1];

            List<String> ignoreFolders = new List<string>();
            if (args[2] != null)
            {
                ignoreFolders = args[2].Split(';').ToList();
            }

            var files = DirSearch(SourcePath, "*.cs", ignoreFolders);
            Console.WriteLine($"{files.Count} files found to zip");

            if (File.Exists(ZipDestination))
            {
                Console.WriteLine("Destination exists. Deleting zip file first");
                File.Delete(ZipDestination);
            }

            int zippedCount = 0;
            using (FileStream zipToOpen = new FileStream(ZipDestination, FileMode.OpenOrCreate))
            {
                using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Create))
                {
                    foreach (var filePath in files)
                    {
                        Console.WriteLine($"Writing {Path.GetFileName(filePath)} to zip {Path.GetFileName(ZipDestination)}");
                        archive.CreateEntryFromFile(filePath, Path.GetFileName(filePath));
                        zippedCount++;
                    }   
                }
            }
            Console.WriteLine($"Zipped {zippedCount} files;");
        }

        static List<String> DirSearch(string sDir, string filePattern, List<String> excludeDirectories)
        {
            List<String> filePaths = new List<string>();
            foreach (string d in Directory.GetDirectories(sDir))
            {
                if (excludeDirectories.Any(ed => ed.ToLower() == d.ToLower()))
                {
                    continue;
                }

                foreach (string f in Directory.GetFiles(d, filePattern))
                {
                    filePaths.Add(f);
                }
                filePaths.AddRange(DirSearch(d, filePattern, excludeDirectories));
            }

            return filePaths;
        }

Takes 3 parameters for source dir, output zip file and a ";" separated list of paths to exclude. I've just built this as a binary. Committed it to source control for simplicity and included it in the pre-build for projects I want the source for.

No error checking really and I'm certain it will fail for missing args. But if anyone wants it. Here it is! Again Thanks to @Luaan for clarifying PDBs aren't all that useful!

Upvotes: 0

Luaan
Luaan

Reputation: 63732

The source code isn't included in the DLLs, and it isn't in the PDBs either (PDBs only contain a link between the addresses in the DLL and the corresponding lines of code in the sources, as well as other trivia like variable names).

A pre-build event is a possible solution - just make sure that it produces a single file that's included in the project. A simple zip archive should work well enough, and it's easy to decompress when you need to access the source. Text compresses very well, so it might make sense to compress it anyway. If you don't want to use zip, anything else will do fine as well - an XML file, for example. It might even give you the benefit of using something like Roslyn to provide syntax highlighting with all the necessary context.

Decompilation isn't necessarily a terrible approach. You're trading memory for CPU, basically. You'll lose comments, but that shouldn't be a problem if your code is very simple. Method arguments keep their names, but locals don't - you'd need the PDBs for that, which is a massive overkill. It actually does depend a lot on the kind of calculations you're doing. For most cases, it probably isn't the best solution, though.

A bit roundabout way of handling this would be a multi-file T4 template. Basically, you'd produce as many files as there are source code files, and have them be embedded resources. I'm not sure how simple this is, and I'm sure not going to include the code :D

Another (a bit weird) option is to use file links. Simply have the source code files in the project as usual, but also make a separate folder where the same files will be added using "Add as link". The content will be shared with the actual source code, but you can specify a different build action - namely, Embedded Resource. This requires a (tiny) bit of manual work when adding or moving files, but it's relatively simple. If needed, this could also be automated, though that sounds like an overkill.

The cleanest option I can think of is adding a new build action - Compile + Embed. This requires you to add a build target file to your project, but that's actually quite simple. The target file is just an XML file, and then you just manually edit your SLN/CSPROJ file to include that target in the build, and you're good to go. The tricky part is that you'll also need to force the Microsoft.CSharp.Core.target to use your Compile + Embed action to be used as both the source code and the embedded resource. This is of course easily done by manually changing that target file, but that's a terrible way of handling that. I'm not sure what the best way of doing that is, though. Maybe there's a way to redefine @(Compile) to mean @(Compile;MyCompileAndEmbed)? I know it's possible with the usual property groups, but I'm not sure if something like this can be done with the "lists".

Upvotes: 1

Related Questions