Second Stanza

December 27, 2008

Unit Testing Workflow Activities

Filed under: .NET Development, WorkFlow — Tags: , — dfbaskin @ 10:30 pm

I’ve been using a technique similar to the code below to unit test custom workflow activities.


using System;
using System.Collections.Generic;
using System.Workflow.Runtime;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime.Hosting;
using System.Workflow.Runtime.Tracking;
using System.Configuration;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TrackingError
{
    public class TestActivity : Activity
    {
        public static readonly DependencyProperty MessageProperty = DependencyProperty.Register( "Message", typeof( String ), typeof( TestActivity ) );

        public string Message
        {
            get { return (String)GetValue( MessageProperty ); }
            set { SetValue( MessageProperty, value ); }
        }

        protected override ActivityExecutionStatus Execute( ActivityExecutionContext executionContext )
        {
            Message = "Success";
            return ActivityExecutionStatus.Closed;
        }
    }

    [TestClass]
    public class ShowTrackingError
    {
        public ShowTrackingError()
        {
        }

        private TestContext testContextInstance;

        public TestContext TestContext
        {
            get { return testContextInstance; }
            set { testContextInstance = value; }
        }

        private static string ConnectionString
        {
            get { return ConfigurationManager.ConnectionStrings["WorkFlowDB"].ConnectionString; }
        }

        private WorkflowRuntime workflowRuntime;
        private AutoResetEvent resetEvent;
        private Dictionary<String, Object> outputArgs;
        private bool completedWorkflow;

        [TestInitialize]
        public void CreateWorkFlowEnvironment()
        {
            bool startedWorkflow = false;
            try
            {
                resetEvent = new AutoResetEvent( false );
                outputArgs = null;
                completedWorkflow = false;

                workflowRuntime = new WorkflowRuntime();

                SharedConnectionWorkflowCommitWorkBatchService sharedConnService = new SharedConnectionWorkflowCommitWorkBatchService( ConnectionString );
                workflowRuntime.AddService( sharedConnService );

                SqlWorkflowPersistenceService persistService = new SqlWorkflowPersistenceService( ConnectionString );
                workflowRuntime.AddService( persistService );

                SqlTrackingService trackingService = new SqlTrackingService( ConnectionString );
                workflowRuntime.AddService( trackingService );

                workflowRuntime.WorkflowCompleted += delegate( object sender, WorkflowCompletedEventArgs e ) {
                    outputArgs = e.OutputParameters;
                    completedWorkflow = true;
                    resetEvent.Set();
                };
                workflowRuntime.WorkflowTerminated += delegate( object sender, WorkflowTerminatedEventArgs e ) {
                    resetEvent.Set();
                };
                workflowRuntime.WorkflowAborted += delegate( object sender, WorkflowEventArgs e ) {
                    resetEvent.Set();
                };

                workflowRuntime.StartRuntime();
                startedWorkflow = true;
            }
            finally
            {
                if( !startedWorkflow )
                    if( workflowRuntime != null )
                    {
                        workflowRuntime.Dispose();
                        workflowRuntime = null;
                    }
            }
        }

        [TestCleanup]
        public void ShutDownWorkFlowEnvironment()
        {
            if( workflowRuntime != null )
            {
                try
                {
                    workflowRuntime.StopRuntime();
                    workflowRuntime.Dispose();
                }
                finally
                {
                    workflowRuntime = null;
                }
            }
        }

        [TestMethod]
        public void UnitTestWorkflowActivity()
        {
            Assert.IsNotNull( workflowRuntime );

            WorkflowInstance wfInst = workflowRuntime.CreateWorkflow( typeof( TestActivity ) );
            Assert.IsNotNull( wfInst );

            wfInst.Start();

            resetEvent.WaitOne( 5000 );

            Assert.IsTrue( completedWorkflow );
            Assert.IsNotNull( outputArgs );
            Assert.AreEqual( "Success", (String)outputArgs["Message"] );
        }
    }
}

(Note that common code like the methods marked with the TestInitialize and TestCleanup attributes are actually in a shared based class, but included in the test class here for our discussion.)

However, the above unit test does not work. The activity is aborted so the completedWorkflow value is never set to true. This behavior is apparent both in this unit test as well as in an application that runs the single activity. Perhaps I’ve misunderstood something, but I believe this should work. The activity should not abort.

It appears that the SqlTrackingService class expects a CompositeActivity, not a Activity-derived class since the error written to the log is:

System.Workflow.Runtime.Hosting Error: 0 : SharedConnectionWorkflowCommitWorkBatchService caught
      exception from commitWorkBatchCallback: System.InvalidCastException: Unable to cast object
      of type 'TrackingError.TestActivity' to type 'System.Workflow.ComponentModel.CompositeActivity'.
   at System.Workflow.Runtime.Tracking.SqlTrackingService.SqlTrackingChannel.InsertWorkflow(DbCommand command, Guid workflowInstanceId, Type workflowType, Activity rootActivity)
   at System.Workflow.Runtime.Tracking.SqlTrackingService.SqlTrackingChannel.ExecuteInsertWorkflowInstance(DbCommand command)
   at System.Workflow.Runtime.Tracking.SqlTrackingService.SqlTrackingChannel.Commit(Transaction transaction, ICollection items)
   at System.Workflow.Runtime.WorkBatch.PendingWorkCollection.Commit(Transaction transaction)
   at System.Workflow.Runtime.WorkBatch.Commit(Transaction transaction)
   at System.Workflow.Runtime.VolatileResourceManager.Commit()
   at System.Workflow.Runtime.WorkflowExecutor.DoResourceManagerCommit()
   at System.Workflow.Runtime.Hosting.SharedConnectionWorkflowCommitWorkBatchService.CommitWorkBatch(CommitWorkBatchCallback commitWorkBatchCallback)

Several work-arounds make this unit test pass:

  • Instead of derving the custom activity from the Activity class, it can be derived from CompositeActivity.
  • The activity can be wrapped in some other activity, such as the TransactionScopeActivity. However, the properties used to test the results of the workflow must be propagated out to the wrapping activity.
  • The SqlTrackingService workflow runtime service can be removed.

I’ve opened a Microsoft Connect feedback item here. A Microsoft forums post is here.

November 12, 2008

Windows WorkFlow Activity Data

Filed under: .NET Development, WorkFlow — Tags: — dfbaskin @ 12:54 am

One of the first questions I had about Windows Workflow (in .NET 3.0) was how data was moved around between workflow activities. Activities, of course, are the meat of a workflow process in that they define the actual work a workflow will address. So if one activity picks up a useful data value that some other activity will later need, what technique should be used to pass this data between these activities?

Windows Workflow Foundation provides several features that aid in accomplishing this task:

  • Activity Properties – each activity may publish it’s own bindable set of properties that can be used as input and output parameters for the activity.
  • Workflow Properties – the workflow itself can publish it’s own set of properties.
  • Declarative Binding – using dependency properties, properties from one activity can be automatically bound to properties on other activities.
  • Workflow Navigation – From within an activity, you can navigate the hierarchy of a workflow process in order to find specific instances of other activities that may have information you need.

These features give you multiple options for moving activity data around within a workflow. However, you must be careful in your implementation since, for example, workflows allow parallel processing and you must be sure to get to the activity instance that you really want. MSDN published a very useful article about these and other related issues here.

To me, the best option is to use declarative binding, for two reasons. The first reason is that declarative binding eliminates any ambiguity about which activity instance you are binding to.

The second reason is to abstract your activity objects from your workflow. When navigating the hierarchy of the workflow, you are making assumptions about how the workflow is structured. Using bindings on properties allows the activity to be more stand-alone and more likely to be reusable. And of course, it allows easier unit testing of the activity.

November 2, 2008

Powershell Script for Shrinking MP3 Files

Filed under: Powershell — Tags: — dfbaskin @ 10:45 pm

I needed to shrink a bunch of lectures in MP3 format so that they would fit on a DVD. I compiled SoX to include MP3 support and then used the following Powershell script to compress the files:


$srcPaths = 'E:\MP3s\Year One', 'E:\MP3s\Year Two';
$dstPath = 'E:\MP3s\DVD';
$pathPrefixLen = 8;

Write-Host;
Write-Host '--------------------------------------------------------------------';
Write-Host 'MP3 Audio Update Utility';
Write-Host '--------------------------------------------------------------------';

foreach( $srcPath in $srcPaths )
{
    Write-Host 'Source: ' $srcPath;
}
Write-Host '  Dest: ' $dstPath;
Write-Host;

foreach( $srcPath in $srcPaths )
{
    Write-Host 'Reading ' $srcPath;
    $fileList = Get-ChildItem -path $srcPath -Recurse *.mp3

    foreach( $srcFile in $fileList )
    {
        $srcFullName = $srcFile.FullName;
        $dstFullName = [System.IO.Path]::Combine( $dstPath, $srcFile.FullName.Substring( $pathPrefixLen ) );
        $tmpFullName = [Regex]::Replace( $dstFullName, '\.mp3$', '.__temp__.mp3', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase );

        if( ! [System.IO.File]::Exists( $dstFullName ) )
        {
            Write-Host $dstFullName;
    
            & 'sox.exe' $srcFullName '-r' '12k' '-c' '1' '-S' $tmpFullName 'dither' | out-null
            if( $lastExitCode -ne 0 )
            {
                Write-Host 'Stopped on error.'
                exit;
            }
    
            [System.IO.File]::Move( $tmpFullName, $dstFullName );
        }
    }
}

A file included in the SoX source code (‘INSTALL’) has instructions for building SoX with MP3 support under Windows. Here are the requirements (excerpted from this file):

Appendix: How To Compile SoX with MP3 Support on Win32
------------------------------------------------------

The following text, untested by the SoX team, was originally written by `Enter
AG' and released to the public domain.

= How To Compile Sox with MP3 Support =

== Requirements ==

The following requirements have been tested. Nevertheless, other versions may
work as well.

o Microsoft Windows (Win32, Win2k, Win2003).

o CMake (Cross Platform Make) version 2.4
  available on [http://www.cmake.org] 

o Microsoft Visual Stuio 2008 (also earlier Versions)

o SoX source distribution version 14.0.1
  available on [http://sox.sourceforge.net] 

o LAME source distribution version 3.97
  available on [http://lame.sourceforge.net] 

o MAD source distribution version 0.15.1b
  available on [http://www.underbit.com/products/mad/] 

October 23, 2008

SQL Server 2005 Installation Problem

Filed under: SQL Server — Tags: — dfbaskin @ 11:36 am

For some reason, when running the SQL Server 2005 Upgrade Advisor (either the stand-alone version or the version that runs within the installation), I was getting the following error:

SQL BPA command line has encountered a problem and needs to close.

The problem, it turns out, is that BPACMD.EXE was not able to load BPAClient.dll for some reason. The work around was to copy the BPAClient.dll to a location that allowed BPACMD.EXE to load it. The file resides in a “bin” directory below a “BPA” directory. I simply created a “BPA\BPAClient” directory and copied BPAClient.dll into this directory. (The reason this works is that the .NET runtime will search this directory when trying to load BPAClient.dll.)

Note that you must copy this file after the SQL Server support files have been installed (usually to the Program Files\Microsoft SQL Server\90\Setup Bootstrap\BPA directory).

A forum discussion of this topic is here.

I am not sure the reason this intervention is required, but I suspect it has something to do with the installation of the .NET 3.5 SP1 runtime.

October 19, 2008

Using the Report Viewer Component with .RDL Files

Filed under: .NET Examples, SQL Server — Tags: , — dfbaskin @ 9:30 pm

You can use Visual Studio to create Report Definition Language (.RDL) files that can be used within SQL Server Reporting Services. The editor for .RDL files allows you to put together the SQL Query, design the report layout, and preview the report all within Visual Studio.

Microsoft has also provided a Report Viewer control that allows you to display reports within your application. This control is not tied to SQL Server since you can use your own data sources as input to the report (though it can also display reports that originate from SQL Server Reporting Services). Reports are defined using the .RDLC format, which is the same format as .RDL files, but ignores certains aspects of the definitions (more details here).

Of course, since a data source can be defined within a Visual Studio project, you lose the ability to preview your report easily. Generally, you have to compile and run the application in order to view your report and verify that it is correct. Being able to quickly view your report output was a nice feature of working with the SQL Server Reporting Services version of the report editor.

However, since the .RDL and .RDLC formats are the same, you can use an .RDL specification as input to the Report Viewer control. All you need to do is to implement some code that runs the query embedded in the .RDL file. By doing so, you may use the Visual Studio 2005 version of the Reporting Services editor (with its handy preview mode) but still use the Report Viewer component within your application.

Here’s source code that shows this technique:



using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using System.Windows.Forms;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using Microsoft.Reporting.WinForms;

namespace ReportView
{
    static class Program
    {
        [STAThread]
        static void Main( string[] argList )
        {
            try
            {
                if( argList.Length != 1 )
                    throw new ArgumentException( "Must specify name of .RDL file." );
                string rdlFileName = argList[0];
                if( !File.Exists( rdlFileName ) )
                    throw new FileNotFoundException( "Could not find .RDL file." );

                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault( false );
                Application.Run( new ReportViewForm( rdlFileName ) );
            }
            catch( Exception ex )
            {
                StringBuilder errMsg = new StringBuilder();
                errMsg.Append( "Application Error generated:\n" );
                for( Exception x = ex; x != null; x = x.InnerException )
                    errMsg.AppendFormat( "   {0} - {1}\n", x.GetType().FullName, x.Message );
                errMsg.AppendFormat( "Stack Trace:\n{0}", ex.StackTrace );
                MessageBox.Show( errMsg.ToString(), "Application Error", MessageBoxButtons.OK, MessageBoxIcon.Error );
            }
        }
    }

    class ReportViewForm : Form
    {
        private Microsoft.Reporting.WinForms.ReportViewer rptView;
        private string reportFileName;

        public ReportViewForm( string fileName )
        {
            reportFileName = fileName;
            rptView = new Microsoft.Reporting.WinForms.ReportViewer();

            SuspendLayout();

            rptView.Dock = System.Windows.Forms.DockStyle.Fill;
            rptView.Name = "rptView";
            rptView.TabIndex = 0;
            Controls.Add( rptView );

            const int border = 20;
            System.Drawing.Size winSize = Screen.PrimaryScreen.WorkingArea.Size;
            Location = new System.Drawing.Point( border, border );
            Size = new System.Drawing.Size( winSize.Width - ( 2 * border ), winSize.Height - ( 2 * border ) );
            StartPosition = FormStartPosition.Manual;
            Name = "ReportViewForm";
            Text = Path.GetFileName( reportFileName );

            ResumeLayout( false );
        }

        const string ReportDefinitionNS = "http://schemas.microsoft.com/sqlserver/reporting/2005/01/reportdefinition";
        const string ReportDesignerNS = "http://schemas.microsoft.com/SQLServer/reporting/reportdesigner";

        protected override void OnLoad( EventArgs e )
        {
            base.OnLoad( e );

            rptView.ProcessingMode = ProcessingMode.Local;
            using( Stream readStream = new FileStream( reportFileName, FileMode.Open, FileAccess.Read, FileShare.Read ) )
                rptView.LocalReport.LoadReportDefinition( readStream );

            XPathDocument xpDoc;
            using( Stream readStream = new FileStream( reportFileName, FileMode.Open, FileAccess.Read, FileShare.Read ) )
                xpDoc = new XPathDocument( readStream );

            ConnectionStringSettings connSett = ConfigurationManager.ConnectionStrings["Default"];
            using( SqlConnection sqlConn = new SqlConnection( connSett.ConnectionString ) )
            {
                sqlConn.Open();

                XPathNavigator xpNav = xpDoc.CreateNavigator();
                XPathExpression xpe = xpNav.Compile( "/r:Report/r:DataSets/r:DataSet" );
                XmlNamespaceManager nsMgr = new XmlNamespaceManager( xpNav.NameTable );
                nsMgr.AddNamespace( "r", ReportDefinitionNS );
                nsMgr.AddNamespace( "rd", ReportDesignerNS );
                xpe.SetContext( nsMgr );
                foreach( XPathNavigator xpNode in xpNav.Select( xpe ) )
                {
                    string dsName = Convert.ToString( xpNode.Evaluate( "string( @Name )", nsMgr ) );
                    string cmdTxt = Convert.ToString( xpNode.Evaluate( "string( r:Query/r:CommandText )", nsMgr ) );
                    if( String.IsNullOrEmpty( dsName ) || String.IsNullOrEmpty( cmdTxt ) )
                        throw new Exception( "Could not obtain query for report definition." );

                    SqlCommand sqlCmd = sqlConn.CreateCommand();
                    sqlCmd.CommandType = CommandType.Text;
                    sqlCmd.CommandText = cmdTxt;

                    DataTable dataTable = new DataTable();
                    SqlDataAdapter sqlAdapter = new SqlDataAdapter( sqlCmd );
                    sqlAdapter.Fill( dataTable );
                    rptView.LocalReport.DataSources.Add( new ReportDataSource( dsName, dataTable ) );
                }
            }

            rptView.RefreshReport();
        }
    }
}


October 13, 2008

Reset Windows TCP/IP Stack

Filed under: Windows OS — Tags: , — dfbaskin @ 3:49 pm

A Norton 360 installation hosed the TCP/IP stack on a Windows XP machine, disabling any network activity. After some research, I found that running the following commands and rebooting the machine fixed the problem:

netsh int ip reset resetlog.txt
netsh winsock reset

August 26, 2008

VMWare Slow Network Share Performance

Filed under: Windows OS — Tags: — dfbaskin @ 11:30 pm

I was experiencing very slow network share performance from a VMWare virtual machine to a share published on the host machine. I spent over two months trying to get this issue resolved with VMWare technical support.

After finally getting the issue escalated, one evening’s work got the issue resolved. It was as simple as just disabling the TCPIP task offload for the network adapter, using the DisableTaskOffload registry setting on the host computer.

Microsoft even posted a knowledgebase article about it, albeit for Virtual Server, not VMWare.

August 23, 2008

Object Initializers and Read Only Properties

Filed under: .NET Development — Tags: — dfbaskin @ 8:07 pm

Auto-implemented properties save you some coding as well as using using object initializers. Here is an example of the syntax (from the previous link):

private class Cat
{
    // Auto-implemented properties
    public int Age { get; set; }
    public string Name { get; set; }
}

static void MethodA()
{
    // Object initializer
    Cat cat = new Cat { Age = 10, Name = "Sylvester" };
}

What about when you want to create immutable classes? Using the “private set;” syntax on an auto-implemented property does not allow you to use the object initializers syntax to set the read-only property. This requires creating a special constructor for this purpose.

This thread contains an interesting discussion of these issues.

ADO.NET Entity Framework ObjectContext Issues

Filed under: .NET Development, ADO.NET Entity Framework — Tags: — dfbaskin @ 8:04 am

Some questions come to mind when using the ObjectContext containing your entities.

First, what should the lifetime of an ObjectContext be? Is it like a connection object that should be opened and then closed quickly? When working with your entities, would it be more common for your entities to be attached or detached from an ObjectContext? For example, on your ASP.NET page or Windows Form, would you have an ObjectContext object and your business entities? Or are these objects manipulated in a lower business layer (as Danny Simmons suggests here)?

This thread discusses these issues.

Related to this (and mentioned in the above links) is the idea of abandoning the changes to a set of entities. At this point, there is no RejectChanges method on the ObjectContext. So how do you reset your entites back to a known state? This thread discusses options for accomplishing this.

Rick Strahl has some good related thoughts.

August 19, 2008

Handling Exceptions in ADO.NET Entity Framework

Filed under: .NET Development, ADO.NET Entity Framework — Tags: , — dfbaskin @ 10:06 pm

A new exception, UpdateException, was added to .NET 3.5 SP1 in order to support the ADO.NET Entity Framework. This exception is thrown when modifications to the object instances cannot be persisted to the data source.

Great! But how do you distinguish between the different reasons that the objects could not be persisted? In other words, how do you tell that the reason for the failure was due to a duplicate key versus a character field that was too long?

One answer is to validate the data in the business object so that you don’t get the exception in the first place. For field constraints, you could make sure your business object checks for valid field lengths before trying to store the data. For unique keys, you could ensure that the key doesn’t exist before persisting the object (doing so in a transaction, of course, to make sure another record doesn’t sneak in and use the key before you). You could, for example, customize your context object to handle these kinds of issues with techniques like this.

The other option is to interpret the exception being thrown. For example, the InnerException property of the UpdateException object might contain an SqlException object that holds details about errors reported by SQL Server. From there, you could look at the exact error codes to determine what happened. Some of us use a specific naming syntax for our SQL table constraints so that we can more easily identify constraint violations. Of course, this technique more closely associates the business objects with the data persistence layer. As we move towards more persistence-agnostic architectures, it would be nice not to tie our error handling so closely to SQL Server.

To get SQL Server 2005 list of error codes, you can use the T-SQL:

SELECT     *
FROM       sys.messages
WHERE      language_id = 1033

If I had my choice, a better solution to me would be another hook in the Entity Framework that would allow me to throw specific, strongly-typed exceptions that more clearly identify the errors being generated by the persistence layer. In other words, I would define exceptions in the business layer and the persistence layer would interpret exceptions or other errors returned from the store and throw these specific, business-layer exceptions for each case that require special handling, such as a DuplicateKeyException or InvalidDataException. The
OptimisticConcurrencyException
is an example of a move in this direction.

« Newer PostsOlder Posts »

Create a free website or blog at WordPress.com.