dnlib after ModuleDefMD.Write(file, options) Assembly attributes are deleted - c#

Module Creation process
var file = Console.ReadLine();
var moduleContext = ModuleDef.CreateModuleContext();
var moduleCreationOptions = new ModuleCreationOptions(moduleContext, CLRRuntimeReaderKind.Mono);
var buffer = File.ReadAllBytes(file);
var moduleDefMD = ModuleDefMD.Load(buffer, moduleCreationOptions);
var moduleWriterOptions = new ModuleWriterOptions(moduleDefMD);
moduleWriterOptions.MetadataLogger = DummyLogger.NoThrowInstance;
moduleWriterOptions.MetadataOptions.Flags |=
MetadataFlags.PreserveStringsOffsets
| MetadataFlags.PreserveUSOffsets
| MetadataFlags.PreserveBlobOffsets
| MetadataFlags.PreserveExtraSignatureData;
moduleWriterOptions.Cor20HeaderOptions.Flags = ComImageFlags.ILOnly;
Add Static Method with single instruction ret and call it in Module cctor
var methodDef = new MethodDefUser(Guid.NewGuid().ToString(), MethodSig.CreateStatic(moduleDefMD.CorLibTypes.Void));
methodDef.IsStatic = true;
methodDef.Body = new CilBody();
methodDef.Body.Instructions.Add(new Instruction(OpCodes.Ret));
moduleDefMD.GlobalType.Methods.Add(methodDef);
var cctorInstructions = moduleDefMD.GlobalType.FindOrCreateStaticConstructor().Body.Instructions;
cctorInstructions.Insert(0, new Instruction(OpCodes.Call, methodDef));
Write ModuleDefMD
var output = Path.Combine(Path.GetDirectoryName(file), "output." + Path.GetExtension(file));
moduleDefMD.Write(output);
Result
assembly attributes are deleted, only single one is left is [assembly: AssemblyVersion("1.0.0.0")], nothing else, no Assembly Name, no Assembly Company etc it causes problems with Assembly.GetAssembly().GetName
I tried to create another project with the same parameters and conditions i.e same .NET Framework versions, same ModuleDefMD writing process, I tried to do everything the same but nothing is help me, this is just like a curse, I'm deeply learing dnlib sources and I didn't found anything interesting that can help me to solve this problem. I open issue on dnlib GitHub

The problem is solved, when I was resolving CustomAttributes via reflection I used to be removing them

Related

RallyAPI: How do I create a UserStory and relate it to a Feature?

So, I'm writing an app to 'flesh out' new clients in Rally. It will have tools to create templates which will add first:
add a 'Feature'
add 'UserStories' under that 'Feature'
add 'Tasks' under those 'UserStories' individually
I have figured out step 1. But how to associate anything I can't figure out from the horrible and cryptic documentation. Here's what I have so far:
var FeatureToAdd = _featureRepository.GetFeatures().FirstOrDefault(x => x.Id == 2);
// Initialize the REST API. You can specify a web service version if needed in the constructor.
RallyRestApi restApi = GetRallyRestApi();
//Create an item
DynamicJsonObject toCreate = new DynamicJsonObject();
toCreate["Name"] = FeatureToAdd.Name;
toCreate["Description"] = FeatureToAdd.Description;
// important to which this belongs, but I no ID I ever use works
//toCreate["Workspace"] = "/workspace/" + WebConfigurationManager.AppSettings["RallyAPIWorkspaceID"];
//toCreate["Project"] = "/project/XXXXX";
//toCreate["Iteration"] = "/iteration/XXXXXX";
// create feature - feature is under PortfolioItem
CreateResult createFeatureResult = restApi.Create("PortfolioItem/Feature", toCreate);
// scrape ID off the end of the reference
var pureId = createFeatureResult.Reference.Substring(createFeatureResult.Reference.LastIndexOf('/') + 1);
// add UserStories
foreach (UserStory u in FeatureToAdd.UserStories)
{
toCreate = new DynamicJsonObject();
toCreate["Name"] =u.Name;
toCreate["Description"] = u.Description;
toCreate["WorkProduct"] = "PortfolioItem/Feature/" + pureId;
//toCreate["WorkProduct"] = createFeatureResult.Reference;<- tried this too
// hierarchicalrequirement = UserStory
CreateResult createUserStoryResult = restApi.Create("hierarchicalrequirement", toCreate);
}
Running this creates both, but no association happens. I get a warning:
Ignored JSON element hierarchicalrequirement.WorkProduct during processing of this request.
Why did it arbitrarily ignore this?...
It ignored WorkProduct because WorkProduct is not a valid field on HierarchicalRequirement. The field you want to specify to set the feature parent of a story is called PortfolioItem.
toCreate["PortfolioItem"] = Ref.GetRelativeRef(createFeatureResult.Reference);
Also, object relationships are specified as in WSAPI as refs (/type/id) so you can just directly pass in the reference from the createFeatureResult.
Sorry you're finding the api to be frustrating. It definitely has some weird dark corners but once you use it a bit and get a feel for how the various domain objects are related I think you'll find it to be quite powerful and consistent.

Unloading AppDomain

Is there a way to unload parent AppDomain?
I am trying to load a different version of an assembly in my new AppDomain, but it keeps loading the version from the parent domain. When I am loading the assembly in the new AppDomain I am showing the correct path.
Or maybe there is another way I can do that?
Thanks in advance.
EDIT
AppDomain MailChimpDomain = AppDomain.CreateDomain("MailChimpDomain");
string path = AppDomain.CurrentDomain.BaseDirectory + "ServiceStack_V3\\ServiceStack.Text.dll";
MailChimpDomain.Load(AssemblyName.GetAssemblyName(path));
EDIT2
Code 2:
var MailDom = AppDomain.CreateDomain("MailChimpDomain");
MailDom.AssemblyLoad += MailDom_AssemblyLoad;
MailDom.AssemblyResolve += new ResolveEventHandler(MailDom_AssemblyResolve);
MailDom.DoCallBack(() =>
{
string name = #"ServiceStack.Text.dll";
var assembly = AppDomain.CurrentDomain.Load(name);
string name2 = #"MailChimp.dll";
var assembly2 = AppDomain.CurrentDomain.Load(name2);
//mailChimp object with API key found in mailChimp profile
MailChimp.MailChimpManager mc = new MailChimp.MailChimpManager("111111111111222f984b9b1288ddf6f0-us1");
//After this line there are both versions of ServiceStack.Text Assembly
MailChimp.Helper.EmailParameter em = new MailChimp.Helper.EmailParameter();
em.Email = strEmailTo;
//Creating email parameters
string CampaignName = "Digest for " + strEmailTo + " " + DateTime.Now.ToShortDateString();
MailChimp.Campaigns.CampaignCreateOptions opt = new MailChimp.Campaigns.CampaignCreateOptions();
opt.ListId = "l338dh";
opt.Subject = strSubject;
opt.FromEmail = strEmailFrom;
opt.FromName = strNameFrom;
opt.Title = CampaignName;
//creating email content
MailChimp.Campaigns.CampaignCreateContent content = new MailChimp.Campaigns.CampaignCreateContent();
content.HTML = strEmailContent;
//Creating new email and sending it
MailChimp.Campaigns.CampaignFilter par = null;
MailChimp.Campaigns.CampaignSegmentOptions SegOpt = null;
MailChimp.Campaigns.CampaignTypeOptions typeOpt = null;
mc.CreateCampaign("regular", opt, content, SegOpt, typeOpt);
MailChimp.Campaigns.CampaignListResult camp2 = mc.GetCampaigns(par, 0, 5, "create_time", "DESC");
foreach (var item in camp2.Data)
{
if (item.Title == CampaignName)
{
mc.SendCampaign(item.Id);
break;
}
}
});
static Assembly MailDom_AssemblyResolve(object sender, ResolveEventArgs args)
{
byte[] rawAssembly = File.ReadAllBytes(Path.Combine(path, args.Name));
return Assembly.Load(rawAssembly);
}
What your code actually does is it loads assembly to your parent domain. If you want to load assembly into child domain you have to do it from inside child domain. This is kind of chicken-egg problem, because the parent assembly (which loads child assembly into child domain) has to be loaded into your child domain as well in order to be executed.
Assuming simple example that you have console application and assembly called MyAssembly.dll it can be done like this:
static void Main(string[] args) {
var domain = AppDomain.CreateDomain("MailChimpDomain");
domain.AssemblyResolve +=new ResolveEventHandler(domain_AssemblyResolve);
domain.DoCallBack(() => {
string path = #"MyAssembly.dll";
var assembly = AppDomain.CurrentDomain.Load(path);
// to do something with the assembly
var type = assembly.GetType("MailChimp.MailChimpManager");
var ctor = type.GetConstructor(new[] { typeof(string) });
var mc = ctor.Invoke(new object[] { "111111111111222f984b9b1288ddf6f0" });
});
}
static Assembly domain_AssemblyResolve(object sender, ResolveEventArgs args) {
byte[] rawAssembly = File.ReadAllBytes(Path.Combine(#"c:\MyAssemblyPath", args.Name));
return Assembly.Load(rawAssembly);
}
In this case child domain has same root directory for resolving assemblies as parent domain (and thus it can execute the code which loads "MyAssembly.dll").
If the code working with reflection is longer than this, you may consider using bootstrapper instead.
I.E. you create new library called MyBootstrapper.dll, you'll reference directly version of ServiceStack.Text.dll and MailChimp.dll you like from MyBootstrapper.dll, and you'll create bootstrap class - lets call it Bootstrapper that will be static and will have single public static method called Run that will do dirty work.
Then inside DoCallBack() method you'll call this bootstrapper instead.
string path = #"MyBootstrapper.dll";
var assembly = AppDomain.CurrentDomain.Load(path);
// to do something with the assembly
var type = assembly.GetType("MyBootstrapper.Bootstrapper");
var method = type.GetMethod("Run", BindingFlags.Static);
method.Invoke(null, null);
// or if the Run method has one parameter of "string" type
var method = type.GetMethod("Run", BindingFlags.Static, Type.DefaultBinder, new[] { typeof(string) }, null);
method.Invoke(null, new object[] { "Parameter to run" });
No, you may not unload the default AppDomain or any assemblies loaded in the default AppDomain.
What you can do, however, is load both versions of the ServiceStack assembly in two child domains. You should be able to unload either of them. However, using types from these domains may prove more difficult than usual. You'll have to do it via remoting.
Given the overhead imposed by this, you should consider using only one version of that assembly (even if this means adapting part of your app, the one that runs in the default domain).

Modify programatically csproj files with Microsoft.Build.Evaluation (instead of Engine)

I would like to read, modify and write back csproj files.
I've found this code, but unfortunately Engine class is depreciated.
Engine engine = new Engine()
Project project = new Project(engine);
project.Load("myproject.csproj");
project.SetProperty("SignAssembly", "true");
project.Save("myproject.csproj");
So I've continued based on the hint I should use Evaluation.ProjectCollection instead of Engine:
var collection = new ProjectCollection();
collection.DefaultToolsVersion = "4.0";
var project = new Project(collection);
// project.Load("myproject.csproj") There is NO Load method :-(
project.FullPath = "myproject.csproj"; // Instead of load? Does nothing...
// ... modify the project
project.Save(); // Interestingly there is a Save() method
There is no Load method anymore. I've tried to set the property FullPath, but the project still seems empty. Missed I something?
(Please note I do know that the .csproj file is a standard XML file with XSD schema and I know that we could read/write it by using XDocument or XmlDocument. That's a backup plan. Just seeing the .Save() method on the Project class I think I missed something if I can not load an existing .csproj. thx)
I've actually found the answer, hopefully will help others:
Instead of creating a new Project(...) and trying to .Load(...) it, we should use a factory method of the ProjectCollection class.
// Instead of:
// var project = new Project(collection);
// project.FullPath = "myproject.csproj"; // Instead of load? Does nothing...
// use this:
var project = collection.LoadProject("myproject.csproj")
Since i can't comment:
This won't work in .net core without first setting the MSBuild.exe path variable. The code to do so can be found here
https://blog.rsuter.com/missing-sdk-when-using-the-microsoft-build-package-in-net-core/
and is written here
private static void SetMsBuildExePath()
{
try
{
var startInfo = new ProcessStartInfo("dotnet", "--list-sdks")
{
RedirectStandardOutput = true
};
var process = Process.Start(startInfo);
process.WaitForExit(1000);
var output = process.StandardOutput.ReadToEnd();
var sdkPaths = Regex.Matches(output, "([0-9]+.[0-9]+.[0-9]+) \\[(.*)\\]")
.OfType<Match>()
.Select(m => System.IO.Path.Combine(m.Groups[2].Value, m.Groups[1].Value, "MSBuild.dll"));
var sdkPath = sdkPaths.Last();
Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", sdkPath);
}
catch (Exception exception)
{
Console.Write("Could not set MSBUILD_EXE_PATH: " + exception);
}
}

Upgraded dotLess error: IImporter does not contain definition for 'Paths'

I have the following code in a project I try to upgrade the "dotless" NuGet package from the "1.2.2.0" to the latest (at moment "1.4.0.0"):
private void GetStylesheetContent(HttpContext context, string name)
{
var conf = BundleConfigSectionHandler.GetConfig();
var elt = conf.Stylesheets.GetBundle(name);
if (elt != null) {
Minifier minifier = null;
if (_conf.Stylesheets.Minify) {
minifier = new Minifier();
}
var files = elt.ListFiles();
var existingFiles = new List<string>();
StringBuilder buffer = new StringBuilder();
foreach (var file in files) {
var physicalFile = context.Request.MapPath(file);
if (File.Exists(physicalFile)) {
existingFiles.Add(physicalFile);
string content;
var path = VirtualPathUtility.GetDirectory(file);
if (file.EndsWith(".less", StringComparison.OrdinalIgnoreCase))
{
var reader = new dotless.Core.Input.VirtualFileReader();
var localpath = VirtualPathUtility.ToAbsolute(file);
content = reader.GetFileContents(localpath);
var parse = new Parser();
parse.Importer = new Importer(reader);
/*Error>*/ parse.Importer.Paths.Add(VirtualPathUtility.ToAbsolute(path));
var eng = new LessEngine(parse);
content = eng.TransformToCss(content, localpath);
The error is on the third line from bottom. It says:
Error 417 'dotless.Core.Importers.IImporter' does not contain a
definition for 'Paths' and no extension method 'Paths' accepting a
first argument of type 'dotless.Core.Importers.IImporter' could be
found (are you missing a using directive or an assembly reference?)
Is a pity that the team didn't left the old method with an [Obsolete] attribute and suggestion to upgrade.
Does anyone know how to replace the "Importer.Paths.Add" method ?
I'm not that familiar with the inner workings of dotless. But looking at the source code for the Importer. paths has been protected since version version 1.2.3. Looking around at the class a little more, it seems like you need to use an instance of dotless.Parser.Tree.Import to add your paths manually.
It does look like this is pretty far off the normal dotless path. So it wouldn't surprise me if the API is a little unstable in these areas. You also may want to look at how bundling works in a question like How to use ASP.Net MVC 4 to Bundle LESS files in Release mode? to see how they handle all the dotless classes.

Creating an Ektron Smart Form Definition via the API

I've been trying to create a smart form definition from another application. The app successfully creates the smart form, but I'm unable to get the FieldList, DisplayXSLT or Schema fields to populate.
This leaves me with a blank smart form definition (less that ideal).
Here's the code I have to perform the action. Any ideas?
// form is a simple POCO with values copied from an existing SmartForm Definition
var config = new SmartFormConfigurationData();
config.SmartformTitle = form.Name;
config.SmartformDescription = form.Description;
config.XmlSchema = form.Schema;
config.PackageDisplayXslt = form.Xslt;
config.FieldList = form.FieldList;
config.Type = EkEnumeration.XmlConfigType.Content;
var api = new SmartFormConfigurationManager(ApiAccessMode.Admin);
api.RequestInformation.ServicesPath = this.EktronServiceHost;
api.RequestInformation.AuthenticationToken = this.GetAdminAuthToken();
api.Add(config);
Update:
I heard back from Ektron Support on this issue. It's not so much a "bug" per-se... It's more a case of this API class looking very similar to the ContentManager but not behaving like it. I expected that since it looked so similar to ContentManager and many of the other classes, I would be able to call Add() and it would just magically work. It turns out the solution is a little more complicated.
Adding a smartform is a two-step process: first Add(), then Update().
The Add method doesn't set all of the fields and in fact passes in NULL for a few of the parameters on the stored procedure that creates the entry in xml_collection_tbl.
The real fun comes in step 2. Basically, you start with the SmartForm's HTML -- the stuff you see when you're in the "Data Design" view for editing the smart form definition and you click that <> button ("HTML") at the bottom of the editor. You run that through a whole bunch of XSLTs that are burried in the WorkArea folder to construct the missing fields, and then you call update. Here's the code that worked for me:
var sfManager = new SmartFormConfigurationManager();
var data = sfManager.GetItem(12);
if (data == null) return;
var sfcData = new SmartFormConfigurationData();
sfcData.Type = EkEnumeration.XmlConfigType.Content;
sfcData.SmartformTitle = "SmartForm12 copy";
sfcData.SmartformDescription = "SmartForm12 copy";
sfcData.XmlSchema = "";
sfcData.PackageDisplayXslt = data.PackageDisplayXslt;
sfcData.FieldList = data.FieldList;
sfcData = sfManager.Add(sfcData);
Response.Write("SmartForm added with id: " + sfcData.Id);
var design = System.IO.File.ReadAllText(Server.MapPath("~/NewsArticleSmartForm.html"));
var contentApi = new ContentAPI();
var schema = contentApi.TransformXsltPackage(design, Server.MapPath("~/WorkArea/ContentDesigner/DesignToSchema.xslt"), true);
var fieldList = contentApi.TransformXsltPackage(design, Server.MapPath("~/WorkArea/ContentDesigner/DesignToFieldList.xslt"), true);
var viewEntryXslt = contentApi.TransformXsltPackage(design, Server.MapPath("~/WorkArea/ContentDesigner/DesignToEntryXSLT.xslt"), true);
var xsltArgs = new XsltArgumentList();
xsltArgs.AddParam("srcPath", "", "/WorkArea/ContentDesigner/");
var viewXsltSource = string.Concat("<root>", design, "<ektdesignpackage_list>", fieldList, "</ektdesignpackage_list></root>");
var viewXslt = contentApi.XSLTransform(viewXsltSource, Server.MapPath("~/WorkArea/ContentDesigner/DesignToViewXSLT.xslt"), true, false, xsltArgs, false);
var initialDocument = contentApi.TransformXsltPackage(design, Server.MapPath("~/WorkArea/ContentDesigner/PresentationToData.xslt"), true);
var sbPackage = new StringBuilder("<ektdesignpackage_forms><ektdesignpackage_form><ektdesignpackage_designs><ektdesignpackage_design>");
sbPackage.Append(design);
sbPackage.Append("</ektdesignpackage_design></ektdesignpackage_designs><ektdesignpackage_schemas><ektdesignpackage_schema>");
sbPackage.Append(schema);
sbPackage.Append("</ektdesignpackage_schema></ektdesignpackage_schemas><ektdesignpackage_lists><ektdesignpackage_list>");
sbPackage.Append(fieldList);
sbPackage.Append("</ektdesignpackage_list></ektdesignpackage_lists><ektdesignpackage_views><ektdesignpackage_view>");
sbPackage.Append(viewEntryXslt);
sbPackage.Append("</ektdesignpackage_view><ektdesignpackage_view>");
sbPackage.Append(viewXslt);
sbPackage.Append("</ektdesignpackage_view></ektdesignpackage_views><ektdesignpackage_initialDocuments><ektdesignpackage_initialDocument>");
sbPackage.Append(initialDocument);
sbPackage.Append("</ektdesignpackage_initialDocument></ektdesignpackage_initialDocuments></ektdesignpackage_form></ektdesignpackage_forms>");
sfcData.PackageXslt = sbPackage.ToString();
sfcData.FieldList = fieldList;
var baseFilename = "SmartForm" + sfcData.Id;
var schemaFilename = "/" + baseFilename + "Schema.xsd";
var xsltFilename = "/" + baseFilename + "Xslt.xslt";
sfcData.XmlSchema = schemaFilename; // The file will be prefixed with /XmlFiles
var unPackDisplayXslt = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"><xsl:output method=\"xml\" version=\"1.0\" encoding=\"UTF-8\" indent=\"yes\"/><xsl:template match=\"/\"><xsl:choose><xsl:when test=\"ektdesignpackage_forms/ektdesignpackage_form[1]/ektdesignpackage_views/ektdesignpackage_view[2]\"><xsl:copy-of select=\"ektdesignpackage_forms/ektdesignpackage_form[1]/ektdesignpackage_views/ektdesignpackage_view[2]/node()\"/></xsl:when><xsl:otherwise><xsl:copy-of select=\"ektdesignpackage_forms/ektdesignpackage_form[1]/ektdesignpackage_views/ektdesignpackage_view[1]/node()\"/></xsl:otherwise></xsl:choose></xsl:template></xsl:stylesheet>";
var displayXslt = contentApi.TransformXsltPackage(sbPackage.ToString(), unPackDisplayXslt, false);
System.IO.File.WriteAllText(Server.MapPath("~/XmlFiles" + xsltFilename), displayXslt);
sfcData.Xslt1 = xsltFilename; // The file will be prefixed with /XmlFiles
sfcData.DefaultXslt = 1;
sfManager.Update(sfcData);
I extracted the HTML for my existing smart form and saved in the root of my site as NewsArticleSmartForm.html. Here's what my file looked like:
<p>Author: <input type="text" name="Author" id="Author" ektdesignns_caption="Author" ektdesignns_name="Author" title="Author" ektdesignns_indexed="false" ektdesignns_nodetype="element" size="24" class="design_textfield" /></p>
<p> </p>
<p>Article Summary: <textarea name="Summary" id="Summary" ektdesignns_caption="Summary" ektdesignns_name="Summary" title="Summary" ektdesignns_indexed="false" ektdesignns_nodetype="element" cols="40" rows="3" class="design_textfield"></textarea>
</p>
<p> </p>
<p>Article Body:</p>
<p> </p>
<ektdesignns_richarea id="Body" name="Body" ektdesignns_caption="Body" ektdesignns_name="Body" title="Body" ektdesignns_indexed="false" ektdesignns_nodetype="element"> </ektdesignns_richarea>
<p> </p>
Good luck!
Original Answer:
Creating a copy of a SmartForm configuration should be fairly straight-forward. The SmartFormConfigurationData object has a Clone() method on it which makes it really easy to create a copy of an existing SmartForm. I say "should be" because it doesn't work.
I had an answer all typed out ready to post; I tried some code and it appeared to work. The new smartform was listed in the workarea, but when I clicked on that new smartform to view its details, I realized something was wrong.
I tried the following code:
var sfManager = new SmartFormConfigurationManager();
var config = sfManager.GetItem(7);
if (config == null) return;
var newSmartForm = config.Clone();
newSmartForm.SmartformTitle += " copy";
sfManager.Add(newSmartForm);
Here are the details from the original smartform:
And here's what the new smartform looked like -- the one I created with the frameworkAPI:
I did find one API method that successfully created a copy of an existing smartform:
var sfApi = new Ektron.Cms.ContentAPI();
var newId = sfApi.ReplicateXmlConfiguration(7, "copied sf title");
The problem with that method is that the smartform must exist on your system and you can only change the title. So, I went digging into the database to see what was happening. It turns out that after calling this ReplicateXmlConfiguration() method, the two database records are identical (except for the expected LastUpdated type of fields).
After calling the frameworkAPI's Update() method (same holds true when calling Add()), the "updated" record is clearly different.
I think we've got a genuine bug here. This happens both in v8.7 sp2 and v9.0 sp1. I've opened a case with Ektron support, and I'll update my answer as I hear back. They have always been very responsive when I've dealt with them in the past.

Categories

Resources