ms Build engine find existing item or remove duplicate items - msbuild

I am currently working on an application that generates C# code files and adds them to an existing project. To edit the project I'm using Microsoft.Build.BuildEngine, and loading the exisiting .csproj file into the Project class.
csproj = new Project(new Engine(),"3.5");
csproj.Load("/myproject.csproj");
After wich I am able to add refeneces and compile items as I need. However I do not know if the files or references are already in the .csproj file, so currently they get added multiple times, which I do not want.
for example:
<Reference Include="System" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Core" />
Does anyone know of a way I can check if a builditem exists before I add it to the project? Or of a way to remove duplicated builditems after I've added them?

You could iterate through the EvaluatedItems of your Project Object
This sample code demonstrates how to check builditem existence before adding it :
// Create a new Engine object.
Engine engine = new Engine(Environment.CurrentDirectory);
// Create a new Project object.
Project csproj = new Project(new Engine(),"3.5");
csproj.Load("/myproject.csproj");
BuildItemGroup referenceItemGroup;
var currentReferences = new HashSet<string>();
// Iterate through each ItemGroup in the Project.
foreach (BuildItemGroup ig in csproj.ItemGroups)
{
// Iterate through each Item in the ItemGroup.
foreach (BuildItem item in ig)
{
if (item.Name == "Reference")
{
currentReferences.Add(item.Include);
referenceItemGroup = ig;
}
}
}
// Add only non existing reference
if (!currentReferences.Contains("NewReferenceToAdd"))
{
if (referenceItemGroup != null)
{
referenceItemGroup.AddNewItem("Reference", "IncludeValue");
}
}
And this one how to remove duplicates item
// Create a new Engine object.
Engine engine = new Engine(Environment.CurrentDirectory);
// Create a new Project object.
Project csproj = new Project(new Engine(),"3.5");
csproj.Load("/myproject.csproj");
var currentReferences = new HashSet<string>();
// Iterate through each ItemGroup in the Project.
foreach (BuildItemGroup ig in csproj.ItemGroups)
{
var itemsToRemove = new List<BuildItem>;
// Iterate through each Item in the ItemGroup.
foreach (BuildItem item in ig)
{
if (item.Name == "Reference")
{
if (currentReferences.Contains(item.Include))
{
itemsToRemove.Add(item);
}
else
{
currentReferences.Add(item.Include);
}
}
}
// Remove duplicate items
foreach (BuildItem itemToRemove in itemsToRemove)
{
ig.RemoveItem(itemToRemove);
}
}

Related

Change name of cshtml file in ASP.NET Core RazorPages

My environment: ASP.NET Core 5 with RazorPages, Webpack 5.
In razor pages (.cshtml) that reference svg files, I want to inline them. This is something Webpack can do (via a plugin), but I'm not sure how to integrate these two tech stacks.
I could write templatised cshtml files, and populate them via webpack:
ContactUs.cshtml.cs
ContactUs.cshtml <------ read by webpack
ContactUs.generated.cshtml <------ generated by webpack
But then how do I force msbuild / aspnet to use the generated file (ContactUs.generated.cshtml) instead of the template file (ContactUs.cshtml) when building?
I suspect the answer is to use IPageRouteModelConvention but I'm unsure how.
(A dirty workaround is to instead use the filenames ContactUs.template.cshtml and ContactUs.cshtml but I prefer something like the above, as "generated" is clearer.)
UPDATE
To simplify the problem:
The compiler looks for Foo.cshtml.cs and Foo.cshtml.
How do I tell it to instead look for Foo.cshtml.cs and Foo.generated.cshtml?
When loading the app, the framework loads for you a set of PageRouteModels which is auto-generated from the razor page folders (by convention). Each such model contains a set of SelectorModel each one of which has an AttributeRouteModel. What you need to do is just modify that AttributeRouteModel.Template by removing the suffixed part from the auto-generated value.
You can create a custom IPageRouteModelConvention to target each PageRouteModel. However that way you cannot ensure the routes from being duplicated (because after modifying the AttributeRouteModel.Template, it may become duplicate with some other existing route). Unless you have to manage a shared set of route templates. Instead you can create a custom IPageRouteModelProvider. It provides all the PageRouteModels in one place so that you can modify & add or remove any. This way it's so convenient that you can support 2 razor pages in which one page is more prioritized over the other (e.g: you have Index.cshtml and Index.generated.cshtml and you want it to pick Index.generated.cshtml. If that generated view is not existed, the default Index.cshtml will be used).
So here is the detailed code:
public class SuffixedNamePageRouteModelProvider : IPageRouteModelProvider
{
public SuffixedNamePageRouteModelProvider(string pageNameSuffix, int order = 0)
{
_pageNameSuffixPattern = string.IsNullOrEmpty(pageNameSuffix) ? "" : $"\\.{Regex.Escape(pageNameSuffix)}$";
Order = order;
}
readonly string _pageNameSuffixPattern;
public int Order { get; }
public void OnProvidersExecuted(PageRouteModelProviderContext context)
{
}
public void OnProvidersExecuting(PageRouteModelProviderContext context)
{
if(_pageNameSuffixPattern == "") return;
var suffixedRoutes = context.RouteModels.Where(e => Regex.IsMatch(e.ViewEnginePath, _pageNameSuffixPattern)).ToList();
var overriddenRoutes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var route in suffixedRoutes)
{
//NOTE: this is not required to help it pick the right page we want.
//But it's necessary for other related code to work properly (e.g: link generation, ...)
//we need to update the "page" route data as well
route.RouteValues["page"] = Regex.Replace(route.RouteValues["page"], _pageNameSuffixPattern, "");
var overriddenRoute = Regex.Replace(route.ViewEnginePath, _pageNameSuffixPattern, "");
var isIndexRoute = overriddenRoute.EndsWith("/index", StringComparison.OrdinalIgnoreCase);
foreach (var selector in route.Selectors.Where(e => e.AttributeRouteModel?.Template != null))
{
var template = Regex.Replace(selector.AttributeRouteModel.Template, _pageNameSuffixPattern, "");
if (template != selector.AttributeRouteModel.Template)
{
selector.AttributeRouteModel.Template = template;
overriddenRoutes.Add($"/{template.TrimStart('/')}");
selector.AttributeRouteModel.SuppressLinkGeneration = isIndexRoute;
}
}
//Add another selector for routing to the same page from another path.
//Here we add the root path to select the index page
if (isIndexRoute)
{
var defaultTemplate = Regex.Replace(overriddenRoute, "/index$", "", RegexOptions.IgnoreCase);
route.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel() { Template = defaultTemplate }
});
}
}
//remove the overridden routes to avoid exception of duplicate routes
foreach (var route in context.RouteModels.Where(e => overriddenRoutes.Contains(e.ViewEnginePath)).ToList())
{
context.RouteModels.Remove(route);
}
}
}
Register the IPageRouteModelProvider in Startup.ConfigureServices:
services.AddSingleton<IPageRouteModelProvider>(new SuffixedNamePageRouteModelProvider("generated"));

How can I delete data from IFileListEntry?

I'm trying to delete datas from IFileListEntry, however I couldn't find out how to do it.
Would you like to look at below code and give me some approaches?. How can I clear or remove all FileListEntry?
========== razor code ==========
<BlazorInputFile.InputFile OnChange="FileUpload" />
========== cs code ==========
IFileListEntry[] fileList;
public async Task FileUpload(IFileListEntry[] files)
{
fileList = files;
var file = fileList.First();
if (file != null)
{
var ms = new MemoryStream();
await file.Data.CopyToAsync(ms);
}
}
You cannot. IFileListEntry is designed to give you access to the files the user selected for upload. If you don't want to process certain files, just don't.

Is it possible to read MSI properties from MSI files in chain before detect phase in bootstrapper?

In "Bundle.wxs" I have a "chain" with "MsiPackages" which contain "InstallConditions". In order for the user to decide which packages he/she wants installed/upgraded I would like to display properties found in them.
For instance, I want to read the property "ProductName" and "ProductVersion" in the "Property" table of every MSI in the chain and display it to the user next to a checkbox for every MSI in the chain. The checkbox is wired to the burn variable used in "InstallConditions".
But the problem is, it doesn't seem like I have access to the MSI files before the "Apply" step. They are not extracted from the Bootstrapper Application EXE before this step. So, my question is, Is there a way to load these values programatically in order to display them to the user before the Apply step? I could use variables and populate them myself with the values but this information is already in the MSI so this seems inefficient.
Is there a way to do this? Thanks for the help.
<Bundle>
<Variable Name="InstallProduct1" Type="string" Value="true" />
<Variable Name="ProductName1" Type="string" Value="My Product 1"/> <!-- Better way? -->
<Variable Name="ProductVersion1" Type="version" Value="1.2.3.4"/> <!-- Better way? -->
<Chain>
<MsiPackage SourceFile="my_product_1.msi"
InstallCondition="InstallProduct1">
</MsiPackage>
</Chain>
</Bundle>
WiX does generate a BootstrapperApplicationData.xml file which includes a lot of the information used to build the exe and is included in the files available at runtime. You can parse that file at runtime in order to access that metadata. Since the file, along with all of our assemblies and .msi files, are placed in a randomly-name temp folder, we can’t know ahead of time where the file will live, so we must use our assembly’s path to find it. You can then parse the XML to get the metadata.
I have a blog post with additional details here: https://www.wrightfully.com/part-3-of-writing-your-own-net-based-installer-with-wix-context-data/
In my case, I use the tag instead of '', so have some of the info available to me may not be there for you, so your experience may vary. I would suggest running a makeshift installer in debug mode and setting a breakpoint to inspect the contents of the XML in order to get a full list of what’s available.
Here’s an example of how I get data from the file in my ManagedBootstrapperApplication (c#). Note: in this example, my domain objects are MBAPrereqPackage, BundlePackage and PackageFeature, each of which take an XML node object in their constructor and further parse the data into the object’s properties.
const XNamespace ManifestNamespace = ( XNamespace) “http://schemas.microsoft.com/wix/2010/BootstrapperApplicationData” ;
public void Initialize()
{
//
// parse the ApplicationData to find included packages and features
//
var bundleManifestData = this.ApplicationData;
var bundleDisplayName = bundleManifestData
.Element(ManifestNamespace + “WixBundleProperties“ )
.Attribute( “DisplayName“)
.Value;
var mbaPrereqs = bundleManifestData.Descendants(ManifestNamespace + “WixMbaPrereqInformation“)
.Select(x => new MBAPrereqPackage(x))
.ToList();
//
//exclude the MBA prereq packages, such as the .Net 4 installer
//
var pkgs = bundleManifestData.Descendants(ManifestNamespace + “WixPackageProperties“)
.Select(x => new BundlePackage(x))
.Where(pkg => !mbaPrereqs.Any(preReq => preReq.PackageId == pkg.Id));
//
// Add the packages to a collection of BundlePackages
//
BundlePackages.AddRange(pkgs);
//
// check for features and associate them with their parent packages
//
var featureNodes = bundleManifestData.Descendants(ManifestNamespace + “WixPackageFeatureInfo“);
foreach ( var featureNode in featureNodes)
{
var feature = new PackageFeature(featureNode);
var parentPkg = BundlePackages.First(pkg => pkg.Id == feature.PackageId);
parentPkg.AllFeatures.Add(feature);
feature.Package = parentPkg;
}
}
///
/// Fetch BootstrapperApplicationData.xml and parse into XDocument.
///
public XElement ApplicationData
{
get
{
var workingFolder = Path.GetDirectoryName(this.GetType().Assembly.Location);
var bootstrapperDataFilePath = Path.Combine(workingFolder, “BootstrapperApplicationData.xml”);
using (var reader = new StreamReader(bootstrapperDataFilePath))
{
var xml = reader.ReadToEnd();
var xDoc = XDocument.Parse(xml);
return xDoc.Element(ManifestNamespace + “BootstrapperApplicationData“);
}
}
}

Building Content Project on the fly doesn't copy when Item is set to "none"

So I m building a tool that creates a ContentProject on the fly and then Builds it (so that it outputs Xnb's)
At the moment the problem is that files that are not to be compiled should be copied into the output directory if marked with CopyToOutputDirectory = 'always' or 'PreserveNewest' . If we were looking at the .contentProj that section for a file that shouldn't be built but should be copied would look like this
<ItemGroup>
<None Include="MyFile.file">
<Name>level1</Name>
<Importer>XmlImporter</Importer>
<Processor>PassThroughProcessor</Processor>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
However, I m building the conntent project on the fly so I need the following code to create the project and add the items
var projectPath = Path.Combine(buildDirectory, "content.contentproj");
_projectRootElement = ProjectRootElement.Create(projectPath);
_projectRootElement.AddImport("$(MSBuildExtensionsPath)\\Microsoft\\XNA Game Studio\\v4.0\\Microsoft.Xna.GameStudio.ContentPipeline.targets");
_contentProject = new Project(_projectRootElement);
_contentProject.SetProperty("XnaPlatform", "Windows");
_contentProject.SetProperty("XnaProfile", "HiDef");
_contentProject.SetProperty("XnaFrameworkVersion", "v4.0");
_contentProject.SetProperty("Configuration", "Debug");
_contentProject.SetProperty("OutputPath", _outputDirectory);
// Register any custom importers or processors.
foreach (string pipelineAssembly in PipelineAssemblies)
{
_contentProject.AddItem("Reference", pipelineAssembly);
}
// Hook up our custom error logger.
_errorLogger = new ErrorLogger();
_buildParameters = new BuildParameters(ProjectCollection.GlobalProjectCollection)
{
Loggers = new ILogger[] { _errorLogger, }
};
//.... removed code that is not required the following is code that adds each item to the project. In case of items that shoulndt compile I m using None (as in xml from project above)
var itemType = compile ? "Compile" : "None";
var items = _contentProject.AddItem(itemType, filename);
var item = items.SingleOrDefault(x => x.EvaluatedInclude.Equals(filename, StringComparison.InvariantCultureIgnoreCase));
item.SetMetadataValue("Link", Path.GetFileName(filename));
item.SetMetadataValue("Name", Path.GetFileNameWithoutExtension(filename));
if (!compile)
item.SetMetadataValue("CopyToOutputDirectory", "Always");
Finally the build code
BuildManager.DefaultBuildManager.BeginBuild(_buildParameters);
var request = new BuildRequestData(_contentProject.CreateProjectInstance(), new string[0]);
var submission = BuildManager.DefaultBuildManager.PendBuildRequest(request);
var execute = Task.Factory.StartNew(() => submission.ExecuteAsync(null, null), cancellationTokenSource.Token);
var endBuild = execute.ContinueWith(ant => BuildManager.DefaultBuildManager.EndBuild());
endBuild.Wait();
In BuildRequest the empty array takes parameters that are targets... I ve been going through many different targets that do different things, from building but not really outputing the files to copy dependency dlls into the main folder but not what I need
Cheers

Check if a file exists in the project in WinRT

I have a WinRT Metro project which displays images based on a selected item. However, some of the images selected will not exist. What I want to be able to do is trap the case where they don't exist and display an alternative.
Here is my code so far:
internal string GetMyImage(string imageDescription)
{
string myImage = string.Format("Assets/MyImages/{0}.jpg", imageDescription.Replace(" ", ""));
// Need to check here if the above asset actually exists
return myImage;
}
Example calls:
GetMyImage("First Picture");
GetMyImage("Second Picture");
So Assets/MyImages/SecondPicture.jpg exists, but Assets/MyImages/FirstPicture.jpg does not.
At first I thought of using the WinRT equivalent of File.Exists(), but there doesn't appear to be one. Without having to go to the extent of trying to open the file and catching an error, can I simply check if either the file exists, or the file exists in the project?
You could use GetFilesAsync from here to enumerate the existing files. This seems to make sense considering you have multiple files which might not exist.
Gets a list of all files in the current folder and its sub-folders. Files are filtered and sorted based on the specified CommonFileQuery.
var folder = await StorageFolder.GetFolderFromPathAsync("Assets/MyImages/");
var files = await folder.GetFilesAsync(CommonFileQuery.OrderByName);
var file = files.FirstOrDefault(x => x.Name == "fileName");
if (file != null)
{
//do stuff
}
Edit:
As #Filip Skakun pointed out, the resource manager has a resource mapping on which you can call ContainsKey which has the benefit of checking for qualified resources as well (i.e. localized, scaled etc).
Edit 2:
Windows 8.1 introduced a new method for getting files and folders:
var result = await ApplicationData.Current.LocalFolder.TryGetItemAsync("fileName") as IStorageFile;
if (result != null)
//file exists
else
//file doesn't exist
There's two ways you can handle it.
1) Catch the FileNotFoundException when trying to get the file:
Windows.Storage.StorageFolder installedLocation =
Windows.ApplicationModel.Package.Current.InstalledLocation;
try
{
// Don't forget to decorate your method or event with async when using await
var file = await installedLocation.GetFileAsync(fileName);
// Exception wasn't raised, therefore the file exists
System.Diagnostics.Debug.WriteLine("We have the file!");
}
catch (System.IO.FileNotFoundException fileNotFoundEx)
{
System.Diagnostics.Debug.WriteLine("File doesn't exist. Use default.");
}
catch (Exception ex)
{
// Handle unknown error
}
2) as mydogisbox recommends, using LINQ. Although the method I tested is slightly different:
Windows.Storage.StorageFolder installedLocation =
Windows.ApplicationModel.Package.Current.InstalledLocation;
var files = await installedLocation.GetFilesAsync(CommonFileQuery.OrderByName);
var file = files.FirstOrDefault(x => x.Name == fileName);
if (file != null)
{
System.Diagnostics.Debug.WriteLine("We have the file!");
}
else
{
System.Diagnostics.Debug.WriteLine("No File. Use default.");
}
BitmapImage has an ImageFailed event that fires if the image can't be loaded. This would let you try to load the original image, and then react if it's not there.
Of course, this requires that you instantiate the BitmapImage yourself, rather than just build the Uri.
Sample checking for resource availability for c++ /cx (tested with Windows Phone 8.1):
std::wstring resPath = L"Img/my.bmp";
std::wstring resKey = L"Files/" + resPath;
bool exists = Windows::ApplicationModel::Resources::Core::ResourceManager::Current->MainResourceMap->HasKey(ref new Platform::String(resKey.c_str()));