tisdag 28 maj 2013

Lagra custom property sträng som binär protobuf-net sträng istället för xml

Hej!

Som en reaktion på mitt föregående inlägg bestämde jag mig för att kika på alternativ för serialisering/deserialisering. Kom över följande "benchmark" (vetenskapligheten i inlägget orkar jag inte analysera).

http://stackoverflow.com/questions/3790728/performance-tests-of-serializations-used-by-wcf-bindings/3793091#3793091

Sagt och gjort, protobuf-net it is.

1. Börjar med att tanka ner dll från https://code.google.com/p/protobuf-net/

2. Följer instruktionerna och dekorerar min databärande klass med attributen [ProtoContract] & [ProtoMember(x)] (https://code.google.com/p/protobuf-net/wiki/GettingStarted)















3. Implementerar ToString() override och den statiska Parse-metoden som min custom property använder när objektet skrivs och läses:














4. Som synes använder jag mig av en statisk helper-klass för ändamålet:



5. Och nu ska det alltså bara vara att tuta å köra! Mindre dret i databasen ist krieg! Medger dock att läsbarheten försämras rejält vid t.ex. hantering av olika objekt-versioner. Men det är ett senare problem! :)






fredag 24 maj 2013

EPiServer Custom Property som lagras som longstring och stringDelayedLoadThreshold

Hej igen. Nej bloggen är inte död, den har bara idlat lite.

Stötte på en irriterande detalj i EPiServer 7 när jag lekte med en custom property (använder legacyeditorn tills jag orkat sätta mig in i Dojo-tänket) vars Data serialiserades till en sträng och lagrades i EPiServers db som en LongString.

Egenskapen fungerade utmärkt i redigeraläget och efter publicering av en sida. Ända tills en IISRESET av sidan gjordes. Då returnerade propertyn alltid null. Troligtvis nåt i cache-mekanismen som strular alltså.

Det visade sig härröra från ett filter för longstrings-egenskaper vars värden är för långa. Lite osäker på vad defaultvärdet är, men när jag satte attributet stringDelayedLoadThreshold="0" för noden siteSettings i EPiServer.config så försvann problemet. Drygt fel...

Länk till var jag hittade lösningen:
http://world.episerver.com/Modules/Forum/Pages/thread.aspx?id=65572&pageIndex=1#reply

Dokumentation för egenskapen:
http://world.episerver.com/Documentation/Class-library/?documentId=cms/7/89421fe0-ae9b-42fe-06d0-224bd9e4a528




torsdag 15 november 2012

EPiServer Powershell deploy script

Tjipp! som Bengt Alsterlind säger här i Värmland (iaf inte hört någon annan använda ordet).

Blivit mycket powershell för min del på sistone. Men måste säga att jag gillar språket mer och mer, ett klart fall framåt från gamla dos-batch. Här kommer ytterligare lite powershell:

Tänkte presentera ett litet förslag på hur ett EPiServer powershell deploy script kan se ut. Alltså ett script som kopierar visst innehåll från en Visual Studio projekt-katalog till en annan katalog i syfte att kunna zippa ihop innehållet och flänga upp på en testserver/prod.server. Har läst att det ska finnas färdiga såna här templates i EPiServer 7 (vet inte om det är MSBuild eller PS), men vet inte mer om detta just nu.

<Dicsclaimer>Observera att alla webbplatser har sina egna krav och behov och att detta script antagligen inte överensstämmer med just dina krav.</Disclaimer> :P


# DeployPackage.ps1
# 2012-11-14 Määts

# Example call:
# ./DeployPackage.ps1 -verbose -c "Release" -s "C:\Path\To\My\EPiServer\Project\" -d "C:\Path\To\My\Deploy\Folder\"

# Script params
param([switch]$verbose, [string]$configurationMode, [string]$sourceDir, [string]$destDir)

$verboseoutput = @"
Executing script with params:
configurationMode: $configurationMode
sourceDir:         $sourceDir
destDir:           $destDir
"@

# Handle switches
if ($verbose.IsPresent) {
    Write-Host $verboseoutput
    $VerbosePreference = "Continue"
} else {
    $VerbosePreference = "SilentlyContinue"
}
# Validate params
if (-NOT($sourceDir -eq $NULL) -and (Test-Path $sourceDir) -eq $FALSE) {
    Write-Host "ERROR: Parameter ""sourceDir"" does not appear to be a valid path... aborting."
    Write-Host $sourceDir
    break
}

if (-NOT($destDir -eq $NULL) -and (Test-Path $destDir) -eq $FALSE) {
    Write-Host "ERROR: Parameter ""destDir"" does not appear to be a valid path... aborting."
    Write-Host $destDir
    break
}
if ($configurationMode -eq $NULL)
{
    Write-Host "ERROR: Parameter ""configurationMode"" not set... aborting."
    break
}
if (([string]$configurationMode).ToLower() -ne "release") {
    Write-Host "INFO: Parameter ""configurationMode"" not set to release mode... aborting."
    break
}

# Declare function
function DeployPackage() {
<#
    .SYNOPSIS
    Creates an EPiServer site deployment package
   
    .DESCRIPTION
   
    .PARAMETER
   
    .EXAMPLE
   
    .INPUTS
   
    .OUTPUTS
#>

    # Function params
    param([string]$configurationMode, [string]$sourceDir, [string]$destDir)
       
    # Variables
    $dateString = (Get-Date -format "yyyy-MM-dd_HH.mm.ss") # String representation of current date that will be used when naming the deploy package
    $fileTypes = @("*.aspx","*.ascx","*.jpg","*.gif","*.png","*.asmx","*.resx","*.xml","*.jpg","*.js","*.css","*.dll",
                   "*.pdb","*.swf","*.txt","*.html","*.htm","*README*","*.php","*.pdn","*.master","*.xsd","*.fla",
                   "*.as","*.asax") # This is the filetypes that the script will copy into the deploy
    $excludeFolders = @("obj")
    $excludeFiles = @("EPiServerErrorLog.txt")
                 
    $newDestDir = "$destDir\DeployPackage_$dateString\"
   
    # Create new folder
    New-Item $newDestDir -type directory
 
    #Get all files to copy
    $filesToCopy = Get-ChildItem $sourceDir -recurse -include $fileTypes
   
    # Copy items
    foreach ($file in $filesToCopy)
    {
        $newFilePath = $file.Fullname.Replace($sourceDir,$newDestDir)
       
        # Create new file (unix touch) in order to maintain folder structure
        New-Item -ItemType File -Path $newFilePath -Force
       
        # Copy file (overwrite)
        Copy-Item $file.Fullname $newFilePath -Force
    }
   
    # Remove unwanted folders
    Get-ChildItem $newDestDir -include $excludeFolders -recurse | Where-Object { $_.PSIsContainer } | Foreach-Object { Remove-Item $_.Fullname -recurse -Force }
   
    # Remove unwanted files
    Get-ChildItem $newDestDir -recurse -include $excludeFiles -Force | Foreach-Object { Remove-Item $_.Fullname }
}

# Call function
DeployPackage -c $configurationMode -s $sourceDir -d $destDir


Kommentar:
För att köra scriptet slänger man in följande i post-build events i Visual Studio för EPiServer webb-projektet.

 Powershell.exe -file "$(SolutionDir)DeployPackage.ps1" -ExecutionPolicy Unrestricted -verbose -c "$(ConfigurationName)" -d "C:\Deploy" -s "$(ProjectDir)\"

 Mitt script ligger alltså i "Solutiondir"-roten. Jag skickar in $(ConfigurationName) eftersom jag enbart vill att en deploy mapp ska skapas när projektet byggs i release-mode. Slutligen skickar jag med aktuell projektkatalog och vart jag vill att deploy-mappen ska skapas.

Som synes har jag slarvat lite i scriptet och inte orkat skriva klart kommentarer. Erkänner också villigt att #Remove unwanted files/folders inte är speciellt snyggt utan hade kunnat filtrerats redan i kopieringsstadiet. Det hade också varit snyggt om jag kunde avslutat med att zippa ihop mappen men detta är inte sååå jobbigt att göra själv jämfört med att sitta och hantera alla filtyper osv. 

Om nån läsare har kritik/bättre idéer så är jag idel öra, är som sagt fortfarande grön i powershell träsket :)

fredag 9 november 2012

SharePoint 2010 - Uppdatera Shared DataSource på alla rapporter i ett documentbibliotek mha Powershell

Aye Caramba!

Idag när jag köttade lite Powershell bestämde jag mig för att lösa ett jäkligt drygt problem jag haft i SharePoint när jag exporterat/importerat (backup/restore) en SiteCollection innehållandes massa rapporter och alla rapporter tappat sin DataSource-link. Vilket innebar massa manuellt klickande och klipp & klistrande.

Med mina nya powershell kunskaper kom jag fram till följande script:

1. Uppdaterar connectionString i min shared datasource och sätter lite andra variabler
2. Anropar SharePoints ReportServer-webbservice och ändrar datasource till ovan nämnda shared datasource på samtliga rapporter.

Nåväl, here goes (tänk på att du får ändra variablerna så de funkar i din miljö):

#Powershell
Clear-Host

# Add SharePoint snapin if needed
if ((Get-PSSnapin -Name Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue)     -eq $null)
{
    Add-PSSnapin Microsoft.SharePoint.Powershell
}

# Variables
$webApplicationUrl = "http://my.hostheader.com/"
$hostHeader = $webApplicationUrl -replace "http://", ""
$connectionString = "Data Source=MyDbServer;Initial Catalog=MyDataBase;Integrated    Security=SSPI;" 
$webApplicationUNCPath = "\\$hostHeader\DavWWWRoot\"
$dataSourcesLibraryName = "Data Sources"
$dataSourceDefinitionName = "MySharedDataSource.rsds"
$dataSourceLink =    "$webApplicationUrl/$dataSourcesLibraryName/$dataSourceDefinitionName"
$dataSourceUNCFilePath = $webApplicationUNCPath + $dataSourcesLibraryName + "\" +     $dataSourceDefinitionName
$reportsLibraryName = "MyReports"
$reportServerUri = "$webApplicationUrl/_vti_bin/ReportServer/ReportService2010.asmx?WSDL"

# Script
$site = Get-SPSite($webApplicationUrl)
$web = $site.OpenWeb()

$dataSources = $web.Lists | Where-Object { $_.Title -eq $dataSourcesLibraryName }
$dataSource = $dataSources.Items | Where-Object { $_.Name -eq   $dataSourceDefinitionName }
$reports = $web.Lists | Where-Object { $_.Title -eq $reportsLibraryName }

## Change properties on the shared data source object (through xml manipulation)
$xml = [xml](Get-Content $dataSourceUNCFilePath)
$xml.DataSourceDefinition.ConnectString = $connectionString
$xml.DataSourceDefinition.CredentialRetrieval = "Integrated"
$xml.DataSourceDefinition.Enabled = "True"
$windowsCredentialsNode = $xml.DataSourceDefinition.ChildNodes | Where-Object { $_.Name -eq "WindowsCredentials" }
if ($windowsCredentialsNode -ne $NULL)
{
    $xml.DataSourceDefinition.RemoveChild($windowsCredentialsNode)
}
$ImpersonateUserNode = $xml.DataSourceDefinition.ChildNodes | Where-Object { $_.Name -eq "ImpersonateUser" }
if ($ImpersonateUserNode -ne $NULL)
{
    $xml.DataSourceDefinition.RemoveChild($ImpersonateUserNode)}
$xml.Save($dataSourceUNCFilePath)

# Iterate through reports and set correct shared datasource
$Proxy = New-WebServiceProxy -Uri $reportServerUri -UseDefaultCredential ;

# Gather the webservice types for later use 
$WebServiceTypes = @{}
foreach ($Type in $Proxy.GetType().Assembly.GetExportedTypes())
{
    $WebServiceTypes.Add($Type.Name, $Type.FullName);
}

# Get list of all reports
$ReportItems = $Proxy.ListChildren("/", $true) | Where-Object { $_.Name -Like "*.rdl" } | Where-Object { $_.Path -Like "$webApplicationUrl/$reportsLibraryName" }
$dataSources = $web.Lists | Where-Object { $_.Title -eq $dataSourcesLibraryName }
$dataSource = $dataSources.Items | Where-Object { $_.Name -eq     $dataSourceDefinitionName }

# Declare DataSourceReference object
$ref = New-Object $WebServiceTypes.DataSourceReference
$ref.Reference = $dataSourceLink;

# Declare DataSourceReference object
$ds = New-Object $WebServiceTypes.DataSource
$ds.Name = $dataSource.DisplayName
$ds.Item = $ref

# Apply DataSource to all Reports
foreach ($ReportItem in $ReportItems)
{
    $dss = $Proxy.GetItemDataSources($ReportItem.Path)      
    $dss[0] = $ds
    Try 
    {
        $Proxy.SetItemDataSources($ReportItem.Path, $dss)
    }
    Catch [System.Exception]
    {
        "Error: Could not fix datasource for report: {0}" -f $ReportItem.Name
    }
}
$Proxy.Dispose()
$site.Dispose()

Edit #1: Kan vara intressant att veta att min sitecollection ligger på webbapplikationens rot-nivå också.
Edit #2: Bytte implementationen med att hämta objekt ur xml eftersom den gamla inte returnerade System.Xml.XmlElement objekt som fungerar ihop med RemoveChild-metoden. Rättade även så att enbart rapporter i ett utvalt dokumentbibliotek ($reportsLibraryName) "fixas" utav scriptet ifall man kör på en farm innehållandes massa olika rapporter.

Sådärja, jämt dretgött detta! :)

tisdag 6 november 2012

Fel vid installation av EPiServer Mail (Invalid Application Pool Name)

Ett annat problem som dök upp nyligen:

Skulle installera EPiServer Mail på en utvecklingsmiljö med EPiServer CMS 6 när följande fel inträffade (i EPiServer Deployment Center):




















Felet orsakas av att egenskapen "AppPoolName" aldrig vidarebefordras från EPiServer Mail-installationsscriptet. Att ändra i detta script så att det skickar med rätt variabel kräver dock manipulering utav objekt-medlemmar i powershell, något som jag inte bemästrar i skrivande stund.

Så ett enklare sätt att lösa felet är följande:

1. Ändra Powershell ExecutionPolicy

Eftersom alla EPiServer-ps-script är signade med nån hashhistoria behöver vi ändra executionpolicy på webbservern så att scriptet tillåts att köras även om vi modifierar det:

Börja med att köra:




Notera vad du får för svar (i detta fall "AllSigned").

Starta en PowerShell prompt och kör:





Svara ja på varningsmeddelandet som dyker upp.

2. Modifiera script "Install Site (SqlServer).ps1

Öppna upp IIS:en och kolla vad namnet på applikationspoolen är för den site där du ska installera EPiServer Mail.

Öppna filen "Install Site (SqlSErver).ps1" (samma fil som refereras till i felmeddelandet ovan).
Ändra rad 157 enligt (jag behöll den gamla implementationen för att kunna ändra tillbaks senare):




Vi ersätter alltså variabel-värdet med ett hårdkodat strängvärde som alltså ska motsvara namnet på applikationspoolen som vi nyss tog reda på.

3. Kör EPiServer Mail installation på nytt

Kör nu EPiServer Mail installationen på nytt via EPiServer Deployment Center. Denna gång bör det fungera!


4. Återställ

Ändra först tillbaks Install Site (SqlSErver).ps1 (ta bort den modifierade raden och återställ den utkommenterade raden).

Ändra sedan tillbaks ExecutionPolicy:n genom att köra Set-ExecutionPolicy till det ursprungliga värdet.


5. Fira att det fungerar

När man löser något drygt problem bör man alltid ta sig tid att fira detta meddelst kaffe & bulle eller annat gôtt.