WOR KITTNZ!

WorKittnz

Sorry… It had to be done.

Posted in Uncategorized | Leave a comment

WF Project Types

There are several templates to choose from when creating a workflow project.

  • Empty Workflow Project
  • Sequential Workflow Console Application
  • State Machine Workflow Console Application
  • Sequential Workflow Library
  • State Machine Workflow Library
  • Workflow Activity Library

So what’s the difference and how do you know which one to choose? As it turns out, like most things in WF there really is no magic going on here, and things are much simpler than they seem. The two "Console Application" project types will result in a project that creates and exe, and all the others will create dlls. Other than that, the only real difference seems to be what initial classes will be generated. If I create a "Workflow Activity Library" project, it will initially contain an activity item called "Activity1". A "Sequential Workflow Library" will contain a sequential workflow item called "Workflow1", etc.

There are some minor project metadata differences so that when you right-click on the project and select the "Add" flyout, it will suggest workflow-related classes to add. Other than that, there seem to be no real differences at all. That’s the thing about WF. You keep looking for "magic", complicated things and then find out that they’re all much simpler than you thought.

Posted in Uncategorized | Leave a comment

WaitContext: A class to disable UI controls during long-running processes

The Goal:

On my current project, we need to disable the UI while performing long-running operations such as the loading and saving of data.  We started off using some dirt-simple code that would wrap the long-running process in a try/finally block, disabling the form at the beginning of the "try", and re-enabling it in the "finally".  It was simple, and worked just fine at first, but started showing its limitations as things have got a little more complicated.  Our forms host UserControl "Views" which sometimes wrap other views as children.  Any one of these views, parent or child, might need to trigger a long-running processes.  To complicate matters further, some of the child views’ processes can be expected to be called as part of the parent views’ processing.

For instance, lets say we have an order processing system, and the customer details view is considered a parent view.  Within that view are multiple child views for displaying things like address information, or a list of that customer’s order history.  Loading, saving, or refreshing the parent view automatically results in the loading, saving, or refreshing of the child views.  In addition, some child views can be loaded, saved, or refreshed individually, and therein lies the problem.  I can’t just repeat my el cheapo try/finally trick in the child views because as each child view finishes its individual processing it will automatically re-enable itself, but the other views may not be finished with their processing yet.  In short, each child should remain disabled until they’ve all finished their work.  This scenario is a complete fabrication, but it illustrates the point.

It occurred to me today that the problem of disabling and enabling the forms was very much like the problem of changing the cursor to and from the hourglass.  I’ve been using a customized and renamed variant of Doogal Bell’s CursorHandler class (http://www.doogal.co.uk/cursor.php) to handle setting and restoring the hourglass cursor on my last couple projects.  It’s simple to use, and you can stack up as many calls to it as you want, and they will always unwind properly.  I thought I’d try something similar to handle my enabling/disabling needs.  Parent view controllers would perform their work inside a using block, which would automatically restore both the hourglass and the enabled/disabled state of a control when it’s finished.

The Difficulty: (Skip ahead if you want, I won’t be offended)

In theory, this should be very simple.  In reality it requires a few tricks.  First of all, if you disable a container control such as a UserControl, a Panel, or a GroupBox, you’ll see that all of the controls inside that container get disabled for you automatically.  When you re-enable the container, the children will go back to their original states.  This is very convenient, and takes a lot of work off our hands normally but it interferes with my plans in this particular scenario.  If you ask the child controls of a disabled parent whether they are enabled or not, they will say they are disabled even when it’s really their parent that’s disabled.

So things would go pretty much like this.  My parent view instantiates a new WaitContext (As I’m calling it), which looks at the parent control and memorizes that it was enabled when it started, and then disables it.  The parent view initiates some process on the child view which in turn instantiates its own WaitContext which looks at the child and memorizes that it was disabled when it started.  The child view’s process completes, and its WaitContext tries to "restore" the original setting, which it remembers as being disabled.  The parent view then wraps up its work and unwinds its WaitContext, which properly sets the parent view back to enabled.  Because the inner WaitContext disabled the child view we now have an enabled parent with a disabled child.  This isn’t what we started with, though.  What we really need is to know whether each individual control is really disabled, or is just disabled by virtue of being contained in a disabled parent.

Thanks to the miracle of Lutz Roeder’s Reflector, I found out that Enabled isn’t just a simple boolean property.  When you ask a control whether it’s enabled, the answer you get back depends on a couple of things.  First of all, it depends on whether that individual control is disabled, and secondly on whether that control’s parent is disabled.  It’s that first part that we’re interested in.  Determining whether a control has been explicitly disabled is done by checking bit 4 of the control’s "state" field via the GetState method.  State contains a number of flags indicating all kinds of things like whether the control is Enabled, Visible, or in the middle of doing something.  Unfortunately for us, the state field is private, and the GetState method is marked Internal, so we can’t see either one of them.

Since I know the name of the method I want, and I know which bit I’m looking for, I can still get what I want via Reflection.  The call to get the original Enabled state of the control looks like this:

OriginalEnabled = (bool)Control.GetType().InvokeMember("GetState", 
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
null, Control, new Object[] { 4 });

Setting the enabled state actually takes two steps.  The call to "SetState" sets the bitfield, and the call to "OnEnabledChanged" forces the control to notice the change and repaint itself.

Control.GetType().InvokeMember("SetState", 
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
null, Control, new Object[] { StateEnabled, enabled }); Control.GetType().InvokeMember("OnEnabledChanged",
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
null, Control, new Object[] { EventArgs.Empty });

The Result:

Wrap your long-running code inside a Using block that instantiates a WaitContext object like this:

using (new WaitContext(this))
{
    // long-running code goes here.
}

There are three different constructors.  The parameterless constructor will change the cursor to the hourglass without disabling anything, and makes the class work just like Doogal’s CursorHandler.  The second constructor takes a Control reference, which is the control to disable.  It changes the cursor to the hourglass (Or spinning donut for Vista users), as well as disabling the specified control.  The third, and final constructor allows you to specify the control to disable as well as what cursor should be displayed.  It’s probably not going to get used, but I like to include it for completeness and flexibility.

The Class:

Here is the finished WaitContext class.

public class WaitContext : IDisposable
{
    #region Fields

    private const BindingFlags flags = 
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic; private const int StateEnabled = 4; #endregion #region Properties private Control Control { get; set; } private Cursor OriginalCursor { get; set; } private bool OriginalEnabled { get; set; } #endregion #region Constructors / Destructors public WaitContext() : this(null) { } public WaitContext(Control control) : this(control, Cursors.WaitCursor) { } public WaitContext(Control control, Cursor newCursor) { SaveState(control, newCursor); } ~WaitContext() { RestoreState(); } #endregion #region Methods [ReflectionPermission(SecurityAction.Demand, MemberAccess = true)] private void EnableControl(bool enabled) { Control.GetType().InvokeMember("SetState",
flags, null, Control, new Object[] { StateEnabled, enabled }); Control.GetType().InvokeMember("OnEnabledChanged",
flags, null, Control, new Object[] { EventArgs.Empty }); } private void RestoreState() { if (Control != null) { Control.Cursor = OriginalCursor; EnableControl(OriginalEnabled); } else { Cursor.Current = OriginalCursor; } } [ReflectionPermission(SecurityAction.Demand, MemberAccess = true)] private void SaveState(Control control, Cursor newCursor) { Control = control; if (control != null) { OriginalCursor = Control.Cursor; Control.Cursor = newCursor; OriginalEnabled = (bool)Control.GetType().InvokeMember("GetState",
flags, null, Control, new Object[] { StateEnabled }); EnableControl(false); } else { OriginalCursor = Cursor.Current; Cursor.Current = newCursor; } } #endregion #region IDisposable Members public void Dispose() { RestoreState(); GC.SuppressFinalize(this); } #endregion }

Footnote: Why yes, I am an adherent of "Oxford commas".

Technorati Tags: ,
Posted in Computers and Internet | 1 Comment

Linq joining to in-memory collections is bad

I found an interesting bug this morning, and figured I’d better write about it so that maybe I’ll remember to avoid it in the future.  We have a Linq query that involves a couple Joins.  The main table we’re interested in is already provided for us by a "FilteredNotes" property like this:

IQueryable<Note> notes = DataContext.Notes;
if (!canViewAllNoteTypes)
{
    if (canViewLegalNoteTypes)
        return DataContext.Notes.Where(n => legalCategoryIds.Contains(n.CategoryId));
    else
        return new List<Note>().AsQueryable();
}
...

The results of this pre-filtered set of data are then joined to two other tables to provide some additional filtering like so:

IQueryable<NoteSummary> results =
    from d in this.FilteredNotes
    join c in this.DataContext.Claims on d.ParentId equals c.ClaimId
    join e in this.DataContext.Enrollees on c.EnrolleeId equals e.EnrolleeId
    where d.ResponsiblePersonId == userId &&
          d.CompletedDate == null &&
          d.ScheduledDate >= startDate &&
          d.ScheduledDate <= endDate
    orderby d.ScheduledDate ascending
    select new NoteSummary
               {
                   ...
}; return results.ToList();

This code has been working great, until I started testing a new security role type.  When I am under the guise of this new type, the query would run for a long time, and then explode with an OutOfMemoryException.  The problem is actually really simple once you see it.  In that first section of code, the author wanted to return an empty list to join against, since the user wasn’t authorized to view any of the categories of notes that we’re interested in here.  The trouble is the way the empty list was created.  Creating a new list with nothing in it, and then calling AsQueryable() generated an empty IQueryable, but it did it in memory.  Since this is the main list to which we’re joining the others, Linq wants to pull them ALL into memory and do the join there.  Normally you might not notice, but since this database has millions of rows in each of the tables we’re joining to, it tends to make things bog down a bit.

The solution is to create our empty list, but to do it in a different way.  We want an IQueryable of Notes that will become part of a single Linq query sent to the database.  We can solve this by changing the FilteredNotes property to retrieve the IQueryable<Note> in a way that defers its execution, but still guarantees it will be blank:

IQueryable<Note> notes = DataContext.Notes;
if (!canViewAllNoteTypes)
{
    if (canViewLegalNoteTypes)
        return DataContext.Notes.Where(n => legalCategoryIds.Contains(n.CategoryId));
    else
        return DataContext.Notes.Where(n => false);
}
...

There, by changing the condition to be simple "false", the retrieval of the Notes can now be part of one big query, but we know that it’ll be empty when that happens.  Problem solved.

Technorati Tags: ,
Posted in Computers and Internet | Leave a comment

Oops

I just realized that a few posts from January were in the wrong place, so I moved them to this… the correct blog.

Posted in Uncategorized | Leave a comment

Hosting the workflow rules designer

Windows Workflow foundation has a built-in dialog for editing business rules. The RuleSetDialog class gives you everything you need to edit business rules, providing Intellisense based on the type of entity you provided to the RuleSetDialog constructor. Assume we have a business entity called Customer, and we want to create a new set of business rules for it. We would instantiate the dialog like this:

Type entityType = typeof(Customer);
RuleSet ruleSet = new RuleSet();
dialog = new RuleSetDialog(entityType, null, ruleSet);
DialogResult result = dialog.ShowDialog(this);

The null is for an optional TypeProvider which is beyond the scope of this article, and isn’t used here. The text boxes in the dialog have full Intellisense showing the properties and methods of the Customer class. The Intellisense isn’t exactly as robust as that in Visual Studio, but it’s better than nothing. From here you can write your If/Then/Else logic, set properties based on other properties, call methods when certain conditions are found, etc.

RuleSetDialog

Since we’re using the WF Rules engine outside of an actual WF Workflow, the loading and saving of the rules is left up to you. There are several examples out there, and the workflow code samples include examples of doing this. Rules can be persisted to .rules files, or to a database via code. The important thing to know here is that the resulting RuleSet doesn’t have any idea what kind of object it was written for. The WF Runtime would presumably take care of this on its own, but since we’re hosting the Rules ourselves, it’s up to us to remember this. The RuleSet itself is basically just a representation of the rules themselves.

Serializing/Deserializing RuleSets
Loading and saving RuleSets from the database isn’t that difficult. It’s just like any other business entity in a typical system, except that it’s not really a single object, but a pretty deep hierarchy representing an expression tree. You could spend weeks designing a database structure to hold the component parts, but fortunately you don’t have to. You can use the WorkflowMarkupSerializer class to turn the whole thing into one big lump of XML, and save that to a nvarchar(max) field in your database. It saves a lot of headaches, and doesn’t take much code to accomplish.

Where I’ve had to host the WF Rules engine myself on client projects, I have created a BusinessRuleSet entity, and given it fields to store the XAML representation of the rules, as well as what classes they apply to, along with whatever auditing fields (CreatedBy, ModifiedDate, etc) are required by project and/or client standards. At runtime, you load the appropriate BusinessRuleSet entity back up, extract the RuleSet, and execute it against an instance of the business entity.

Note: I have called the entity "BusinessRuleSet", and not "RuleSet" in order to avoid confusing naming conflicts with WF’s RuleSet class.

After closing the RuleSetDialog you can call its RuleSet property to get the RuleSet that was being edited. Using the WorkflowMarkupSerializer’s Serialize method, we’ll turn it into a XAML string ready for saving. Here’s a helper method that encapsulates the whole process, and a matching deserialization method to use when loading the rules back from the database.

private string SerializeRuleSet(RuleSet ruleSet)
{
StringBuilder ruleSetXml = new StringBuilder();
using (XmlWriter xmlWriter = XmlWriter.Create(ruleSetXml))
{
new WorkflowMarkupSerializer().Serialize(xmlWriter, ruleSet);
}
return ruleSetXml.ToString();
}

private RuleSet DeserializeRuleSet(string ruleSetXmlDefinition)
{
RuleSet result = null;
if (!String.IsNullOrEmpty(ruleSetXmlDefinition))
{
using (XmlReader reader = XmlReader.Create
(new StringReader(ruleSetXmlDefinition)))
{
result = serializer.Deserialize(reader) as RuleSet;
}
}
return result;
}

Ensuring RuleSets are valid for the instances they run against

So, if the rules don’t know about their target entity types, what’s to stop you from loading the Customer RuleSet and applying it against an Order instance? Verifying that a RuleSet can be applied to a specific entity is the responsibility of the RuleValidation class. The constructor for the RuleValidation class takes the type of item you want to run rules against, but oddly enough, doesn’t take the rules themselves. The RuleValidation instance is then passed to a RuleExecution class, which links it up to an actual instance of the class you want to validate. Finally, you call the Execute method on the RuleSet, passing in the RuleExecution instance. There are a lot of moving parts here, but in the end it looks pretty much like this:

RuleValidation validation = new RuleValidation(entity.GetType(), null);
RuleExecution execution = new RuleExecution(validation, entity);
RuleSet ruleSet = DeserializeRuleSet(businessRuleSet.RuleSet);
ruleSet.Execute(execution);

The RuleValidation will be used during execution to verify that every property or method mentioned in the rule is actually available on the entity we’re executing against, and to throw an exception if not. This may seem a little obtuse, and over-complicated, but it’s going to allow us do something pretty cool.

Limiting the surface area visible to the RuleSet designer
When you first start using the RuleSetDialog to edit rules, you will probably notice that the Intellisense in the If/Then/Else textboxes will show every member of the type you are writing a rule for. Visibility doesn’t seem to matter, and every private and protected member of the class becomes available to write rules against. Since the idea of hosting the WF Rules engine inside the app in the first place is so that we can allow business analysts and other non-programmer staff to modify and define business rules, we probably don’t want to open everything up quite that wide, and would prefer to expose a more limited subset of properties and methods.

The seemingly over-complicated series of objects required to execute a RuleSet in the previous section will now work to our advantage because t makes it possible to "lie" to the dialog about what Type we’ll eventually be executing the RuleSet against. We could actually write a RuleSet against the Customer type and execute it against the Employee type, so long as the RuleSet only touched properties that those two classes have in common. We won’t be doing anything quite that drastic, but the basic idea will form the basis for restricting what properties of an object are visible to the dialog.

We can create a rules-specific interface definition that only includes those items we want the rules engine to have access to. We can then implement this interface on the actual Business Entity class without having to do any extra work because all the members of the interface should already be present on the entity class. This is a kind of over-simplified Façade, for lack of a better term.

Implementing the rules interface
We’ll start with a base interface which exposes common functionality that all business entities can be expected to have, such as ID properties, audit fields, etc. It will also serve as a "marker" interface which will allow us to find the rules interface more easily later on. Here is a simplistic case which illustrates the point:

public interface IRulesEntity
{
decimal Id { get; }
}

interface ICustomer : IRulesEntity
{
string FirstName { get; set; }
string LastName { get; set; }
string Address1 { get; set; }
string Address2 { get; set; }
}

Showing the RuleSetDialog
Now, when it comes time to instantiate the RuleSetDialog, we’ll just pass it the ICustomer interface instead of the actual Customer class. The dialog’s Intellisense will now only offer those properties and methods that are available through the ICustomer interface. To do this we’ll need a way to find the rules interface for a given entity class at runtime, and we’ll use some reflection and Linq functionality to do this.

Remember that BusinessRuleSet is our custom class that stores the RuleSet XAML and the full name of the type it applies to. With this information, we can find the class, and its rules interface if it has one.

public void ShowDialog(BusinessRuleSet businessRuleSet)
{
string activityName = businessRuleSet.ActivityName;
Type baseType = typeof(IRulesEntity);
Type entityType = baseType.Assembly.GetType(activityName);
Type interfaceType = EntityType.GetInterfaces()
.Where(t => (t != baseType) && (baseType.IsAssignableFrom(t)))
.SingleOrDefault();
RuleSet ruleSet = DeserializeRuleSet(businessRuleSet.RuleSet);
dialog = new RuleSetDialog(entityType, null, ruleSet);
DialogResult result = dialog.ShowDialog(this);
}

Of course, we want to make sure that we gracefully handle the case where we’ve been asked to edit a BusinessRuleSet for a class that has no rules interface. For my current project, when adding a new RuleSet, the user must choose from a list of available classes which, of course, only contains classes which implement a rules interface, so this isn’t a problem. Finding the appropriate classes can be accomplished through a similar combination of reflection and Linq querying.

public List<Type> GetEntities()
{
Type baseType = typeof(IRulesEntity);
List<Type> types = new List<Type>();
baseType.Assembly.GetTypes()
.Where(t => t.IsClass && (baseType.IsAssignableFrom(t)))
.Each(entityType =>
{
Type interfaceType = entityType.GetInterfaces()
.Where(t => (t != baseType)
&& (baseType.IsAssignableFrom(t)))
.SingleOrDefault();
types.Add(entityType);
});
}

The call through SingleOrDefault will throw an error if a single class implements more than one Rules interface, which we’ve decided not to support in our current project. If you want to support multiple Rules interfaces, you can implement that yourself. The ".Each" syntax is an extension method against IEnumerable, which is also pretty cool. It looks like this:

public static void Each<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}

Steve Harman has an explanation of the .Else extension here: http://stevenharman.net/blog/archive/2008/01/15/get-ruby-esq-each-iterators-in-c-3.0.aspx.Technorati Tags: ,,,

Posted in Uncategorized | 1 Comment

Reflection cheat

Today I needed to retrieve a Type by name, but from a foreign assembly.  Without going into the gory details, it’s related to instantiating Windows Workflow Foundation’s RuleSetDialog after a rule has been loaded in from a database.

I could have made a call to Assembly.GetExecutingAssembly().GetReferencedAssemblies(), and iterated over the results looking for the one I wanted, but I’d still have to end up looking for it by name, and anyone who knows me knows that I hate string comparisons.  Since I already knew what assembly the thing I’m looking for is in, that also seems like a waste of time.  So I found a bit of a cheat.  I’m happy enough with it to put it here in the hopes that it will be useful to someone else someday.

Since I already know what assembly my business entities are in, I should be able to get a reference to the assembly by starting with one of the types from there.  In this case I chose the BusinessEntityBase class because it’s nicely meaningless on its own, and because the whole world would collapse anyway if this class were to be moved or renamed, so it seems like a stable thing to anchor myself to.  From there, I can easily get to that type’s Assembly property, and call GetType() on it, passing the name of the actual business entity I really want.  Here it is in its full two lines of glory.

Type businessEntityBaseType = typeof(BusinessEntityBase);
Type entityType = businessEntityBaseType.Assembly.GetType(typeName);

Here, typeName is a string containing the fully qualified name of the business entity I want to edit a rule for.

Posted in Computers and Internet | Leave a comment

Enumerating classes which implement a generic interface

Wow… exciting topic, eh?  It is actually somewhat interesting, though.

Background:
I’m working on a project where we’ve had to split validation away from the business entities for a couple reasons.  First of all, business entities live at a lower level than the logic classes responsible for loading and saving them.  They are kind of ignorant of their surroundings, and so not all validation rules can be expressed from there.  Instead, the validation rules are considered "business logic", and as such, they live in the "logic" assembly.  The problem is that we need validation to be kicked off by Linq attempting to save the entities via the OnValidate method.

What we settled on was a ValidationManager class which lives in the Business assembly, but gets populated from the Logic assembly at runtime with a dictionary which is used to look up the proper validator for a given entity.  The IValidator<TEntity> interface is defined in the Business assembly, but any common assembly would do.  So the business entities know what a validator looks like without having to have a reference to the validators themselves.

The next hurdle, and the one responsible for this post’s title, is doing the registration.  At first, we just had a static constructor for the base logic class that had one line for each validator, associating it with the business entity it validates and pushing it into the dictionary.  This works just fine, but it turns out it’s pretty easy to forget to register a validator when a new type of entity is created.  You’ve got the entity, and you’ve got the validator, but unless you register them, nothing will actually happen during the OnValidate method.

Solution:
Instead of manually adding code to register each validator, I’m now using some reflection code to find all the classes which implement IValidator<T>, and register them automatically.  The code is short, and pretty sweet if I must say so myself.

    Type validatorType = typeof(IValidator<>);
    foreach (Type objType in Assembly.GetExecutingAssembly().GetTypes())
    {
        foreach (Type interfaceType in objType.GetInterfaces())
        {
            if (interfaceType.IsGenericType && (interfaceType.GetGenericTypeDefinition() == validatorType))
            {
                Type argType = interfaceType.GetGenericArguments()[0];
                ValidationManager.RegisterValidator(argType, objType);
            }
        }
    }

Now I never have to manually register another validator again.  This loop could be expanded to register other types of classes (such as workflow classes) as well by adding additional branches to the inner if block.

Posted in Computers and Internet | Leave a comment

Mashed

We’re finally home from CodeMash 2008.  It was great, but I’m glad to be back home to my own house and my own bed… even if the house was 53 degrees when we got here.  All the sessions were great, although there were a couple "Beuhler…. Beuhler" moments there.  Probably the most grueling part of the CodeMash event is that initial 14-1/2 hour Thursday.  If next year’s event is expanded to three days as was mentioned as a possibility to the crowd, then I think they need to make sure that the first night has a dinner, and the second night has the party.

I love this event.  It’s been great both years, and I hope it continues into the future.  My suggestion for how to expand the event if it is to be expanded is basically to add an "experts" track in which the sessions are deeper dives for people who are already familiar with their topics.  Unfortunately, I don’t know how well this fits in with the CodeMash ethos of trying to expose developers to other developers’ worlds.  I can’t expect a Ruby programmer to join in on a heavy-duty WCF session.  On the other hand, I don’t expect people to be experts in all areas, either.  I certainly wouldn’t have gone to an expert-level workflow foundation session this year… I just don’t have the exposure yet for it to be meaningful.  But it would be nice to have the option to take at least one of my sessions and hit something I’ve already been exposed to in greater detail.

I’m looking forward to next year already.

Technorati Tags:
Posted in Uncategorized | Leave a comment

Two sessions down

Jeff’s Silverlight talk was pretty good.  Unfortunately for me I think I’ve seen it before up at the Microsoft building.  It’s a good talk, but I guess if I’d thought about it beforehand I probably should have done something else.  He’s a great speaker, though.

Second session, about the Castle project not so much.  It seemed a bit ill-prepared to me, and while I had hoped to get some more exposure to the DI/IOC stuff, that turned out to be a very small percentage of the session.  Some of the rest of the stuff is very interesting if not for the overlap with Enterprise Library and/or .net core abilities.  Most of the projects I’m on are using the EL, so it’s in my best interest usually to "keep it in the family" so to speak.  I guess if the EL weren’t around, then I’d be looking for this kind of stuff.  Kind of like ditching NHibernate when Linq came out.  It’s not a 100% match, but it’s so much easier for the stuff that it DOES do, that I can forgive the shortcomings, and wait for the Entity Framework to fill in the gaps.

Technorati Tags:
Posted in Uncategorized | Leave a comment