<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss2full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><!--Generated by Squarespace V5 Site Server v5.13.159 (http://www.squarespace.com) on Fri, 24 May 2013 20:34:40 GMT--><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" version="2.0"><channel><title>Geoff Hudik's Tech-E Blog</title><link>http://www.geoffhudik.com/tech/</link><description>Programming, .net, IT, gadget and geek related stuff</description><lastBuildDate>Fri, 24 May 2013 20:23:54 +0000</lastBuildDate><copyright>Copyright 2011 Geoff Hudik</copyright><language>en-US</language><generator>Squarespace V5 Site Server v5.13.159 (http://www.squarespace.com)</generator><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/rss+xml" href="http://feeds.feedburner.com/thnk2wn" /><feedburner:info uri="thnk2wn" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><item><title>ASP.NET NLog Sql Server Logging and Error Handling Part 2</title><category>.net</category><category>NLog</category><category>Ninject</category><category>asp.net mvc</category><category>error handling</category><category>logging</category><category>sql server</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Fri, 24 May 2013 19:00:00 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/ynXBOv3AiRw/aspnet-nlog-sql-server-logging-and-error-handling-part-2.html</link><guid isPermaLink="false">632421:7353983:33713201</guid><description>Be sure to check out &lt;a href="http://www.geoffhudik.com/tech/2013/5/20/aspnet-nlog-sql-server-logging-and-error-handling-part-1.html"&gt;Part 1&lt;/a&gt; as this post builds upon it and the two go hand in hand.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Series Overview&lt;/h3&gt;

&lt;a href="http://www.geoffhudik.com/tech/2013/5/20/aspnet-nlog-sql-server-logging-and-error-handling-part-1.html"&gt;Part 1&lt;/a&gt; - Setting up logging with ASP.NET MVC, NLog and SQL Server&lt;br/&gt;&lt;br/&gt;

Part 2 - Unhandled exception processing, building an error report, emailing errors, and custom error pages.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Custom Error Handling Attribute&lt;/h3&gt;
Added in FilterConfig.RegisterGlobalFilters and bound in DiagnosticModule, AppErrorHandlerAttribute invokes reporting the unhandled exception and setting the error view to be displayed to the end user. An enableErrorPages appSetting controls whether any of this is done; for local debugging or a dev web server having this off might be desirable.&lt;br/&gt;

&lt;pre class="brush:csharp"&gt;
namespace NLogSql.Web.Infrastructure.ErrorHandling
{
    public class AppErrorHandlerAttribute : FilterAttribute, IExceptionFilter
    {
        [Inject]
        public IErrorReporter Reporter { get; set; }

        public void OnException(ExceptionContext exceptionContext)
        {
            if (exceptionContext.ExceptionHandled) return;

            if (ConfigurationManager.AppSettings["enableErrorPages"] == "false")
            {
                AppLogFactory.Create&amp;lt;AppErrorHandlerAttribute&amp;gt;().Error(
                    "Unexpected error. enableErrorPages is false, skipping detailed "
					+ "error gathering. Error was: {0}",
                    exceptionContext.Exception.ToString());
                return;
            }

            Ensure.That(Reporter, "Reporter").IsNotNull();
            Reporter.ReportException(exceptionContext);

            SetErrorViewResult(exceptionContext);
        }

        private static void SetErrorViewResult(ExceptionContext exceptionContext)
        {
            var statusCode = new HttpException(null, exceptionContext.Exception)
				.GetHttpCode();

            exceptionContext.Result = new ViewResult
            {
                ViewName = MVC.Shared.Views.ViewNames.Error,
                TempData = exceptionContext.Controller.TempData,
                //ViewData = new ViewDataDictionary&amp;lt;ErrorModel&amp;gt;(new ErrorModel())
            };

            exceptionContext.ExceptionHandled = true;
            exceptionContext.HttpContext.Response.Clear();
            exceptionContext.HttpContext.Response.StatusCode = statusCode;
            exceptionContext.HttpContext.Response.TrySkipIisCustomErrors = true;
        }
    }
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Logging and Reporting the Error&lt;/h3&gt;
In the same namespace the ErrorReporter class invokes generation of an error report and logs and emails the error report. 
The overload with customActivityMessage would generally be used with handled exceptions where it may still be desirable to 
report the exception in cases.&lt;br/&gt;

&lt;pre class="brush:csharp"&gt;
public class ErrorReporter : IErrorReporter
{
	private readonly ILog _log;

	public ErrorReporter(ILog log)
	{
		_log = Ensure.That(log, "log").IsNotNull().Value;
	}

	private string CustomActivityMessage { get; set; }

	public void ReportException(ControllerContext controllerContext, 
		Exception exception, string customActivityMessage = null)
	{
		this.CustomActivityMessage = customActivityMessage;
		ReportException(new ExceptionContext(controllerContext, exception));
	}

	public void ReportException(ExceptionContext exceptionContext)
	{
		var errorInfo = new ErrorReportInfo(exceptionContext, this.CustomActivityMessage);
		errorInfo.Generate();
		_log.Error("Unexpected error: {0}", errorInfo.ReportText);

		if (errorInfo.Errors.Any())
			_log.Error("Error generating error report. Original exception: {0}", 
				exceptionContext.Exception);

		// sending mail can be a little slow, don't delay end user seeing error page
		Task.Factory.StartNew(
		state =&amp;gt;
		{
			var errorReport = (ErrorReportInfo)state;
			DependencyResolver.Current.GetService&amp;lt;IErrorEmailer&amp;gt;()
				.SendErrorEmail(errorReport);
		},
		errorInfo).ContinueWith(t =&amp;gt;
		{
			if (null != t.Exception)
				_log.Error("Error sending email: " + t.Exception.ToString());    
		});
	}
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Building the Error Report&lt;/h3&gt;

&lt;table border="0" cellspacing="0" cellpadding="0"&gt;&lt;tr&gt;
&lt;td valign="top" style="padding-right: 10px;"&gt;
Various diagnostic info classes are responsible for building different diagnostic "sub reports".  
Each inherits from DiagnosticInfoBase which is a glorified StringBuilder, with functionality to build both a plain text version of the report (logged to DB) as well as an HTML formatted version (used for emails). 
&lt;br/&gt;&lt;br/&gt;
The base class has safe appending and formatting functionality and ensures that any error in generating a part of the report doesn't stop the whole process. 
&lt;/td&gt;
&lt;td&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/DiagnosticClasses.jpg"/&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

A sample implementation:&lt;br/&gt;

&lt;pre class="brush:csharp"&gt;
public class FormInfo : DiagnosticInfoBase
{
	private readonly HttpRequestBase _request;

	public FormInfo(HttpRequestBase request)
	{
		_request = request;
	}

	protected override void GenerateReport()
	{
		StartTable();
		var keys = _request.Form.AllKeys.OrderBy(s =&amp;gt; s).ToList();

		foreach (var name in keys)
		{
			var value = _request.Form[name];

			if (null != value &amp;&amp; name.Contains("password", StringComparison.OrdinalIgnoreCase))
			{
				value = new string('*', value.Length);
			}

			AppendRow(name, value);
		}

		EndTable();
	}
}
&lt;/pre&gt;

The ErrorReportInfo class combines the output of each section for the full error report.&lt;br/&gt;&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/ErrorReportInfo.jpg"/&gt;
&lt;br/&gt;
&lt;br/&gt;

In the end this produces a &lt;a href="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/SampleErrorReport.html" target="_blank"&gt;sample HTML email report like this&lt;/a&gt;.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Sending the Email&lt;/h3&gt;

Sending the email logs the result on failure or success, 

&lt;pre class="brush:csharp"&gt;
public interface IErrorEmailer
{
	void SendErrorEmail(ErrorReportInfo errorInfo);
}

public class ErrorEmailer : IErrorEmailer
{
	private readonly IMailer _mailer;
	private readonly ILog _log;

	public ErrorEmailer(IMailer mailer, ILog log)
	{
		_mailer = Ensure.That(mailer, "mailer").IsNotNull().Value;
		_log = Ensure.That(log, "log").IsNotNull().Value;
	}

	public void SendErrorEmail(ErrorReportInfo errorInfo)
	{
		try
		{
			var subject = string.Format("{0} Error", 
				Assembly.GetExecutingAssembly().GetName().Name);

			if (null != errorInfo.Server &amp;&amp; null != errorInfo.Location
				&amp;&amp; !string.IsNullOrWhiteSpace(errorInfo.Location.ControllerAction)
				&amp;&amp; !string.IsNullOrWhiteSpace(errorInfo.Server.HostName))
			{
				subject = string.Format("{0}: {1} - {2}", subject, 
					errorInfo.Server.HostName, errorInfo.Location.ControllerAction);
			}

			var to = AppSettings.Default.Email.ErrorMessageTo;
			_mailer.SendMail(to, subject, errorInfo.ReportHtml);

			_log.Info("Sent email: {0} to {1}", subject, to);
		}
		catch (Exception ex)
		{
			_log.Error("Error sending error report email: {0}", ex);
		}
	}
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;The Error View&lt;/h3&gt;

At the end of AppErrorHandlerAttribute the SetErrorViewResult invoked the Views\Shared\Error.cshtml page. According to the HTTP status code, content such as text, images and styles differ. Separate error pages may have more advantages if there are a larger number of differences; a single page was less work here.&lt;br/&gt;

&lt;pre class="brush:html"&gt;
@section styles{
    &amp;lt;link rel="stylesheet" href="~/Content/Error.css"/&amp;gt;
}

@{
    var statusTitleMap = new Dictionary&amp;lt;int, string&amp;gt; {
             {404, "Something got lost in the shuffle"},
             {410, "Gone like yesterday"},
             {500, "Something go boom"}
         };
}

@section hero{
    &amp;lt;div id="error-body" class="container-fluid"&amp;gt;
        &amp;lt;div class="row-fluid"&amp;gt;
            &amp;lt;div class="span3"&amp;gt;
                @{ var errorClass = (Response.StatusCode == 404 || Response.StatusCode == 410) ? "_" + Response.StatusCode : "_500";}
                &amp;lt;div id="errorImageBlock" class="@errorClass"&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
    
            &amp;lt;div class="span8 offset1"&amp;gt;
                &amp;lt;div class="row-fluid"&amp;gt;
                    &amp;lt;div class="span12 text-center" id="status-code"&amp;gt;
                        @Response.StatusCode
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class="row-fluid"&amp;gt;
                    &amp;lt;div class="span12" id="well-this"&amp;gt;
                        @statusTitleMap[Response.StatusCode]
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
        
                &amp;lt;div class="row-fluid"&amp;gt;
                    &amp;lt;div class="span11" id="message"&amp;gt;
                    @switch (Response.StatusCode)
                    {
                        case 404:
                            @:We cannot find the page you are looking for. If you typed in the address, double check the spelling. If you got here by clicking a link, 
                            @: &amp;lt;a href="mailto:customerservice@domain.com?subject=Page%20Not%20Found"&amp;gt;let us know&amp;lt;/a&amp;gt;.
                            break;
                        case 410:
                            @:The page you are looking for is gone (permanently). If you feel you reached this page incorrectly, &amp;lt;a href="mailto:customerservice@domain.com?subject=Link%20Gone"&amp;gt;let us know&amp;lt;/a&amp;gt;.
                            break;
                        case 500:
                            @:Oh dear, something's gone wrong. Our team has already been alerted to the problem and will fix it as soon as possible! 
                            break;
                    }  
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Testing Errors&lt;/h3&gt;

At the bottom of the home page are some links to test out error functionality.&lt;br/&gt;&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/HomePage.jpg"/&gt;
&lt;br/&gt;&lt;br/&gt;

&lt;a href="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/ErrorPage500.jpg" class="fancybox" title="500 Error Page" alt="Click for larger version"&gt;
	&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/ErrorPage500Small.jpg"/&gt;
&lt;/a&gt;
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Handling Not Found and Gone Permanently&lt;/h3&gt;

404s have some special handling in Global.asax.cs:&lt;br/&gt;

&lt;pre class="brush:csharp"&gt;
protected void Application_EndRequest()
{
	if (Context.Response.StatusCode == 404)
	{
		if (Request.RequestContext.RouteData.Values["fake404"] == null)
		{
			Response.Clear();

			var rd = new RouteData();
			rd.Values["controller"] = MVC.Error.Name;
			rd.Values["action"] = MVC.Error.ActionNames.NotFound;

			var c = (IController)DependencyResolver.Current.GetService&amp;lt;ErrorController&amp;gt;();
			Request.RequestContext.RouteData = rd;
			c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
		}
	}
}
&lt;/pre&gt;

That code makes me itch a bit and could be done better but there is a reason for it. If you're wondering why not just use custom error pages, the answer is that doing so for a 404 page ends up producing a 301 redirect to then 404 on the custom not found page. For SEO purposes that is usually considered a bad practice. If you don't have a public site or as much SEO concern, that may be acceptable but in this case it wasn't.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Error Controller&lt;/h3&gt;

The error controller sets and logs the response status codes and returns the error view. For the Gone action, usually there'd be routes defined for legacy URLs that would direct to that action.&lt;br/&gt;
 
&lt;pre class="brush:csharp"&gt;
public partial class ErrorController : Controller
{
	private readonly ILog _log;

	public ErrorController(ILog log)
	{
		_log = Ensure.That(log, "log").IsNotNull().Value;
	}

	public virtual ActionResult NotFound()
	{
		Response.StatusCode = (int)HttpStatusCode.NotFound;
		RouteData.Values["fake404"] = true;
		_log.Write(LogType.Warn, new { Code = "404" }, 
			"404 Not Found for {0}", Request.Url);
		return View("Error");
	}

	public virtual ActionResult Gone()
	{
		Response.StatusCode = (int)HttpStatusCode.Gone;
		Response.Status = "410 Gone";
		Response.TrySkipIisCustomErrors = true;
		_log.Write(LogType.Warn, new {Code = "410"}, 
			"410 gone permanently for {0}", Request.Url);
		return View("Error");
	}
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Future Enhancements&lt;/h3&gt;

&lt;ul&gt;
	&lt;li&gt;The diagnostic info report building classes are pretty quick and messy in spots and could use cleanup.&lt;/li&gt;
	
	&lt;li&gt;Error view work including better responsive design and user-acceptable images :)&lt;/li&gt;

	&lt;li&gt;Admin error view to inspect log data and errors&lt;/li&gt;
	
	&lt;li&gt;Use and integration of apps such as &lt;a href="http://appfail.net/"&gt;appfail.net&lt;/a&gt; and &lt;a href="https://newrelic.com/"&gt;New Relic&lt;/a&gt; to monitor errors and performance. This app used those tools in conjunction with the custom error handling and logging functionality.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;The Code&lt;/h3&gt;

The code for this series is available at &lt;a href="https://github.com/thnk2wn/NLogSql.Web"&gt;https://github.com/thnk2wn/NLogSql.Web&lt;/a&gt;&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/ynXBOv3AiRw" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-33713201.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2013/5/24/aspnet-nlog-sql-server-logging-and-error-handling-part-2.html</feedburner:origLink></item><item><title>ASP.NET NLog Sql Server Logging and Error Handling Part 1</title><category>.net</category><category>NLog</category><category>Ninject</category><category>asp.net mvc</category><category>error handling</category><category>logging</category><category>sql server</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Tue, 21 May 2013 05:51:00 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/Jrx77du2RUg/aspnet-nlog-sql-server-logging-and-error-handling-part-1.html</link><guid isPermaLink="false">632421:7353983:33685523</guid><description>It was the eleventh hour before a web app was to go live and there was nothing in place for error handling or logging. You're shocked I'm sure (that's my sarcastic voice). Normally with web apps in the past I've used &lt;a href="https://code.google.com/p/elmah/"&gt;ELMAH&lt;/a&gt; for unhandled error logging and NLog with text file targets for other logging.
&lt;br/&gt;&lt;br/&gt;

ELMAH works great but in this case I was told it was pulled for some reason. With no time to find out the story there I threw together something to capture basic error details and send an email. Later that grew and was customized and ELMAH never returned though perhaps it should have. Still there are benefits of doing this yourself including more customization, less bloat, less hunting through perhaps lacking documentation etc.
&lt;br/&gt;&lt;br/&gt;

On the logging front I still wanted to use NLog but text file logging was no longer a fit. This app had a few instances behind a load balancer and central logging was needed plus it needed a higher volume of logging in some troublesome workflows, more so than the occasional errors or warnings here and there. I had not used NLog with SQL Server before but this seemed like the perfect time to do so.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Series Overview&lt;/h3&gt;

Part 1 - Setting up logging with ASP.NET MVC, NLog and SQL Server&lt;br/&gt;&lt;br/&gt;

&lt;a href="http://www.geoffhudik.com/tech/2013/5/24/aspnet-nlog-sql-server-logging-and-error-handling-part-2.html"&gt;Part 2&lt;/a&gt; - Unhandled exception processing, building an error report, emailing errors, and custom error pages.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Setup and Configuration&lt;/h3&gt;
I began by installing the &lt;a href="http://nuget.org/packages/NLog/"&gt;NLog&lt;/a&gt; NuGet package and the &lt;a href="http://nuget.org/packages/NLog.Extended/"&gt;NLog.Extended&lt;/a&gt; package for asp.net specific information into the ASP.NET MVC 4 project. The &lt;a href="http://nuget.org/packages/NLog.Config/"&gt;NLog.Config&lt;/a&gt; package is useful for a standalone NLog config file but for web apps with existing config transforms I find embedding the configuration in Web.config easier.
&lt;br/&gt;&lt;br/&gt;

In web.config under configuration/configSections, the NLog section is defined:&lt;br/&gt;
&lt;pre class="brush:xml"&gt;
	&amp;lt;section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog" /&amp;gt;
&lt;/pre&gt;

Under &amp;lt;configuration&amp;gt; my initial NLog config looked like the below, using the async attribute on targets to make each target work asynchronously:&lt;br/&gt;
&lt;pre class="brush:xml"&gt;
&amp;lt;nlog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" throwExceptions="true" 
    internalLogLevel="Debug" internalLogFile="${basedir}/NlogInternal.log"&amp;gt;
    &amp;lt;!-- go to http://nlog-project.org/wiki/Configuration_file for more information --&amp;gt;
    &amp;lt;extensions&amp;gt;
      &amp;lt;add assembly="NLog.Extended" /&amp;gt;
    &amp;lt;/extensions&amp;gt;
    &amp;lt;targets async="true"&amp;gt;
      &amp;lt;target xsi:type="Database" name="dbTarget" connectionStringName="Logger" 
	  commandText="exec sLogEvent_Insert @time_stamp, @level, @logger, @userName, @url, 
	    @machineName, @sessionId, @threadId, @referrer, @userAgent, @code, @message"&amp;gt;
        &amp;lt;parameter name="@time_stamp" layout="${date}" /&amp;gt;
        &amp;lt;parameter name="@level" layout="${level}" /&amp;gt;
        &amp;lt;parameter name="@logger" layout="${logger}" /&amp;gt;
        &amp;lt;parameter name="@userName" layout="${identity}" /&amp;gt;
        &amp;lt;parameter name="@url" layout="${aspnet-request:serverVariable=Url}" /&amp;gt;
        &amp;lt;parameter name="@machineName" layout="${machinename}" /&amp;gt;
        &amp;lt;parameter name="@sessionId" layout="${aspnet-sessionid}" /&amp;gt;
        &amp;lt;parameter name="@threadId" layout="${threadid}" /&amp;gt;
        &amp;lt;parameter name="@referrer" 
			layout="${aspnet-request:serverVariable=HTTP_REFERER}" /&amp;gt;
        &amp;lt;parameter name="@userAgent" 
			layout="${aspnet-request:serverVariable=HTTP_USER_AGENT}" /&amp;gt;
		&amp;lt;parameter name="@code" layout="${event-context:item=Code}" /&amp;gt;
        &amp;lt;parameter name="@message" layout="${message}" /&amp;gt;
      &amp;lt;/target&amp;gt;
      &amp;lt;target name="debugTarget" xsi:type="Debugger" 
		layout="${time}|${level:uppercase=true}|${logger}|${message}" /&amp;gt;
    &amp;lt;/targets&amp;gt;
    &amp;lt;rules&amp;gt;
      &amp;lt;!-- Levels: Off, Trace, Debug, Info, Warn, Error, Fatal --&amp;gt;
      &amp;lt;logger name="*" minlevel="Trace" writeTo="debugTarget,dbTarget" /&amp;gt;
    &amp;lt;/rules&amp;gt;
&amp;lt;/nlog&amp;gt;
&lt;/pre&gt;

Later I swapped to explicit use of &lt;a href="https://github.com/nlog/nlog/wiki/AsyncWrapper-target"&gt;AsyncWrapper&lt;/a&gt; targets as it allowed me to control the behavior more explicitly and mirrored the reality at runtime, at the cost of being more verbose.&lt;br/&gt;
&lt;pre class="brush:xml"&gt;
&amp;lt;nlog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" throwExceptions="true" 
    internalLogLevel="Debug" internalLogFile="${basedir}/NlogInternal.log"&amp;gt;
    &amp;lt;!-- go to http://nlog-project.org/wiki/Configuration_file for more information --&amp;gt;
    &amp;lt;extensions&amp;gt;
      &amp;lt;add assembly="NLog.Extended" /&amp;gt;
    &amp;lt;/extensions&amp;gt;
    &amp;lt;targets&amp;gt;
      &amp;lt;target name="asyncDbWrapperTarget" xsi:type="AsyncWrapper" queueLimit="10000" 
	  timeToSleepBetweenBatches="50" batchSize="100" overflowAction="Block"&amp;gt;
        &amp;lt;target xsi:type="Database" name="dbTarget" connectionStringName="Logger" 
		commandText="exec sLogEvent_Insert @time_stamp, @level, @logger, @userName, @url, 
		@machineName, @sessionId, @threadId, @referrer, @userAgent, @code, @message"&amp;gt;
          &amp;lt;parameter name="@time_stamp" layout="${date}" /&amp;gt;
          &amp;lt;parameter name="@level" layout="${level}" /&amp;gt;
          &amp;lt;parameter name="@logger" layout="${logger}" /&amp;gt;
          &amp;lt;parameter name="@userName" layout="${identity}" /&amp;gt;
          &amp;lt;parameter name="@url" layout="${aspnet-request:serverVariable=Url}" /&amp;gt;
          &amp;lt;parameter name="@machineName" layout="${machinename}" /&amp;gt;
          &amp;lt;parameter name="@sessionId" layout="${aspnet-sessionid}" /&amp;gt;
          &amp;lt;parameter name="@threadId" layout="${threadid}" /&amp;gt;
          &amp;lt;parameter name="@referrer" 
			layout="${aspnet-request:serverVariable=HTTP_REFERER}" /&amp;gt;
          &amp;lt;parameter name="@userAgent" 
			layout="${aspnet-request:serverVariable=HTTP_USER_AGENT}" /&amp;gt;
          &amp;lt;parameter name="@code" layout="${event-context:item=Code}" /&amp;gt;
          &amp;lt;parameter name="@message" layout="${message}" /&amp;gt;
        &amp;lt;/target&amp;gt;
      &amp;lt;/target&amp;gt;
      &amp;lt;target name="asyncDebugWrapperTarget" xsi:type="AsyncWrapper" queueLimit="10000" 
	  timeToSleepBetweenBatches="50" batchSize="100" overflowAction="Block"&amp;gt;
        &amp;lt;target name="debugTarget" xsi:type="Debugger" 
			layout="${time}|${level:uppercase=true}|${logger}|${message}" /&amp;gt;
      &amp;lt;/target&amp;gt;
    &amp;lt;/targets&amp;gt;
    &amp;lt;rules&amp;gt;
      &amp;lt;!-- Levels: Off, Trace, Debug, Info, Warn, Error, Fatal --&amp;gt;
      &amp;lt;logger name="*" minlevel="Trace" 
		writeTo="asyncDebugWrapperTarget,asyncDbWrapperTarget" /&amp;gt;
    &amp;lt;/rules&amp;gt;
 &amp;lt;/nlog&amp;gt;
&lt;/pre&gt;

The database target's connectionStringName value of Logger points to the DB connection string to use.&lt;br/&gt;
&lt;pre class="brush:xml"&gt;
&amp;lt;connectionStrings&amp;gt;
&amp;lt;add name="Logger" providerName="System.Data.SqlClient" 
   connectionString="Data Source=(local);Initial Catalog=Chinook.Logs;Persist Security Info=True;User ID=ChinookLogger;Password=L0gger!" /&amp;gt;
&amp;lt;/connectionStrings&amp;gt;
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;The Database&lt;/h3&gt;

Our DBA setup a separate database for logging data to set DB properties differently as log data and transactional app data are different beasts. The sample app for this post is written against the &lt;a href="http://chinookdatabase.codeplex.com/"&gt;Chinook&lt;/a&gt; sample database so I named the log database Chinook.Logs. The primary table script follows, minus the indexes, SET statements and the like.&lt;br/&gt;
&lt;pre class="brush:sql"&gt;
CREATE TABLE [dbo].[LogEvent](
	[LogId] [int] IDENTITY(1,1) NOT NULL,
	[LogDate] [datetime] NOT NULL,
	[EventLevel] [nvarchar](50) NOT NULL,
	[LoggerName] [nvarchar](500) NOT NULL,
	[UserName] [nvarchar](50) NULL,
	[Url] [nvarchar](1024) NULL,
	[MachineName] [nvarchar](100) NOT NULL,
	[SessionId] [nvarchar](100) NULL,
	[ThreadId] [int] NULL,
	[Referrer] [nvarchar](1024) NULL,
	[UserAgent] [nvarchar](500) NULL,
	[Code] [nvarchar](10) NULL,
	[LogMessage] [nvarchar](max) NOT NULL,
	[PartitionKey] [tinyint] NOT NULL,
PRIMARY KEY CLUSTERED 
(
	[LogId] ASC,
	[PartitionKey] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)
&lt;/pre&gt;

The table is pretty straightforward with perhaps a couple of exceptions. The Code field was to be used for custom error codes (HTTP or otherwise) or "tagging" log statements. PartitionKey was added by our DBA mainly for archiving log data but also for efficient querying by day; more on this to come. Finally I originally had the string columns as varchar but we found that NLog used nvarchar and a conversion was happening with each insert that had a slight performance impact.
&lt;br/&gt;&lt;br/&gt;

The insert sproc grabs the day of the week and inserts that as the partition key and the rest of the data just passes through.&lt;br/&gt;
&lt;pre class="brush:sql"&gt;
CREATE PROCEDURE [dbo].[sLogEvent_Insert]
	@time_stamp datetime,
	@level nvarchar(50),
	@logger nvarchar(500),
	@userName nvarchar(50),
	@url nvarchar(1024),
	@machineName nvarchar(100),
	@sessionId nvarchar(100),
	@threadId int,
	@referrer nvarchar(1024),
	@userAgent nvarchar(500),
	@code nvarchar(10),
	@message nvarchar(max)
AS
BEGIN
	SET NOCOUNT ON;
	Declare @currentDate Datetime
	declare @partitionKey tinyint
	
	set		@currentDate = getdate()
	set		@partitionKey = DATEPART(weekday, @currentDate)

	INSERT INTO [dbo].[LogEvent]
           ([LogDate]
           ,[EventLevel]
           ,[LoggerName]
           ,[UserName]
           ,[Url]
           ,[MachineName]
           ,[SessionId]
           ,[ThreadId]
           ,[Referrer]
           ,[UserAgent]
		   ,[Code]
           ,[LogMessage]
		   ,[PartitionKey])
     VALUES
           (@time_stamp
           ,@level
           ,@logger
           ,@userName
           ,@url
           ,@machineName
           ,@sessionId
           ,@threadId
           ,@referrer
           ,@userAgent
		   ,@code
           ,@message
		   ,@partitionKey);
END
&lt;/pre&gt;

An identical log table was created but with the name LogEvent_Switched. The cleanup sproc was created by our DBA:&lt;br/&gt;

&lt;pre class="brush:sql"&gt;
CREATE PROCEDURE [DBA].[WeekdayPartitionCleanup_PartitionSwitching] 
AS
BEGIN
	SET NOCOUNT ON;
	declare @partitionKey int
	declare @SQLCommand nvarchar(1024)
	truncate table [Chinook.Logs].dbo.LogEvent_Switched;

	set @partitionKey = datePart(weekday, getdate()) + 1
	if(@partitionkey &amp;gt;7)
		set @partitionKey = 1

	set @SQLCommand = 'alter table [Chinook.Logs].dbo.LogEvent switch partition  ' 
	  + cast(@partitionKey as varchar) + ' to [Chinook.Logs].dbo.LogEvent_Switched;'
	exec sp_executesql @SQLCommand
END
&lt;/pre&gt;

That was scheduled to run daily. So if today is Friday, datePart(weekday, getdate()) returns 6, +1 is Saturday so all of last Saturday's log records get automagically switched over to the LogEvent_Switched table. This leaves the last 6 days of log records in LogEvent and the 7th day in LogEvent_Switched. If you don't have the Enterprise edition and can't use partitioning, regular deletes may work but could be &lt;a href="http://stackoverflow.com/questions/4169283/maintenance-stored-procedure-how-to-delete-without-blocking-replication"&gt;problematic&lt;/a&gt; when deleting a large number of rows during frequent inserts or selects.
&lt;br/&gt;&lt;br/&gt;

Finally permission to execute the sproc was granted and then it was back to .net land.
&lt;pre class="brush:sql"&gt;
GRANT EXECUTE on dbo.sLogEvent_Insert to ChinookLogger;
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Wiring up the Logging Code&lt;/h3&gt;

First an interface that mostly mirrors NLog's Logger class.&lt;br/&gt;

&lt;pre class="brush:csharp"&gt;
namespace NLogSql.Web.Infrastructure.Diagnostics.Logging
{
    public interface ILog
    {
        void Debug(string format, params object[] args);
        void Error(string format, params object[] args);
        void Fatal(string format, params object[] args);
        void Info(string format, params object[] args);
        void Trace(string format, params object[] args);
        void Warn(string format, params object[] args);

        // custom
        void Write(LogType type, object properties, string message, params object[] args);

        bool IsDebugEnabled { get; }
        bool IsErrorEnabled { get; }
        bool IsFatalEnabled { get; }
        bool IsInfoEnabled { get; }
        bool IsTraceEnabled { get; }
        bool IsWarnEnabled { get; }
    }
}
&lt;/pre&gt;

The functionality of the Log class is provided mostly by inheriting from NLog's Logger class. 
One custom method is defined to allow passing custom log properties much like you would in ASP.NET (new {code = "404"}).&lt;br/&gt;

&lt;pre class="brush:csharp"&gt;
using System.ComponentModel;
using System.Globalization;
using NLog;

namespace NLogSql.Web.Infrastructure.Diagnostics.Logging
{
    public class Log : Logger, ILog
    { 
        public void Write(LogType type, object properties, string message, 
			params object[] args)
        {
            var info = new LogEventInfo(LogLevel.FromOrdinal((int)type), Name, 
				CultureInfo.CurrentCulture, message, args);

            if (null != properties)
            {
                foreach (PropertyDescriptor propertyDescriptor 
					in TypeDescriptor.GetProperties(properties))
                {
                    var value = propertyDescriptor.GetValue(properties);
                    info.Properties[propertyDescriptor.Name] = value;
                }
            }

            Log(info);
        }
    }

    public enum LogType
    {
        Trace, Debug, Info, Warn, Error, Fatal
    }
}
&lt;/pre&gt;

A log instance is typically created via injecting ILog into the constructor of the class needing logging. 
Here I use &lt;a href="http://www.ninject.org/"&gt;Ninject&lt;/a&gt; to wire this up.&lt;br/&gt;

&lt;pre class="brush:csharp; highlight:[17,25]"&gt;
using System;
using System.Linq;
using System.Web.Mvc;
using NLog;
using NLogSql.Web.Infrastructure.Diagnostics.Logging;
using NLogSql.Web.Infrastructure.ErrorHandling;
using Ninject.Activation;
using Ninject.Modules;
using Ninject.Web.Mvc.FilterBindingSyntax;

namespace NLogSql.Web.DI
{
    public class DiagnosticsModule : NinjectModule
    {
        public override void Load()
        {
            Bind&amp;lt;ILog&amp;gt;().ToMethod(CreateLog);
            Bind&amp;lt;IErrorReporter&amp;gt;().To&amp;lt;ErrorReporter&amp;gt;();
            Bind&amp;lt;IEventLogWriter&amp;gt;().To&amp;lt;EventLogWriter&amp;gt;();
            Bind&amp;lt;IErrorEmailer&amp;gt;().To&amp;lt;ErrorEmailer&amp;gt;();

            Kernel.BindFilter&amp;lt;AppErrorHandlerAttribute&amp;gt;(FilterScope.Controller, 0);
        }

        private static ILog CreateLog(IContext ctx)
        {
            var p = ctx.Parameters.FirstOrDefault(x =&amp;gt; 
				x.Name == LogConstants.LoggerNameParam);
            var loggerName = (null != p) ? p.GetValue(ctx, null).ToString() : null;

            if (string.IsNullOrWhiteSpace(loggerName))
            {
                if (null == ctx.Request.ParentRequest)
                {
                    throw new NullReferenceException(
                        "ParentRequest is null; unable to determine logger name; " 
						+ "if not injecting into a ctor a parameter for the "
						+ "logger name must be provided");
                }

                var service = ctx.Request.ParentRequest.Service;
                loggerName = service.FullName;
            }

            var log = (ILog)LogManager.GetLogger(loggerName, typeof(Log));
            return log;
        }
    }
}
&lt;/pre&gt;

In some cases we can't easily do ctor injection - for example in the application class of Global.asax.cs or in a filter attribute. 
For these exception cases AppLogFactory is used to create an instance of ILog.&lt;br/&gt;

&lt;pre class="brush:csharp; highlight: [37,38]"&gt;
using System;
using System.Diagnostics;
using System.Web.Mvc;
using Ninject;
using Ninject.Parameters;

namespace NLogSql.Web.Infrastructure.Diagnostics.Logging
{
    /// &amp;lt;summary&amp;gt;
    /// Creates a log object for those instances where one cannot be injected (i.e. app startup).
    /// Generally you should just ctor inject ILog
    /// &amp;lt;/summary&amp;gt;
    public static class AppLogFactory
    {
        public static ILog Create()
        {
            var declaringType = new StackTrace(1, false).GetFrame(1)
				.GetMethod().DeclaringType;

            if (declaringType != null)
            {
                var loggerName = declaringType.FullName;
                return Create(loggerName);
            }

            throw new InvalidOperationException(
				"Could not determine declaring type; specify logger name explicitly");
        }

        public static ILog Create&amp;lt;T&amp;gt;()
        {
            return Create(typeof(T).FullName);
        }

        public static ILog Create(string loggerName)
        {
            var log = Kernel.Get&amp;lt;ILog&amp;gt;(
				new ConstructorArgument(LogConstants.LoggerNameParam, loggerName));
            return log;
        }

        private static IKernel Kernel
        {
            get
            {
                return DependencyResolver.Current.GetService&amp;lt;IKernel&amp;gt;();
            }
        }
    }

    public class LogConstants
    {
        public const string LoggerNameParam = "loggerName";
    }
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Exercising the Log&lt;/h3&gt;

First a simple test, injecting ILog into a controllor ctor, ensuring it isn't null using the 
&lt;a href="http://nuget.org/packages?q=ensure.that"&gt;Ensure.That&lt;/a&gt; NuGet package and logging a count around a database fetch.&lt;br/&gt;

&lt;pre class="brush:csharp; highlight: [10,16,18]"&gt;
public partial class HomeController : Controller
{
	private readonly IMappingService _mappingService;
	private readonly ILog _log;
	private readonly IMusicService _musicService;

	public HomeController(IMusicService musicService, ILog log, IMappingService mappingService)
	{
		_mappingService = Ensure.That(mappingService, "mappingService").IsNotNull().Value;
		_log = Ensure.That(log, "log").IsNotNull().Value;
		_musicService = Ensure.That(musicService, "musicService").IsNotNull().Value;
	}
	
	public virtual ActionResult Index()
	{
		_log.Debug("Retrieving genres");
		var genres = GetGenres().Result;
		_log.Info("Retrieved {0} music genres from the database", genres.Count);
		// ...
	}
}
&lt;/pre&gt;

To get a little more value, an action filter to log each controller action executed along with the execution time.&lt;br/&gt;

&lt;pre class="brush:csharp; highlight: [29,46,47]"&gt;
public class ActionTrackerAttribute : ActionFilterAttribute
{
	private Stopwatch Watch { get; set; }
	private ILog Log { get; set; }
	private ActionExecutingContext FilterContext { get; set; }

	private string ActionName
	{
		get { return FilterContext.ActionDescriptor.ActionName; }
	}

	private string ControllerName
	{
		get { return FilterContext.ActionDescriptor.ControllerDescriptor.ControllerName; }
	}

	private Uri Url
	{
		get { return FilterContext.RequestContext.HttpContext.Request.Url; }
	}

	public override void OnActionExecuting(ActionExecutingContext filterContext)
	{
		base.OnActionExecuting(filterContext);

		try
		{
			FilterContext = filterContext;
			Log = AppLogFactory.Create&amp;lt;ActionTrackerAttribute&amp;gt;();
			Log.Trace("Executing {0}.{1}", ControllerName, ActionName);
			Watch = Stopwatch.StartNew();
		}
		catch (Exception ex)
		{
			Trace.WriteLine(ex);
		}
	}

	public override void OnResultExecuted(ResultExecutedContext filterContext)
	{
		base.OnResultExecuted(filterContext);

		try
		{
			Watch.Stop();
			Log.Info("Executed {0}.{1} for {2} in {3:##0.000} second(s)", 
				ControllerName, ActionName, Url, Watch.Elapsed.TotalSeconds);
		}
		catch (Exception ex)
		{
			Trace.WriteLine(ex);
		}
	}
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Troubleshooting Failed Logging&lt;/h3&gt;

When setting up logging initially or making changes later, logging might not work. 
Usually I'd first fire up SQL Server Profiler and watch for calls to the log insert sproc. 
If I didn't see them then I'd know it's probably a configuration issue. In the nlog tag in the 
config file I set throwExceptions="true" internalLogLevel="Debug" internalLogFile="${basedir}/NlogInternal.log" 
but in practice that rarely seemed to do anything; turning off async temporarily may further help troubleshoot.
&lt;br/&gt;&lt;br/&gt;

First I'd check when creating the log if the log levels were enabled as expected from the config.&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/LogWatch.jpg"/&gt; 
&lt;br/&gt;&lt;br/&gt;

Next I'd check the configuration in code with a small class, also useful for changing config at runtime.&lt;br/&gt;
&lt;pre class="brush:csharp"&gt;
public class LogManagement
{
	public static AsyncTargetWrapper GetAsyncDbWrapperTarget()
	{
		var target = (AsyncTargetWrapper)LogManager.Configuration
			.FindTargetByName("asyncDbWrapperTarget");
		return target;
	}
}
&lt;/pre&gt;

From there I'd further inspect the configuration to see if everything looks okay.&lt;br/&gt; 
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/LogManagementWatch.jpg"/&gt; 
&lt;br/&gt;&lt;br/&gt;

If I did see the insert sproc call in SQL Server Profiler then I'd check DB setup and permissions and grab the SQL from Profiler...&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/Profiler.jpg"/&gt; 
&lt;br/&gt;&lt;br/&gt;

... and paste into a query window, then format the Sql (I use &lt;a href="http://architectshack.com/PoorMansTSqlFormatter.ashx"&gt;Poor Man's TSql Formatter&lt;/a&gt;) and execute to check for errors.&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/SqlInsert.jpg"/&gt; 
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Evaluating Async Logging&lt;/h3&gt;
Because I'm paranoid, OCD, detailed and curious I wanted to ensure the async behavior and compare to non-async. 
On the home page I added a link to generate batch log records, corresponding to this controller action.&lt;br/&gt;

&lt;pre class="brush:csharp"&gt;
public virtual ActionResult BatchTest()
{
	var sw = Stopwatch.StartNew();
	const int count = 10000;
	for (var i = 0; i &amp;lt; count; i++)
	{
		_log.Trace("Testing {0}", i);
	}
	sw.Stop();
	var msg = string.Format("{0} log invokes in {1:##0.000} seconds", count, 
		sw.Elapsed.TotalSeconds);
	_log.Info(msg);

	return new ContentResult { Content = msg };
}
&lt;/pre&gt;

The async result:&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/BatchTestAsync.jpg"/&gt; 
&lt;br/&gt;&lt;br/&gt;

And the non-async result:&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/BatchTestNonAsync.jpg"/&gt; 
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Evaluating Log Queries&lt;/h3&gt;
Usually when inspecting log records I'd select just those columns I'm interested in and order by LogId DESC.&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/LogEventRecords.jpg" width="750" height="195"/&gt; 
&lt;br/&gt;&lt;br/&gt;

Once enough log data was there it was helpful to query on the columns that would typically be filtered on and evaluate the execution plan and response times. 
From there indexes were added as needed for columns such as EventLevel, URL, UserName, etc. and execution times compared afterwards.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Preparing for Deployment&lt;/h3&gt;
In Web.Config.Release NLog config transformations are performed to change the log level from Trace to Info, to remove debugger output, and to tone down NLog internal issue reporting.&lt;br/&gt;
&lt;pre class="brush:xml"&gt;
&amp;lt;nlog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xdt:Transform="SetAttributes"
        throwExceptions="false" internalLogLevel="Warn" 
		internalLogFile="${basedir}/NlogInternal.log"&amp;gt;
    &amp;lt;rules&amp;gt;
       &amp;lt;!-- Levels: Off, Trace, Debug, Info, Warn, Error, Fatal --&amp;gt;
       &amp;lt;logger name="*" minlevel="Info" writeTo="asyncDbWrapperTarget" 
	      xdt:Transform="SetAttributes" xdt:Locator="Match(name)" /&amp;gt;
    &amp;lt;/rules&amp;gt;
&amp;lt;/nlog&amp;gt;
&lt;/pre&gt;

&lt;a href="http://visualstudiogallery.msdn.microsoft.com/69023d00-a4f9-4a34-a6cd-7e854ba318b5"&gt;Slow Cheetah&lt;/a&gt; preview:&lt;br/&gt;
&lt;a href="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/SlowCheetah.jpg" class="fancybox" title="Slow Cheetah Web.Config.Release" alt="Click for larger version"&gt;
	&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2013/05/nlogsqlweb/SlowCheetahSmall.jpg" border="0"/&gt; 
&lt;/a&gt;

Note that removing the nlog xsd attribute in the root web.config (and all transforms) was needed for the transformation to work correctly. Also at one point I tried a replace transform to replace the entire nlog tag but found it didn't work correctly and it created too much duplicate content between configurations.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Some Queries&lt;/h3&gt;
&lt;br/&gt;
Everything today:&lt;br/&gt;
&lt;pre class="brush:sql"&gt;
SELECT *
FROM LogEvent WITH (NOLOCK)
WHERE PartitionKey = (
		SELECT DATEPART(weekday, getdate())
		)
ORDER BY logid DESC;
&lt;/pre&gt;

Errors and warnings in the past 2 hours:&lt;br/&gt;
&lt;pre class="brush:sql"&gt;
SELECT *
FROM LogEvent WITH (NOLOCK)
WHERE eventlevel IN (
		'Error'
		,'Warn'
		)
	AND DATEDIFF(hh, LogDate, GETDATE()) &amp;lt;= 2
ORDER BY logid DESC;
&lt;/pre&gt;

Errors by day and url:&lt;br/&gt;
&lt;pre class="brush:sql"&gt;
SELECT cast(logdate AS DATE) [Day]
	,url
	,count(*) ErrorCount
FROM LogEvent WITH (NOLOCK)
WHERE eventlevel = 'Error'
	AND len(url) &amp;gt; 0
GROUP BY cast(logdate AS DATE)
	,url
ORDER BY Day DESC
	,Url;
&lt;/pre&gt;

Errors by logger name / class name / component area:&lt;br/&gt;
&lt;pre class="brush:sql"&gt;
SELECT LoggerName
	,count(*) AS ErrorCount
FROM LogEvent WITH (NOLOCK)
WHERE EventLevel = 'Error'
GROUP BY LoggerName
ORDER BY count(*) DESC;
&lt;/pre&gt;

Unhandled exceptions today:&lt;br/&gt;
&lt;pre class="brush:sql"&gt;
SELECT *
FROM LogEvent WITH (NOLOCK)
WHERE PartitionKey = (
		SELECT DATEPART(weekday, getdate())
		)
	AND eventlevel = 'Error'
	AND LoggerName IN ('NLogSql.Web.Infrastructure.ErrorHandling.IErrorReporter')
ORDER BY logid DESC;
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Future Enhancements / Considerations&lt;/h3&gt;

&lt;ul&gt;
	&lt;li&gt;Web view to read, search, filter log maybe with some SignalR goodness&lt;/li&gt;
	&lt;li&gt;AppId type column if logging from multiple apps w/o desire for multiple log databases&lt;/li&gt;
	&lt;li&gt;Possibly a separate error table that the log table could link to for errors with the full error report split up&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;The Code&lt;/h3&gt;

The code for this series is available at &lt;a href="https://github.com/thnk2wn/NLogSql.Web"&gt;https://github.com/thnk2wn/NLogSql.Web&lt;/a&gt;&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/Jrx77du2RUg" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-33685523.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2013/5/20/aspnet-nlog-sql-server-logging-and-error-handling-part-1.html</feedburner:origLink></item><item><title>Using T4 to Generate Typesafe Enum Classes and Resource Files</title><category>I18N</category><category>T4</category><category>domain</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Tue, 02 Oct 2012 04:25:00 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/GiAPoqrJwrU/using-t4-to-generate-typesafe-enum-classes-and-resource-file.html</link><guid isPermaLink="false">632421:7353983:29567170</guid><description>In working on the domain layer of an application, I wrote a couple of typesafe enumeration classes that mirrored data in a couple of reference tables in a database. If you are not familiar with the pattern, Jimmy Bogard's &lt;a href="http://lostechies.com/jimmybogard/2008/08/12/enumeration-classes/"&gt;Enumeration classes post&lt;/a&gt; explains the pattern and rationale. The classes I wrote inherit from a modified version of the Enumeration class presented in Jimmy's post. The basic idea was to avoid switches and provide more functionality than an enum could offer. In our case the enum members mirrored a reference table and would be used for domain logic and for dropdown lists.
&lt;br/&gt;&lt;br/&gt;

As the app progressed we had more of a need to do this for other reference tables, some of which had several records. At this point we decided to look into T4 to generate the typesafe enum classes. Since this application has a worldwide audience, the display names of the enum members needed globalization consideration since some of the enum values would end up in dropdown lists displayed to end users. I decided to create two T4 files, one to generate resource files with display name strings, and another to generate the enum classes that would use the resource strings.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Reference Datasource&lt;/h3&gt;
This application uses XML files for various database definitions including reference data; these files get parsed to build the SQL Server database in automated fashion.
&lt;br/&gt;&lt;br/&gt;

A sample of reference_data.xml looks something like:&lt;br/&gt;

&lt;pre class="brush:xml; toolbar: false;"&gt;
&amp;lt;referenceData&amp;gt;
	&amp;lt;refTable name="CONTACT_TYPE" alias="ContactType" default="Person"&amp;gt;
		&amp;lt;record CONTACT_TYPE_ID="1" CONTACT_TYPE_NAME="Company" RANK="2"&amp;gt;
			&amp;lt;langs field="CONTACT_TYPE_NAME"&amp;gt;
				&amp;lt;lang name="en"&amp;gt;Company&amp;lt;/lang&amp;gt;
				&amp;lt;lang name="de"&amp;gt;Gesellschaft&amp;lt;/lang&amp;gt;
				&amp;lt;lang name="es"&amp;gt;Empresa&amp;lt;/lang&amp;gt;
				&amp;lt;lang name="zh-CN"&amp;gt;公司&amp;lt;/lang&amp;gt;
				&amp;lt;lang name="fr"&amp;gt;Soci&amp;#233;t&amp;#233;&amp;lt;/lang&amp;gt;
			&amp;lt;/langs&amp;gt;
		&amp;lt;/record&amp;gt;
		&amp;lt;record CONTACT_TYPE_ID="2" CONTACT_TYPE_NAME="Person" RANK="1"&amp;gt;
			&amp;lt;langs field="CONTACT_TYPE_NAME"&amp;gt;
				&amp;lt;lang name="en"&amp;gt;Person&amp;lt;/lang&amp;gt;
				&amp;lt;lang name="de"&amp;gt;Person&amp;lt;/lang&amp;gt;
				&amp;lt;lang name="es"&amp;gt;Persona&amp;lt;/lang&amp;gt;
				&amp;lt;lang name="zh-CN"&amp;gt;人&amp;lt;/lang&amp;gt;
				&amp;lt;lang name="fr"&amp;gt;Personne&amp;lt;/lang&amp;gt;
			&amp;lt;/langs&amp;gt;
		&amp;lt;/record&amp;gt;
	&amp;lt;/refTable&amp;gt;
&amp;lt;/referenceData&amp;gt;
&lt;/pre&gt;

&lt;br/&gt;

&lt;h3&gt;Schema Metadata and Parsing&lt;/h3&gt;

Since multiple projects needed to perform T4 code generation off database schema information, I defined some T4 include files in a shared project to define the schema structure and XML parsing logic. This would make the T4 code easier and prevent duplicating XML parsing logic in multiple T4 files.
&lt;br/&gt;&lt;br/&gt;

First SchemaMetadata.ttinclude defines the classes that hold the schema information in a T4-friendly manner: &lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/SchemaMetadata.ttinclude.png"/&gt;
&lt;br/&gt;&lt;br/&gt;

Next SchemaReader.ttinclude would get consumed by T4 files to parse the XML data and return friendly objects defined in SchemaMetadata.ttinclude:&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/SchemaReader.ttinclude.png"/&gt;
&lt;br/&gt;&lt;br/&gt;

This shared T4 folder also included MultipleOutputHelper.ttinclude from &lt;a href="http://damieng.com/blog/2009/11/06/multiple-outputs-from-t4-made-easy-revisited"&gt;this Damien Guard post&lt;/a&gt; to make splitting T4 output into multiple files a bit easier.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;RefDataResources.tt&lt;/h3&gt;
This T4 file generates one resource file per language and will setup the custom tool to produce the designer generated file to reference the resources in code:&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/RefDataResources_1.png"/&gt;
&lt;br/&gt;

Inside the function block of this file the schema data is loaded and the resource files are generated:&lt;br/&gt;

&lt;pre class="brush:csharp; toolbar: false;"&gt;
const string ReferenceData = "ReferenceData";

private SchemaMetadata LoadSchema()
{
	var loader = new SchemaReader(Host);
	var schema = loader.Load(SchemaReader.LoadOption.ReferenceData);
	return schema;
}

private void GenerateResourceFiles(SchemaMetadata schema, Manager manager, 
	DirectoryInfo t4DirInfo)
{
	var distinctLanguages = schema.ReferenceData.DistinctLanguages;
	foreach (var lang in distinctLanguages)
	{
		var resxNameNoExt = ("en" != lang) ? ReferenceData + "." + lang : ReferenceData;
		var resxName = resxNameNoExt + ".resx";
		manager.StartNewFile(resxName);

		var resxFilename = Path.Combine(t4DirInfo.FullName, resxName);
		// use .net's ResXResourceWriter so we don't have to worry about the XML format
		using (ResXResourceWriter  resx = new ResXResourceWriter(resxFilename))
		{
			var strings = schema.ReferenceData.StringsForLanguage(lang);

			foreach (var warn in schema.Warnings)
			{
				base.Warning(warn);
			}

			foreach (var de in strings)
			{
				try 
				{
					resx.AddResource(de.Key, de.Value);
				}
				catch (Exception ex)
				{
					base.Warning(ex.ToString());
				}
			}

			resx.Generate();
			resx.Close();
		}

		// we've written the file but outside the process of T4. 
		// In order to get the file to automatically added as a new output file 
		// underneath the t4 file, we must write the generated content to output stream
		Write(File.ReadAllText(resxFilename));

		manager.EndBlock();
		
	} // end for each lang loop
}
&lt;/pre&gt;
&lt;br/&gt;

In the above code block there are a couple of things worth pointing out. First, the filename of the main / default language resource file will be ReferenceData.resx for English (en), otherwise ReferenceData.&lt;b&gt;lang&lt;/b&gt;.resx for other languages. Second, the output from .NET's ResXResourceWriter gets read in with File.ReadAllText and written to T4 output with Write; otherwise the generated content would just exist on disk and would not get added into the project nested under the T4 file.
&lt;br/&gt;&lt;br/&gt;

Finally, to get the designer generated class created, a function is created to set the custom tool property on the main ReferenceData.resx file that was generated. For the initial add that would be enough. However we also invoke execution of the custom tool with RunCustomTool() to handle the case where reference data is modified later on and the T4 transformation is performed again:&lt;br/&gt;

&lt;pre class="brush:csharp; toolbar: false;"&gt;
private void SetCustomToolOnMainResourceFile(DirectoryInfo t4DirInfo)
{
	// WARNING: You are entering the dark land of EnvDTE COM. You've been warned
	var hostServiceProvider = (IServiceProvider) Host;
	var dte = (EnvDTE.DTE) hostServiceProvider.GetService(typeof(EnvDTE.DTE));
	var filename = Path.Combine(t4DirInfo.FullName, "ReferenceData.resx");
	var projectItem = dte.Solution.FindProjectItem(filename);
	projectItem.Properties.Item("CustomTool").Value = "PublicResXFileCodeGenerator";
	projectItem.Properties.Item("CustomToolNamespace").Value = "App.Domain";
	var projItemObj = (VSProjectItem)projectItem.Object;
	projItemObj.RunCustomTool();
}
&lt;/pre&gt;
&lt;br/&gt;

Running the T4 produces the following files:&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/RefDataResourcesFiles.png"/&gt;
&lt;br/&gt;&lt;br/&gt;

The resource strings are created with a TableName_FieldNameValue format:&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/ReferenceDataResXData.png"/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;Enums.tt&lt;/h3&gt;
Enums.tt reads the schema data just as in RefDataResources.tt and generates the C# typesafe enum class. The full source is available with the sample code for this post.&lt;br/&gt;
&lt;a class="fancybox" href="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/Enums.tt.png" title="click for more"&gt;
	&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/Enums.tt.png"/&gt;
&lt;/a&gt;
&lt;br/&gt;&lt;br/&gt;

A small example of the generated output (ContactType.generated.cs):&lt;br/&gt;

&lt;pre class="brush:csharp; toolbar: false;"&gt;
//------------------------------------------------------------------------------
// &amp;lt;auto-generated&amp;gt;
//     This code was generated from a template.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// &amp;lt;/auto-generated&amp;gt;
//------------------------------------------------------------------------------
using System;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;

namespace App.Domain
{
	public partial class ContactType : Enumeration
	{		
		/// &amp;lt;summary&amp;gt;
		/// ContactType of Company (ID: 1, "Company")
		/// &amp;lt;/summary&amp;gt;
		[GeneratedCode("TextTemplatingFileGenerator", "11")]
		public static readonly ContactType Company 
			= new ContactType( 1, ReferenceData.ContactType_Company );

		/// &amp;lt;summary&amp;gt;
		/// ContactType of Person (ID: 2, "Person")
		/// &amp;lt;/summary&amp;gt;
		[GeneratedCode("TextTemplatingFileGenerator", "11")]
		public static readonly ContactType Person 
			= new ContactType( 2, ReferenceData.ContactType_Person );


		[ExcludeFromCodeCoverage, DebuggerNonUserCode, GeneratedCode("TextTemplatingFileGenerator", "11")]
		private ContactType( int value, string displayName ) : base( value, displayName ) { }

		[ExcludeFromCodeCoverage, DebuggerNonUserCode, GeneratedCode("TextTemplatingFileGenerator", "11")]
		public static ContactType Default()
		{
			return Person;
		}

		[UsedImplicitly, Obsolete("ORM runtime use only")]
		[ExcludeFromCodeCoverage, DebuggerNonUserCode, GeneratedCode("TextTemplatingFileGenerator", "11")]
		private ContactType() { }
	}
}
&lt;/pre&gt;
&lt;br/&gt;

Of course there's more value in automatically generating all of the reference enums or at least those with more members:&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/Holiday.generated.cs.png"/&gt;
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Testing It Out&lt;/h3&gt;

First some basic tests just to ensure the resource files and strongly typed resource class generated correctly:&lt;br/&gt;

&lt;pre class="brush:csharp; toolbar: false;"&gt;
using System.Globalization;
using NUnit.Framework;
using System.Threading;

namespace App.Domain.Tests
{
    [TestFixture, Category("Unit")]
    public class ReferenceDataI18NTests
    {
        [Test]
        public void English_To_French_Strings_Change()
        {
            var orig = Thread.CurrentThread.CurrentUICulture;
            Assert.AreEqual("Company", ReferenceData.ContactType_Company);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("fr-FR");
            Assert.AreEqual("Société", ReferenceData.ContactType_Company);
            Thread.CurrentThread.CurrentUICulture = orig;
        }

        [Test]
        public void English_To_Spanish_Strings_Change()
        {
            var orig = Thread.CurrentThread.CurrentUICulture;
            Assert.AreEqual("Company", ReferenceData.ContactType_Company);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("es");
            Assert.AreEqual("Empresa", ReferenceData.ContactType_Company);
            Thread.CurrentThread.CurrentUICulture = orig;
        }

        [Test]
        public void English_To_German_Strings_Change()
        {
            var orig = Thread.CurrentThread.CurrentUICulture;
            Assert.AreEqual("Company", ReferenceData.ContactType_Company);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("de");
            Assert.AreEqual("Gesellschaft", ReferenceData.ContactType_Company);
            Thread.CurrentThread.CurrentUICulture = orig;
        }

        [Test]
        public void English_To_Chinese_Taiwan_Strings_Change()
        {
            var orig = Thread.CurrentThread.CurrentUICulture;
            Assert.AreEqual("Company", ReferenceData.ContactType_Company);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("zh-CN");
            Assert.AreEqual("公司", ReferenceData.ContactType_Company);
            Thread.CurrentThread.CurrentUICulture = orig;
        }
    }
}
&lt;/pre&gt;
&lt;br/&gt;

Just to make sure the Enumeration class works as expected, some tests to exercise it through one of the concrete classes:
&lt;br/&gt;

&lt;pre class="brush:csharp; toolbar: false;"&gt;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;

namespace App.Domain.Tests
{
    [TestFixture(
        Description = "Tests base Enumeration class via PhoneType concrete class")]
    [Category("Unit")]
    public class EnumerationTests
    {
        [Test]
        public void FromValue_Matches_Type_Value()
        {
            var fromValue = Enumeration.FromValue&amp;lt;PhoneType&amp;gt;(PhoneType.Cell.Value);
            Assert.AreEqual(PhoneType.Cell, fromValue);

            fromValue = Enumeration.FromValue&amp;lt;PhoneType&amp;gt;(PhoneType.Voice.Value);
            Assert.AreEqual(PhoneType.Voice, fromValue);
        }

        [Test]
        public void FromDisplayName_Matches_Type_Name()
        {
            var fromName = Enumeration.FromDisplayName&amp;lt;PhoneType&amp;gt;(PhoneType.Fax.DisplayName);
            Assert.AreEqual(PhoneType.Fax.DisplayName, fromName.DisplayName);

            fromName = Enumeration.FromDisplayName&amp;lt;PhoneType&amp;gt;(PhoneType.Pager.DisplayName);
            Assert.AreEqual(PhoneType.Pager.DisplayName, fromName.DisplayName);
        }

        [Test]
        public void ToString_Equals_DisplayName()
        {
            Assert.AreEqual(PhoneType.Cell.DisplayName, PhoneType.Cell.ToString());
        }

        [Test]
        public void Absolute_Difference_Math_Is_Correct()
        {
            var diff = Enumeration.AbsoluteDifference(PhoneType.Cell, PhoneType.Voice);
            Assert.AreEqual(3, diff);
            diff = Enumeration.AbsoluteDifference(PhoneType.Voice, PhoneType.Cell);
            Assert.AreEqual(3, diff);
        }

        [Test]
        public void GetAll_Contains_Expected_Members()
        {
            var all = Enumeration.GetAll&amp;lt;PhoneType&amp;gt;().ToList();
            Assert.AreEqual(4, all.Count);
            Assert.NotNull(all.FirstOrDefault(x =&amp;gt; x == PhoneType.Cell));
            Assert.NotNull(all.FirstOrDefault(x =&amp;gt; x == PhoneType.Fax));
            Assert.NotNull(all.FirstOrDefault(x =&amp;gt; x == PhoneType.Pager));
            Assert.NotNull(all.FirstOrDefault(x =&amp;gt; x == PhoneType.Voice));
            Assert.NotNull(all.FirstOrDefault(x =&amp;gt; x == PhoneType.Default()));
        }

        [Test]
        public void Equality_Two_Are_Equal()
        {
            var one = PhoneType.Cell;
            var two = PhoneType.Cell;
            Assert.AreEqual(one, two);
            Assert.True(one == two);
        }

        [Test]
        public void Equality_Two_Different_Not_Equal()
        {
            var one = PhoneType.Cell;
            var two = PhoneType.Voice;
            Assert.AreNotEqual(one, two);
            Assert.True(one != two);
        }

        [Test]
        public void Equality_One_Null_Not_Equal()
        {
            PhoneType.Cell.Equals(null).Should().BeFalse();
        }

        [Test]
        public void Compare_To_Succeeds()
        {
            var diff = PhoneType.Cell.CompareTo(PhoneType.Fax);
            diff.Should().Be(1);

            diff = PhoneType.Fax.CompareTo(PhoneType.Cell);
            diff.Should().Be(-1);
        }

        [Test]
        public void Invalid_Parse_Number_Throws()
        {
            Assert.That(() =&amp;gt; { Enumeration.FromValue&amp;lt;PhoneType&amp;gt;(999); }, Throws.Exception);
        }
    }
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;There's More&lt;/h3&gt;

&lt;ul&gt;
	&lt;li style="margin-bottom:8px;"&gt;For simplicity some complexity was removed from the T4. This includes reference tables that have foreign keys to other reference tables (including strongly typed properties and ctor params) and resource strings for fields other than the primary display name.&lt;/li&gt;
	
	&lt;li style="margin-bottom:8px;"&gt;One thing that bothered me initially with this was it felt a bit like introducing Data Driven Design into an otherwise Domain Driven Design paradigm. This was offset somewhat with aliases for table names (or a naming convention pattern) and reference tables could be selectively ignored in code generation through the use of attributes or other means. Another possible issue is class name conflicts with existing types in the domain project; this could be offset with a different namespace and/or naming convention.&lt;/li&gt;
	
	&lt;li style="margin-bottom:8px;"&gt;Many apps load reference data from the database each time it's needed, or load it once and cache it until invalidated. Other apps may let users edit select sets of reference type data. If the data is likely to change during an app session or if users can edit some of it, chances are it isn't truly reference data to begin with. With a good deployment process, any compiled reference data info can be easily deployed and various reference data is likely to be tied to app business and presentation rules anyway.&lt;/li&gt;
	
	&lt;li style="margin-bottom:8px;"&gt;Several of the enum classes would have corresponding partial classes for extended logic. For this reason, various code generation type attributes were places directly on generated members and not on the class itself, per &lt;a href="http://blogs.msdn.com/b/codeanalysis/archive/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute.aspx"&gt;various guidance&lt;/a&gt;.
	&lt;/li&gt;
	
	&lt;li style="margin-bottom:8px;"&gt;An example use in the domain would be a ContactType enum property on a Contact object and requiring different fields when attempting to add contacts of different types. This app uses &lt;a href="http://stackoverflow.com/questions/6366956/mapping-an-iusertype-to-a-component-property-in-fluent-nhibernate"&gt;this component strategy&lt;/a&gt; in Fluent NHibernate to map the data from the database reference table into the domain class. For the UI side, Enumeration.GetAllMembers can be used along with AutoMapper to get the id and text values into simple ViewModel types for select lists.
	&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Code&lt;/h3&gt;

&lt;a href="http://www.geoffhudik.com/storage/blogs/tech/2012/10/t4refdata/T4RefDataCode.zip"&gt;T4RefDataCode.zip&lt;/a&gt;&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/GiAPoqrJwrU" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-29567170.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2012/10/2/using-t4-to-generate-typesafe-enum-classes-and-resource-file.html</feedburner:origLink></item><item><title>AutoMapper's ForMember</title><category>AutoMapper</category><category>C#</category><category>mapping</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Fri, 03 Aug 2012 21:24:17 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/yFthkrrAGvY/automappers-formember.html</link><guid isPermaLink="false">632421:7353983:21272323</guid><description>Calling AutoMapper's ForMember has been bugging me lately with having to deal with its member configuration options like so:
&lt;br/&gt;

&lt;pre class="brush:csharp; toolbar: false;"&gt;
Mapper.CreateMap&amp;lt;Contact, AddressBookDetailsModel&amp;gt;()
	.ForMember( d =&amp;gt; d.PhoneNumbers, o =&amp;gt; o.MapFrom( s =&amp;gt; s.Phones ) );
&lt;/pre&gt;

The member configuration options provides much more than just MapFrom but 99 times out of 100 I'm only dealing with MapFrom. 
What I'd really like is something like this:
&lt;br/&gt;

&lt;pre class="brush:csharp; toolbar: false;"&gt;
Mapper.CreateMap&amp;lt;Contact, AddressBookDetailsModel&amp;gt;()
	.ForMember( d =&amp;gt; d.PhoneNumbers).MapFrom( s=&amp;gt; s.Phones );				
&lt;/pre&gt;

In looking around I did find &lt;a href="http://trycatchfail.com/blog/post/A-More-Fluent-API-For-AutoMapper.aspx"&gt;http://trycatchfail.com/blog/post/A-More-Fluent-API-For-AutoMapper.aspx&lt;/a&gt;. 
It looked promising but (a) the code was incomplete and (b) it broke down for more complex mappings.
&lt;br/&gt;&lt;br/&gt;

I'd like the syntax above but for now I went with something quicker to implement that gave me a shorter syntax though it might not be as syntactically sweet:
&lt;br/&gt;	
	
&lt;pre class="brush:csharp; toolbar: false;"&gt;
public static class AutoMapperExtensions
{
	public static IMappingExpression&amp;lt;TSource, TDestination&amp;gt; MapItem&amp;lt;TSource, TDestination, TMember&amp;gt;(
		this IMappingExpression&amp;lt;TSource, TDestination&amp;gt; target,
		Expression&amp;lt;Func&amp;lt;TDestination, object&amp;gt;&amp;gt; destinationMember,
		Expression&amp;lt;Func&amp;lt;TSource, TMember&amp;gt;&amp;gt; sourceMember )
	{
		return target.ForMember( destinationMember, opt =&amp;gt; opt.MapFrom( sourceMember ) );
	}
}
&lt;/pre&gt;

Now when creating maps I don't need to fuss with an extra lambda and method call:
&lt;pre class="brush:csharp; toolbar: false;"&gt;				
Mapper.CreateMap&amp;lt;Contact, AddressBookDetailsModel&amp;gt;()
	.MapItem( d =&amp;gt; d.PhoneNumbers, s=&amp;gt; s.Phones );				
&lt;/pre&gt;

I can do something similar to simplify Ignore calls:&lt;br/&gt;

&lt;pre class="brush:csharp; toolbar: false;"&gt;		
public static class AutoMapperExtensions
{
	public static IMappingExpression&amp;lt;TSource, TDestination&amp;gt; Ignore&amp;lt;TSource, TDestination&amp;gt;(
		this IMappingExpression&amp;lt;TSource, TDestination&amp;gt; target,
		Expression&amp;lt;Func&amp;lt;TDestination, object&amp;gt;&amp;gt; destinationMember)
	{
		return target.ForMember( destinationMember, opt =&amp;gt; opt.Ignore() );
	}
	
	/* other code removed for brevity */
}
&lt;/pre&gt;&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/yFthkrrAGvY" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-21272323.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2012/8/3/automappers-formember.html</feedburner:origLink></item><item><title>A Week With the New MacBook Air</title><category>Apple</category><category>Mac</category><category>MacBook</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Mon, 16 Jul 2012 01:00:00 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/Z3HSc2NKYSI/a-week-with-the-new-macbook-air.html</link><guid isPermaLink="false">632421:7353983:18174076</guid><description>&lt;h3&gt;Time for a New Computer&lt;/h3&gt;

My mid-2009 MacBook Pro has been good to me as my first Mac. However after 3+ years it was showing its age with slow performance, particularly with any heavy disk access and especially running Windows. It stopped resuming from sleep quite a bit and started locking up more. I had previously upgraded it from 4 to 8 GB of memory and considered switching it to use an SSD but wanted more. I usually end up replacing my computers every 2-4 years or so anyway with 3 being the average of late. So I decided to start backing everything up with &lt;a href="http://www.crashplan.com/"&gt;CrashPlan&lt;/a&gt; and start shopping around.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;To Stick with Apple or Not&lt;/h3&gt;

I've always been a Windows guy and a Microsoft developer and have been happy there personally and professionally. I originally bought a Mac for iPhone development and because I thought it was wise to diversify and mix things up. I've certainly learned a lot and have enjoyed the Mac life for the most part. However I still prefer Windows 7 over Mac OS X in many ways; I'm more a fan of iOS than OS X. I still feel a Mac at home and Windows at work is a good arrangement, I like Apple's innovation and hardware, and I can run both operating systems well on a Mac but not the other way around. That's enough to keep me locked into the "dark side" for now.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Apple's New Lineup&lt;/h3&gt;

I was really tempted to buy the new retina MackBook Pro after drooling a bit at the Apple Store. The display was beautiful to look at, especially text readability and any high-res images, and I liked the smaller form factor and extra power, among other things. 
&lt;br/&gt;&lt;br/&gt;

However there were a few things with the rMBP that gave me pause:

&lt;ul&gt;
	&lt;li style="margin-bottom: 8px;"&gt;&lt;b&gt;Price&lt;/b&gt; - A 256GB HD isn't large enough for me so that means the $2800 model; add in tax and AppleCare and it's up to nearly $3,500. Talk about $ticker $hock.&lt;/li&gt;

	&lt;li style="margin-bottom: 8px;"&gt;&lt;b&gt;Portability&lt;/b&gt; - After using a MacBook Air for a bit, that light, super portable form factor is hard to give up. The retina MBP is quite lighter than my MBP but it still feels heavy and large next to the Air.&lt;/li&gt;

	&lt;li style="margin-bottom: 8px;"&gt;&lt;b&gt;The Bleeding Edge&lt;/b&gt; - The icons in many third party apps look pretty bad with such an HD display. While major apps were/are updated quickly, others will take some time. I have more concerns about website images which aren't likely to be upgraded any time soon just to clean them up for HD displays at the cost of slower load times. I've also seen posts regarding plasma style image burn-in, overheating, and resolution issues in Windows to name a few.
	&lt;/li&gt;
&lt;/ul&gt;

Those things were enough for me to decide against a rMBP for now. I considered the standard MBP which was still refreshed enough and a decent middle ground between the extremes of the rMPB and the Air. Ultimately though it was still pricey and portability is more important to me these days. So I decided it was time to look seriously at the MBA.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Evaluating the Air&lt;/h3&gt;

The initial Air model seemed more like a lightweight netbook for email and surfing by execs. Last year's model seemed beefed up enough for several developers to be happy with them as dev machines. With the 2012 model especially I felt like they had sufficient power and the benchmarks beat my MBP. I decided to go ahead and buy a fully loaded 13" Air with 512GB flash storage, 8GB RAM, and a 2.0GHz dual core i7. I figured I had 14 days to return it if it didn't work for me. 
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Initial Impressions&lt;/h3&gt;

&lt;h4&gt;Connections&lt;/h4&gt;
Connecting my Apple LED Cinema display (prior generation model) was a little awkward. First the power cord doesn't fit the Air; I knew this ahead of time but wasn't fond of another cord to run and leaving the monitor's laptop power cord hanging felt strange. The other minor annoyance was that the thunderbolt port was on the right side of the Air, instead of the left side where the Mini DisplayPort is on my MBP. This meant a further, tighter cord stretch with my desk setup. However those are minor complaints and at least Thunderbolt is backwards compatible with Mini DisplayPort connections and the monitor still works great.
&lt;br/&gt;&lt;br/&gt;

I decided to bite the bullet and spring for a $99 super drive. While I rarely use optical discs, I did have some installs of large software packages that I preferred not to re-download, and some older software that can't easily be found online. Additionally I have data on some discs that I occasionally need to read and didn't want to connect a really old and bulky external disc drive I had. 

&lt;h4&gt;Weight and Size&lt;/h4&gt;
I got an idea of weight and size of the Air in the Apple store but it wasn't until using it at home that I really appreciated it. My MBP which never felt *that* heavy suddenly felt like a tank. Before I rarely unplugged it and removed it from my desk but with the Air I found myself taking it all over instead of reaching for my iPad as much or returning to my desk.

&lt;h4&gt;Speed&lt;/h4&gt;
Cold boot time with the SSD (my first) was just over 20 seconds, including my fumbling around with the mouse and keyboard to type my password. Installs and app launches were very zippy and I was quite pleased. Going to and from sleep was nearly instantaneous.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Bootcamp Debacle&lt;/h3&gt;

&lt;h4&gt;To Bootcamp or not to Bootcamp&lt;/h4&gt;
Bootcamp bothered me in the past with subpar drivers from Apple and having to partition the hard disk and dual boot or pay a performance penalty using a VM like Parallels or Fusion against Bootcamp. However there were enough issues with going VM-only that brought me back to Bootcamp:

&lt;ul&gt;
	&lt;li style="margin-bottom: 8px;"&gt;&lt;b&gt;Phone emulators&lt;/b&gt; - Some mobile dev emulators such as Windows Phone wouldn't run under Parallels; the VM inside a VM Inception type problem. However I believe Fusion worked and maybe this has since been fixed in Parallels.&lt;/li&gt;

	&lt;li style="margin-bottom: 8px;"&gt;&lt;b&gt;Silverlight&lt;/b&gt; - there was at least one Silverlight runtime issue that was specific to running inside a VM.&lt;/li&gt;

	&lt;li style="margin-bottom: 8px;"&gt;&lt;b&gt;Compatibility, performance&lt;/b&gt; - some other select apps and games had problems running virtually either compatibility wise or performance wise.&lt;/li&gt;
&lt;/ul&gt;

To the best of my knowledge the situation with these types of issues hasn't changed much. Issues aside, if I'm going to be doing longer periods of Windows development on my Mac, it is convenient to just boot into Windows in ways (especially with the speedy SSD). So bootcamp it is.
&lt;br/&gt;

&lt;h4&gt;Bootcamp Setup While Half Asleep&lt;/h4&gt;
I made the mistake of setting up Bootcamp while half asleep and without coffee and I gave it no forethought whatsoever. I did this 3 years ago after all, who needs forethought? So have a laugh at my expense. Go on.
&lt;br/&gt;&lt;br/&gt;

&lt;div style="text-decoration:underline;"&gt;Fail 1&lt;/div&gt;
Attempted to get away with 4GB USB thumb drive. Win7 was a bit over 3GB but w/bootcamp drivers etc. it wasn't enough. 8GB thumb drive it is.
&lt;br/&gt;&lt;br/&gt;

&lt;div style="text-decoration:underline;"&gt;Fail 2&lt;/div&gt;
Tried creating an ISO from a Win7 DVD using Toast and my MBP. Somehow it produced a corrupt ISO.
&lt;br/&gt;&lt;br/&gt;

&lt;div style="text-decoration:underline;"&gt;Fail 3&lt;/div&gt;
Tried creating an ISO from a Win7 DVD using Disk Utility and my MBP:
&lt;ul&gt;
	&lt;li&gt;Selected disc in Disk Utility&lt;/li&gt;
	&lt;li&gt;File-&amp;gt;New Disc Image From&lt;/li&gt;
	&lt;li&gt;Chose the CD/DVD .cdr option&lt;/li&gt;
	&lt;li&gt;Saved .cdr to my public folder&lt;/li&gt;
	&lt;li&gt;Renamed .cdr to .iso (seriously Apple why not .iso)&lt;/li&gt;
	&lt;li&gt;Pointed Bootcamp assistant on my Air to Win7 iso on my MBP public folder&lt;/li&gt;
	&lt;li&gt;Copied 25% of Windows files and hung indefinitely&lt;/li&gt;
&lt;/ul&gt;

&lt;div style="text-decoration:underline;"&gt;Fail 4&lt;/div&gt;
At this point I start to wake up and remembered I had the superdrive. Forget the dang thumbdrive. In Bootcamp Assistant I dedicated 200GB to the Windows partition, leaving some 230GB to the Mac side. However Bootcamp assistant did not give the option of using the external disc drive; only a thumb drive. I was able to uncheck that but when rebooting it tried to boot off the thumb drive and not the DVD. I tried holding down the Option key and selecting the disc drive but received a "CDBOOT: Couldn't find BOOTMGR" error.
&lt;br/&gt;&lt;br/&gt;

&lt;div style="text-decoration:underline;"&gt;Fail 5 / Win 1&lt;/div&gt;
Okay well that disc should have been bootable but maybe the Air doesn't allow booting off a Windows disc from an external disc drive. Fine, back to the thumb drive it is; I copied the iso from my Air to it and went back to Bootcamp Assistant. I chose the same partition size as before and I rebooted. Now I was in Windows setup and after formatting the bootcamp partition I was into Windows and all looked good for a moment. Then I noticed networking wasn't working along with about everything else. I realized the bootcamp drivers weren't installed and for some crazy reason I thought for a moment that they should have been automagically installed already. Coffee Geoff, coffee. I browsed to the bootcamp setup on the thumb drive and afterwards all was right with the world.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Setting Up For Development&lt;/h3&gt;
After about a hundred Windows Updates, I installed Visual Studio 2010 w/SP1, VS2012 RC, ReSharper, various VS extensions, DropBox, SnagIt, SkyDrive, and other misc apps. I fired up Visual Studio 2012 and opened some projects and all felt good. However I was more curious of how it performed from the Mac side. I installed Parallels 7 and dedicated 3GB of RAM and 2 CPUs to the bootcamp VM.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Performance&lt;/h3&gt;
Here is screencast I captured that gives an idea of performance.

&lt;br/&gt;&lt;br/&gt;
&lt;iframe src="http://player.vimeo.com/video/45681666" width="500" height="313" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen&gt;&lt;/iframe&gt;
&lt;br/&gt;&lt;br/&gt;

So I could launch Windows and startup apps in a VM, open VS 2012 w/various addons, load a modest sized solution like SignalR, build and run it in ~2 minutes which is certainly acceptable to me.
&lt;br/&gt;

&lt;h4&gt;Other Misc. Timings&lt;/h4&gt;
&lt;ul&gt;
	&lt;li&gt;Windows and VS 2012 fully loaded and ready via Parallels: ~45 seconds&lt;/li&gt;
	&lt;li&gt;git clone SignalR: &amp;lt; 8 seconds&lt;/li&gt;
	&lt;li&gt;Initial SignalR build.cmd run (includes executing unit tests): under 2 mins&lt;/li&gt;
	&lt;li&gt;Cold boot time into Mac including entering password and startup apps: &amp;lt; 22 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;MBP Comparision&lt;/h4&gt;
The performance blows away my MBP. Getting Windows and VS 2012 fully loaded via Parallels for example could take as long as 13 minutes on my MBP, compared with 45 seconds on the Air. Although then again my MBP doesn't have an SSD and that's the majority of the difference along with 3 years of age. It would be more interesting to compare development related timings between the Air and the rMBP.
&lt;br/&gt;

&lt;h4&gt;The Boot Selection Screen&lt;/h4&gt;
When holding down the Option key at startup to boot Windows, it took longer than on my MBP to kick off Windows after selecting it and hitting Enter. My best guess was the addition of the WiFi dropdown on the Air that I do not get on my MBP. I have several wireless networks in my area and it appears the delay is trying to scan and list them all. I wonder if that feature can be turned off?
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Putting it to Work&lt;/h3&gt;

&lt;h4&gt;Heat and Fan&lt;/h4&gt;
Over more extended Visual Studio development sessions (Parallels or straight boot camp) or during movie editing with Quicktime or iMovie, the laptop got a bit warm and the fan would kick on for a while and seemed rather noisy. Part of that is more noticeable because of the otherwise silent operation I think. In general the Air didn't get too hot or too loud for too long. 
&lt;br/&gt;

&lt;h4&gt;Battery Life&lt;/h4&gt;
Battery life was excellent, coming close to the the advertised 7 hour mark, at least while staying in Mac land. Rebooting into Windows and staying there drained it a good deal quicker but still pretty good life.
&lt;br/&gt;

&lt;h4&gt;Windows&lt;/h4&gt;
It is a shame the bootcamp drivers dumb things down so Windows cannot take full advantage of the hardware like the Mac side can (graphics switching, CPU boost, smarter power mgt). I'm not sure whether to buy the conspiracy theories that Apple is trying to make Windows look bad or if it is more that there is not much incentive for them to take the kind of time to support that.
&lt;br/&gt;

&lt;h4&gt;A Freak Dock Issue&lt;/h4&gt;
One strange issue happened when I plugged in the Air to charge it and then accidentally deleted the Desktop background image. The fan kicked on hard and the CPU usage went up to almost 100%. When I checked things out it was the Dock process. Killing it and other apps didn't help. A reboot fixed the issue and it hasn't happened since. I did see others mentioning a similar issue online in reference to an older version of Parallels.
&lt;br/&gt;&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/07/DockCPU.jpg"/&gt;

&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Summary&lt;/h3&gt;
At the end of the day this is still a modestly powered portable laptop and not a power workstation. So far though I do not see any indication that it can't serve well as my primary personal development machine. I'm sure over time as I load it up more it'll slow down and it won't be ideal for heavy HD video editing or games. Worst case scenario I'll switch over to my work laptop or back to a desktop for heavy duty work should it get to that but I'm not sure it will. I would rather sacrifice some power for the portability, price and battery life and I cannot see myself giving this up yet. Time will be the ultimate test but so far, so good.&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/Z3HSc2NKYSI" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-18174076.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2012/7/15/a-week-with-the-new-macbook-air.html</feedburner:origLink></item><item><title>Trying Out Jobs in PowerShell</title><category>Build</category><category>powershell</category><category>release management</category><category>scripting</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Wed, 04 Jul 2012 20:51:51 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/vw-EncgcxsE/trying-out-jobs-in-powershell.html</link><guid isPermaLink="false">632421:7353983:17316594</guid><description>An older app in our workplace stack is a webforms website project and it has a large enough directory structure to take a while to compile the site. 
I have to run the site a fair amount for a rewrite effort and it changes enough to make the initial build and run painfully slow. 

&lt;br/&gt;&lt;br/&gt;
Since I have been working on a build related PowerShell module lately, I thought precompiling the "legacy" site might be a good candidate for a background job. 
I have not used jobs in PowerShell before and for whatever reason I was having a hard time finding good, complete examples of using them. There were also 
some things that tripped me up, so here is an example for the future version of me to reference some day.
&lt;br/&gt;

&lt;pre class="brush:powershell"&gt;
param (
	$TrunkPath = "D:\Projects\MyApp\trunk"
)	

function Start-WebsitePrecompile
{    
    $logFile = (join-path $TrunkPath "\build\output\logs\BackgroundCompile.log")
    "Build log file is $logFile"
        
    $msg = @"
Starting background compile of site. Use Get-Job to check progress. 
You may go on about your merry way but may want to leave the host open until complete.
"@
    
    Write-Output $msg
    $job = Start-Job -InputObject $TrunkPath -Name MyAppPageCompile -ScriptBlock {
        # doesn't appear transcription is supported here
        $trunk = $input

        Set-Alias aspnetcompile $env:windir\Microsoft.NET\Framework\v4.0.30319\aspnet_compiler.exe
        
        # see website solution file for these values
        $virtualPath = "/web"
        $physicalPath = (join-path $trunk "\web")
        $compilePath = $trunk + "\PrecompiledWeb\web"
        aspnetcompile -v $virtualPath -p $physicalPath -f -errorstack $compilePath        
    }
    
    # output details
    $job    
   
    Register-ObjectEvent $job -MessageData $logFile -EventName StateChanged `
        -SourceIdentifier Compile.JobStateChanged `
        -Action {            
            $logFile = $event.MessageData
            Set-Content -Force -Path $logFile `
            	-Value $(Receive-Job -Id $($Sender.Id) -Keep:$KeepJob | Out-String)
            #$eventSubscriber | UnregisterEvent
            Unregister-event -SourceIdentifier Compile.JobStateChanged
            $eventSubscriber.Action | Remove-Job
            Write-Host "Job # $($sender.Id) ($($sender.Name)) complete. Details at $logFile." 
        }
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Some Notes&lt;/h3&gt;

&lt;ul&gt;
	&lt;li style="margin-bottom: 9px;"&gt;Everything inside the job's script block will be executed in another PowerShell process; anything from outside the script block 
	that needs to be used inside must be passed into the script block with the InputObject parameter ($input). While this might be obvious it does mean potential refactoring 
	considerations.  
	&lt;/li&gt;	
	&lt;li style="margin-bottom: 9px;"&gt;It didn't appear transcription was supported inside the script block which was disappointing.&lt;/li&gt;
	&lt;li style="margin-bottom: 9px;"&gt;I half expected that Start-Job would provide a parameter for a block to call when the job was complete. 
		Register-ObjectEvent works but it's a bit verbose and isn't even mentioned in many posts talking about job management.&lt;/li&gt;
	&lt;li style="margin-bottom: 9px;"&gt;Like the job script block, the event handler action script block cannot refer to anything from the outside other than 
		anything passed into the block with the MessageData parameter and automatic variables such as $event, $eventSubscriber, $sender, $sourceEventArgs, and $sourceArgs.&lt;/li&gt;
	&lt;li style="margin-bottom: 9px;"&gt;I went through some trial and error in getting the output from the job in the completed event. The code on line 36 and 37 worked fine 
		but it was not the most obvious initial syntax.
	&lt;/li&gt;
	&lt;li style="margin-bottom: 9px;"&gt;There are a couple of ways to unregister events such as line 38, but I found that when I called the function again I received an error 
		that the event was already subscribed to, so it was clear the unregistration was not working for some reason. The current code is working but similar code did not 
		work previously. I dunno man, gremlins. 
	&lt;/li&gt;
	&lt;li style="margin-bottom: 9px;"&gt;Event handler cleanup strikes me as a bit odd and this &lt;a href="http://poshcode.org/2205"&gt;Register-TemporaryEvent&lt;/a&gt; script is worth a look.&lt;/li&gt;
&lt;/ul&gt;

I am tempted to refactor more of this developer build module process to use more background jobs to do more in parallel. However it is a bit tricky in that many of the functions 
are called both individually and chained together in driver functions and they need to work both in "foreground" and "background" modes. It would also mean a loss of rich progress 
reporting and things get more difficult in terms of debugging, output, code sharing, etc. Also, while multiple cores may help with parallel work, there's a law of diminishing returns 
to be considered as well as machine performance while attempting to do other work while PowerShell crunches away.&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/vw-EncgcxsE" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-17316594.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2012/7/4/trying-out-jobs-in-powershell.html</feedburner:origLink></item><item><title>PowerShell Activity Progress with Time Estimates</title><category>powershell</category><category>scripting</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Mon, 25 Jun 2012 03:00:00 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/3B9Zubh1w4Q/powershell-activity-progress-with-time-estimates.html</link><guid isPermaLink="false">632421:7353983:16924241</guid><description>&lt;h3&gt;The Goal&lt;/h3&gt;
Recently I worked on a PowerShell module to do various application build related functions on development machines. One side objective was multi-level activity progress reporting with time estimation so activity duration could be gauged over time. The desire was for this to be done generically and quickly as it was more of a nice-to-have.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;In Action&lt;/h3&gt;
While running this, progress reporting looked something like the below.&lt;br/&gt;&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/06/powershellactivitytime/PowerShellActivityTime.jpg"&gt;
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Calls to Record Activity Progress&lt;/h3&gt;

Some low-level functions in this module might be called directly at times and other driver functions might get called to chain together several activities. Either way progress is recorded at each level. Each function simply invokes a call to record the start of an activity as the first step and stopping it as the last.
&lt;br/&gt;

&lt;pre class="brush:powershell"&gt;
function New-Build() # params omitted, body simplified for brevity
{	
    Start-Activity "Performing a new build"
	$percentComplete = 0
	
	Log-Progress "Creating code" -PercentComplete $percentComplete		
	New-CodeGen
	$percentComplete += 33
	
	Log-Progress "Creating config" -PercentComplete $percentComplete		
	New-Config
	$percentComplete += 33
	
	Log-Progress "Creating DB" -PercentComplete $percentComplete
	New-DB -common
	
	Stop-Activity
}

function New-CodeGen ([bool]$buildStatusUpdate = $true)
{	
    Start-Activity "Running CodeGen with NAnt"
	# ... real work done here ...
	Stop-Activity
}

# ...
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Starting an Activity&lt;/h3&gt;

First a stack is created at the script level to store the activities. The Start-Activity function creates a new object to store the activity name, start time, total duration and estimated duration (more on that later). This information is pushed onto the stack and output is sent to Write-Progress and Write-Output. 
&lt;br/&gt;

&lt;pre class="brush:powershell"&gt;
$_activityStack = new-object Collections.Stack

function Start-Activity([string]$activity = $(throw "activity is required"))
{
    Write-Output "Activity starting: $activity"
    
    $act = new-object PSObject
    $act | add-member -membertype NoteProperty -name "StartTime" -value $(get-date)
	$act | add-member -membertype NoteProperty -name "Name" -value $activity    
	$act | add-member -membertype NoteProperty -name "TotalSeconds" -Value 0
    $act | add-member -membertype NoteProperty -name "EstimatedSeconds" `
		-Value (Get-ActivityEstimatedSeconds $activity)
    
    $_activityStack.Push($act)
    Log-Progress $activity
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Stopping an Activity&lt;/h3&gt;

Stop-Activity will pop the most recent activity off the stack and calculate the duration. It then writes the completion data to progress and output as well as to a stats file that stores the durations by activity name.
&lt;br/&gt;

&lt;pre class="brush:powershell"&gt;
function Stop-Activity
{
    $id = $_activityStack.Count
    $act = $_activityStack.Pop()
    $ts = $(get-date) - $act.StartTime
	$act.TotalSeconds = $ts.TotalSeconds
    $time = ""
    
    if ($ts.TotalMinutes -ge 1) { $time = "{0:##.00} minute(s)" -f $ts.TotalMinutes }
    else { $time = "{0:##.00} second(s)" -f $ts.TotalSeconds }
    
	# TODO: add in $act.EstimatedSeconds if &gt; 0
    $status = ("'{0}' complete in {1}" -f $act.Name, $time)
    Write-Progress -Activity $act.Name -Status $status -Completed -Id $id
    Write-Output $status
	
	Write-Stats $act 
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Writing Activity Stats&lt;/h3&gt;

Write-Stats takes in the activity object and adds it to an array. If the stats (CSV) filename exists it reads it in, sorts the data in descending time order, 
adds up to $maxKeep (50) existing records into the array, and removes the existing file. The stats filename is then written out with the most recent records.
&lt;br/&gt;

&lt;pre class="brush:powershell"&gt;
function Write-Stats ($act = $(throw "activity is required"))
{	
	# initialize an array to hold recent stats for this activity
 	$recent=@()
	$recent += $act
	$statsFile = Get-StatsFilename
	$maxKeep = 50 # across all activities; several different, want a few of each
	
	if (Test-Path $statsFile)
	{
	 	# get a list of the $maxKeep-1 most recent stats and add each path to the $recent array
		# | Where-Object {$_.Name -eq $act.Name}
	 	Import-CSV $statsFile | Sort StartTime -Descending `
			| Select -Last ($maxKeep -1) | foreach {$recent+=$_}
		# remove the file as we have the data in memory and want to re-write w/top item # and desc time sort
		Remove-Item $statsFile -force
	}
	
	$recent | select StartTime, Name, TotalSeconds | Export-Csv $statsFile -NoTypeInformation
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Getting Activity Estimated Time&lt;/h3&gt;

Time estimation is done via reading in the CSV file, filtering on the activity name, adding the completion time for each to an array, and averaging those values.
&lt;br/&gt;

&lt;pre class="brush:powershell"&gt;
function Get-ActivityEstimatedSeconds([string]$activityName)
{
    $statsFile = (Get-StatsFilename)
    $avg = -1
    
    if (Test-Path $statsFile) 
    {
        $totalSeconds=@()
        Import-CSV $statsFile | Where-Object {$_.Name -eq $activityName} `
			| foreach {$totalSeconds+=$_.TotalSeconds}
        $m = $totalSeconds | measure-object -ave
        $avg = $m.Average
    }	
	
    return $avg
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Misc. Functions&lt;/h3&gt;

Log-Progress writes both to standard out and to progress. It retrieves the current activity without removing it from the stack, formats the estimated completion time 
calculated earlier, and adds that to the progress information. The number of current activities is used as the progress bar id since there will be multiple levels; in 
my case everything is done serially. Initially I set the estimated seconds argument on Write-Progress but found it misleading; my script shells out to various other apps 
and that is blocking - it won't update via a timer or anything like that.
&lt;br/&gt;

&lt;pre class="brush:powershell"&gt;
function Log-Progress([string]$msg, [int]$percentComplete = 0)
{
    Write-Output $msg
    $act = $_activityStack.Peek()
    $id = $_activityStack.Count
	# the problem with -SecondsRemaining is it won't auto update w/timer or anything 
	# so it will be helpful at first and then quickly misleading
	
	$actName = $act.Name
	
	if ($act.EstimatedSeconds -ge 0)
	{
		$estFinish = $act.StartTime.AddSeconds($act.EstimatedSeconds)
		$actName += " - Est. Finish @ " + $estFinish.ToString("hh:mm:ss tt")
	}
	
    Write-Progress -Activity $actName -Status $msg -PercentComplete $percentComplete `
        -Id $id #-SecondsRemaining $act.EstimatedSeconds
}
&lt;/pre&gt;

For simplicity all activity details are stored in the same file, up the max limit defined in Write-Stats.
&lt;br/&gt;

&lt;pre class="brush:powershell"&gt;
function Get-StatsFilename
{
	# considered a separate file per activity but we can filter out of one file
	# too much clutter w/sep and would have to build a safe filename with activity name
	$file = (Join-Path (Get-StatsFilePath) "Stats.csv")
	return $file
}

function Get-StatsFilePath
{
	$path = (join-path $env:LOCALAPPDATA "MyCompany\MyApp\build_output\stats")
	if (!(Test-Path $path)) { New-Item $path -type directory | Out-Null }
	return $path
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;In Conclusion&lt;/h3&gt;

Other options on timing activities include things like the &lt;a href="http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx"&gt;Stopwatch&lt;/a&gt; class 
or PowerShell's &lt;a href="http://technet.microsoft.com/library/ee176899.aspx"&gt;Measure-Command&lt;/a&gt;. I'll use those more for ad-hoc measuring here or there. In 
the case of my module though, I found this "CSV stack" approach to work well as a simple, generic way to time everything across the board. If there are a large number 
of activity records being kept, deep function chaining, and/or threading work this approach might be a bit more problematic and slow.&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/3B9Zubh1w4Q" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-16924241.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2012/6/24/powershell-activity-progress-with-time-estimates.html</feedburner:origLink></item><item><title>CodeStock 2012 Windows Phone App</title><category>WP7</category><category>conferences</category><category>mobile</category><category>windows phone</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Wed, 16 May 2012 18:50:37 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/zl8VxNpievs/codestock-2012-windows-phone-app.html</link><guid isPermaLink="false">632421:7353983:16294898</guid><description>&lt;table cellspacing="18" style="margin-top:-10px;"&gt;
&lt;tr&gt;
&lt;td valign="top"&gt;
The &lt;a href="http://codestock.org"&gt;CodeStock&lt;/a&gt; conference is coming up soon on June 15th in downtown Knoxville. As such I have upgraded the 
&lt;a href="http://www.geoffhudik.com/tech/2011/5/10/codestock-2011-app-for-windows-phone-7.html"&gt;CodeStock 2011 Windows Phone app&lt;/a&gt; for this year's 
conference and am pleased to announce it has been published to the marketplace.
&lt;br/&gt;&lt;br/&gt;
&lt;/td&gt;
&lt;td valign="top" width="200"&gt;

&lt;div style="margin-bottom:10px;"&gt;
&lt;a style="font-size: larger;" href="http://windowsphone.com/s?appid=8b065e23-79bb-4a3c-8f65-0bc26a17ffbb"&gt;Marketplace&lt;/a&gt;&lt;br/&gt;
&lt;/div&gt;


&lt;div style="margin-bottom:10px;"&gt;
	&lt;a style="font-size: larger;" href="https://vimeo.com/42290848"&gt;Video&lt;/a&gt;
&lt;/div&gt;

&lt;div style="margin-bottom:0px;"&gt;
	&lt;a style="font-size: larger;" href="https://github.com/thnk2wn/codestock-winphone"&gt;Source Code&lt;/a&gt;
&lt;/div&gt;

&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="2"&gt;
&lt;h3&gt;Changes Over Last Year's Version&lt;/h3&gt;
I have not had much time this year to enhance the application but below are highlights of what changed for the 2012 version. For more details see the 
&lt;a href="https://github.com/thnk2wn/codestock-winphone/commits/master"&gt;commit history&lt;/a&gt;. 

&lt;ul&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Mango upgrade&lt;/b&gt; - OS target upgrade from 7.0 to 7.1 and many fixes to address breaking changes&lt;/li&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Map view&lt;/b&gt; - new map view with various points of interest and directions. Points of interest are 
		externally configured so if you have suggestions, let me know.&lt;/li&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Branding&lt;/b&gt; - logo, title, date, image and theme related changes&lt;/li&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Source control and hosting&lt;/b&gt; - switch from HG and bitbucket to git and github&lt;/li&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;New app, not an upgrade&lt;/b&gt; - Due to various factors this year's version was submitted as a new app and last year's was hidden. If by chance you have last year's version installed, uninstall it first.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;Potential Future Changes&lt;/h3&gt;
There is a chance I might make additional updates before the conference and might consider pull requests if you are so inclined. 
Feel free to &lt;a href="https://github.com/thnk2wn/codestock-winphone/issues"&gt;add an issue&lt;/a&gt; for any bugs or feature requests.
&lt;br/&gt;&lt;br/&gt;

Some changes I'd like to see in the future:&lt;br/&gt;

&lt;ul&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Live tile&lt;/b&gt; - maybe showing next session via schedule and/or favorite&lt;/li&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Better twitter integration&lt;/b&gt; - i.e. more of a native twitter client instead of mobile twitter links.&lt;/li&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;More graceful public WiFi handling&lt;/b&gt; - better error message when an HTML response (from a WiFi logon page) 
		was received instead of expected JSON. (change pending Marketplace approval in v2.2)&lt;/li&gt;
&lt;/ul&gt;

On another Windows Phone project I started refactoring the Phone.Common assembly out into multiple NuGet packages that are a bit more generic. 
I have not had the time to finish that or incorporate it into the CodeStock app yet.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Overview Video&lt;/h3&gt;
The below video gives an overview of most of the features minus a few miscellaneous ones.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;iframe src="http://player.vimeo.com/video/42290848?title=0&amp;amp;byline=0&amp;amp;portrait=0" width="398" height="728" frameborder="0"&gt;&lt;/iframe&gt;

&lt;br/&gt;&lt;br/&gt;

This page can also be accessed via &lt;a href="http://www.geoffhudik.com/codestock-wp7"&gt;http://www.geoffhudik.com/codestock-wp7&lt;/a&gt;.&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/zl8VxNpievs" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-16294898.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2012/5/16/codestock-2012-windows-phone-app.html</feedburner:origLink></item><item><title>Build Automation Part 4: Database and Report Deployments</title><category>.net</category><category>SQL</category><category>deployments</category><category>oracle</category><category>powershell</category><category>release management</category><category>scripting</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Mon, 14 May 2012 19:16:24 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/s7mZkg7Domc/build-automation-part-4-database-and-report-deployments.html</link><guid isPermaLink="false">632421:7353983:16219566</guid><description>&lt;h3&gt;Series Index&lt;/h3&gt;
&lt;a href="http://www.geoffhudik.com/tech/2012/5/5/build-automation-part-1-overview-and-pre-build-tasks.html"&gt;Build Automation Part 1: Overview and Pre-build Tasks&lt;/a&gt;&lt;br/&gt;
&lt;a href="http://www.geoffhudik.com/tech/2012/5/8/build-automation-part-2-building-and-packaging.html"&gt;Build Automation Part 2: Building and Packaging&lt;/a&gt;&lt;br/&gt;
&lt;a href="http://www.geoffhudik.com/tech/2012/5/9/build-automation-part-3-app-deployment-script.html"&gt;Build Automation Part 3: App Deployment Script&lt;/a&gt;&lt;br/&gt;
Build Automation Part 4: Database and Report Deployments&lt;/a&gt;&lt;br/&gt;
&lt;br/&gt;

Unlike deploying the application itself in its entirety each time, database and report items have required incremental deployments due to their nature and sheer size. The question becomes how to manage these increments in coordination with the application bits to ensure everything is in sync.
&lt;br/&gt;&lt;br/&gt;

I do not pretend to have all the answers here. I was hoping this part of the process could get overhauled more with our last release but there is only so much time in the day.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Some Challenges&lt;/h3&gt;

&lt;h4&gt;Size, Dependencies, Impact Analysis&lt;/h4&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-4-db-reports/SchemaSize.jpg" align="left" hspace="10" vspace="5"/&gt; On the (Oracle) database side of our operational app we have around 800 tables, 500+ packages, 500+ views, 300+ functions and procedures, and a variety of other object types across 3 schemas. Running schema compares / diffs and doing sync scripts has been a time consuming pain in the past, regardless of various tools we have tried.
&lt;br/&gt;&lt;br/&gt;

The situation is complicated by some 10 database environments, other app databases our app is dependent on, various apps depending on our database, and dynamic SQL embedded into report RDL files or coming from ORM's like Entity Framework. Dependency and impact analysis can be difficult, particularly across systems.
&lt;br/&gt;&lt;br/&gt;

On the report side this app has over 600 SSRS reports scattered over 8 servers, several folders and different versions of SQL Server Reporting Services. 
&lt;br/&gt;

&lt;h4&gt;Source Control&lt;/h4&gt;
Source control for reports has not been a problem so much with using SSRS, other than TFS generating a lot of unnecessary merge conflicts on those XML RDL files. 
&lt;br/&gt;&lt;br/&gt;

On the database side we have had some success using &lt;a href="http://www.quest.com/tv/All-Videos/1254973803001/How-to-Use-Toad-for-Oracle-Team-Coding/Video/"&gt;Team Coding in Toad&lt;/a&gt; with the &lt;a href="http://visualstudiogallery.msdn.microsoft.com/bce06506-be38-47a1-9f29-d3937d3d88d6"&gt;TFS MSSCCI Provider&lt;/a&gt;. Quest came out with a &lt;a href="http://www.toadworld.com/Blogs/tabid/67/EntryId/649/Configuring-Toad%C2%AE-Team-Coding-to-use-Microsoft-Team-Foundation-Server-2010.aspx"&gt;native TFS provider&lt;/a&gt; but it did not support TFS work item association which ruled out our use of it.
&lt;br/&gt;&lt;br/&gt;

The MSSCCI provider "works" with Toad for basic changes like packages, procedures, functions, triggers, views, etc. but boy it is not without a lot of quirks. So much so that for a while I saved any database object script changes to files on my desktop out of fear my changes would get overwritten, which used to happen quite a bit. The other problem is that not all changes are source controlled such as table schema changes, data changes etc. 
We offset that via some SQL file attachments to tasks tied to a project.
&lt;br/&gt;&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-4-db-reports/ToadCheckin.jpg"/&gt;&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Pulling Report and Database Changes&lt;/h3&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-4-db-reports/TAM-Changeset.jpg"/&gt;&lt;br/&gt;&lt;br/&gt;

I posted before about a custom &lt;a href="http://www.geoffhudik.com/tech/2011/8/17/tfs-artifact-manager.html"&gt;TFS Artifact Manager&lt;/a&gt; (hereinafter "TAM") tool for pulling down report and database changeset files and task attachments either for a single work item or a large batch of related work items, such as all tasks linked through a series of scenarios, backlog items and bugs tied to a project. I won't repeat those details here but we currently still use the tool to gather up all the report and database changes for a given project release. It far from perfect but it beats manual guesswork and building sync scripts from database compares.  
&lt;br/&gt;&lt;br/&gt;

The &lt;a href="http://www.geoffhudik.com/tech/2011/8/17/tfs-artifact-manager.html"&gt;TAM&lt;/a&gt; tool is also used to pull artifacts from various single task ad-hoc changes made outside of any official product release. Many reports can be added to the application dynamically through some dynamic report forms with common parameters; new records are added to the database and the reports are deployed outside of any app deployment. Likewise there are occasional database changes made independent of the application.
&lt;br/&gt;&lt;br/&gt;

There are other tools mentioned in the &lt;a href="http://www.geoffhudik.com/tech/2011/8/17/tfs-artifact-manager.html"&gt;TAM&lt;/a&gt; post that we may try using more in the future. Also, Troy Hunt has a good series of deployment posts including this one regarding &lt;a href="http://www.troyhunt.com/2011/02/automated-database-releases-with.html"&gt;Automated database releases with TeamCity and Red Gate&lt;/a&gt;. Doing exactly that with our schemas would make me a bit nervous but perhaps with tweaks and some experiments in the future.
&lt;br/&gt;&lt;br/&gt;

Additionally I posted a review on &lt;a href="http://www.geoffhudik.com/tech/2011/9/12/red-gate-schema-compare-for-oracle-review.html"&gt;Red Gate's Schema Compare for Oracle&lt;/a&gt; which can be quite a useful tool. We don't rely on it as heavily anymore with custom tools and processes but it is handy to use it to double-check things after deployments or for doing more one-off database syncs.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Deploying Reports&lt;/h3&gt;

&lt;h4&gt;Ad hoc Report Deployments&lt;/h4&gt;
When deploying from Development to Test, our CI process picks up any reports merged from $/MyApp/Reports/Dev/ to $/MyApp/Reports/Main/ and automatically deploys those via PowerShell and &lt;a href="http://msdn.microsoft.com/en-us/library/ms162839.aspx"&gt;RS Utility&lt;/a&gt;. Any corresponding database changes are manually applied before that. Environments beyond Test currently require attaching 
report and database changes to a Help Desk ticket and routing to a DBA. The DBA runs any database changes and deploys the reports using the script mentioned in the next section.
&lt;br/&gt;

&lt;h4&gt;App Report Deployments&lt;/h4&gt;
For reports to be deployed with a given build of an application, the process is basically the same as the ad-hoc process in respect to going from Dev to Test. One difference is on timing of merging the 
report changes in source control to correspond with any dependent changes to the application code. When moving beyond the Test environment, all reports tied to a given project work item are pulled using the &lt;a href="http://www.geoffhudik.com/tech/2011/8/17/tfs-artifact-manager.html"&gt;TAM&lt;/a&gt; tool. They are then deployed in mass using PowerShell and the SSRS web service (without RS Utility), in a manner similar to this post on 
&lt;a href="http://www.geoffhudik.com/2011/10/13/uploading-ssrs-reports-with-powershell.html"&gt;Uploading SSRS Reports with PowerShell&lt;/a&gt;.
&lt;br/&gt;&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-4-db-reports/UploadReports.jpg"/&gt;
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Deploying Database Changes&lt;/h3&gt;

We currently do not have much automation around ad hoc database deployments but not much is needed there.
&lt;br/&gt;&lt;br/&gt;

For app database changes we start by pulling the database changes using the &lt;a href="http://www.geoffhudik.com/tech/2011/8/17/tfs-artifact-manager.html"&gt;TAM&lt;/a&gt; tool. In that tool a script "package" is built by choosing the scripts to include and specifying any needed order or execution. Previously someone (dev or DBA depending on environment) would either execute all those manually by hand in Toad, or build out an index/driver script and run that. It was not as bad as it might sound, given the tool produced combined SQL scripts for views, packages, procs, etc. Still it was tedious if there were a number of data or schema migration scripts to be run in order.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Executing Database Scripts With PowerShell&lt;/h3&gt;

Our resident Oracle expert &lt;a href="https://twitter.com/#!/jimtilson"&gt;Jim Tilson&lt;/a&gt; ("&lt;a href="http://allthingsoracle.com/experts/jim-tilson/"&gt;The Optimizer&lt;/a&gt;") had the idea of creating a PowerShell script that used SQL*Plus to generically execute all database scripts in a given directory. I paired with him to get the basic interaction going but this is his brainchild and work. He should probably be the one explaining this but no telling when that slacker will get around to blogging :). If you have any interest in Oracle, SQL optimization, database tech in general, or Ruby, you should &lt;a href="https://twitter.com/#!/jimtilson"&gt;reach out to him on Twitter&lt;/a&gt; and ask him to blog more (and tweet more while he is at it). At any rate this might be useful for others so I will post the code and attempt to explain it.
&lt;br/&gt;

&lt;h4&gt;Structure&lt;/h4&gt;

The script expects all the database script files to be located in subfolders where the script resides, one folder per schema name, and no subfolders 
with each schema folder (not recursive).
&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-4-db-reports/DbScriptFolders.jpg"/&gt;
&lt;br/&gt;&lt;br/&gt;

Each file in a schema folder will be executed regardless of filename extension. Ordering is based on filename; our TAM tool prefixes a numeric wart on each file to ensure an obvious order. At the moment the script does not explicitly specify a name ordering but that's the default. 
&lt;br/&gt;&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-4-db-reports/DatabaseScriptFiles.jpg"/&gt;
&lt;br/&gt;

&lt;h4&gt;Executing Scripts for Each Schema&lt;/h4&gt;

At the bottom of the script, the server TNS name is prompted for and a couple things are set before running the main run-scripts function.
&lt;br/&gt;

&lt;pre class="brush:powershell; highlight: [3, 4]"&gt;
set-location (Get-ScriptDirectory)
$dateWart = Get-DateWart
$server = read-host "Enter the server TNS name"
run-scripts
echo "Successfully ran all scripts."
&lt;/pre&gt;

Run-Scripts invokes a function to run the scripts for each schema, passing along the user and server info. This could be made more generic 
by assuming any subfolder where the PowerShell script resides represents a schema with database scripts to run.
&lt;br/&gt; 

&lt;pre class="brush:powershell;"&gt;
function Run-Scripts
{
    foreach ($user in @("USER_SCHEMA_1", "USER_SCHEMA_2", "USER_SCHEMA_3")) 
	{ 
		run-scriptsforschema -server $server -user $user
	}
}
&lt;/pre&gt;

&lt;h4&gt;Running Scripts for a Schema&lt;/h4&gt;

This function will temporarily set location to the schema subfolder corresponding to the user/schema name passed in. It prompts for a password for a later connection to that schema. Finally it enumerates all files in the schema folder, calls a Run-Script function to execute each, and writes progress as it goes. 
&lt;br/&gt;&lt;br/&gt;

Each successfully executed file is moved into a Completed subfolder. That file move is important as many migration scripts are written assuming they will only be run once and we have been bit by DBA's accidentally running scripts more than once.

&lt;pre class="brush:powershell; highlight: [9, 17, 22]"&gt;
function Run-ScriptsForSchema($user)
{
    echo "Running scripts for $user."
    push-location    
    set-location $user    
	$password = get-password($user)    
    ensure-directory(".\Completed")

    $files = @(get-childitem | where {!$_.PsIsContainer})
	$count = 0
	
    foreach ($fileInfo in $files)
    {        
        write-progress -activity "Running scripts for $user" `
			-currentoperation $fileinfo.name -status ("Executing") `
			-PercentComplete (100*$count/$files.count)
        Run-Script $user $password $fileInfo
        $count++
        write-progress -activity "Running scripts for $user" `
			-currentoperation $fileinfo.name -status ("Done") `
			-PercentComplete (100*$count/$files.count)
        move-item -path $fileInfo.fullname -destination ".\Completed" -Force
    }
    
    write-progress -activity "Running scripts for $user" -status ("Complete") -completed
    pop-location    
    echo "Completed scripts for $user"
}
&lt;/pre&gt;

&lt;h4&gt;Running a Script&lt;/h4&gt;

The Run-Script function takes care of some logging and calls a normalize function to tack on additional SQL before and after 
the SQL contained in the file (error handling, commits, etc.); more on that in a moment. Function notes follow.
&lt;br/&gt;

&lt;ul&gt;
	&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Path&lt;/b&gt; - the sqlplus location should be in the SYSTEM PATH environment variable so fully qualifying it should not be needed. In my case the location is 
C:\app\&lt;i&gt;[username]&lt;/i&gt;\product\11.2.0\client_1\.&lt;/li&gt;
&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;-L Parameter&lt;/b&gt; - instructs the app to only attempt to log on once; otherwise w/bad credentials it can get hung awaiting input.&lt;/li&gt;
&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;-M Parameter&lt;/b&gt; - indicates HTML output is desired from sqlplus.&lt;/li&gt;
&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Credentials&lt;/b&gt; - The server name captured earlier is passed in along with the user/schema and password parameter values.&lt;/li&gt;
&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;SQL&lt;/b&gt; - the normalize script function returns a SQL string and that is piped into sqlplus to be executed.&lt;/li&gt;
&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Output&lt;/b&gt; - the output is sent to $logFile and the 2&gt;$1 sends standard error to standard output.&lt;/li&gt;
&lt;li style="margin-bottom: 7px;"&gt;&lt;b&gt;Error checking&lt;/b&gt; - Finally &lt;a href="http://blogs.msdn.com/b/powershell/archive/2006/09/15/errorlevel-equivalent.aspx"&gt;$LASTEXITCODE&lt;/a&gt; is checked to see what sqlplus.exe exited with; if 0 it was successful, otherwise it is the Oracle error number. The process stops on any error; manual changes might be needed to address any problems then the script can be run again.&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="brush:powershell; highlight: [7, 8, 10, 12]"&gt;
function Run-Script($user, $password, $fileInfo)
{
    $logDir = ("..\Logs\{0}\{1}" -f $dateWart, $user)
    ensure-directory $logDir
    $logFile = join-path $logDir ($fileInfo.basename + ".html")
    
    (normalize-script $fileinfo.fullname) | sqlplus.exe -L -M "HTML ON SPOOL ON" `
		-S "$user/""$password""@$server" &gt;&gt; $logfile 2&gt;$1

    $lec = $LASTEXITCODE
    
    if ($lec -ne 0)
    {
        write-error ("ERROR executing {0}!" -f $fileInfo.FullName)
        exit
    }
}
&lt;/pre&gt;

&lt;h4&gt;Adjusting the SQL&lt;/h4&gt;

There are two critical adjustments made to the SQL read from the database script files to execute. The first is detecting a SQL error and exiting SQL*PLus with the error code. The other is issuing a commit at the end; most of our data related scripts do not include a commit as often they are ran and verified before issuing a commit. It is worth reading over the &lt;a href="http://docs.oracle.com/cd/B19306_01/server.102/b14357/ch12052.htm"&gt;WHENEVER SQLERROR&lt;/a&gt; documentation as some types of errors will not trigger an exit; fully checking for all types of errors might require something more brittle like scanning the log files for certain error phrases.
&lt;br/&gt;

&lt;pre class="brush:powershell;highlight: [4, 7, 8]"&gt;
function normalize-script($filename)
{
@"
    whenever sqlerror exit sql.sqlcode
    set echo off
    set termout off
    $([string]::join("`n", (get-content $fileinfo.fullname -readcount 0)))
    commit;
    exit
"@
}
&lt;/pre&gt;

&lt;h4&gt;Helper Functions&lt;/h4&gt;

At the top of the script are some helper functions and an interop services dll is loaded for later use in translating the secure password to a plain text string to be passed along to SQL*Plus.
&lt;br/&gt;

&lt;pre class="brush:powershell; highlight: [20, 21, 22]"&gt;
[Reflection.Assembly]::LoadWithPartialName("System.Runtime.InteropServices")

function Get-ScriptDirectory
{
    Split-Path ((Get-Variable MyInvocation -scope script).Value.MyCommand.Path)
}

function ensure-directory($dir)
{
    if (!(test-path $dir)) { new-item $dir -type directory }
}

function Get-DateWart()
{
    get-date -uformat "%Y %m %d %H %M %S"
}

function get-password($user)
{
    $enterpassword = read-host -AsSecureString "Password for $user@$server"
    [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(`
		[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($enterpassword));
}
&lt;/pre&gt;

&lt;h4&gt;Analyzing the Results&lt;/h4&gt;
The script stores logs under a Logs\&lt;i&gt;[Timestamp]&lt;/i&gt;\&lt;i&gt;Schema&lt;/i&gt;\ folder for troubleshooting and verification purposes.&lt;br/&gt;&lt;br/&gt;
&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-4-db-reports/Logs.jpg"/&gt;
&lt;br/&gt;

&lt;h4&gt;SQL*Plus Alternatives&lt;/h4&gt;
One alternative to SQL*Plus is using OracleCommand's ExecuteNonQuery method in Oracle.DataAccess.dll. I tried this approach back when I created an OracleScriptExecutor utility app that was designed to easily run SQL Scripts against multiple schemas. It was a bit of a nightmare that I do not recommend. For one you have to deal with annoyances like &lt;a href="http://boncode.blogspot.com/2009/03/oracle-pls-00103-encountered-symbol.html"&gt;linefeed issues&lt;/a&gt;, semicolon and BEGIN/END block issues, and it is quite difficult to deal with multiple scripts combined in one SQL file (i.e. '/' delimited). It almost requires a full blown SQL parsing engine to handle it correctly so I'd rather delegate that pain to a tool like SQL*Plus that already handles such complexity.
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;In Conclusion&lt;/h3&gt;
This really only scratches the surface of the problems and solutions in dealing with database and report deployments. With some time and TLC I am sure this beast could be further tamed. Thoughts, suggestions, tips, helpful tools, processes? Leave a comment!
&lt;br/&gt;&lt;br/&gt;

I am running out of time in this series but hopefully I can touch on some CI details with TeamCity next.&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/s7mZkg7Domc" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-16219566.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2012/5/14/build-automation-part-4-database-and-report-deployments.html</feedburner:origLink></item><item><title>Build Automation Part 3: App Deployment Script</title><category>.net</category><category>CI</category><category>deployments</category><category>powershell</category><category>release management</category><category>scripting</category><dc:creator>Geoff Hudik</dc:creator><pubDate>Wed, 09 May 2012 19:32:29 +0000</pubDate><link>http://feedproxy.google.com/~r/thnk2wn/~3/nm9xej073sY/build-automation-part-3-app-deployment-script.html</link><guid isPermaLink="false">632421:7353983:16198602</guid><description>&lt;h3&gt;Series Index&lt;/h3&gt;
&lt;a href="http://www.geoffhudik.com/tech/2012/5/5/build-automation-part-1-overview-and-pre-build-tasks.html"&gt;Build Automation Part 1: Overview and Pre-build Tasks&lt;/a&gt;&lt;br/&gt;
&lt;a href="http://www.geoffhudik.com/tech/2012/5/8/build-automation-part-2-building-and-packaging.html"&gt;Build Automation Part 2: Building and Packaging&lt;/a&gt;&lt;br/&gt;
Build Automation Part 3: App Deployment Script&lt;br/&gt;
&lt;a href="http://www.geoffhudik.com/tech/2012/5/14/build-automation-part-4-database-and-report-deployments.html"&gt;Build Automation Part 4: Database and Report Deployments&lt;/a&gt;&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;Types of Deployment Scripts&lt;/h3&gt;
We currently have different deployment scripts that are run independently though there is a desire to chain them together in automated fashion when time allows.

&lt;ul&gt;
&lt;li&gt;Application - Deploys the application bits&lt;/li&gt;
&lt;li&gt;Database - Deploys the database script changes for a given project release&lt;/li&gt;
&lt;li&gt;Reports - Deploys SSRS report changes for a given project release&lt;/li&gt;
&lt;/ul&gt;

The database and report artifact deployments are deserving of a dedicated post to this in the future. This post will focus on the application deployment script.
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;Deployment Script Skeleton&lt;/h3&gt;
Looking at the script in a top-down fashion is best I think. Some of the helper functions being called will be listed later.
&lt;br/&gt;

&lt;h4&gt;Top&lt;/h4&gt;

&lt;pre class="brush:powershell;"&gt;
param (
    [string]$DeployToServer = "",
    [switch]$SkipBackup = $false,
    [switch]$automated = $false
) 

$global:ErrorActionPreference = "Stop"

# error on uninitialized variables, non-existent properties, bad function calls
Set-StrictMode -version 2
&lt;/pre&gt;

At the top of the script some script preferences are set and the following parameters are defined:
&lt;br/&gt;

&lt;ul&gt;
&lt;li style="margin-bottom: 7px;"&gt;DeployToServer - Name of the target server to deploy to. The idea is this would bypass a prompt where the user chose the server to deploy to. This would generally be set by a CI process. At the moment this isn't being used in the script as our build server went down in flames before I could make use of this.&lt;/li&gt;

&lt;li style="margin-bottom: 7px;"&gt;SkipBackup - Allows turning off default behavior of backing up the existing app installation on the target deployment server.&lt;/li&gt;

&lt;li style="margin-bottom: 7px;"&gt;Automated - This was meant to control whether the script ever paused for any user-interaction or not. It is used in a couple of places but not fully implemented. It could be replaced by use of DeployToServer but I wanted to be explicit. Typically this would just be set from a CI build.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Bottom&lt;/h4&gt;

After all the functions in the deployment script, the following script level variables are defined. I went with an $_ naming convention for script level variables; $script:variableName may have been better but more verbose.
&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
$_scriptPath = (get-scriptdirectory)

# import generic deployment script functions
. (join-path $_scriptPath "Deploy-Common.ps1")

$_packagePath = $_scriptPath # for now anyway, just being explicit here
$_transcriptFile = (join-path $_scriptPath "DeployTranscript.txt")

$_targetServer = ""
$_targetClickOnceDir = ""
$_targetSatelliteDir = ""
$_targetServicesDir = ""
$_targetRoot = ""
$_envName = ""

$_activity = "Initializing"
$_zipFileObject = $null

$_successfulDeploy = $false

$_scriptErrorMessage = ""
$_clickOnceUrl = ""
$_deployTS = $null
&lt;/pre&gt;

At the very bottom is the call to the main Publish-App function with error handling, invoking the product web page if successful, 
and transcript logging and emailing.
&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
try 
{
    Init
    Publish-App
    
    if (!$script:automated)
    {
        $_clickOnceUrl = ("http://{0}.domain.com/MyApp/" -f $_targetServer)
        "Launching app landing page at $_clickOnceUrl"
        Invoke-InternetExplorer $_clickOnceUrl
    }
    
    $_successfulDeploy = $true
}
catch [System.Exception]
{
    $_scriptErrorMessage = ("Deployment failed with error: {0}{1}{1}Script: {2}  Line,Col: {3},{4}" `
		-f $_.Exception.ToString(), [System.Environment]::NewLine,  $_.InvocationInfo.ScriptName, `
		$_.InvocationInfo.ScriptLineNumber, $_.InvocationInfo.OffsetInLine)
    Write-Warning $_scriptErrorMessage    
}
finally
{
    Write-Progress "Done" "Done" -completed
    
    if (Get-CanTranscribe)
    {
        Stop-Transcript
        Send-Notification
    }
}

if (!$_successfulDeploy) { invoke-item  $_transcriptFile }

# if not an automated deploy then pause
if (!$script:automated)
{
    "`nPress enter to continue..."
    [void][System.Console]::ReadLine()
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Initialization&lt;/h3&gt;

The Init function kicks off transcription, reads in build version information from a file (&lt;a href="http://www.geoffhudik.com/tech/2012/5/5/build-automation-part-1-overview-and-pre-build-tasks.html"&gt;See Initialization in Part 1&lt;/a&gt;), 
tacks on to the PATH environment variable, and sets up the PowerShell UI shell.
&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
function Init
{
    #setting $ErrorActionPreference here doesn't appear to effect anything
    try { Stop-Transcript | out-null } catch { }    
    
    if (Get-CanTranscribe) { Start-Transcript -path $_transcriptFile }

    $buildInfo = Get-BuildInfo
    $env:path += ";$env:windir\Microsoft.NET\Framework\v4.0.30319;$env:windir\System32"
   
    # customize window shell
    if ($Host -and $Host.UI -and $Host.UI.RawUI)
    {
        try 
        {
            $ui = (Get-Host).UI.RawUI                
            $ui.WindowTitle = ("Deploy MyApp {0} ({1})" `
				-f $buildInfo.AppVersion, $buildInfo.FileVersion)
            
            if (!(Get-IsHostISE))
            {
                $ui.BackgroundColor = "DarkBlue"
                $ui.ForegroundColor = "White"
                $bufferSize = $ui.BufferSize
                $bufferSize.Width = 120
                $bufferSize.Height = 9000
                $ui.BufferSize = $bufferSize
                $winSize = $ui.WindowSize    
                $winSize.Width = 120
                $winSize.Height = 70
                $ui.WindowSize = $winSize
            }
        }
        catch [System.Exception]
        {
            ("Error configuring host UI: {0}" -f $_.Exception.Message)
        }
    }
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Deployment Target Menu&lt;/h3&gt;

This function is intended for user-interactive execution of the deployment; in our case this was mostly for production-level environments where another group performed the deployment. 
However it also came in handy in some other scenarios where we could not do completely automated continuous deployment, such as a period where our CI server was all hosed up. 
Additionally there are times where it is handy to push a custom build from anywhere to anywhere, outside of the normal process flow.
&lt;br/&gt;&lt;br/&gt;

For automated CI deployments a target server name would be passed in and this user menu interaction would be skipped. This function displays a menu of valid target environments to deploy to, sets the target server accordingly, 
and invokes a deployment function with that target server name.
&lt;br/&gt;&lt;br/&gt;

For the menu I originally tried just using $Host.ui.PromptForChoice as in &lt;a href="http://blogs.technet.com/b/jamesone/archive/2009/06/24/how-to-get-user-input-more-nicely-in-powershell.aspx"&gt;this article by James O'Neill&lt;/a&gt;. 
However I did not like how that laid out; everything was rendered horizontally instead of vertically and some things ran together or were not spaced to my liking. That lead 
me to &lt;a href="http://jdhitsolutions.com/blog/2011/12/friday-fun-a-powershell-console-menu/"&gt;this post by Jeff Hicks&lt;/a&gt; which my menu is based on; the Show-Menu function in 
the switch statement below is his function which I did not modify.
&lt;br/&gt;

&lt;pre class="brush:powershell; highlight: [24,42,43,47]"&gt;
function Publish-App
{
    $menu = " `
    1 Development `
    2 Iteration `
    3 Test `
    4 Pre-release Training `
    5 Production `
    6 Post-release Training `
    `    
    C Cancel `
`    
Your choice "
   
    $targetServer = ""
    $script:_envName = ""
    $buildInfo = Get-BuildInfo
	$title = ("Select Target Destination for MyApp {0} ({1})" `
		-f $buildInfo.AppVersion, $buildInfo.FileVersion)

    # Keep looping and running the menu until the user selects a valid item or cancels.
    Do
    {
        switch (Show-Menu $menu $title -clear)
        {
           "1" { $targetServer = "dev-server"; $script:_envName = "Development"; }
           "2" { $targetServer = "itr-server"; $script:_envName = "Iteration"; }
           "3" { $targetServer = "tst-server"; $script:_envName = "Test"; }
           "4" { $targetServer = "pre-server"; $script:_envName = "Pre-release"; }
           "5" { $targetServer = "prod-server"; $script:_envName = "Production"; }
           "6" { $targetServer = "post-server"; $script:_envName = "Post-release"; }
           "C" { Write-Output "Cancel"; return; }           
        }
    
    } While (!$script:_envName -and !$targetServer)
    
    if ($targetServer -and $script:_envName)
    {    
        $choiceYes = New-Object System.Management.Automation.Host.ChoiceDescription "&amp;Yes", "Answer Yes."
    	$choiceNo = New-Object System.Management.Automation.Host.ChoiceDescription "&amp;No", "Answer No."
    	$options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceYes, $choiceNo)
    	$result = $host.ui.PromptForChoice("Confirm deployment target", `
			"Deploy My App to $_envName ($targetServer)?", $options, 0)
        
        if ($result -eq 0)
        {
            Publish-ToServer $targetServer -skipBackup:$script:skipBackup
        }
        else 
        {
            Write-Output "Deployment cancelled"
        }
    }
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-3-appdeploy/DeployMenu.jpg"&gt;
&lt;br/&gt;&lt;br/&gt;

&lt;h3&gt;Server Deployment Driver Function&lt;/h3&gt; 
This function sets some script level variables such as common folder locations, and calls functions to 
perform cleanup, backup any existing installation 
(&lt;a href="http://www.geoffhudik.com/tech/2012/4/6/compression-experiments-in-the-build-and-deployment-process.html"&gt;see this post&lt;/a&gt;), and deploy ClickOnce, satellite and service files.
&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
function Publish-ToServer (
    [string] $targetServer = $(throw "targetServer is required"),
    [switch] $skipBackup = $false )
{
    $startTime = [DateTime]::Now
    Write-Output "`n"
    Set-Activity "Deploying to $targetServer"
    Write-Log "Beginning deploy to target server $targetServer"
    $script:_targetServer = $targetServer
    $script:_targetRoot = "\\$targetServer\Share$"
    $script:_targetClickOnceDir = "\\$targetServer\Share$\MyApp\ClickOnce"
    $script:_targetSatelliteDir = "\\$targetServer\Share$\MyApp\Satellite"
    $script:_targetServicesDir = "\\$targetServer\Share$\MyApp\Services"
    
    Clear-OldFiles
    if (!$skipBackup) { Backup-ExistingInstall }
    
    Publish-ClickOnceFiles
    Publish-SatelliteFiles
    Publish-Service
    
    $script:_deployTS = [DateTime]::Now - $startTime   
    Write-Log ("Published to $targetServer in {0:N0} minute(s) and {1} second(s)`n" `
		-f [math]::floor($_deployTS.TotalMinutes), $_deployTS.Seconds)
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Cleanup Before Backup&lt;/h3&gt;
Before backing up the existing target folders, the script does some cleanup to remove some files and folders that are not desirable for backup. 
Mostly this cleanup is around removing old ClickOnce folders; because of the version naming convention there will quickly be a large number 
of folders.
&lt;br/&gt;&lt;br/&gt;

&lt;img src="http://www.geoffhudik.com/storage/blogs/tech/2012/05/build-automation-3-appdeploy/ClickOnceFolders.jpg"&gt;
&lt;br/&gt;&lt;br/&gt;

The below function will look for folders ordered by modified time descending and select the first one to determine the most recent ClickOnce 
folder (you could argue some holes with that logic). It will then keep only that one, getting rid of all the others. In this way it will only be backing up the last ClickOnce folder. 
If you are starting fresh this is not a problem per se but in my case there were many existing versions out there already.
&lt;br/&gt;

&lt;pre class="brush:powershell;highlight: [48,49,50,60]"&gt;
function Clear-OldFiles
{
    Set-Activity "Cleaning up old ClickOnce versions"
	Clear-OldClickOnceAppFiles $_targetClickOnceDir	
	# additional cleanup here removed...
}

function Clear-OldClickOnceAppFiles ($rootDir)
{
    if (!(Test-Path $rootDir))
    {
        Write-Log "$rootDir doesn't exist; nothing to do"
        return;
    }

    # exclude on subdirectory names doesn't seem to work
	# so we'll just rename dir, grab most recent child, copy over    
    # http://tinyurl.com/copy-item-exclude
    $appFilesDir = (join-path $rootDir "\Application Files")
    
    if (!(Test-Path $appFilesDir))
    {
        Write-Log "Didn't find Application Files folder at $appFilesDir; nothing to do"
        return
    }
    
    Write-Log ("Removing old ClickOnce app files beyond one version back from {0}" `
		-f $appFilesDir)
    $folders = @(Get-ChildItem -Path $appFilesDir -recurse `
		| Where-Object {$_.PSIsContainer})
        
    if ($folders.Length -le 1)
    {
        Write-Log ("No old versions to remove (folder count was {0}); exiting" `
			-f ($folders.Length))
        return
    }
    else 
    {
        Write-Log ("Found {0} ClickOnce version folder(s)" -f ($folders.Length))
    }
    
    Write-Log "Renaming $appFilesDir to Application Files Temp"
    Rename-Item $appFilesDir "Application Files Temp"    
    
    Write-Log "Determining most recent ClickOnce app files subfolder"
    $appFilesTempDir = (join-path $rootDir "\Application Files Temp")    
    $mostRecentAppFilesDir = Get-ChildItem -Path $appFilesTempDir `
		| Where-Object {$_.PSIsContainer} `
		| Sort-Object LastWriteTime -Descending | Select-Object -First 1
    Write-Log "Most recent app files dir is $mostRecentAppFilesDir"        
    
    New-Item $appFilesDir -type directory
    Write-Log ("Copying {0} to $appFilesDir" -f ($mostRecentAppFilesDir.FullName))
    copy-item -recurse $mostRecentAppFilesDir.FullName $appFilesDir    
    
    $folderCount = ((Get-ChildItem -Path $appFilesTempDir `
		| Where-Object {$_.PSIsContainer})).Length
    Write-Log ("Removing {0} old version(s)" -f ($folderCount-1))
    Remove-Dir $appFilesTempDir
    
    Write-Log "Old ClickOnce app files removed"
}
&lt;/pre&gt;

&lt;br/&gt;
&lt;h3&gt;Deploying Satellite Files&lt;/h3&gt;

Because some of the client satellite files are loaded directly off the network by this app, some files will be locked if users are running the app. 
This function first calls a helper function that uses PowerShell remoting to disconnect file sessions to a remote server. I'm not a fan of loading assemblies 
directly off a network share; I think syncing the client files with the server and then loading on the client is better but it is what it is.
&lt;br/&gt;&lt;br/&gt;

The function then deletes all the files in the target Satellite directory minus the config file. At the moment the deployment script is not updating 
configuration files though that was the plan in the beginning.
&lt;br/&gt;&lt;br/&gt;

There is also a hackish sleep call between deleting files and copying as there were sporadic access denied errors of a locking / timing nature. 
Finally a call is made to a copy helper function that has support for additional attempts on error as well as outputting the results of what was copied.
&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
function Publish-SatelliteFiles
{
    Set-Activity "Deploying Staging files"    
    Disconnect-FileSessions $_targetServer   
           
    # exclude deleting config - currently configured by hand until it can be automated
    Remove-RootFilesInDir $_targetSatelliteDir -exclude "MyApp.Client.exe.config"
    
    "Pausing between delete and copy"
    Start-Sleep -s 3
    
    Copy-Files -from "$_packagePath\Satellite\**" -dest $_targetSatelliteDir `
		-recurse -attempts 2
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Deploying ClickOnce Files&lt;/h3&gt;

The ClickOnce deployment is similiar.
&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
function Publish-ClickOnceFiles
{
    Set-Activity "Deploying ClickOnce files"
    Disconnect-FileSessions $_targetServer
    
    Remove-Dir (join-path $_targetClickOnceDir "Application Files")    
    Remove-RootFilesInDir $_targetClickOnceDir
    
    "Pausing between delete and copy"
    Start-Sleep -s 3
    
    Copy-Files -from "$_packagePath\ClickOnce\**" -dest $_targetClickOnceDir `
		-recurse -attempts 2
    Copy-Files -from "$_packagePath\BuildInfo.csv" -dest $_targetClickOnceDir
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Deploying Service Files&lt;/h3&gt;

I posted &lt;a href="http://www.geoffhudik.com/tech/2012/3/22/install-a-windows-service-remotely-with-powershell.html"&gt;Install a Windows Service Remotely with PowerShell&lt;/a&gt; a while 
back so refer to it for additional details such as the functions Uninstall-Service, Install-Service and Start-Service.
&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
function Publish-Service
{
    Set-Activity "Deploying Service files"
    $serviceName = "MyAppDataService"
    
    Write-Log "Stopping, uninstalling service $serviceName on $_targetServer"
    Uninstall-Service $serviceName $_targetServer
    "Pausing to ensure files are not locked during delete..."
    Start-Sleep -s 5 # Yeah I know, don't beat me up over this
    
    Remove-RootFilesInDir $_targetServicesDir    
   
    Copy-Files "$_packagePath\Services\**" $_targetServicesDir -recurse
    New-RestartServiceCommand
    
    Install-Service `
    -ServiceName $serviceName `
    -TargetServer $_targetServer `
    -DisplayName "MyApp Data Service" `
    -PhysicalPath "D:\Apps\MyApp\Services\MyApp.DataService.exe" `
    -Username "NT AUTHORITY\NetworkService" `
    -Description "Provides remote TCP/IP communication between the MyApp client application and the database tier."
        
    Start-Service $serviceName $_targetServer
}
&lt;/pre&gt;

The New-RestartServiceCommand function creates a batch file that restarts the Windows service. On each target server 
there is a scheduled task that invokes this batch file daily late at night. Originally that was done to help ensure any 
memory and network resources were properly released in the event of unexpected issues. The scheduled task is currently 
a one-time manual setup process though creating it could certainly be automated as well.
&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
function New-RestartServiceCommand
{    
    $file = (join-path $_targetServicesDir "MyAppServiceRestart.bat")
    "Creating $file for the nightly scheduled task to restart the service"
    if (Test-Path $file) { Remove-Item -Force $file }
    Add-Content $file "REM This is for automatically restarting the MyApp data service via a nightly scheduled task"
    Add-Content $file "net stop `"MyApp Data Service`""
    Add-Content $file "net start `"MyApp Data Service`""
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Some Common Helper Functions&lt;/h3&gt;
Some of the common helper functions used are below (functions detailed in other referenced posts are omitted).
&lt;br/&gt;

&lt;h4&gt;File I/O&lt;/h4&gt;

&lt;pre class="brush:powershell;"&gt;
function Copy-Files([string]$from, [string]$dest, [switch]$recurse, [int]$attempts = 1)
{
    "Copying $from to $dest with recurse $recurse" 
    $result = $null
    for ($i=1; $i -le $attempts; $i++)
    {
        try 
        {
            $result = Copy-Item -Recurse:$recurse -Force -PassThru $from `
				-Destination $dest
            break
        }
        catch [System.Exception]
        {
            if ($i -lt $attempts)
            {
                ("Copy failed: '{0}'. Pausing. Max attempts: {1}, Attempts: {2}" `
					-f $_.Exception.Message, $attempts, $i)
                Start-Sleep -s 3
            }
            else { throw }
        }
    }
    
    if ($result) {foreach ($i in $result) {("Copied {0}" -f $i.FullName)}}
}

function Remove-Dir([string]$path)
{
    if (Test-Path $path)
    {
        Write-Output "Removing folder '$path'"
        Remove-Item -recurse -force $path
    }
}

function Remove-RootFilesInDir([string]$path, [string]$pattern = "*.*", `
	[string[]]$exclude)
{
    $deleteWhat = (join-path $path $pattern)
    "Removing $deleteWhat"
    remove-item -Force $deleteWhat -Exclude $exclude
}
&lt;/pre&gt;

&lt;h4&gt;PowerShell Host Related&lt;/h4&gt;

&lt;pre class="brush:powershell;"&gt;
function Get-CanTranscribe
{
    # probably not the best way to answer this question but will at least rule out ISE
    return (!(Get-IsHostISE))
}

function Get-IsHostISE
{
    return ((Get-Host).Name -eq "Windows PowerShell ISE Host")
}

function get-scriptdirectory 
{ 
    if (Test-Path variable:\hostinvocation) 
    {
        $FullPath=$hostinvocation.MyCommand.Path
    }
    else 
    {
        $FullPath=(get-variable myinvocation -scope script).value.Mycommand.Definition
    }
    if (Test-Path $FullPath)
    { 
        return (Split-Path $FullPath) 
    }
    else
    { 
        $FullPath=(Get-Location).path
        Write-Warning ("Get-ScriptDirectory: Powershell Host &lt;" + $Host.name `
			+ "&gt; may not be compatible with this function, the current directory &lt;" `
			+ $FullPath + "&gt; will be used.")
        return $FullPath
    }
}
&lt;/pre&gt;

&lt;h4&gt;Miscellaneous&lt;/h4&gt;

&lt;pre class="brush:powershell;"&gt;
# note that net session \\computername /delete won't work w/remote deployment
#     NET SESSION displays incoming connections only.
#     In other words it must be run on the machine that is acting as the server.
# Enabling PS Remoting: http://technet.microsoft.com/en-us/magazine/ff700227.aspx
# 1) On target server ensure that winrm service is running
#    In PowerShell: get-service winrm
#
# 2) Enable PS remoting on the target server
#    Enable-PSRemoting –force
function Disconnect-FileSessions ([string]$server = $(throw "server is required"))
{
    "Disconnecting file sessions to $server"    
    $S=NEW-PSSESSION –computername $server
    INVOKE-COMMAND –Session $s –scriptblock { (NET SESSION /delete /y) }
    REMOVE-PSSESSION $S
}

function Invoke-InternetExplorer([string]$url)
{
    $IE=new-object -com internetexplorer.application
    $IE.navigate2($url)
    $IE.visible=$true
}

function Send-Email($from, $to, $subject, $body, $smtpServer = "mail.domain.com", `
	$attachment = $null, $isHtmlBody = $true)
{
    $smtp = new-object Net.Mail.SmtpClient($smtpServer)    
    $msg = new-object Net.Mail.MailMessage
    $msg.From = $from
    $msg.To.Add($to)
    $msg.Subject = $subject
    $msg.IsBodyHtml = $isHtmlBody
    $msg.Body = $body
    
    if ($attachment)
    {
        $att = new-object Net.Mail.Attachment($attachment)
        $msg.Attachments.Add($att)
    }
    
    $smtp.Send($msg)
    $att.Dispose | out-null
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;Other Functions&lt;/h3&gt;
Other functions that are not as generic/common in nature but are included in the main script follow.

&lt;h4&gt;Retrieving Build Information&lt;/h4&gt;

The build info file discussed previously in this series is packaged in the same directory as the script 
and read for displaying in the PowerShell console and in sending a deployment notification.&lt;br/&gt;

&lt;pre class="brush:powershell;"&gt;
function Get-BuildInfo
{    
	$buildInfoFile = (join-path (scriptdirectory) "BuildInfo.csv")
	return Import-Csv $buildInfoFile
}
&lt;/pre&gt;

A sample of this file:&lt;br/&gt;

&lt;pre&gt;
"AppVersion","FileVersion","BuiltOn","ClickOnceRevision","ClickOncePublishVersion"
"3.3.0.17","2012.05.07.1008","5/7/2012 10:08:53 AM","117","3.3.0.117"
&lt;/pre&gt;

&lt;h4&gt;Logging, Diagnostics and Progress&lt;/h4&gt;

&lt;pre class="brush:powershell;"&gt;
function Set-Activity([string]$activity)
{
    $script:_activity = $activity
    Write-Log "Current Activity: $_activity"
}

function Write-Log ([string]$message)
{
    write-output $message
    write-progress -activity $_activity -status $message
}
&lt;/pre&gt;

&lt;h4&gt;Backup Functions&lt;/h4&gt;
Functions such as Backup-ExistingInstall, Backup-Dir, and Compress-Files are included in 
&lt;a href="http://www.geoffhudik.com/tech/2012/4/6/compression-experiments-in-the-build-and-deployment-process.html"&gt;Compression Experiments In the Build and Deployment Process&lt;/a&gt;.

&lt;h4&gt;Deployment Email&lt;/h4&gt;

&lt;pre class="brush:powershell;"&gt;
function Send-Notification
{
    $buildInfo = Get-BuildInfo
    $env = $script:_envName
    $appVer = $buildInfo.AppVersion
    $fileVer = $buildInfo.FileVersion
    $publishVer = $buildInfo.ClickOncePublishVersion
    $builtAt = $buildInfo.BuiltOn
    
    $deployText = "deployed"
    if (!$_successfulDeploy) {$deployText = "deployment failed"}
    
    $subject = "MyApp v {0} {1} to {2} ({3})" -f $buildInfo.AppVersion, `
		$deployText, $script:_envName, $_targetServer
    $deployedFrom = [Environment]::MachineName
    $deployedBy = [Environment]::UserName
    $deployedAt = [DateTime]::Now.ToString("G")
    
    $successOrFail = ""
    
    if ($_successfulDeploy) { $successOrFail = "Successful: True" }
    else 
    {
        $successOrFail = "Successful: False`n`n" + "Error: " + $_scriptErrorMessage + "`n"
    }
    
    $deployTime = ""
    if ($_deployTS)
    {
        $deployTime = ("Deployment completed in {0:N0} minute(s) and {1} second(s)`n" `
			-f [math]::floor($_deployTS.TotalMinutes), $_deployTS.Seconds)
    }
    
    $br = "&lt;br/&gt;"
    $body = @"
MyApp deployment results follow.$br$br

$successOrFail$br$br

Environment: $env ($_targetServer)$br
Run Webpage: $_clickOnceUrl$br$br

App Version: $appVer$br
Publish Version: $publishVer$br
File Version: $fileVer$br$br

Built At: $builtAt$br$br

Deployed from $deployedFrom by $deployedBy. Deployment details are attached.$br
$deployTime$br$br
This message was sent by an automated process.
"@
    
    $to = "PRODUCT_SUPPORT@domain.com"
    Send-Email -from "$deployedBy@domain.com" -to $to `
        -subject $subject -body $body -attachment $_transcriptFile
}
&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;In Conclusion&lt;/h3&gt;

That wraps up the initial version of this deployment script. Potential changes going forward are:
&lt;ul&gt;
&lt;li&gt;Adjustments to re-integrate this with CI (build server is currently down)&lt;/li&gt;
&lt;li&gt;Updating app config files from the script&lt;/li&gt;
&lt;li&gt;Automating creation of a scheduled task to restart the service&lt;/li&gt;
&lt;li&gt;Script refactoring and cleanup&lt;/li&gt;
&lt;li&gt;Kicking off database and/or report script deployments from app script&lt;/li&gt;
&lt;li&gt;"Down for maintenance" page for users&lt;/li&gt;
&lt;li&gt;Dependent apps - another web app uses some business and data components of this app and it should be updated 
when this app is deployed&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

Potential future posts may be added soon on database and report deployment scripts and CI setup.&lt;img src="http://feeds.feedburner.com/~r/thnk2wn/~4/nm9xej073sY" height="1" width="1"/&gt;</description><wfw:commentRss>http://www.geoffhudik.com/tech/rss-comments-entry-16198602.xml</wfw:commentRss><feedburner:origLink>http://www.geoffhudik.com/tech/2012/5/9/build-automation-part-3-app-deployment-script.html</feedburner:origLink></item></channel></rss>
