loading...
Cover image for Action-Oriented C#
Tech Elevator

Action-Oriented C#

integerman profile image Matt Eland Updated on ・3 min read

.NET Quality (7 Part Series)

1) Common .NET Gotchas 2) Action-Oriented C# 3 ... 5 3) Safe .NET Feature Flags with FeatureToggle 4) How C# 8 Helps Software Quality 5) Safer C# with the nameof operator 6) Future-proofing .NET Tests with NUnit Values Attributes 7) Experimental C# with Scientist .NET

Five years ago I hit a plateau. My code hit a certain level of quality and flexibility and stopped improving. Here's how I used aspects of functional programming to keep climbing.

My code was pretty SOLID, but there was still a lot of very similar code, despite actively trying to remove duplication whenever possible. It wasn't exact duplication, but it was clear patterns throughout code that made maintenance more trouble than it should have been.

Then I discovered how to apply Actions and Funcs to further improve my code.

Let's look at a hypothetical example:

// Associates Degree
if (resume.HasAssociatesDegree)
{
    try
    {
        resume.AssociatesDegreeScore = CalculateAssociatesDegreeScore();
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Could not calculate associates degree score: {ex.Message}");
        resume.AssociatesDegreeScore = 0;
    }
}
else
{
    resume.AssociatesDegreeScore = 0;
}

// Bachelors Degree
if (resume.HasBachelorsDegree)
{
    try
    {
        resume.BachelorsDegreeScore = CalculateBachelorsDegreeScore();
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Could not calculate bachelors degree score: {ex.Message}");
        resume.BachelorsDegreeScore = 0;
    }
}
else
{
    resume.BachelorsDegreeScore = 0;
}

// Masters Degree
if (resume.HasMastersDegree)
{
    try
    {
        resume.MastersDegreeScore = CalculateMastersDegreeScore();
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Could not calculate masters degree score: {ex.Message}");
        resume.MastersDegreeScore = 0;
    }
}
else
{
    resume.MastersDegreeScore = 0;
}

While this is a contrived example, this illustrates the problem. In this scenario you have a repetitive pattern of checking a property, then doing some custom logic that should not be invoked if the property was false, then storing the result in a custom property on the object.

It's a simple little routine, but the duplication is obvious and it resists extracting methods out because the CalculateX methods cannot be called if the resume doesn't have the related degree.

Additionally, let's say that a bug occurred and a code change was needed. You now need to make the same change in 3 places. If you miss one, it's likely going to cause bugs. Additionally, the similarities tempt you to do copy / paste driven development which is an anti-pattern and a potential source for bugs if not all lines that needed to be modified were modified.


What I learned that changed my approach is that passing around Action and Func types lets you get a lot more flexibility out of your methods by giving them configurable behavior.

Action is a generic signature of a method to invoke without a return type.

For example, a signature of something that takes in a boolean and integer parameter would look like this: Action<bool, int> myAction;

Func is very similar to an Action except it returns a value. The type of the value returned is the last generic type argument. For example, a Func<bool, int> would take in a boolean and return an integer.

So, we can use a Func in our example to extract a method with some configurable behavior:

private decimal EvaluateEducation(
  bool hasAppropriateDegree, 
  string degreeName, 
  Func<decimal> calculateFunc)
{
    if (hasAppropriateDegree)
    {
        try
        {
            // Invoke the function provided and return its result
            return calculateFunc(); 
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine($"Could not calculate {degreeName} score: {ex.Message}");
        }
    }

    return 0;
}

This improves our calling code to the following brief segment:

resume.AssociatesDegreeScore = EvaluateEducation(resume.HasAssociatesDegree, 
  "associates", 
  () => CalculateAssociatesDegreeScore());

resume.BachelorsDegreeScore = EvaluateEducation(resume.HasBachelorsDegree, 
  "bachelors", 
  () => CalculateBachelorsDegreeScore());

resume.MastersDegreeScore = EvaluateEducation(resume.HasMastersDegree, 
  "masters", 
  () => CalculateMastersDegreeScore());

This is much simpler, though the syntax is a little harder to read. What we're doing in the third parameter to the method is declaring a lambda expression similar to those you use for LINQ queries.

If you needed to work with a Func that uses parameters (say, you had a Func<int, decimal> signature, you could change your logic to the following:
(myInt) => myInt + CalculateMastersDegreeScore();


While this was a fairly contrived example, hopefully it illustrates the power of passing around Func and Action to various methods. This is the foundation that functional programming is built upon, but in small doses this can be extremely helpful in object-oriented programming as well.

While this syntax makes the code a little harder to read, the benefits in maintainability are real and the odds of introducing duplication-related defects is much lower.

Give it a try on a side project or particular area of duplication and let me know what you think.

.NET Quality (7 Part Series)

1) Common .NET Gotchas 2) Action-Oriented C# 3 ... 5 3) Safe .NET Feature Flags with FeatureToggle 4) How C# 8 Helps Software Quality 5) Safer C# with the nameof operator 6) Future-proofing .NET Tests with NUnit Values Attributes 7) Experimental C# with Scientist .NET

Posted on by:

integerman profile

Matt Eland

@integerman

Matt is committed to helping people achieve greater things. After over three decades of coding, Matt put away his mechanical keyboard and made teaching his primary job as he looks to help others grow.

Tech Elevator

Tech Elevator is an intensive in-person education provider helping individuals and companies acquire in-demand technology skills for the modern workforce.

Discussion

markdown guide
 

I once tried to build out a very functional API in C# using partial application, currying, delegates, ect... ect...

It was a lot of fun but left me with code I couldn't understand when I came back to it several months later.

I do love the power of higher order functions in C# but the language syntax makes it noisy and it feels out of place next to all the Object Oriented code.

Often, instead of passing around delegates, if I want to implement something like the strategy pattern, I'll make a single method interface - since this is effectively the same thing as a function.

Then I don't have to worry about the generic delegates Func<...> and Action<...> and my interface's signature is more like the rest of the code base (and easier for other developers to reason about).

That said, I do love exploring these functional approaches as they help me think about the different ways to solve problems.

I guess it's all up to your personal taste and the taste of your team!

Thanks for the post 🙏.

 

I agree with the syntax issues. TypeScript has type aliases where you can name a union of types for example. .NET cod use something similar.

That said, some of the most flexible aspects of .net and many popular libraries rely on Action and Func under the covers. Thankfully the syntax to invole a lambda expression is cleaner than the parameter signature definition.

 

You can use delegates or functional interfaces to have alias.

A functional interface is an interface with only one method, as suggested above.

With the delegate, you can have:

public delegate decimal CalculateScore();

This is totally compatible with Lambdas and Funcs. And a safe way to do a Strategy Pattern like you want.

 

Yah, definitely didn't want it to seem like I dislike them or avoid them.

I tend to use them for internal APIs and infrastructure code - less so for business logic service methods.

I just saw on Twitter today that someone is actively championing discriminated unions for C# 9 😁:

Both TypeScript's and F#'s allowance of not explicitly typing params and return types helps reduce that function + generic noise.

We could do the aliasing with C# named delegates, but then we are kinda just trading one noise for another (with more type limitations).

 

Y'got some typos in the code to fix. Otherwise great article.

 

Thanks. Only serves to highlight the dangers of copy-paste driven development. :-)