Subsonic Access To App.Config Connection Strings From Referenced DLL in Powershell Script - powershell

I've got a DLL that contains Subsonic-generated and augmented code to access a data model. Actually, it is a merged DLL of that original assembly, Subsonic itself and a few other referenced DLL's into a single assembly, called "PowershellDataAccess.dll. However, it should be noted that I've also tried this referencing each assembly individually in the script as well and that doesn't work either.
I am then attempting to use the objects and methods in that assembly. In this case, I'm accessing a class that uses Subsonic to load a bunch of records and creates a Lucene index from those records.
The problem I'm running into is that the call into the Subsonic method to retrieve data from the database says it can't find the connection string. I'm pointing the AppDomain at the appropriate config file which does contain that connection string, by name.
Here's the script.
$ScriptDir = Get-Location
[System.IO.Directory]::SetCurrentDirectory($ScriptDir)
[Reflection.Assembly]::LoadFrom("PowershellDataAccess.dll")
[System.AppDomain]::CurrentDomain.SetData("APP_CONFIG_FILE", "$ScriptDir\App.config")
$indexer = New-Object LuceneIndexingEngine.LuceneIndexGenerator
$indexer.GeneratePageTemplateIndex("PageTemplateIndex");
I went digging into Subsonic itself and the following line in Subsonic is what's looking for the connection string and throwing the exception:
ConfigurationManager.ConnectionStrings[connectionStringName]
So, out of curiosity, I created an assembly with a single class that has a single property that just runs that one line to retrieve the connection string name.
I created a ps1 that called that assembly and hit that property. That prototype can find the connection string just fine.
Anyone have any idea why Subsonic's portion can't seem to see the connection strings?

Did you add the System.Configuration assembly to your PowerShell session? The following works for me:
PS> gc .\app.config
<?xml version='1.0' encoding='utf-8'?>
<configuration>
<connectionStrings>
<clear />
<add name="Name"
providerName="System.Data.ProviderName"
connectionString="Valid Connection String;" />
</connectionStrings>
</configuration>
PS> [appdomain]::CurrentDomain.SetData("APP_CONFIG_FILE", "$home\app.config")
PS> Add-Type -AssemblyName System.Configuration
PS> [Configuration.ConfigurationManager]::ConnectionStrings['Name']
Name : Name
ConnectionString : Valid Connection String;
...

Related

Change all Web.*.config files in first stage in release pipeline

I need to change the items inside appsettings on all the Web.*.config files in first stage step. That is I can't do transformation in every step in release pipeline. The reason is that I use Episerver DXC/DXP.
I have 4 stages; "Upload Package", "Integration", "Preproduction", and "Production".
The values is stored i Azure Key Vault.
Is there any smart way to do this?
Did you read the guide on config transforms for DXC? https://world.episerver.com/digital-experience-cloud-service/development-considerations/environment-configurations/
If File transformation is not suitable for your project, what about using powershell script to do the item change?
Sample:
Here is my example web.product.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="service.tasks" type="HRNetTaskService.TaskConfigurationSection, TaskService" />
</configSections>
<connectionStrings>
<add name="Production" connectionString="xxxx" providerName="System.Data.SqlClient" />
</connectionStrings>
<appSettings>
<add key="RestServiceUrl" value="https://sample.net" />
</appSettings>
</configuration>
Now I want to update the connectionString of .config file. Add replace.ps1 into repos with below scripts, then call this replace.ps1 file in Powershell task via passing corresponding dynamic value:
Param(
[string]$source,
[string]$connectionstring
)
$webConfig = $source
$doc = (Get-Content $webConfig) -as [Xml]
$root = $doc.get_DocumentElement();
$activeConnection = $root.connectionStrings.SelectNodes("add");
$activeConnection.SetAttribute("connectionString", $connectionstring);
$doc.Save($webConfig)
Here $(ProductValue) is the variable that you configured in Azure key vault. Its call way is same with the pipeline variable. Just you need link the Azure key vault into azure devops, then combine it with Variable group.
What I was trying to do was replace variables in config files from Azure Key Vault before transformation on config files because it can't be done (at this point) during the release pipeline when using Episerver DXC. What I did was replacing them during the build pipeline instead.
Made the variable substitution in Powershell during the build pipeline. Import the Key Vault secrets as separate task before the Powershell task, list all the one I would use as environment variables in the Powershell task.
The environment variables I named the same as the one it should replace in the config files (ex SomeApiKey_Integration). Go through the config files, look for two anything between two double underscores and replace them with value from the environment variable ((Get-ChildItem $variable).Value).
In the config files and environment variable they are named as previous stated, SomeApiKey_Integration. Key Vault name and environment variable value as SomeApiKey-Integration.

Update Build number in App config xml file on build pipeline

I have a build pipeline in Azure DevOps, I need to update the build number in my apconfig exe file that will be $(Build.BuildNumber).
I just tried this way:
Adding a variable name = BuildNumber value = $(Build.BuildNumber).
And in my apconfig.exe file have a key same like <add key="BuildNumber" value="1812201901" />.
Why I have tried like this way: thinking like it will update in the config file if variable name match with the key.
But it is not working. can anyone please help? I have just started in CI/CD.
Update Build number in App config xml file on build pipeline
Just like the Shayki said, using the Replace Tokens extension should be the directly way to resolve this issue.
But since you need to request to get this extension, as workaround, you could also use power shell scripts to resolve this issue, you can check below my test powershell scripts:
$currentDirectory = [IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path)
$appConfigFile = [IO.Path]::Combine($currentDirectory, 'App.config')
$appConfig = New-Object XML
$appConfig.Load($appConfigFile)
foreach($BuildNumber in $appConfig.configuration.add)
{
'name: ' + $BuildNumber.name
'BuildNumber: ' + $BuildNumber.value
$BuildNumber.value = '123456789'
}
$appConfig.Save($appConfigFile)
As result, the app.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<add key="BuildNumber" value="123456789" />
</configuration>
Note: Set the powershell scripts at the same folder of the app.config file.
Hope this helps.
You can use the Replace Tokens extension and in the apconfig.exe file put this:
<add key="BuildNumber" value="__BuildNumber__" />
Configure the task to search variables with __ prefix and suffix:
Now the value will be replaced with the value of the BuildNumber variable you configured (equal to Build.BuildNumber).

using migrate.exe from powershell script

As part of my continuous integration flow I need to apply the latest migration generated by EntityFramework to a database.
After some research I learned I can use the following script to accomplish that.
#copy migrate.exe to path\to\project\binfolder
Copy-Item packages\EntityFramework*\tools\migrate.exe path\to\project\binFolder
#apply latest migration
path\to\project\binFolder\migrate.exe pathto\projectGenerated.dll /startupConfigurationFile = "pathTo\Web.config"
I am saving this in a file and using power shell to run it.
I have two connection strings inside the web.config file like the following
<connectionStrings>
<add name="firstConnName" connectionString="connectionstring" providerName="System.Data.SqlClient" />
<add name="secondConnName" connectionString="connectionstring" providerName="System.Data.SqlClient" />
And in code I have the following so that entity framework uses secondConnName
public partial class myContext : DbContext
{
static myContext()
{
Database.SetInitializer<repreeContext>(null);
}
public myContext()
: base("Name=secondConnName")
{
Configuration.ProxyCreationEnabled = false;
}
...
I ran the powershell script above and this is the error i am getting
System.Data.Entity.Migrations.Design.ToolingException: No connection string named 'secondConnName' could be fo
und in the application config file.
at System.Data.Entity.Migrations.Design.ToolingFacade.Run(BaseRunner runner)
at System.Data.Entity.Migrations.Design.ToolingFacade.Update(String targetMigration, Boolean force)
at System.Data.Entity.Migrations.Console.Program.Run()
at System.Data.Entity.Migrations.Console.Program.Main(String[] args)
ERROR: No connection string named 'secondConnName' could be found in the
application config file.
Using EntityFramework 5.0, and Powershell 4.0
Any idea where I went wrong?
Thanks for the help

Publish SSRS by Octopus

I'm building the set up to deploy my SSRS reports through Octopus Deploy, I found out one Octopus Library and I'm working on it, but I've had some issues:
1º ---- Message error: (The path is alright, but it keeps with the same warning)
WARNING: Unable to find datasource SalesDrivers in /Sales Drivers/Data Sources
2º ---- The method doesn't exist
Method invocation failed because [Microsoft.PowerShell.Commands.NewWebserviceProxy.AutogeneratedTypes.WebServiceProxy3er_ReportService2005_asmx_wsdl.ReportingService2005] doesn't contain a method named 'LoadReportDefinition'.
The powershell function from the template\library that is throwing the error can been seen below:
#region Update-ReportParamters()
Function Update-ReportParameters($ReportFile)
{
# declare local variables
$ReportParameters = #();
# necessary so that when attempting to use the report execution service, it doesn't puke on you when it can't find the data source
$ReportData = (Remove-SharedReferences -ReportFile $ReportFile)
# get just the report name
$ReportName = $ReportFile.SubString($ReportFile.LastIndexOf("\") + 1)
$ReportName = $ReportName.SubString(0, $ReportName.IndexOf("."))
# create warnings object
$ReportExecutionWarnings = $null
# load the report definition
Write-Host "*********************************************"
#Write-Host $ReportData
#(Remove-SharedReferences -ReportFile $ReportFile)
#Write-Host $ReportExecutionWarnings
$ExecutionInfo = $ReportExecutionProxy.LoadReportDefinition($ReportData, [ref] $ReportExecutionWarnings);
# loop through the report execution parameters
foreach($Parameter in $ExecutionInfo.Parameters)
{
# create new item parameter object
$ItemParameter = New-Object "$ReportServerProxyNamespace.ItemParameter";
# fill in the properties except valid values, that one needs special processing
Copy-ObjectProperties -SourceObject $Parameter -TargetObject $ItemParameter;
# fill in the valid values
$ItemParameter.ValidValues = Convert-ValidValues -SourceValidValues $Parameter.ValidValues;
# add to list
$ReportParameters += $ItemParameter;
}
# force the parameters to update
Write-Host "Updating report parameters for $ReportFolder/$ReportName"
if ($IsReportService2005) {
$ReportServerProxy.SetReportParameters("$ReportFolder/$ReportName", $ReportParameters);
}
elseif ($IsReportService2010) {
$ReportServerProxy.SetItemParameters("$ReportFolder/$ReportName", $ReportParameters);
}
else { Write-Warning 'Report Service Unknown in Update-ReportParameters method. Use ReportService2005 or ReportService2010.' }
}
Anyone knows how I could sort it out?
I have solved a similar problem but took a slightly different approach. Rather than using powershell and octopus directly I used the useful open source tool RSBuild to deploy the reports. It is pretty easy to bundle up the rsbuild.exe executable (it is tiny) and a deploy.config along with your reports inside the octopus package. Then you can use octopus's substitution feature to rewrite the config file and Powershell function to execute the executable. This also has the advantage that you can deploy easily without octopus, the config for data sources and reports is declarative in XML rather than procedural in Powershell and the smarts of your scripted deployment can live alongside your reports rather than buried in Octopus.
So my config looks a bit like:
<?xml version="1.0" encoding="utf-8" ?>
<Settings>
<Globals>
<Global Name="CollapsedHeight">0.5in</Global>
</Globals>
<ReportServers>
<ReportServer Name="RS1" Protocol="http" Host="${ReportServer}" Path="${ReportServerPath}" Timeout="30" />
</ReportServers>
<DataSources>
<DataSource Name="Source1" Publish="true" Overwrite="true" TargetFolder="Data Sources" ReportServer="RS1">
<ConnectionString>data source=${ReportServer};initial catalog=${DatabaseName}</ConnectionString>
<CredentialRetrieval>Store</CredentialRetrieval>
<WindowsCredentials>False</WindowsCredentials>
<UserName>${RepotrUser}</UserName>
<Password>${ReportsPassword}</Password>
</DataSource>
</DataSources>
<Reports>
<ReportGroup Name="Details" DataSourceName="Source1" TargetFolder="Reports"
ReportServer="RS1" CacheTime="10080">
<Report Name="BusinessReportABC">
<FilePath>reports\BusinessReportABC.rdl</FilePath>
</Report>
<!--More reports here-->
</ReportGroup>
</Reports>
</Settings>
My deployed octopacked artefacts contain RSBuild.Core.dll, RSBuild.exe, deploy.config and the reports files
Then I simply call the executable using powershell:
PS> rsbuild deploy.config

How to write AssemblyVersion to file using MSBuild?

FinalEdit: Despite relative directories not working in the first post, it worked if I simply removed the $(MsBuildThisFileDirectory) from the Exec line.
Edit2: I added the new targets to the DefaultTargets. Which now runs them by default. However, timing was now off with the postbuild command. I added <Exec Command="call $(MsBuildThisFileDirectory)documentation\tools\GenerateDocumentation.bat" IgnoreExitCode="false" /> to the target, but it gives an error that C:\Users\my is not a valid batch file because of the space which is actually C:\Users\my program\documentation\tools\GenerateDocumentation.bat. Putting quotes around the path gives me error MSB4025 that Name cannot begin with $.
Edit: I have tried stijn's code and it works when I explicitly run it from the command line using /t:RetrieveIdentities, but for some reason it doesn't seem to run otherwise.
I have been using Doxygen to generate documentation for my source code, however, I would like to be able to do it automatically. I wrote a simple .bat script to run Doxygen with my desired config file and compile the output into a .chm help file, but I have been unable to change the revision number automatically in Doxygen.
I was attempting to simply update the config file by adding a new line to the config file with the new revision number using MSBuild, but I have been unable to get anything to print or even create a new file when none is present.
The code I have so far I have gotten from other similar questions, but I cannot seem to get it to work.
<ItemGroup>
<MyTextFile Include="\documentation\DoxygenConfigFile.doxyconfig"/>
<MyItems Include="PROJECT_NUMBER = %(MyAssemblyIdentitiesAssemblyInfo.Version)"/>
</ItemGroup>
<Target Name="RetrieveIdentities">
<GetAssemblyIdentity AssemblyFiles="bin\foo.exe">
<Output TaskParameter="Assemblies" ItemName="MyAssemblyIdentities"/>
</GetAssemblyIdentity>
<WriteLinesToFile File="#(MyTextFile)" Lines="#(MyItems)" Overwrite="false" Encoding="UTF8" />
</Target>
Encoding is wrong, it should be UTF-8
When working with items/properties, the % and # and $ must come right before the (, no spacing in between: %(MyAssemblyIdentitiesAssemblyInfo.Version)
MyAssemblyIdentitiesAssemblyInfo does not exist, you probably meant MyAssemblyIdentities
Look up how msbuild evaluates properties and items. Basically what it will do in your script is evaluate MyItems, but at that time MyAssemblyIdentities does not yet exist so is empty, and only afterwards the GetAssemblyIdentity gets executed. Fix this by enforcing correct evaluation order: put your items inside the target and make it depend on another target that creates MyAssemblyIdentities before evaluating your items.
To summarize:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="GetAssemblyIdentities">
<GetAssemblyIdentity AssemblyFiles="bin\foo.exe">
<Output TaskParameter="Assemblies" ItemName="MyAssemblyIdentities"/>
</GetAssemblyIdentity>
</Target>
<Target Name="RetrieveIdentities" DependsOnTargets="GetAssemblyIdentities">
<ItemGroup>
<MyTextFile Include="\documentation\DoxygenConfigFile.doxyconfig"/>
<MyItems Include="PROJECT_NUMBER = %(MyAssemblyIdentities.Version)"/>
</ItemGroup>
<WriteLinesToFile File="#(MyTextFile)" Lines="#(MyItems)"
Overwrite="false" Encoding="UTF-8" />
</Target>
</Project>
Note this will only work if you invoke msbuild in the directory where the script is, else the paths (documentation/foo) will be wrong. That could be fixed by using eg $(MsBuildThisFileDirectory)\bin\foo.exe)