I am exceptionally embarrassed that I only made one (ONE!) post in all of 2011. I sincerely apologize for that. I am not going to make another empty promise that I'll post more, but perhaps I'll make a declaration of my intention to post more in 2012.
But I'm writing today to announce that I'm starting yet another adventure, not unlike my post from August 13, 2009 titled Jacksonville, Here I Come!. I have just accepted a new position at a small consulting company in St. Augustine, and I will be leaving my position at Fanatics on Friday, March 23, 2012. It's been a crazy two and a half years, which has given me experience with a highly scalable e-commerce application on two different platforms (one ASP.NET WebForms, the other ASP.NET MVC), as well as a wealth of additional experience in areas like web analytics, multivariant testing, performance testing, WCF and AppFabric, MSMQ, jQuery and AJAX, algorithms, metaprogramming, database tuning and advanced querying, and many, many more. I'm extremely grateful for the opportunity to make software solutions to keep up with the company's rapid growth and I will always look back on this position with good memories.
I'm excited about the new position, and looking forward to using this new experience (and gaining even more) working on a wide variety of projects. Catch you on the flip side!
Tuesday, March 6, 2012
Yet Another Adventure
Posted by
Paul Irwin
at
10:19 AM
0
comments
Links to this post
Tuesday, March 15, 2011
Mocking Static Methods for Unit Testing
As you probably know, unit tests are supposed to be fast, lightweight tests of your code logic. They are not supposed to do any disk I/O, network communication, or long-running CPU calculations. If you have cleanly decoupled code that doesn’t call any static methods and everything is programmed to the interface, you can mock the intensive operations very easily.
Although if you’ve tried unit testing a class that calls static methods that perform such intensive operations, you quickly realize that you must separate them out from your code so that they can be mocked. But if the methods are in a library you can’t modify, you usually end up very frustrated. There are a few easy ways to mock static methods, and you can choose which option works best for your needs.
Background
In our example, we have a class that has a single static method. Let’s pretend that this does some intensive activity that you don’t want to do while running your unit test suite. This class is in a library that you can’t modify and make non-static – we’re just looking at the code here as an example.
namespace SomeLibraryYouCantModify
{
public static class SomeStaticClass
{
public static bool SomeStaticMethod(string input)
{
// Let's pretend this method hits a database or service.
return !string.IsNullOrEmpty(input);
}
}
}
So we have one method that takes a string and returns bool. Keep this in mind.
Now, here’s an example of the static method being used. Note that we can not (by default) mock the static call – it’s a tight coupling that can not be easily broken.
namespace SomeLibraryYouCanModify
{
public class SomeUntestableClass
{
public int SomeMethod(string input)
{
var value = SomeStaticClass.SomeStaticMethod(input);
return value ? 1 : 0;
}
}
}
Below you will find a variety of options to make this class unit testable without the static call.
Option 1 – Wrapper Class
The first option which is extremely easy to do and very Inversion-of-Control-friendly is to make a wrapper class that has the exact same signature as the static methods you’re needing to call, but the only difference is that they are non-static. Each of these methods calls the static version and returns the value (if not void). Then, you can extract an interface to use for wiring up with IoC and mocking (such as with Moq).
In this example, we take the same signature as our static class above, but create a non-static wrapper class.
namespace SomeLibraryYouCanModify
{
public class StaticWrapper : IStaticWrapper
{
public bool SomeStaticMethod(string input)
{
return SomeStaticClass.SomeStaticMethod(input);
}
}
}
With our static wrapper in place, we now extract an interface:
namespace SomeLibraryYouCanModify
{
public interface IStaticWrapper
{
bool SomeStaticMethod(string input);
}
}
Perfect. Now we have a non-static wrapper class to our static methods that can be mocked. Here’s how to use it in the caller class:
namespace SomeLibraryYouCanModify
{
public class WrapperMethod
{
IStaticWrapper _wrapper;
public WrapperMethod(IStaticWrapper wrapper)
{
_wrapper = wrapper;
}
public int SomeMethod(string input)
{
var value = _wrapper.SomeStaticMethod(input);
return value ? 1 : 0;
}
}
}
This class uses constructor injection, but you can do any Inversion-of-Control style you prefer (property injection, etc.). The main point is that we now have a hook where we can insert a mocked wrapper that does not call the static method. Here’s how our test class might look:
[TestClass]
public class WrapperMethodTests
{
private class TestWrapper : IStaticWrapper
{
public bool SomeStaticMethod(string input)
{
return !string.IsNullOrEmpty(input);
}
}
[TestMethod]
public void SomeMethod_GivenNull_ShouldReturnZero()
{
var wrapper = new TestWrapper();
var wm = new WrapperMethod(wrapper);
var output = wm.SomeMethod(null);
Assert.AreEqual(0, output);
}
}
Notice how the TestWrapper is a private class just used for unit testing, and it does not do any time or I/O intensive operations. This allows our unit tests to execute quickly, providing quick turn-around when things break.
You could also use a mocking framework like Moq to create a mocked IStaticWrapper in the test example above.
Option 2 – Delegates
If the amount of static operations you’re trying to mock is small, say one or two method calls, you may want to go the delegate route. In this approach, you take optional delegates into the constructor so that by default the class calls the static method, but in your unit tests you can specify a custom delegate (such as a lambda method) that is called instead.
Here’s our caller class using the delegate method:
namespace SomeLibraryYouCanModify
{
public class DelegateMethod
{
Func<string, bool> _delegate;
public DelegateMethod()
{
_delegate = SomeStaticClass.SomeStaticMethod;
}
public DelegateMethod(Func<string, bool> method)
{
_delegate = method;
}
public int SomeMethod(string input)
{
var value = _delegate(input);
return value ? 1 : 0;
}
}
}
If you aren’t familiar with generic delegates, Func<T, TResult> used above is a delegate for a method that takes a parameter of type T and returns a result of type TResult. So now our class, by default, sets the private delegate field to the static method, but in our unit tests we can specify a custom method instead. Here’s a unit test using this approach that passes a lambda into the constructor:
[TestClass]
public class DelegateMethodTests
{
[TestMethod]
public void SomeMethod_GivenNull_ShouldReturnZero()
{
var dm = new DelegateMethod(i => !string.IsNullOrEmpty(i));
var output = dm.SomeMethod(null);
Assert.AreEqual(0, output);
}
}
We get the same result as the wrapper method – the test executes quickly, does not call the static method, and we can verify that the logic inside SomeMethod is correct.
Option 3 – Moles
Moles is an isolation framework from Microsoft that allows you to mock static method calls without rewriting any implementation code. You can even mock calls to .NET framework methods and properties, like DateTime.Now. The benefits are easy to write unit tests and less refactoring time, although at the expense of maintaining tightly-coupled code. Another drawback of Moles I’ve experienced is that it does not work well in a team environment with Team Foundation Server, because it creates proxy DLLs that are hosted in your unit test project, and multiple users checking out and checking in these DLLs becomes a pain very quickly, especially since the default checkout action is an exclusive lock. However, if you’re a sole developer working on a project, it is a worthwhile choice.
You can download Moles (with or without Pex) from Microsoft Research’s Moles Project site. Make sure that you download the correct version for your CPU (x86 vs x64).
After installing, you can right click the reference to the library you’d like to mole in your unit test project, and select Add Moles Assembly.
Now that a Moles assembly has been added to the project, you can now create a unit test and mock that call. You must make sure to import the “.Moles” version of the namespace as seen in the example below. All types in the assembly that can be mocked begin with the letter “M”, and for static methods, the method becomes a property (named with the method name followed by all of the parameter types) that is of a delegate type. Then you can “set” that property to a new delegate, such as a lambda in the example below:
using SomeLibraryYouCantModify.Moles;
namespace LibraryTestProject
{
[TestClass]
public class MoledTests
{
[TestMethod]
[HostType("Moles")]
public void SomeMethod_GivenNull_ShouldReturnZero()
{
var c = new SomeUntestableClass();
MSomeStaticClass.SomeStaticMethodString = i => !string.IsNullOrEmpty(i);
var output = c.SomeMethod(null);
Assert.AreEqual(0, output);
}
}
}
I’ve bolded the interesting lines. First, you have to import the “.Moles” namespace, as I mentioned earlier. Second, you must add the HostType(“Moles”) to any methods that are to be run in the Moles host. If a test method is not run in the Moles host, it will not be able to use the Moled type, and will let you know with an exception. Finally, the “SomeStaticClass” has a moled “MSomeStaticClass” that provides the mockable methods. The “SomeStaticMethod” method becomes “SomeStaticMethodString”, with the String being added to the name to represent the type of the first and only parameter to the method. This allows for method overloading, as different parameter types will cause different property names on the moled type. Then, the property is set to the given lambda, which is identical to the Delegates example above.
Conclusion
Out of the three methods, I end up preferring the Wrapper method the most, because it is easy to use with IoC containers and Moq. However, it is probably the most amount of work, because you have to create 2 new types, one for the wrapper class and the other for the interface. Also, it means that you have an entire type that has zero coverage, because you can not write unit tests for this wrapper class as it would call the static method.
The delegate method is great for one or two method isolation, as it’s easy to do, doesn’t need to involve the IoC container, and can be easily mocked with constructor overloads. However, it’s the least intuitive code to read, and is clunky to use for more than two method calls.
The Moles method is certainly clever and very impressive on first use. It works great for small projects with one developer, and can allow you to achieve greater code coverage very quickly. However, it is slower to start the unit test run due to the Moles host, it doesn’t help you decouple your code, and it is a pain for multiple developers working on the same project due to source control conflicts.
Obviously, the ideal situation is to have a non-static method to call. If you can get the developer of the library to do it for you, they can create a non-static version of the type/methods that are called by the static version, so that their existing static API is maintained and you get the benefit of being able to wire up the dependency with dependency injection. Clearly, though, this isn’t always possible.
Hope this helps!
Posted by
Paul Irwin
at
7:13 PM
4
comments
Links to this post
Labels: c#, dependency injection, inversion of control, mocking, moles, moq, static, unit testing
Monday, December 27, 2010
ObservableStack<T>
I had been looking around in the .NET framework for a Stack<T> equivalent of ObservableCollection<T> to use in a WPF application. Couldn’t find one, so I implemented one based on those two classes. Hope this helps!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Collections;
namespace AdventuresDotNet
{
public class ObservableStack<T> : IEnumerable<T>, ICollection, INotifyCollectionChanged, INotifyPropertyChanged
{
T[] _array;
const int _defaultCapacity = 4;
static T[] _emptyArray;
int _size;
object _syncRoot;
int _version;
static ObservableStack()
{
_emptyArray = new T[0];
}
public ObservableStack()
{
_array = _emptyArray;
_size = 0;
_version = 0;
_syncRoot = new object();
}
public ObservableStack(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException("collection");
var is2 = collection as ICollection<T>;
if (is2 != null)
{
int count = is2.Count;
_array = new T[count];
is2.CopyTo(_array, 0);
_size = count;
}
else
{
_size = 0;
_array = new T[4];
using (var enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
this.Push(enumerator.Current);
}
}
}
}
public ObservableStack(int capacity)
{
if (capacity < 0)
throw new ArgumentException("Invalid capacity.");
_array = new T[capacity];
_size = 0;
_version = 0;
}
public void Push(T item)
{
if (_size == _array.Length)
{
T[] dest = new T[(_array.Length == 0) ? 4 : (2 * _array.Length)];
Array.Copy(_array, 0, dest, 0, _size);
_array = dest;
}
_array[_size++] = item;
_version++;
OnCollectionAdded(item);
OnPropertyChanged("Count");
}
public T Pop()
{
if (_size == 0)
throw new InvalidOperationException("Empty stack, can not pop.");
_version++;
T local = _array[--_size];
_array[_size] = default(T);
OnCollectionRemoved(local, _size);
OnPropertyChanged("Count");
return local;
}
public T Peek()
{
if (_size == 0)
throw new InvalidOperationException("Empty stack, can not peek.");
return _array[_size - 1];
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
void OnCollectionReset()
{
var c = CollectionChanged;
if (c != null)
c(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
void OnCollectionAdded(object item)
{
var c = CollectionChanged;
if (c != null)
c(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
void OnCollectionRemoved(object item, int index)
{
var c = CollectionChanged;
if (c != null)
c(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
}
void OnPropertyChanged(string propertyName)
{
var p = PropertyChanged;
if (p != null)
p(this, new PropertyChangedEventArgs(propertyName));
}
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
public void Clear()
{
Array.Clear(_array, 0, _size);
_size = 0;
_version++;
OnCollectionReset();
OnPropertyChanged("Count");
}
public bool Contains(T item)
{
int index = _size;
var comparer = EqualityComparer<T>.Default;
while (index-- > 0)
{
if (item == null)
{
if (_array[index] == null)
return true;
}
else if (_array[index] != null && comparer.Equals(_array[index], item))
{
return true;
}
}
return false;
}
public void CopyTo(T[] array, int arrayIndex)
{
if (array == null)
throw new ArgumentNullException("array");
if (arrayIndex < 0 || arrayIndex > array.Length)
throw new ArgumentOutOfRangeException("arrayIndex");
if ((array.Length - arrayIndex) < _size)
throw new ArgumentException();
Array.Copy(_array, 0, array, arrayIndex, _size);
Array.Reverse(array, arrayIndex, _size);
}
public int Count
{
get { return _size; }
}
public bool IsSynchronized
{
get { return false; }
}
public object SyncRoot
{
get { return _syncRoot; }
}
void ICollection.CopyTo(Array array, int index)
{
Array.Copy(this._array, 0, array, index, this._size);
Array.Reverse(array, index, this._size);
}
public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
{
ObservableStack<T> _stack;
int _index;
int _version;
T currentElement;
internal Enumerator(ObservableStack<T> stack)
{
_stack = stack;
_version = _stack._version;
_index = -2;
currentElement = default(T);
}
public T Current
{
get
{
if (_index < 0)
throw new InvalidOperationException();
return currentElement;
}
}
public void Dispose()
{
_index = -1;
}
object IEnumerator.Current
{
get
{
if (_index < 0)
throw new InvalidOperationException();
return currentElement;
}
}
public bool MoveNext()
{
bool flag;
if (_version != _stack._version)
throw new InvalidOperationException("Version mismatch.");
if (_index == -2)
{
_index = _stack._size - 1;
flag = _index >= 0;
if (flag)
currentElement = _stack._array[_index];
return flag;
}
if (_index == -1)
return false;
flag = --_index >= 0;
if (flag)
{
currentElement = _stack._array[_index];
return flag;
}
currentElement = default(T);
return flag;
}
public void Reset()
{
if (_version != _stack._version)
throw new InvalidOperationException("Version mismatch.");
_index = -2;
currentElement = default(T);
}
}
}
}
Posted by
Paul Irwin
at
9:45 AM
0
comments
Links to this post
Monday, October 4, 2010
HttpContext.Current and Threads with QueueUserWorkItem
If you’ve got an ASP.NET (including MVC) application that uses ThreadPool.QueueUserWorkItem with a reset event or anything of the like, you can not access HttpContext.Current (or rather, it is null) on those threads. This makes exception logging difficult, especially if you want to log the URL that is currently being requested.
After a little digging, I discovered that the current HttpContext is actually in thread-local storage, which explains why child threads don’t have access to it. Luckily, you can pass a reference to it in your child thread. Include a reference to HttpContext in the “state” object of your callback method, and then you can store it to HttpContext.Current on that thread. If you notice, HttpContext.Current is not a read-only property – it has a setter as well. Set HttpContext.Current = myStateObj.HttpContextReference where myStateObj is your state object.
I feel like this post is a bit self-explanatory, but here’s an obligatory example if you don’t follow:
private AutoResetEvent s_reset = new AutoResetEvent(false);
protected void Page_Load(object sender, EventArgs e)
{
var state = new WorkerState() { HttpContextReference = HttpContext.Current };
ThreadPool.QueueUserWorkItem(new WaitCallback(Worker), state);
try
{
s_reset.WaitOne();
}
finally
{
s_reset.Close();
}
}
void Worker(object state)
{
var mystate = state as WorkerState;
if (mystate != null && mystate.HttpContextReference != null)
{
HttpContext.Current = mystate.HttpContextReference;
}
HttpContext.Current.Response.Write("Threading!");
s_reset.Set();
}
private class WorkerState
{
public HttpContext HttpContextReference { get; set; }
}
And, of course, it works:
Hope this helps!
Posted by
Paul Irwin
at
2:51 PM
4
comments
Links to this post
Labels: asp.net, multithreading, Threading
Monday, September 20, 2010
Combining and Abusing Delegates
I had never before thought much about combining delegates until I came across some code that combined them using lambdas, and it got me thinking about other ways to use them. Consider the following example. Note: crazy code ahead!
public class Programmer
{
Action<string> a;
public Programmer(string name)
{
this.Name = name;
}
public string Name { get; set; }
public bool ShouldDesign
{
set
{
if (value)
a += s => { Console.WriteLine(Name + " should design " + s); };
}
}
public bool ShouldDevelop
{
set
{
if (value)
a += s => { Console.WriteLine(Name + " should develop " + s); };
}
}
public bool ShouldDeploy
{
set
{
if (value)
a += s => { Console.WriteLine(Name + " should deploy " + s); };
}
}
public void Work(string programName)
{
a(programName);
}
}
And then used in a console app like this:
static void Main(string[] args)
{
var paul = new Programmer("Paul");
paul.ShouldDesign = true;
paul.ShouldDevelop = true;
paul.ShouldDeploy = false;
paul.Work("my application");
Console.ReadKey();
}
And the output…
I do not encourage the kind of code I’ve created here, but maybe this will get you thinking!
Posted by
Paul Irwin
at
2:00 PM
0
comments
Links to this post
Tuesday, September 14, 2010
Thoughts on the Microsoft Ecosystem
I had the great opportunity to touch a Windows Phone 7 device at the Jacksonville Code Camp a few weeks ago, and it sold me – I’ll likely give up my iPhone 4 for a Windows Phone 7 device when they ship this fall. The biggest selling point as a C# developer is that I can develop both data-driven apps in Silverlight and games in XNA for Windows Phone 7 in the language I use every day – C#. So that got me thinking about where else C# and the .NET platform can be used, both at work and at home.
- Windows 7 client apps in WPF (with multi-touch for tablets)
- Rich Internet Apps with Silverlight 4 (again with multi-touch)
- Websites with ASP.NET and ASP.NET MVC
- Scaffolding data-driven sites with ASP.NET Dynamic Data
- Windows Phone 7 data-driven apps in Silverlight
- Games for Windows Phone 7 with XNA
- Games for PC with XNA
- Games for Xbox 360 with XNA
- Middleware real-time services with WCF
- Middleware queued services with WCF, WF, and MSMQ
- SQL Server integration with CLR stored procedures
- Windows Media Center apps with the Media Center SDK
- Embedded device apps with WPF (Windows Embedded)
- Small-scale embedded devices with .NET Micro Framework (including Netduino)
- Windows Home Server add-ins with the WHS SDK
- Cloud apps with the Windows Azure SDK
- SharePoint parts and sites
- Office add-ins for Outlook, Excel, Word, and others
- Surface development with the Surface SDK (which will be in our homes within a few years)
- … and probably a dozen other things I’m forgetting.
That’s a huge list, and I’ve heard complaints from people that the ecosystem has gotten too big, and therefore too much to keep track of and learn. I don’t look at it that way – I view all of these varied and important areas as all using the same foundation, C# and the .NET framework. If you know C# and the framework, you can jump from one to the other with a minimal learning curve. A WPF developer can jump to Silverlight and then to Surface and then to Windows Media Center and then to Windows Phone 7 with Silverlight without much to learn. An ASP.NET developer can jump to ASP.NET MVC and to Windows Azure and to SharePoint, again with minimal learning. Compared to jumping to another framework or another language, jumping between any of these technologies is relatively painless.
It’s a great time to be a .NET developer, and the potential with all of these technologies excites me!
Posted by
Paul Irwin
at
10:53 PM
0
comments
Links to this post
Labels: c#
Friday, August 20, 2010
My First Workflow Foundation (WF) 4 App
I came across a book in our company’s “library” today – Microsoft Windows Workflow Foundation, Step by Step – and I started thinking about whether or not I should try to learn WF. Frankly, I haven’t been interested so far. Being a front-end developer, I haven’t really developed many service-level applications or processes, and of the ones I’ve touched, it seems like remoting, WCF, and/or MSMQ works rather well for those tasks. However, I came across NServiceBus this past week and it got me thinking more in the mindset of a service designer, and WF piqued my interest. A good developer tries to be well-rounded and knowledgeable about other technologies, even if he or she doesn’t use them on a daily basis.
I started flipping through the first chapter of this book. I quickly realized that it’s a 3 year old book (a lifetime in software technologies) and is about .NET 3.0 – the red-headed stepchild release of .NET. WF, of course, has seen 2 revisions since, with .NET 3.5 and again with 4.0. Nonetheless, I enjoyed the first chapter, and I started to see where WF could be useful. (Despite, that is, a question I posted on twitter asking if anyone has used WF, to which I got zero responses.)
I came home, fired up my beast of a work laptop (Core i7 4core/8thread, 8GB RAM), and launched Visual Studio 2010. File – New Project. Immediately I noticed a difference from the first chapter of this book. In the .NET 3 version, you have to choose between a Sequential or State Machine workflow type and then between a console app or library, or an “activity library”, or an empty workflow project. In VS2010, you only have 2 non-library Workflow project types – WCF Workflow Service, and Workflow Console App. I chose the Workflow Console App because it more closely matched the example. Then the similarities came to a screeching halt.
I hadn’t kept up with WF, otherwise I should have known this. WF in 4.0 uses XAML to describe workflows, and the only “code” in the workflow are “expressions” in… wait for it… VB. Yes, even though I created a C# Workflow Console App, the expressions are in Visual Basic. Furthermore, the example in the book is a .cs file (not .xaml), and has an accompanying .Designer.cs file as a “code-behind”. Better still, when you create “Code” activities in the older WF, they are methods in a C# file. The only equivalent to the “Code” activity I found in WF 4 is the “InvokeMethod” activity, which can only call a method on a type not defined in the workflow.
Still, I was able to accomplish a fairly basic If/Else workflow in WF4. Here’s what it looks like:
Fairly trivial. If a “PostalCodeIsValid”, then I write a line to the console saying it’s valid, and if not, I write a line saying it’s not. Notice the VB ampersands for string concatenation in the “Text” fields.
Look at the Condition field. I’m executing a method on a “Workflow1Helper” static class I created for simplicity. That method simply returns “PostalCode.Length == 5” which is not a true postal code validation routine, but could be replaced with one. Luckily, the Workflow1Helper class is written in C#.
It seems like this is the pattern for WF4 workflows – define your workflow in XAML, and farm out your code to other types. Makes since, especially from a separation of concerns aspect, but it’s hardly similar to WF in .NET 3.0.
So where does that PostalCode field come from? You must define it as an “Argument”. If you look at the bottom of the workflow designer, you’ll see this pane:
The Arguments tab brings up this pane:
That’s where I created the PostalCode argument. You can define any type and number of arguments you want, think of them as “parameters” to your workflow when you invoke it (more on invocation later). Also note you can assign a Default Value, i.e. “New MyType()” (alas, sigh, again in VB.)
Lastly, the invocation. There’s a Program.cs file that gets created as part of the project, and it’s a standard console app entry point class. I just had to modify the invocation part to pass the PostalCode argument:
(I apologize for the code-screenshot, I am lazy and haven’t installed the PreCode plug-in yet.)
Obviously, hitting F5 will run the app, and we get a message saying it’s valid. Yay.
Now time for the dramatic conclusion. WF4 seems much more polished than WF in .NET 3 appears in the book. I love that it’s XAML (although you really don’t want to go in and modify it, it’s pretty ugly), and I love that the code is separated from the workflow logic. It’s all very professional – easy to understand, self-documenting processes, and very easy to grasp. I also created my second WF app, this time as a WCF service, and I think that’s a much more useful paradigm than a console app, especially if it’s possible to use with a MSMQ binding (which I haven’t tried).
For the negatives, I dislike the fact that a C# WF app needs to use VB expressions, because I dislike having to switch gears. I don’t mind coding in VB if I have to, but would it have killed them to allow C# expressions?
I want to read up more on WF, as well as it’s migration from 3 –> 3.5 –> 4, as well as play around more with the WCF version of it before writing another blog post on the subject.
Bottom line, fire up Visual Studio 2010 and create your first WF app and go to town. Come up with some cool real-world ways to use it, and let me know, will you?
Posted by
Paul Irwin
at
7:08 PM
0
comments
Links to this post
