After being bitten by this for the N’th time I’m going to write down the solution this time:
EPiServer dlls are missing from /bin
There, I said it. Now I don’t need to research this the next time it happens.
After being bitten by this for the N’th time I’m going to write down the solution this time:
EPiServer dlls are missing from /bin
There, I said it. Now I don’t need to research this the next time it happens.
In my current project we had used NHibernate/Fluent NHibernate (version 2.1.2.4000) for some custom data access for data which shouldn’t live in the EPiServer DB.
Then suddenly we started to integrate EPiServer Community 3.2 (EPiServer Relate + v1.0.1.0) into our solution and stumbled a bit on the fact that it used a different version of NHibernate (version 1.2.0.4000 to be exact)
on the verge of giving up and rewriting our custom data access to raw ADO.net I stumbled upon the solution: using assemblyBinding to instruct .Net *where* to look for the different versions of the dlls. Here’s a snippet from web.config:
<configuration>
...
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Iesi.Collections" publicKeyToken="aa95f207798dfdb4" culture="neutral" />
<codeBase version="1.0.0.3" href="dlllib\nhibernate1.2\Iesi.Collections.dll" />
<codeBase version="2.1.2.4000" href="dlllib\nhibernate2.1\Iesi.Collections.dll" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="NHibernate" publicKeyToken="aa95f207798dfdb4" culture="neutral" />
<codeBase version="1.2.0.4000" href="dlllib\nhibernate1.2\NHibernate.dll" />
<codeBase version="2.1.2.4000" href="dlllib\nhibernate2.1\NHibernate.dll" />
</dependentAssembly>
...
</assemblyBinding>
</runtime>
</configuration>
So what I did was to have a folder structure like this
- site
- – bin
- – dlllib
- – - nhibernate1.2
- – - – Nhibernate.dll (v1.2.0.4000)
- – - – Iesi.Collections.dll (v1.0.0.3)
- – - nhibernate2.1
- – - – Nhibernate.dll (v2.1.2.4000)
- – - – Iesi.Collections.dll (v2.1.2.4000)
So there it is, no more headscratching when different versions of the same dlls are needed. “codeBase” to the rescue!
I had the “pleasure” of creating a dummy web service having only the required interface to work with. and discovered that the service had a method which took polymorphic data.
After spending a bit of time poking at it with a stick I realized that the magic trick was to tag the service methods with XmlInclude attributes for each of the child types that could be sent over.
Also I realized that I’m not a huge fan of polymorphic web service methods.
I showed my strongly typed language handling in EPiServer to a guy at work who immediately pointed out a weakness: ReSharper will harass you and try to get you to add using statements all over the code, which in turn would reduce readability of the language strings.
This is because the hierarchical information of the language files was encoded into namespaces. I remodelled it to use nested public static classes instead and it works just the same, here’s the code:
<#@ template debug="true" hostSpecific="true" #><#@ assembly name="System.Xml"#><#@ assembly name="System.Xml.Linq" #><#@ assembly name="System.Core" #><#@ import namespace="System.Xml.Linq"#><#@ import namespace="System.Linq"#><#@ import namespace="System.Collections.Generic"#><#
int skipLevels = 1; //depends on xml file, for standard episerver use 2
string xmlFilePath = @"\lang.xml";
string baseCodeGenNamespace = "Lang";
XDocument xdoc = XDocument.Load(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+xmlFilePath);
GenerateLanguageAccessors(xdoc.Root, baseCodeGenNamespace,skipLevels);
#><#+
void GenerateLanguageAccessors(XNode xNode, string baseNS, int skipLevels)
{
this.WriteLine("public static class " + baseNS + "{");
GenerateLanguageAccessorsRecursive(xNode, new List<string>(), skipLevels, new List<string>());
this.WriteLine("}");
}
void GenerateLanguageAccessorsRecursive(XNode xNode, List<string> ns, int skipLevels, List<string> cache)
{
string indenting = Times("\t", ns.Count - skipLevels);
if (xNode is XText)
{
if (ns.Count < skipLevels) return;
string theKey = "/" + string.Join("/", ns.Skip(skipLevels+1).ToArray());
string s = indenting + "public static string Text{ get{ return Utils.LanguageUtil.Translate(\"<KEY>\");} }\n";
s += indenting + "public static string TextOrDefault(string defaultString){ return Utils.LanguageUtil.TranslateOrDefault(\"<KEY>\",defaultString);}";
s = s.Replace("<KEY>", theKey);
this.WriteLine(s);
return;
}
if (xNode is XElement)
{
var xElement = xNode as XElement;
var newNameSpace = ns.ToArray().ToList();
newNameSpace.Add(xElement.Name.ToString());
var pathKey = string.Join(".", newNameSpace);
var shouldWriteClass = (ns.Count > skipLevels && !cache.Contains(pathKey));
if (shouldWriteClass) this.WriteLine(indenting + "public static class " + newNameSpace.Last() + "{");
cache.Add(pathKey);
foreach (var subNode in xElement.Nodes())
{
GenerateLanguageAccessorsRecursive(subNode, newNameSpace, skipLevels, cache);
}
if (shouldWriteClass) this.WriteLine(indenting + "}");
}
}
string Times(string input, int times)
{
string ret = "";
for(int i = 0; i<times; i++)
{
ret += input;
}
return ret;
}
#>
the reason everything is squashed together in the top is to avoid getting a lot of linebreaks in the start of the file.
Using nested classes comes with two small limitations: you cannot have the language string contain two identical names in a row (for example: “/strings/pages/frontpage/frontpage/heading”) and you should never have it end with Text:
(“/strings/pages/frontpage/heading/Text”)
(the C# compiler refuses to compile the generated code in both these two cases with the error “member names cannot be the same as their enclosing type”)
Using T4 to generate multiple versions of xml config files with inheritance
(NB: baked into this sample is a method for generating multiple output files from T4, it is stolen shamelessly from Damien Guard (thanks Damien!), except I couldn’t get his deleting of files no longer needed to work (it deleted needed files as well) so I commented out that bit and it worked like a charm.
For a full description of that part of the script go to http://damieng.com/blog/2009/11/06/multiple-outputs-from-t4-made-easy-revisited and read Damiens excellent blog)
quite a while back I got really irritated with how .Net does configuration files. My main gripe is that there is no easy way to
relate the configuration for the local dev environment, build server, test server(s), staging server(s) and production server(s).
let me give you an example: In a standard Web Application Project the web.config file is a single file which is used to define all the configuration
data for the entire site:
- WCF web services exposed
- WCF web services used
- logging
- App settings
- etc.
Now i do know that you can split the file up where each section can have its own file, and use different files for the different environments,
but that doesn’t really solve the problem as
- you end up with HEAPS of files if you want fine grained control
- you cannot really use that strategy for having differences within a section (appSettings for instance)
- you need to do a lot of file copying magic in order to get the correct sections together for a workable web.config for a given environment
- even if you only need one tiny difference between say testserver1 and testserver2 the entire section must be duplicated into two different files
(major DRY violation)
I have spent a lot of time pondering how to do this better (WAY too much time actually, but then I’m not really that smart
) and have come up with
a way to define configuration files using a sort of inheritance scheme. That way you can have a setup like this:
- web.config.root ( – file that defines 90% of the configuration, everything that is common throughout all environments)
- web.config.build ( – contains specifics for the build server )
- web.config.dev ( – contains common specifics for local dev instances )
- web.config.dev.local ( one per dev, not checked into SCM – contains specifics for each dev, local testing ground)
- web.config.test ( – defines common specifics for all test servers )
- web.config.test.testserver1 ( – defines specifics for a given test machine )
- etc.
and then have some engine process these source files in a controlled order. For instance:
- local web.config (used for local development) = web.config.root + web.config.dev + web.config.dev.local
- buildserver web.config = web.config.root + web.config.build
- testserver1 web.config = web.config.root + web.config.test + web.config.test.testserver1
so that for the testserver1 config file the web.config.root would first be applied, then settings in web.config.test would be applied, adding to
and overriding the settings from the root file. and lastly the settings in web.config.test.testserver1 would be applied, adding to and overriding
the settings from the resulting merge between root and test. (phew!)
There are two main problems with such a setup that must be solved:
A) how to identify which setting to override when there are multiple possibilities
B) how to define a setting in a base config file, then delete that setting in an overriding config file
for solving A) I have two rules:
- if the element is an only child (or at least the only child with that element name) then that element can be
overridden / expanded by subsequent input files.
- if an element has siblings with the same element name then it must be identified by an identificator attribute.
For standard .net configuration they are (in prioritized order) “id”, “name”, “key”, “path”.
- if there are equal siblings without any of these identificators the whole process throws up and informs that an ambiguity exists.
for solving B) I introduce the magic keyword DELETEME. I’ll give two examples:
1)
<configuration> <location path="Documents" DELETEME="true" /> </configuration>
<configuration>
<system.web>
<httpRuntime maxRequestLength="DELETEME" />
</system.web>
</configuration>
the config in 1) will delete the entire configuration for location path=”documents”
the config in 2) will delete the attribute maxRequestLength but otherwise leave the httpRuntime element alone
so, on to the code. To get the files generated automatically I’m using T4. I use two files, the first is a .ttinclude file which defines both the
xml merging logic and logic for generating multiple output files:
first, here’s the ConfigurationMerger.ttinclude file
<#@ assembly name="System.Core"#>
<#@ assembly name="System.Data.Linq" #>
<#@ assembly name="EnvDTE"#>
<#@ assembly name="System.Xml"#>
<#@ assembly name="System.Xml.Linq"#>
<#@ import namespace="System"#>
<#@ import namespace="System.CodeDom"#>
<#@ import namespace="System.CodeDom.Compiler"#>
<#@ import namespace="System.Collections.Generic"#>
<#@ import namespace="System.Data.Linq"#>
<#@ import namespace="System.Data.Linq.Mapping"#>
<#@ import namespace="System.IO"#>
<#@ import namespace="System.Linq"#>
<#@ import namespace="System.Reflection"#>
<#@ import namespace="System.Text"#>
<#@ import namespace="System.Xml.Linq"#>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating"#>
<#+
Dictionary<string, string> MergeConfigFiles(string inputFilesFolder, string outputFilesFolder, Dictionary<string, string[]> setup){
var ret = new Dictionary<string, string>();
var merger = new XmlConfigMerge("name", "id", "key");
Dictionary<string, string> configFiles = new Dictionary<string, string>();
DirectoryInfo di = new DirectoryInfo(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+inputFilesFolder);
foreach(var f in di.GetFiles())
{
configFiles[f.Name] =File.ReadAllText(f.FullName);
}
foreach (var outputFileName in setup.Keys)
{
List<XDocument> xdocs = new List<XDocument>();
foreach (var inputFileName in setup[outputFileName])
{
if (configFiles.ContainsKey(inputFileName))
{
xdocs.Add(XDocument.Parse(configFiles[inputFileName]));
}
}
string result = merger.MergeConfigs(xdocs[0], xdocs.Skip(1).ToList()).ToString();
ret[outputFilesFolder+outputFileName] = result;
}
return ret;
}
public class XmlConfigMerge
{
public string DeleteKeyword = "DELETEME";
public XmlConfigMerge(params string[] unique)
{
UniqueAttributes = unique.ToList();
}
public List<string> UniqueAttributes { get; set; }
private XElement FindByAttr(IEnumerable<XElement> elements, string attribute, string value)
{
return elements.FirstOrDefault(e => HasAttribute(e, attribute, value));
}
private bool HasAttribute(XElement elem, string attributeName)
{
var attr = elem.Attribute(attributeName);
if (attr != null
&& !string.IsNullOrEmpty(attr.Value)
)
{
return true;
}
return false;
}
private bool HasAttribute(XElement elem, string attributeName, string attributeValue)
{
if (HasAttribute(elem, attributeName))
{
if (0 ==
string.Compare(elem.Attribute(attributeName).Value, attributeValue,
StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
}
return false;
}
public XDocument MergeConfigs(XDocument template, List<XDocument> dataFiles)
{
if (dataFiles == null || dataFiles.Count == 0) return template;
var temp = template;
foreach (var data in dataFiles)
{
temp = MergeConfigs(temp, data);
}
return temp;
}
private XDocument MergeConfigs(XDocument template, XDocument data)
{
var rootNodeTemplate = template.Root;
var rootNodeData = data.Root;
MergeConfigs(rootNodeTemplate, rootNodeData);
return template;
}
private void MergeConfigs(XElement template, XElement data)
{
bool shouldDeleteNode = ShouldDeleteNode(data);
if (shouldDeleteNode)
{
template.Remove();
return;
}
MergeAttributes(template, data);
if (!data.HasElements)
{
if (data.Value == DeleteKeyword) template.Value = string.Empty;
else if (!string.IsNullOrEmpty(data.Value)) template.Value = data.Value;
return;
}
var templateElements = template.Elements();
var dataElements = data.Elements();
foreach (var dataNode in dataElements)
{
KeyValue uniquey = GetUniqueKey(dataNode);
var matchingNodes = templateElements.Where(x => x.Name == dataNode.Name);
if (matchingNodes.Count() == 0)
{
template.Add(dataNode);
continue;
}
if (uniquey == null && matchingNodes.Count() > 1)
{
throw new ApplicationException("Cannot merge into file containing multiple undelimited equal siblings, needs attribute with name: " + string.Join(", ", UniqueAttributes.ToArray()));
}
if (uniquey == null && matchingNodes.Count() == 1)
{
var templateNode = templateElements.Where(x => x.Name == dataNode.Name).First();
MergeConfigs(templateNode, dataNode);
continue;
}
if (uniquey != null)
{
var ambigous =
templateElements.Count(
x =>
x.Attribute(uniquey.Key) != null &&
x.Attribute(uniquey.Key).Value.ToLower() == uniquey.Value.ToLower()) > 1;
if (ambigous)
{
throw new ApplicationException("Cannot merge into file containing multiple equal siblings with same identifier, needs unique value for attribute with name: " + string.Join(", ", UniqueAttributes.ToArray()));
}
var templateNode = (from x in templateElements
where
x.Attribute(uniquey.Key) != null
&& x.Attribute(uniquey.Key).Value.ToLower() == uniquey.Value.ToLower()
select x).FirstOrDefault();
if (templateNode == null)
{
template.Add(dataNode);
continue;
}
else
{
MergeConfigs(templateNode, dataNode);
continue;
}
}
}
return;
}
private void MergeAttributes(XElement template, XElement data)
{
foreach (var attrib in data.Attributes())
{
var value = attrib.Value;
if (value == DeleteKeyword)
{
value = null;
}
template.SetAttributeValue(attrib.Name, value);
}
}
private KeyValue GetUniqueKey(XElement element)
{
foreach (var key in UniqueAttributes)
{
var elementsUnique = element.Attributes(key);
if (elementsUnique.Count() == 1)
{
return new KeyValue { Key = key, Value = elementsUnique.First().Value };
}
}
return null;
}
private bool ShouldDeleteNode(XElement data)
{
var deleteattrib = data.Attributes(DeleteKeyword);
if (deleteattrib.Count() == 1 && bool.Parse(deleteattrib.First().Value))
{
return true;
}
return false;
}
}
public class KeyValue
{
public string Key { get; set; }
public string Value { get; set; }
}
// CodegenManager class records the various blocks so it can split them up
class CodegenManager {
private class Block {
public String Name;
public int Start, Length;
}
private Block currentBlock;
private List<Block> files = new List<Block>();
private Block footer = new Block();
private Block header = new Block();
private ITextTemplatingEngineHost host;
private StringBuilder template;
protected List<String> generatedFileNames = new List<String>();
public static CodegenManager Create(ITextTemplatingEngineHost host, StringBuilder template) {
return (host is IServiceProvider) ? new VSCodegenManager(host, template) : new CodegenManager(host, template);
}
public void StartNewFile(String name) {
if (name == null)
throw new ArgumentNullException("name");
CurrentBlock = new Block { Name = name };
}
public void StartFooter() {
CurrentBlock = footer;
}
public void StartHeader() {
CurrentBlock = header;
}
public void EndBlock() {
if (CurrentBlock == null)
return;
CurrentBlock.Length = template.Length - CurrentBlock.Start;
if (CurrentBlock != header && CurrentBlock != footer)
files.Add(CurrentBlock);
currentBlock = null;
}
public virtual void Process(bool split) {
if (split) {
EndBlock();
String headerText = template.ToString(header.Start, header.Length);
String footerText = template.ToString(footer.Start, footer.Length);
String outputPath = Path.GetDirectoryName(host.TemplateFile);
files.Reverse();
foreach(Block block in files) {
String fileName = Path.Combine(outputPath, block.Name);
String content = headerText + template.ToString(block.Start, block.Length) + footerText;
generatedFileNames.Add(fileName);
CreateFile(fileName, content);
template.Remove(block.Start, block.Length);
}
}
}
protected virtual void CreateFile(String fileName, String content) {
if (IsFileContentDifferent(fileName, content))
File.WriteAllText(fileName, content);
}
public virtual String GetCustomToolNamespace(String fileName) {
return null;
}
public virtual String DefaultProjectNamespace {
get { return null; }
}
protected bool IsFileContentDifferent(String fileName, String newContent) {
return !(File.Exists(fileName) && File.ReadAllText(fileName) == newContent);
}
private CodegenManager(ITextTemplatingEngineHost host, StringBuilder template) {
this.host = host;
this.template = template;
}
private Block CurrentBlock {
get { return currentBlock; }
set {
if (CurrentBlock != null)
EndBlock();
if (value != null)
value.Start = template.Length;
currentBlock = value;
}
}
private class VSCodegenManager: CodegenManager {
private EnvDTE.ProjectItem templateProjectItem;
private EnvDTE.DTE dte;
private Action<String> checkOutAction;
private Action<IEnumerable<String>> projectSyncAction;
public override String DefaultProjectNamespace {
get {
return templateProjectItem.ContainingProject.Properties.Item("DefaultNamespace").Value.ToString();
}
}
public override String GetCustomToolNamespace(string fileName) {
return dte.Solution.FindProjectItem(fileName).Properties.Item("CustomToolNamespace").Value.ToString();
}
public override void Process(bool split) {
if (templateProjectItem.ProjectItems == null)
return;
base.Process(split);
projectSyncAction.EndInvoke(projectSyncAction.BeginInvoke(generatedFileNames, null, null));
}
protected override void CreateFile(String fileName, String content) {
if (IsFileContentDifferent(fileName, content)) {
CheckoutFileIfRequired(fileName);
File.WriteAllText(fileName, content);
}
}
internal VSCodegenManager(ITextTemplatingEngineHost host, StringBuilder template)
: base(host, template) {
var hostServiceProvider = (IServiceProvider) host;
if (hostServiceProvider == null)
throw new ArgumentNullException("Could not obtain IServiceProvider");
dte = (EnvDTE.DTE) hostServiceProvider.GetService(typeof(EnvDTE.DTE));
if (dte == null)
throw new ArgumentNullException("Could not obtain DTE from host");
templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
checkOutAction = (String fileName) => dte.SourceControl.CheckOutItem(fileName);
projectSyncAction = (IEnumerable<String> keepFileNames) => ProjectSync(templateProjectItem, keepFileNames);
}
private static void ProjectSync(EnvDTE.ProjectItem templateProjectItem, IEnumerable<String> keepFileNames) {
var keepFileNameSet = new HashSet<String>(keepFileNames);
var projectFiles = new Dictionary<String, EnvDTE.ProjectItem>();
var originalFilePrefix = Path.GetFileNameWithoutExtension(templateProjectItem.get_FileNames(0)) + ".";
foreach(EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems)
projectFiles[projectItem.get_FileNames(0)] = projectItem;
// Remove unused items from the project
foreach(var pair in projectFiles)
if (!keepFileNames.Contains(pair.Key) && !(Path.GetFileNameWithoutExtension(pair.Key) + ".").StartsWith(originalFilePrefix))
//pair.Value.Delete();
// Add missing files to the project
foreach(String fileName in keepFileNameSet)
if (!projectFiles.ContainsKey(fileName))
templateProjectItem.ProjectItems.AddFromFile(fileName);
}
private void CheckoutFileIfRequired(String fileName) {
var sc = dte.SourceControl;
if (sc != null && sc.IsItemUnderSCC(fileName) && !sc.IsItemCheckedOut(fileName))
checkOutAction.EndInvoke(checkOutAction.BeginInvoke(fileName, null, null));
}
}
}
#>
This file can be used as is with no modifications. Just put it into the root directory of the project
and then there is the actual T4 file which will cause stuff to be generated: configuration.tt
(you can call it anything you like)
<#@ template debug="true" hostSpecific="true" #>
<#@ output extension=".log" #>
<#@ include file="ConfigurationMerger.ttinclude" #>
<#
var setup = new Dictionary<string, string[]>();
string inputFilesRelativeRootPath = @"\";
string outputFilesRelativeRootPath = @"\";
//////////////////////////////////////////////////////////////
//Configure the configuration generation process like this:
//setup[<outputfilename>] = new []{<input1>,<input2>,<input3>,..};
//
// configs are merged in the order given (input 3 overrides input 2, which overrides input1)
//
// make your modifications below here
//////////////////////////////////////////////////////////////
inputFilesRelativeRootPath = @"\config\input";
setup[@"web.config"] = new []{"web.config.template","web.config.dev","web.config.dev.local"};
setup[@"config\output\web.config.test1"] = new []{"web.config.template","web.config.test","web.config.test.testserver1"};
setup[@"config\output\web.config.test2"] = new []{"web.config.template","web.config.test","web.config.test.testserver2"};
setup[@"config\output\web.config.build"] = new []{"web.config.template","web.config.build"};
////////////////////////////////////////////////////////////////
//End of custom configuration, the rest is just static stuff:
////////////////////////////////////////////////////////////////
var configs = MergeConfigFiles(inputFilesRelativeRootPath, outputFilesRelativeRootPath, setup);
var manager = CodegenManager.Create(Host, GenerationEnvironment);
foreach(var filepath in configs.Keys){
#>
-------------- <#= System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+filepath #> --------
<#= configs[filepath] #>
------------------------------------------------------------
<#
manager.StartNewFile(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+filepath);
#><#= configs[filepath] #><#
manager.EndBlock();
}
manager.Process(true);
#>
This file should be tweaked to fit your project (you only need to tweak within the top part as indicated).
For instance: in this example the output files are all placed in the same directory with
different file endings, but that can be customized any way you like. (for instance having all the files called web.config but be in separate directories)
In addition to the output files, a separate log file is written called Configuration.log. This file contains all the resulting config files for easy
reference.
Normally the code(config) generation will only be triggered by a modification to the .tt file, but there is an easy way to get T4 to fire on every build.
I would suggest not checking the generated files into source control but rather have the CI server ( you DO use continuous integration, right?) generate the files when it performs the build
Problem
Typically when developing with EPiServer in the past I have created a helper method to provide a shorthand for accessing language strings:
public static class Lang
{
public static string Translate(string key)
{
//call standard EPiServer API
}
public static string Translate(string key, string defaultValue)
{
//call standard EPiServer API, return defaultValue if no match found
}
}
the trouble is the key parameter, it is on the form “/Forms/RegisterForm/Firstname” for instance, which means that every developer needs to know which language strings have been defined and get their full xpath from the language strings (no spelling errors, please), this makes discovering common strings a bit painful as Visual studio does not provide the handy navigator in the bottom for xml files as it does for .aspx files.
I wanted to point T4 at one (or more) language file(s) and get generated accessors so I could get intellisense when accessing language strings.
Solution
what I ended up with was this T4 code (which I put into a file called EPiLanguageAccessors.tt:
<#@ template debug="true" hostSpecific="true" #>
<#@ assembly name="System.Xml"#>
<#@ assembly name="System.Xml.Linq" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Xml.Linq"#>
<#@ import namespace="System.Linq"#>
<#@ import namespace="System.Collections.Generic"#>
<#
int skipLevels = 2; //depends on xml file, for standard episerver use 2
string xmlFilePath = @"\lang\lang.xml";
string baseCodeGenNamespace = "Lang";
XDocument xdoc = XDocument.Load(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+xmlFilePath);
GenerateLanguageAccessors(xdoc.Root, new List<string>(),baseCodeGenNamespace,skipLevels, new List<string>());
#>
<#+
void GenerateLanguageAccessors(XNode xNode, List<string> ns, string baseNS, int skipLevels, List<string> cache)
{
if (xNode is XText)
{
if (ns.Count < skipLevels) return;
string theNameSpace = string.Join(".", ns.Skip(skipLevels).Take(ns.Count - (skipLevels)).ToArray());
if (string.IsNullOrEmpty(theNameSpace))
{
theNameSpace = baseNS;
}
else
{
theNameSpace = baseNS + "." + theNameSpace;
}
string theKey = "/" + string.Join("/", ns.Skip(skipLevels).ToArray());
string s = "namespace " + theNameSpace + "{ public static partial class Get{ public static string Text(){ return Utils.LanguageUtil.Translate(\"" + theKey + "\");} public static string TextOrDefault(string defaultString){ return Utils.LanguageUtil.Translate(\"" + theKey + "\",defaultString);} } }";
if(!cache.Contains(theNameSpace))
{
this.WriteLine(s);
cache.Add(theNameSpace);
}
return;
}
if (xNode is XElement)
{
var xElement = xNode as XElement;
var newNameSpace = ns.ToArray().ToList();
newNameSpace.Add(xElement.Name.ToString());
foreach (var subNode in xElement.Nodes())
{
GenerateLanguageAccessors(subNode, newNameSpace, baseNS, skipLevels, cache);
}
}
}
#>
The top part of the .tt file is needed to reference the right stuff for the text transformation,
the middle part is where you specify the specifics of what you want, for instance skiplevel is needed to skip “/language/language” which is a part of the xml files, but not used for accessing strings, also here is where I define the relative path to the language file which is the “master” which will be used for code generation.
the bottom part is the algorithm which creates namespaces for each string with translation logic.
so where previously I would have to use a magic string in the call to LanguageUtil.Translate("/App/Registration/Step1/EulaText"); I can now call Lang.App.Registration.Step1.EulaText.Get.Text();
Getting it to run on each build
T4 out of the box runs the transformation each time the template file is changed. In our case that is not too useful, as we would rather have the transformation run each time the “master” language file is changed. AFAIK it isn’t possible to instruct T4 to fire when some custom file is changed, but you can get it to fire on each build.
There are a few options, for a run down you might want to head over to http://www.olegsych.com which is a goldmine of T4 information.
I found the simplest option to spread across a team (no need to install anything extra to the dev machines) is to create a file called FireT4OnBuild.targets, put it into the root of the project and populate it like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="ExecuteT4Templates">
<ItemGroup>
<T4Templates Include=".\**\*.tt" />
</ItemGroup>
<Exec
WorkingDirectory="C:\Program Files (x86)\Common Files\microsoft shared\TextTemplating\10.0\"
Command="TextTransform "%(T4Templates.FullPath)" -out "%(T4Templates.RootDir)%(T4Templates.Directory)%(T4Templates.Filename).cs" " />
</Target>
</Project>
You might want to change the path for the working directory (my setup is for .net 4.0 running on windows 7 (64bit)
if the devs use different setups you might want to copy the texttemplating files to a common path (SolutionDir/Lib/T4 for instance) and reference that path
Then you need to unload the project containing the transformation (I’m guessing your xxxxx.Web.csproj ) and edit the .csproj file (right click => unload, then right click => edit )
and add the line
<Import Project="$(MSBuildProjectDirectory)\FireT4OnBuild.targets" />
Now T4 should be firing every time you touch the .tt file(s) and also every time you perform a build. Since ExecuteT4Templates is put before Build the generated code will be in place in time for the build
If you need to serialize / deserialize JSON for communicating with javascript for instance you can use the .net DataContractSerializer. The only requirement is that the classes be tagged with [DataContract] and all the members you want to survive the transformation be tagged with [DataMember] (all the built in data types are already [DataContract]s )
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace Utilities
static class JsonUtil
{
public static string Serialize(object obj)
{
var jsonSerializer = new DataContractJsonSerializer(obj.GetType());
string returnValue = "";
using (var memoryStream = new MemoryStream())
{
using (var xmlWriter = JsonReaderWriterFactory.CreateJsonWriter(memoryStream))
{
jsonSerializer.WriteObject(xmlWriter, obj);
xmlWriter.Flush();
returnValue = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
}
}
return returnValue;
}
public static T Deserialize<T>(string json)
{
T returnValue;
using (var memoryStream = new MemoryStream())
{
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
memoryStream.Write(jsonBytes, 0, jsonBytes.Length);
memoryStream.Seek(0, SeekOrigin.Begin);
using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(memoryStream, Encoding.UTF8, XmlDictionaryReaderQuotas.Max, null))
{
var serializer = new DataContractJsonSerializer(typeof (T));
returnValue = (T) serializer.ReadObject(jsonReader);
}
}
return returnValue;
}
}
}
recently switched projects, and I was immediately struck by how much infrastructure I had just taken for granted in the previous one., even though I had done a large part in building it.
One of the conveniences I had been getting so used to was to have a WCF ajax web service perform some task and return JSON-serialized data to the client, which in and of itself is pretty standard stuff, but we would have some of that data be actual html-snippets that would be inserted directly into the page. Still somewhat straight forward, you might say? What if I added that we would use the *same user controls” when a service returned html snippets as we would use when rendering the page normally.
If this sounds intriguing, do read on:
Lets use an example: say that we are creating a web site where users can mark pages as a favourite page so that they will have that page available in a global shortcut menu in the right column. We will need an ajax service to work as a custom profile service (we’ll just call it ProfileService) that will store whether a page is a given users favourite page or not. When a page is set to be a favourite ,an ajax call will be made to the ProfileService which will update the profile store and perform a rendering of the “favourites overview” web control whose html will be returned to the client in the form of a specialized service result which will expose the html and other meta data as a JSON string which can easily be turned into a javascript object.
1)
First and foremost we need to have a WCF service defined in web.config (if you add an “WCF ajax service” from the right click menu in solution explorer you get the stuff you need. If you have trouble with doing that in your solution because of some weird config-o-rama in your project (it happens) just make a new web application project and add one there and copy the config changes from that web.config into your web.config
2)
Then we need to reference that service in a scriptmanager on the page. it is as simple as:
<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Path="~/path/to/SomeService.svc" />
</Scripts>
</asp:ScriptManager>
3)
The data being returned to the client should be in the format of a JSON string to be easily accessible from javascript. There are quite a few libraries out there that will serialize objects to JSON, but the .net library itself does a decent enough job if the classes to be serialized are all marked with the DataContract attribute. Here’s an implementation of a JsonServiceResult which encapsulates all the serialization logic:
/// General JSON service result container
[DataContract(Namespace = "")]
public class JsonServiceResult
{
[DataMember]
public bool Success;
/// Can contain messages to end user
[DataMember]
public string Message;
/// If needed, the Result should be a json serialized string
[DataMember]
public string Result;
public JsonServiceResult()
{
// needs an empty constructor for contract serialization.
}
//as setting up a serializer for a given type is somewhat expensive, this can be cached in a static variable,
//no need to worry about thread safety as the worst case scenario is that a serializer is created a couple of times
private static readonly Dictionary<Type, DataContractJsonSerializer> _serializers = new Dictionary<Type, DataContractJsonSerializer>();
public JsonServiceResult(bool success, string message, object result)
{
Success = success;
Message = message;
if (result == null){Result = "null"; //json deserialization doesn't like nulls
} else if (result is string){ Result = result.ToString(); //no need to serialize it if only a string
} else {
DataContractJsonSerializer jsonSerializer = GetJsonSerializer(result);
using (var memoryStream = new MemoryStream())
using (var xmlWriter = GetJsonWriter(memoryStream))
Result = WriteJsonObject(result, memoryStream, jsonSerializer, xmlWriter);
}
}
private static string WriteJsonObject(object result, MemoryStream memoryStream, XmlObjectSerializer jsonSerializer, XmlDictionaryWriter xmlWriter)
{
jsonSerializer.WriteObject(xmlWriter, result);
xmlWriter.Flush();
var json = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
return json;
}
private static XmlDictionaryWriter GetJsonWriter(Stream memoryStream)
{
var xmlWriter = JsonReaderWriterFactory.CreateJsonWriter(memoryStream);
return xmlWriter;
}
private static DataContractJsonSerializer GetJsonSerializer(object result)
{
Type serializerType = result.GetType();
if (!_serializers.ContainsKey(serializerType))
{
_serializers[serializerType] = new DataContractJsonSerializer(serializerType);
}
return _serializers[serializerType];
}
}
A few points worth noting:
-the namespace is set to blank: this has no big effect for DataContract types, except if you want to instantiate them from javascript. Like ServiceContracts (which should always have a blank namespace IMHO) they are given their .net namespace as the namespace if none are provided, making it bloody cumbersome to use them in javascript.
- the JsonServiceResult reuses the .net JsonDataContractSerializers (they are stored in a static variable as they are instantiated, as they are not free to create but totally reusable.
4)
Next it is the ajax service.
the ProfileService.svc.cs file could look something like this:
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
[ServiceContract(Namespace = "")]
public class ProfileService
{
[OperationContract]
public JsonServiceResult SetPageAsFavouriteForCurrentUser(int pageId)
{
TheProfileStore.AddFavouritePage(pageId);
Renderer renderer = new Renderer();
FavouritesControl favouritesControl = (FavouritesControl)renderer.LoadControl("~/Path/To/UserControl/FavouritesControl.ascx");
List<string> currentFavourites = TheProfileStore.GetFavouritesForCurrentUser();
favouritesControl.FavouritesList = currentFavourites;
string htmlsnippet = renderer.RenderControl(favouritesControl);
FavouritesHolder holder = new FavouritesHolder
{
LastChanged = TheProfileStore.GetFavouritesLastChangedForCurrentUser(),
HtmlSnippet = htmlsnippet
};
return new JsonServiceResult(true, "Page added to favourites", holder);
}
}
[DataContract(Namespace = "")]
public class FavouritesHolder
{
[DataMember]
public string HtmlSnippet { get; set; }
[DataMember]
public DateTime LastChanged{ get; set; }
}
Ok, it might be a little far fetched, but in order to show the proper Json serializing and deserializing in action I needed to have to return something more than just the html, as that would be too easy-peasy-japaneasy so i snuck a timestamp in there, I did it for you, dear reader, so there!
A couple of points to notice:
-the service has an empty namespace. As explained earlier that means it will be accessible in Javascript as only ProfileService (and not the full namespace)
-the service is set to be AspNetCompatibilityRequirementsMode.Required, this is not strictly necessary for all services, but as this service needed to figure out who the current user was it was necessary here. Without it there will be no user info to be found in HttpContext.Current
-there’s a few lines which has nothing to do with the demonstration of ajax/WCF/JSON as such, but are there for a sense of completeness: the ones referring to a static TheProfileStore
-there’s an interesting use of a class named Renderer, which is where the rendering magic happens, we will look at that next:
6)
The renderer class is created to be able to render web controls (user controls) if given their virtual path on the server. First a dummy page is newed up into which the control is loaded. then that control may receive any data it needs in its lifecycle. Then the control is rendered (using a new dummy Page object), and the resulting html is returned
Here’s the code:
public class Renderer
{
public Control LoadControl(string path)
{
return new Page().LoadControl(path);
}
public string RenderControl(Control control)
{
string html;
var page = new Page();
page.Controls.Add(control);
using (var writer = new StringWriter())
{
HttpContext.Current.Server.Execute(page, writer, false);
html = writer.ToString();
}
return html;
}
}
7)
The endresult of all this is that one can perform the following stuff in the javascript
function setPageAsfavourite(pageId){
ProfileService.SetPageAsFavouriteForCurrentUser(pageId, onSetFavouriteReturn, onServiceError);
}
function onServiceError(error){
alert(error);
}
function onSetfavouriteReturn(serviceResponse){
if (serviceResponse.Success) {
var result = Sys.Serialization.JavaScriptSerializer.deserialize(serviceResponse.Result); //here we deserialize json into a js object
$('#userFavourites').html(result.Htmlsnippet);
$('#favouritesLastUpdated').html(result.LastChanged);
}
}
You might ask yourself why I bothered to do all this when all I really needed to do was to put an updatepanel around the control in question and handle a postback event, well doing that would mean that:
1) the entire page lifecycle for the current page would run, which would be harder on the server and slower for the end user
2) all the pages containing this shortcut list would need to be aware that they could get a postback from that control.
3) it would be more work to add effects when the html arrived on the client
4) any script tags in the resulting html would *not* be executed (updatepanels suppresses the running of javascript in the partial update html)
so, in part one I explained some context for the project and the build setup ending up with how ccnet gets nant to deploy compiled code artifacts etc. to a local folder on the build machine (a subfolder under d:\deploy) so that a completely fine runnable version of the SOA stack runs on the build server. in this part I’ll explain how we take the artifacts from this location and install them on our other environments.
Specifically, I’ll be talking about:
1) Databases
2) code
3) configuration
4) backup / restore
Let’s start
1) Databases
DBs are usually the hardest to work with deployment-wise, no exception here. Let me just start off by saying that I’m convinced that using a migration scheme for DBs for instance using for instance migrator.net would be SO much easier than the route we’ve taken (hindsight is always 20-20, oh well) but we do have a solid getup ourselves even though it reeks of MacGyverisms.
There are approx 15 databases (most solutions have their own database) and they are all built by the default.build file as explained in part one just before the unittests are run. The mechanism for building the DB is basically as follows: a dev does modifications to the DB in SQL Server Manager, then runs a proprietary .exe file scripting out the DB schema into sql script files which are then checked in to svn. When the build process later runs a different proprietary .exe file those same sql files ar run in the correct order on each build. effectively re-creating the DB from scratch on the buildserver.
Naturally we cannot use the same process in our other environments, so what we do is run a product called Redgate Sql Compare to sync the changes into the “next” environment (the environments are build => devtest => test => production.) This is a somewhat tedious and error-prone manual process (especially when deploying to production at 00:15 am) and the tool is not perfect (especially when it comes to the norwegian letters ø å æ )
2) code
We use powershell to deploy our code. In our scripts we divide the codebase into the website, the internal services, the external services and the scheduled tasks as this distinction reflects what we usually deploy on every go. I’ll omit the external services from the examples as they are really fragmented into a lot of subsets.
Powershell is a really powerful scripting environment, especially if you’re already familiar with Microsoft.Net, but it has two major drawbacks (at least as of version 1 which is the one we’re using):
1) it’s hard to split scripts into multiple files. Effectively what you are left with is doing the following:
$scriptToBeIncluded = Resolve-Path 'D:\temp\some\absolute\script\path\ScriptToBeIncluded.ps1' . $scriptToBeIncluded
2) If you do a foreach($item in $someItems) and $someItems is null (or $null rather in powershell) you will actually get one “hit” into the foreach body where $item is set to the value $null. Exactly which use case the powershell team had in mind when they coded that monstrosity I will not even begin to speculate, but it does lead to a helluvalot null checks throughout the scripts.
When developing the scripts I used Powershell GUI which is an opensource free editor with inbuilt debugger. Works like a charm!.
So, on to the code:
We have 4 powershell script files:
a) DeployMachines.ps1 (contains the definitions of all the machines in all the environments, where stuff is placed. These definitions are placed inside anonymous objects called $server throughout the code)
b) DeploymentCommonFunctions.ps1 (contains powershell functions which operate on the before mentioned server objects)
c) DEPLOY_SCRIPT.ps1 (contains a wizard guiding the user through the deployment process)
d) Backup_Script.psq (contains wizard for more fine-grained control of backups, I’ll ignore it for this writeup)
# Deploy_machines.ps1
#
# This file contains the definition of the servers to which deploys are done.
# This means that no servers should be hard-coded in other scripts, but they should include this script
# to get the server definitions. This script is included in the script DeploymentCommonFunctions.ps1
# so any script including that script will also get this script
#
#
#
# Common paths / filters to be used for most or all deploy scripts
#
$script_loaded_machines = $true
"loaded script Deploy_Machines.ps1"
$development_mode = $false #when in development mode, no actual changes will happen on disk, but script logs all actions it would have done
$deploy_staging_area = "d:\serverdeploy\"
$config_exclude = "*.config","thumbs.db","*.txt","*.pdb","*.properties"
$config_include = "*.config","robots.txt","*.properties"
$servicedirexclude = @("LoggingService","Deploy.*")
$wait_seconds_after_task_shutdown = 60 # 60 seconds default
$wait_seconds_after_services_shutdown = 30 # 30 seconds default
$deploy_log_file = "d:\deployment\Logs\deployment_log.txt"
$all_servers = @()
$servertype_customizations = @()
#
# Servertype definitions
#
$servertypes = New-Object Object `
| Add-Member -MemberType NoteProperty -Name InternalServices -Value "InternalServices" -PassThru `
| Add-Member -MemberType NoteProperty -Name Web -Value "Web" -PassThru `
| Add-Member -MemberType NoteProperty -Name Tasks -Value "Tasks" -PassThru
#available object properties, since we have created this object the key values can be accessed through $key.
$key = New-Object Object `
| Add-Member -MemberType NoteProperty -Name ServerType -Value "ServerType" -PassThru `
| Add-Member -MemberType NoteProperty -Name ServerName -Value "ServerName" -PassThru `
| Add-Member -MemberType NoteProperty -Name Drive -Value "Drive" -PassThru `
| Add-Member -MemberType NoteProperty -Name Environment -Value "Environment" -PassThru `
| Add-Member -MemberType NoteProperty -Name Path -Value "Path" -PassThru `
| Add-Member -MemberType NoteProperty -Name Number -Value "Number" -PassThru `
| Add-Member -MemberType NoteProperty -Name BuildPath -Value "BuildPath" -PassThru `
| Add-Member -MemberType NoteProperty -Name BackupPath -Value "BackupPath" -PassThru `
| Add-Member -MemberType NoteProperty -Name OnlyUpgradeExistingDirs -Value "OnlyUpgradeExistingDirs" -PassThru
#create a server object from the input data
function Create-Machine([string] $Id,[string] $Environment, [string] $ServerType, [string] $ServerName,`
[string] $Drive,[string] $Path,[string] $BackupPath,[bool] $OnlyUpgradeExistingDirs, `
[string] $DeployFromTestServer, [string] $BuildPath){
$valid_servertype = Get-Member -InputObject $servertypes -Name $ServerType
if($valid_servertype -eq $null){
Throw [System.ArgumentException]
}
foreach($server in $all_servers){
if($Id -eq $server.Id){
$msg = [System.String]::Format("Server with Id = {0} already defined in server list",$Id)
Throw ($msg)
}
}
$new_server = New-Object Object `
| Add-Member -MemberType NoteProperty -Name Id -Value $Id -PassThru `
| Add-Member -MemberType NoteProperty -Name Environment -Value $Environment -PassThru `
| Add-Member -MemberType NoteProperty -Name ServerType -Value $ServerType -PassThru `
| Add-Member -MemberType NoteProperty -Name ServerName -Value $ServerName -PassThru `
| Add-Member -MemberType NoteProperty -Name Drive -Value $Drive -PassThru `
| Add-Member -MemberType NoteProperty -Name Path -Value $Path -PassThru `
| Add-Member -MemberType NoteProperty -Name BackupPath -Value $BackupPath -PassThru `
| Add-Member -MemberType NoteProperty -Name OnlyUpgradeExistingDirs -Value $OnlyUpgradeExistingDirs -PassThru `
| Add-Member -MemberType NoteProperty -Name DeployFromTestServer -Value $DeployFromTestServer -PassThru `
| Add-Member -MemberType NoteProperty -Name BuildPath -Value $BuildPath -PassThru
return $new_server
}
#
# machine definitions
#
# The machine definitions are created by making dynamic objects containing the following properties
# - Id -> the Id of the deployment target on the form <environment>_<servertype>_<number>
# - Environment -> devtest, test (also used when hotfixing), prod
# - ServerType -> must match entries in $servertypes object
# - ServerName -> must match an entry in c:\windows\system32\drivers\etc\hosts on the build machine
# - Drive -> the disk drive on which stuff is installed / backed up
# - Path -> the root path (on the defined drive) to deploy to (and backup from)
# - BackupPath -> the path (on the defined drive) to backup to
# - OnlyUpgradeExistingDirs (SPECIAL) -> Will only deploy a directory if the directory already exists in deploy target
# - DeployFromTestServer -> (when preparing for deploy to test or prod: which deployment target Id to get files (dlls etc) from)
# - BuildPath -> (when preparing for deploy to devtest or hotfixing to test: which path on the build server to get files (dlls etc) from)
#
# a separate array is defined for each type of server. If a server has more than one type
# (web servers also host a service for instance) then it needs to be defined in all the relevant arrays
#
#Explanation of parameters: *Id* *env* *type* *machinename* X:\ *depl.path* *backuppath* *onlyUpgr.* *GetFromId* *BuildPath*
$all_servers += Create-Machine "devtest_tasks_1" "devtest" "tasks" "10.67.2.15" "e" "Tasks" "Backups" $true $null "d:\DEPLOY\Tasks\"
$all_servers += Create-Machine "test_tasks_1" "test" "tasks" "10.50.1.133" "c" "Code\Tasks" "Backups" $true "devtest_tasks_1" "\\HotfixBuildServer\d$\DEPLOY\Tasks\"
$all_servers += Create-Machine "prod_tasks_1" "prod" "tasks" "192.168.8.16" "d" "Code\Tasks" "Backups" $true "test_tasks_1" $null
#and on and on the list goes.. we have 58 entries in total, only 3 showed here
$servertype_customizations += New-Object Object `
| Add-Member -MemberType NoteProperty -Name ServerType -Value $servertypes.Web -PassThru `
| Add-Member -MemberType NoteProperty -Name ServiceNames -Value @() -PassThru `
| Add-Member -MemberType NoteProperty -Name StopTaskSqlAgent -Value $false -PassThru `
| Add-Member -MemberType NoteProperty -Name RunsIIS -Value $true -PassThru `
| Add-Member -MemberType NoteProperty -Name ExcludeSubDirs -Value @() -PassThru
$servertype_customizations += New-Object Object `
| Add-Member -MemberType NoteProperty -Name ServerType -Value $servertypes.InternalServices -PassThru `
| Add-Member -MemberType NoteProperty -Name ServiceNames -Value @("BEProtocolService","SystemBetProcessingService") -PassThru `
| Add-Member -MemberType NoteProperty -Name StopTaskSqlAgent -Value $true -PassThru `
| Add-Member -MemberType NoteProperty -Name RunsIIS -Value $true -PassThru `
| Add-Member -MemberType NoteProperty -Name ExcludeSubDirs -Value @( "ExternalInfoService") -PassThru
$servertype_customizations += New-Object Object `
| Add-Member -MemberType NoteProperty -Name ServerType -Value $servertypes.Tasks -PassThru `
| Add-Member -MemberType NoteProperty -Name ServiceNames -Value @("SQLSERVERAGENT") -PassThru `
| Add-Member -MemberType NoteProperty -Name StopTaskSqlAgent -Value $false -PassThru `
| Add-Member -MemberType NoteProperty -Name RunsIIS -Value $false -PassThru `
| Add-Member -MemberType NoteProperty -Name ExcludeSubDirs -Value @() -PassThru
b) Deployment_Common_Functions.ps1
this is the real “meat” of the deployment script. it contains functionality for
1) preparing the deployment to a directory on the build server
- fetching the code to be deployed from “previous environment” (for devtest that means the code just built)
- fetching the configuration files from the very environment we are preparing a deploy for. The assumption being that whatever configuration is currently in that environment is the correct one, and any changes that needs to be done because of this deploy needs to be done manually on those files after they’ve been prepared.
2) deploying the prepared files to a target in an environment
3) backing up from a target in an environment
4) restoring a backup
5) cleaning up list of backups from environments
if($script_loaded_machines -eq $false){
$deploy_machines_init = Resolve-Path 'D:\deployment\Deploy_Machines.ps1'
. $deploy_machines_init
}
"loaded script DeploymentCommenFunctions.ps1"
$script_loaded_common = $true
#
# DeploymentCommonFunction.ps1
#
# This script contains useful functions to be called from other more specific scripts
# To include this script in another script the following should be typed in the top of the other script:
#
# $deploy_functions_init = Resolve-Path 'D:\deployment\DeploymentCommonFunctions.ps1'
# . $deploy_functions_init
#
#
################## ENVIRONMENT STUFF #############
function log([string] $message){
Write-Host $message
}
function trace([string] $message){
if($development_mode){
Write-Host $message
}
}
function Log-Deploy([string] $message){
$log_time = [DateTime]::Now.ToString("yyyy-MM-dd HH-mm")
$log_message = $log_time +" - "+$message
Add-Content $deploy_log_file $log_message
}
function Exit-Deploy(){
Log-Deploy "-----------------EXITed from deploy-script"
exit
}
########## PARSING #############
function Prompt-YesNo($text){
Write-Host $text
$input = Read-Host
if($input -eq "y" -or $input -eq "Y"){
Write-Host "You selected YES"
return $true
}elseif($input -eq "n" -or $input -eq "N"){
Write-Host "You selected NO"
return $false
}
Write-Host "could not parse your answer, exiting"
Exit-Deploy
}
########### PROMPTS ###################
function Prompt-WhichServers($servers){
log ""
log "On which servers do you want to operate"
log "input their ids comma separated"
log "(default = all )"
foreach($server in $servers){
if($server -eq $null){
continue
}
$n = $server.Id
$sn = $server.ServerName
Write-Host " $n - $sn"
}
$input = Read-Host
$deploy_servers = @()
switch($input){
""{$deploy_servers = $servers}
default{
foreach($n in $input.Split(',')){
$n = $n.Trim()
$deploy_servers += $servers | Where{$_.Id -eq $n}
}
}
}
return $deploy_servers
}
function Prompt-BeginDeploy($servers){
log ""
log "We are ready to deploy to the following targets:"
$a = @{Expression={$_.Id};Label="ID";width=4}, `
@{Expression={$_.ServerName};Label="Server";width=25}, `
@{Expression={$_.Drive};Label="Disk";width=4}, `
@{Expression={$_.Path};Label="Deploy path";width=25}, `
@{Expression={$_.ServerType};Label="Type";width=25}
foreach($server in $deploy_servers){
$sn = $server.ServerName
$sd = $server.Drive
$sp = $server.Path
$st = $server.ServerType
$si = $server.Id
log "($si): \\$sn\$sd`$\$sp ($st)"
}
log "Press enter to continue"
$input = Read-Host
}
function Prompt-ChildrenFilter($servers){
$childrenFilter = @()
foreach($server in $servers){
$server_root = Get-DeployUNCPath $server
foreach($temp in Get-ChildItem $server_root){
$addIt = $true
foreach($alreadyInList in $childrenFilter){
if($alreadyInList -eq $temp.Name){
$addIt = $false
break
}
}
if($addIt){
$childrenFilter += $temp.Name
}
}
}
Write-Host "Which Children do you want?"
Write-Host "(comma separated)"
for($i = 1; $i -le $childrenFilter.Length; $i++){
$child = $childrenFilter[$i-1]
Write-Host " $i - $child"
}
$input = Read-Host
$newChildrenFilter = @()
switch($input){
""{$newChildrenFilter = $childrenFilter}
default{
foreach($n in $input.Split(',')){
$n = $n.Trim()
$i = [int]::Parse($n)
$newChildrenFilter += $childrenFilter[$i-1]
}
}
}
Write-Host "So, you want:"
for($i = 1; $i -le $newChildrenFilter.Length; $i++){
$child = $newChildrenFilter[$i-1]
Write-Host " $child"
}
Write-Host "Press Esc now if this is wrong, enter to continue"
$input = Read-Host
return $newChildrenFilter
}
function Prompt-WantsChildrenFilter($servertype){
return Prompt-YesNo "Do you want to deploy single instances of $servertype (y/n)"
}
############# GETTERS ON "SERVER" OBJECTS
#
# Gets the path on which stuff will be deployed for a given server object
#
function Get-DeployUNCPath($server){
return "\\"+$server.ServerName+"\"+$server.Drive+"$\"+$server.Path+"\"
}
#
# Gets the path from which stuff will be deployed to test for a given server object
#
function Get-PrepareSourcePath($server){
if($server.IsTest){
return $server.BuildPath
}
else{
$source_server = $all_servers | where{$_.ServerType -eq $server.ServerType -and $_.IsTest -eq $true -and $_.Id -eq $server.DeployFromTestServer}
return Get-DeploySourcePath $source_server
}
}
#
# Gets the path on which stuff will be prepared
#
function Get-PreparePath($server){
return $deploy_staging_area+$server.Environment+"\"+$server.ServerType+"\"+$server.ServerName+"\"+$server.Path+"\"
}
#
# Gets the path on which stuff will be backed up to
#
function Get-BackupUNCPath($server){
return "\\"+$server.ServerName+"\"+$server.Drive+"$\"+$server.BackupPath+"\"
}
############## START / STOP REMOTE SERVICES #####################
#
# starts a service on a specified machine
#
function Start-RemoteService([string] $machine_name, [string] $service_name){
log "starting service $service_name on $machine_name"
if($development_mode){return}
$servicecontroller = [System.ServiceProcess.ServiceController]::GetServices($machine_name) | where{ (($_.name -eq $service_name) -or ($_.displayname -eq $service_name))}
if ($servicecontroller -eq $null)
{
continue;
}
if($servicecontroller.Status -ne 'Running'){
$servicecontroller.Start()
$servicecontroller.WaitForStatus('Running',(new-timespan -seconds 60))
}
log "service $service_name started on $machine_name"
}
#
# Stops a service on a specifiec machine
#
function Stop-RemoteService([string] $machine_name, [string] $service_name){
log "stopping service $service_name on $machine_name"
if($development_mode){return}
$servicecontroller = [System.ServiceProcess.ServiceController]::GetServices($machine_name) | where{ (($_.name -eq $service_name) -or ($_.displayname -eq $service_name))}
if ($servicecontroller -eq $null)
{
return;
}
if($servicecontroller.Status -ne 'Stopped'){
$servicecontroller.Stop()
$servicecontroller.WaitForStatus('Stopped',(new-timespan -seconds 60))
}
log "service $service_name stopped on $machine_name"
}
################# BASE OPERATIONAL FUNCTIONS (works on paths, no prompting) ###################
#
# Backs up from a given location to a different given location on the same machine and same drive
#
function Backup-DeployedStuff([string]$machine_name, [string] $drive_letter, [string] $deployment_location, [string] $backup_location="", [string] $backup_name){
if($backup_name -eq $null -or $backup_name -eq ""){
$backup_name = [DateTime]::Now.ToString("yyyy-MM-dd HH-mm")
}
$full_deploy_path ="\\"+$machine_name+"\"+$drive_letter+"$\"+$deployment_location
$backups_path ="\\"+$machine_name+"\"+$drive_letter+"$\"+$backup_location+"\"+$backup_name
log "backing up from $full_deploy_path to $backups_path"
if($development_mode){return}
if(! (Test-Path ($backups_path) -PathType Container) ){
md ($backups_path)
}
Get-ChildItem $full_deploy_path -Recurse -Force | Copy-Item -Destination {Join-Path $backups_path $_.FullName.Replace($full_deploy_path,"")}
}
function Restore-DeployedStuff([string]$machine_name, [string] $drive_letter, [string] $deployment_location, [string] $backup_location="", [string] $backup_name){
if($backup_name -eq $null -or $backup_name -eq ""){
throw "No backup-name specified"
}
$backups_path ="\\"+$machine_name+"\"+$drive_letter+"$\"+$backup_location+"\"+$backup_name
$full_deploy_path ="\\"+$machine_name+"\"+$drive_letter+"$\"+$deployment_location
log "restoring up from $backups_path to $full_deploy_path"
if($development_mode){return}
if(! (Test-Path ($backups_path) -PathType Container) ){
throw "no backup found at $backups_path"
}
Remove-Item $full_deploy_path -Recurse -Force
md $full_deploy_path
Get-ChildItem $backups_path -Recurse -Force | Copy-Item -Destination {Join-Path $full_deploy_path $_.FullName.Replace($backups_path,"")}
}
#
# Deploys Services from one root path to another.
# It actually deploys the child directories of the root path, and not the root path itself
#
function Deploy-Subdirectories([string] $source_root_path, [string] $destination_root_path, $childrenFilter){
log "starting to deploy children inder under $source_root_path to $destination_root_path"
$filterChildren = $true
if($childrenFilter -eq $null -or $childrenFilter.Length -eq 0){
$filterChildren = $false
}
foreach($child_path in Get-ChildItem $source_root_path){
$childname = $child_path.Name
if($filterChildren){
if(!($childrenFilter -contains $childname)){
continue;
}
}
$destination_path = $destination_root_path + $childname
$source_path = $source_root_path + $childname
Deploy-Directory $source_path $destination_path
}
}
#
# Deploys from one directory to another,
# It first deletes every file (but leaves directories alone) under the destination directory
# then copies everything from the source to the destination
# (this is done to keep directory security settings)
#
function Deploy-Directory([string] $source_path, [string] $destination_path){
log "deploying everything under $source_path to $destination_path"
if($development_mode){return}
$removequeue = Get-ChildItem $destination_path -Recurse -Force | Where{$_.psIsContainer -eq $false}
foreach($item_to_remove in $removequeue){
if($item_to_remove -ne $null -and $item_to_remove.FullName -ne $null){
Remove-Item $item_to_remove.FullName -Force
}
}
Copy-Directory $source_path $destination_path
}
#
# Copies the content recursively from one directory to another directory
# however it only copies things that does not already exist in the destination
# it never replaces files or folders (this is done to keep directory security settings)
#
function Copy-Directory([string] $source_path, [string] $destination_path){
if($development_mode){return}
$copyqueue = New-Object System.Collections.ArrayList
Get-ChildItem $source_path -Recurse -Force | foreach {$null = $copyqueue.Add($_)}
foreach($copyqueue_item in $copyqueue){
$destination = $destination_path + $copyqueue_item.FullName.Substring($source_path.Length)
if(!(Test-Path $destination)){
Copy-Item $copyqueue_item.FullName $destination -Force
}
}
}
function Prepare-Directory([string]$code_source_path, [string] $config_source_path, [string] $prepare_area_path){
log "Copying compiled code from $code_source_path to $prepare_area_path"
log "Copying configs from $config_source_path to $prepare_area_path"
if($development_mode){return}
$source_files = Get-ChildItem -Recurse -Exclude $config_exclude $code_source_path
if($source_files -eq $null){
Log-Deploy "WARNING: Could not find any files to deploy from source $code_source_path Should the target be removed from the environment? "
return;
}
if (!(Test-Path $prepare_area_path))
{
md $prepare_area_path
}
foreach($source_file in $source_files){
$destination_file = Join-Path $prepare_area_path $source_file.FullName.Substring($code_source_path.length)
$directory_exists = Test-Path $destination_file -PathType Container
if($source_file.PSIsContainer -and $directory_exists){
continue
}
else{
Copy-Item $source_file $destination_file -Force
}
}
# Copy-Item $source_files -Destination {Join-Path $prepare_area_path $_.FullName.Substring($code_source_path.length)} -ErrorAction SilentlyContinue
$configs = Get-ChildItem -Recurse -Include $config_include $config_source_path
if($configs -eq $null){
continue
}
foreach($config in $configs){
if($config_source_path.EndsWith("\")){
$config_source_path = $config_source_path.Substring(0,$config_source_path.Length - 1)
}
if($prepare_area_path.EndsWith("\")){
$prepare_area_path = $prepare_area_path.Substring(0,$prepare_area_path.Length - 1)
}
$config_target_dir = $prepare_area_path + $config.Directory.FullName.Substring($config_source_path.Length)
if(Test-Path -Path $config_target_dir -PathType Container){
Copy-Item $config.FullName -Destination $config_target_dir
}
}
}
function Do-Remove-AllExceptSvn($path){
$fullPath = $path.FullName
trace "deleting area $fullPath"
if(! $development_mode){
if (!(Test-Path $path))
{
md $path
}
$escExcDir = "\.svn"
$items = Get-ChildItem $path -Recurse -Force | Where {$_.FullName -notMatch $escExcDir} |sort FullName -Descending
foreach($item in $items){
if(!$item.PSIsContainer -and $item -ne $null ) {
Remove-Item $item.FullName -Force
}
}
$items = Get-ChildItem $path -Recurse -Force | Where {$_.FullName -notMatch $escExcDir -and $_.PSIsContainer -eq $true} |sort FullName -Descending
foreach($item in $items){
if($item -eq $null){
continue
}
$subItems = Get-ChildItem $item.FullName -Recurse -Force | Where { $_.PSIsContainer -eq $false}
if($subItems -eq $null -or $subItems.Length -eq 0 ){
Remove-Item $item.FullName -Force
}
}
}
}
function Remove-AllExceptSvn($path, $childrenFilter){
if (!(Test-Path $path))
{
md $path
}
if($childrenFilter -eq $null -or $childrenFilter.Length -eq 0 ){
Do-Remove-AllExceptSvn $path
}else{
$childPaths = Get-ChildItem $path
foreach($childPath in $childPaths){
if($childrenFilter -contains $childPath.Name){
Do-Remove-AllExceptSvn $childPath.FullName
}
}
}
}
############### FUNCTIONS WORKING ON OTHER FUNCTIONS (takes server objects as input) ############
#
# Backs up a given server object
#
function Backup-Server($server){
Backup-DeployedStuff $server.ServerName, $server.Drive, $server.Path, $server.BackupPath
}
function Prepare-Deploy([object] $server, [bool] $fetch_from_buildserver, $childrenToPrepare){
$are_children_tentative = $server.OnlyUpgradeExistingDirs
$environment = $server.Environment
$server_prepare_path = Get-PreparePath $server
$customizations = $servertype_customizations | where {$_.ServerType -eq $server.ServerType}
Remove-AllExceptSvn $server_prepare_path $childrenToPrepare
$deploy_source = $server.BuildPath
if(!$fetch_from_buildserver ){
$deploy_source = $null
$s = $all_servers | where {$_.Id -eq $server.DeployFromTestServer -and $_.ServerType -eq $server.ServerType }
$deploy_source = Get-DeployUNCPath $s
}
if($deploy_source -eq $null){
$Id = $server.Id
$msg = [System.String]::Format("deployment source not initialized for server {0}.Fetch from build = {1}",$Id, $fetch_from_buildserver)
Throw
}
$filterChildren = $true
if($childrenToPrepare -eq $null -or $childrenToPrepare.Length -eq 0){
$filterChildren = $false
}
$prepare_target_base = $server_prepare_path
$serverUNCPath = Get-DeployUNCPath $server
$thetype = $are_children_tentative.GetType()
"are children tentative? $are_children_tentative ( $thetype )"
if($are_children_tentative){
foreach($sub_dir in Get-ChildItem $deploy_source){
$child_name = $sub_dir.Name
if($filterChildren){
$skip = $true;
foreach($child_filter_name in $childrenToPrepare){
if($child_name -eq $child_filter_name){
$skip = $false;
break;
}
}
if($skip){
continue;
}
}
$deploy_path = $serverUNCPath + $sub_dir.Name
$prepare_target = $prepare_target_base +$sub_dir+"\"
$child_deploy_source = $deploy_source + $sub_dir.Name+"\"
if(!(Test-Path $deploy_path -PathType Container)){
#this child dir does not exist on server, so we won't update it automagically
continue
}
$should_deploy = $true
foreach($exclude in $customizations.ExcludeSubDirs){
if($exclude -eq $child_name){
$should_deploy = $false
}
}
if(! $should_deploy){
continue
}
trace "Creating prepare area $prepare_target"
if(! $development_mode -and !(Test-Path $prepare_target -PathType Container)){
md $prepare_target
}
Prepare-Directory $child_deploy_source $deploy_path $prepare_target
}
}else{
$prepare_target = $prepare_target_base
trace "Creating prepare area $prepare_target"
if(! $development_mode -and !(Test-Path $prepare_target)){
md $prepare_target
}
$exists_source = Test-Path $deploy_source
Prepare-Directory $deploy_source $serverUNCPath $prepare_target
}
}
function Deploy-PerServer($deploy_servers, $childrenFilter){
foreach($deploy_server in $deploy_servers){
if($deploy_server -eq $null){
continue
}
$deploy_path = Get-DeployUNCPath $deploy_server
$source_path = Get-PreparePath $deploy_server
Log-Deploy " - $source_path => $deploy_path"
$stopstart = $servertype_customizations | where {$_.ServerType -eq $deploy_server.ServerType}
if($stopstart.StopTaskSqlAgent){
foreach($task_server in $all_servers | Where{$_.Environment -eq $deploy_server.Environment -and $_.ServerType -eq $servertypes.Tasks} ){
log "stopping SQLSERVERAGENT on $task_server"
Stop-RemoteService $task_server.ServerName "SQLSERVERAGENT"
}
log "waiting for $wait_seconds_after_task_shutdown secs to be sure tasks are done before we begin"
if(!$development_mode){
Start-Sleep -Seconds $wait_seconds_after_task_shutdown #sleep so tasks can finish before we pull down applications
}
log "wakey wakey, get on with it"
}
foreach($service_name in $stopstart.ServiceNames){
Stop-RemoteService $deploy_server.ServerName $service_name
}
if($stopstart.ServiceNames.length -gt 0){
log "waiting for $wait_seconds_after_services_shutdown secs to be sure tasks are done"
if(!$development_mode){
Start-Sleep -Seconds $wait_seconds_after_services_shutdown #sleep so tasks can finish before we pull down applications
}
log "wakey wakey, get on with it"
}
if($deploy_server.OnlyUpgradeExistingDirs){
Deploy-Subdirectories $source_path $deploy_path $childrenFilter
}
else{
Deploy-Directory $source_path $deploy_path
}
foreach($service_name in $stopstart.ServiceNames){
Start-RemoteService $deploy_server.ServerName $service_name
}
if($stopstart.StopTaskSqlAgent){
foreach($task_server in $all_servers | Where{$_.Environment -eq $deploy_server.Environment -and $_.ServerType -eq $servertypes.Tasks} ){
log "starting SQLSERVERAGENT on $task_server"
Start-RemoteService $task_server.ServerName "SQLSERVERAGENT"
}
}
}
}
function Deploy-ServerType($servertype, $environment, $shouldPromptForSubDirectories){
$start_time = [DateTime]::Now
Log-Deploy "Starting $servertype deployment to $environment"
$servers = $all_servers | Where {$_.Environment -eq $environment -and $_.ServerType -eq $servertype}
$deploy_servers = Prompt-WhichServers $servers
$childrenFilter = $null
if($shouldPromptForSubDirectories){
$shouldPromptForSubDirectories = Prompt-WantsChildrenFilter $servertype
}
if($shouldPromptForSubDirectories){
$childrenFilter = Prompt-ChildrenFilter $deploy_servers
}
Prompt-BeginDeploy $deploy_servers
Deploy-PerServer $deploy_servers $childrenFilter
$end_time = [DateTime]::Now
$duration = $end_time - $start_time
Write-Host "FINISHED!!! and it only took $duration"
Log-Deploy "Finished $servertype deployment to $deploy_environment_name ($duration)"
}
function Deploy-All($environment){
$start_time = [DateTime]::Now
Log-Deploy "Are you sure you want to do a full deployment? (have you made a backup?)"
Log-Deploy "Press Escape to exit, Enter to continue"
$input = Read-Host
Log-Deploy "Starting deployment of everything to $environment"
$servers = $all_servers | Where {$_.Environment -eq $environment}
#stop tasks
foreach($task_server in $all_servers | Where{$_.Environment -eq $deploy_server.Environment -and $_.ServerType -eq $servertypes.Tasks} ){
if($deploy_server -eq $null){
continue
}
log "stopping SQLSERVERAGENT on $task_server"
Stop-RemoteService $task_server.ServerName "SQLSERVERAGENT"
}
#stop running services on all machines
foreach($deploy_server in $servers){
if($deploy_server -eq $null){
continue
}
$stopstart = $servertype_customizations | where {$_.ServerType -eq $deploy_server.ServerType}
foreach($service_name in $stopstart.ServiceNames){
Stop-RemoteService $deploy_server.ServerName $service_name
}
}
#wait until stuff has quieted down
log "waiting for $wait_seconds_after_task_shutdown secs to be sure tasks are done before we begin"
if(!$development_mode){
Start-Sleep -Seconds $wait_seconds_after_task_shutdown #sleep so tasks can finish before we pull down applications
}
log "wakey wakey, get on with it"
# Perform file deploy
foreach($deploy_server in $servers){
if($deploy_server -eq $null){
continue
}
$deploy_path = Get-DeployUNCPath $deploy_server
$source_path = Get-PreparePath $deploy_server
Log-Deploy " - $source_path => $deploy_path"
if($deploy_server.OnlyUpgradeExistingDirs){
Deploy-Subdirectories $source_path $deploy_path $null
}
else{
Deploy-Directory $source_path $deploy_path
}
}
#stop running services on all machines
foreach($deploy_server in $servers){
if($deploy_server -eq $null){
continue
}
$stopstart = $servertype_customizations | where {$_.ServerType -eq $deploy_server.ServerType}
foreach($service_name in $stopstart.ServiceNames){
Start-RemoteService $deploy_server.ServerName $service_name
}
}
foreach($task_server in $all_servers | Where{$_.Environment -eq $deploy_server.Environment -and $_.ServerType -eq $servertypes.Tasks} ){
log "starting SQLSERVERAGENT on $task_server"
Start-RemoteService $task_server.ServerName "SQLSERVERAGENT"
}
$end_time = [DateTime]::Now
$duration = $end_time - $start_time
Write-Host "FINISHED!!! and it only took $duration"
Log-Deploy "Finished $servertype deployment to $deploy_environment_name ($duration)"
}
function Prepare-ServerType($servertype, $environment, [bool]$shouldPromptForSubDirectories = $false){
Log-Deploy "Starting $servertype Deploy Preparations to $environment"
$start_time = [DateTime]::Now
$servers = $all_servers | where{$_.ServerType -eq $servertype -and $_.Environment -eq $environment}
if($shouldPromptForSubDirectories){
$shouldPromptForSubDirectories = Prompt-WantsChildrenFilter $servertype
}
if($shouldPromptForSubDirectories){
$childrenFilter = Prompt-ChildrenFilter $servers
}
foreach($server in $servers){
if($server -ne $null){
Prepare-Deploy $server $prepare_from_buildserver $childrenFilter
}
}
$end_time = [DateTime]::Now
$duration = $end_time - $start_time
Write-Host "FINISHED!!! and it only took $duration"
Log-Deploy "Finished $servertype Preparations ($deploy_environment_name) - ($duration)"
}
3)
The last piece of the puzzle is the wizard guiding the user through the process. really simple .bat file stuff:
$script_loaded_machines = $false
$script_loaded_common = $false
$script_loaded_backup = $false
$script_loaded_externaldeploy = $false
$script_loaded_externalprepare = $false
$script_loaded_failsafe = $false
if($script_loaded_backup -eq $false){
$include = Resolve-Path 'D:\deployment\Make_Backups.ps1' #contains the wizard for doing backups, nothing much to see here
. $include
}
$ui = (Get-Host).UI.RawUI
$ui.ForegroundColor = "black"
function Do-Backup(){
$ui.ForegroundColor = "darkblue"
Create-Backups -environment $environment
}
function Do-Restore(){
$ui.ForegroundColor = "darkcyan"
Restore-Backups -environment $environment
}
function Do-CleanBackups(){
$ui.ForegroundColor = "darkcyan"
Delete-Backups -environment $environment
}
function Prepare-Everything($environment){
""
"About to prepare for a full deploy"
"Press escape to abort, Enter to continue"
""
$input = Read-Host
$all_start_time = [DateTime]::Now
foreach($typeholder in $servertype_customizations){
$servertype = $typeholder.ServerType
Log "Preparing servertype $servertype for $environment"
Prepare-ServerType -environment $environment -servertype $servertype $false
}
$all_end_time = [DateTime]::Now
$duration = $all_end_time - $all_start_time
Write-Host "FINISHED!!! and it only took $duration"
}
function Do-Prepare(){
$prepare_from_buildserver = $false
if($environment -eq "devtest"){
$prepare_from_buildserver = $true
}
if($environment -eq "test"){
Log ""
Log "Is this a hotfix? (y / n) (y==fetch from hotfix server)"
$input = Read-Host
if($input -eq "n" -or $input -eq "N"){
Log "Not hotfix: Fetch files from previous env"
}
elseif($input -eq "y" -or $input -eq "Y")
{
Log "Hotfix: Fetches from hotfix build server"
$prepare_from_buildserver = $true
}
else{
Exit-Deploy
}
}
$ui.ForegroundColor = "darkgreen"
""
""
"What do you want to prepare?"
"1 - Services"
"2 - Web"
"3 - Tasks"
"100 - Everyting in $environment"
"(default = quit the script)"
$input = Read-Host
""
"(you chose $input)"
switch($input){
"1"{ Prepare-ServerType $servertypes.InternalServices $environment $true}
"2"{ Prepare-ServerType $servertypes.Web $environment $false}
"3"{ Prepare-ServerType $servertypes.Tasks $environment $true}
"100"{Prepare-Everything $environment}
default{Exit-Deploy}
}
}
function Do-Deploy(){
$ui.ForegroundColor = "DarkRed"
""
"What do you want to deploy?"
"1 - Web Services"
"2 - Web Site "
"3 - Database Tasks"
"100 - Everyting"
"(default = quit the script)"
""
$input = Read-Host
switch($input){
"1" { Deploy-ServerType $servertypes.InternalServices $environment $true}
"2" { Deploy-ServerType $servertypes.Web $environment $false}
"3" { Deploy-ServerType $servertypes.Tasks $environment $true}
"100" { Deploy-All -environment $environment }
default{Exit-Deploy }
}
}
Log-Deploy "----------------STARTed deploy script"
$ui.BackgroundColor = "White"
$ui.ForegroundColor = "Black"
"Welcome to the deployment script"
""
""
"Before you run this script to deploy to devtest you need to have:"
" - run c:\ProjectWork\ThisProject\buildsolutions.bat locally"
" - checked in inn c:\ProjectWork\ThisProject\services\SharedAssemblies to subversion"
" - built all the projects in CruiseControl.net ( http://buildsvc ) - (zzz_FullBuild_ClickToBuildEverything) "
""
"[Dev mode=$development_mode] (when in dev mode, no changes are made to anything)"
""
$do_quit = $false
""
""
"Which environment do you want to operate on?"
""
"0 - devtest"
"1 - test/hotfix"
"99 - prod"
""
$environment = "devtest"
$input = Read-Host
switch($input){
"0"{
$environment = "devtest"
}
"1"{
$environment = "test"
}
"99"{
$environment = "prod"
$ui.BackgroundColor = "Red"
$ui.ForegroundColor = "White"
""
"ARE YOU REALLY SURE YOU WANT TO OPERATE ON THE PRODUCTION ENVIRONMENT? (press escape to abort)"
""
$ui.ForegroundColor = "Black"
$ui.BackgroundColor = "White"
$input = Read-Host
}
default{Exit-Deploy}
}
while(! $do_quit){
$ui.ForegroundColor = "black"
"What do you want to do?"
"1 - Back up the code and config currently in $environment "
"2 - Prepare a deploy to staging area on buildserver ( $environment )"
"3 - deploy to $environment "
"8 - Delete an old backup from $environment"
"9 - Restore an old backup into $environment"
"exit - avslutte"
"(default = avslutte)"
$input = Read-Host
switch($input){
"1"{ Do-Backup }
"2"{ Do-Prepare}
"3"{ Do-Deploy}
"8"{ Do-CleanBackups}
"9"{ Do-Restore}
default{Exit-Deploy}
}
}
I’ve covered a lot of ground here, it took me bloody ages to write it up (sorry to the commenter who needed it some weeks ago).
This strategy for build and deployment has a few weaknesses i know of (and probably a lot I don’t know), most notably:
- we use commonly compiled dlls which are checked into svn. ideally we should be referencing them directly from the buildserver so we didn’t need a two-phased build
- Database changes are completely decoupled from the code, so as the code is moved from environment to environment, the database has to be kept manually (though through tools) updated.
- configuration changes are bloody hard to get from environment to environment in .Net, you either have to check in web.config in N versions (one for each machine in each environment) or you have to keep them all manually up to date.
All in all this strategy has worked out very well for us these last 6 months, and errors stemming from deployment issues are now almost nonexistent. (we still get the odd DB-sync glitch or configuration sync glitch) but now the script just does its thing for 15 minutes and then you’re set.
I hope this has been of use to someone.
good night
I usually add this little gem in my projects:
public static List<T> ToList<T>(this List<T> list)
{
return list;
}
so that calling ToList on something that is already a list doesn’t require looping through all the content (which is the default implementation of IEnumerable[t].ToList())
I also tend to put it into the System.Linq namespace so that it comes automatically when I get the original method. (i know it’s not the recommended way, but GOD it’s so practical)
premature optimization is the root of all evil, but small invisible ones isn’t (imho)
Theme: Rubric. Blog at WordPress.com.