New C# 7 features in action: Local Functions in C# 7

C# 7’s Local Functions

1. Overloading not supported.

2. Accessibility modifier (public, private, protected) not allowed.

3. Compiler will issue a warning if not used

4. All variables in the enclosing scope, including local variables can be accessed: (as shown in example below)

invalidData local variable of  IsValidInputDataWithLogging function can be accessed by LogInvalidDataMessages local function.

public static bool IsValidInputDataWithLogging(Dictionary<string, string> inputData)
{
	var output = HasValidValues();
	var invalidData = output.Where(x => x.Value.Status == false);
	// Some Code.

	LogInvalidDataMessages();

	return false;

	void LogInvalidDataMessages() // LogInvalidDataMessages local function.
	{
		foreach (var item in invalidData) // LogInvalidDataMessages can access invalidData defined in the enclosing scope.
		{
			// Some Code.
		}
	}

	Dictionary<string, OperationResult> HasValidValues() // HasValidValues local function.
	{
		foreach (var inputString in inputData) // HasValidValues can access inputData dictionary.
		{
			// Some Code.
		}
	}
}

5. Are in scope for the entire method in which they are present:  (as shown in example below)

public static bool IsValidInputDataWithLogging(Dictionary<string, string> inputData)
{
        var output = HasValidValues();
	var invalidData = output.Where(x => x.Value.Status == false);
	void LogInvalidDataMessages() // LogInvalidDataMessages local function.
	{
		// Some Code.
	}
	
	// Some Code.
	
	LogInvalidDataMessages();

	Dictionary<string, OperationResult> HasValidValues() // HasValidValues local function.
	{
		// Some Code.
	}
	
	// Some Code.
}

Use Cases for C# 7’s Local Functions

A. Parameter validation scenario

In the below example, RequestIsValid local function is used to validate the parameters of InsertNews function.

public bool InsertNews(News request)
{
	var validationResult = RequestIsValid();
	if (validationResult.isValid == false)
	{
		Console.Write($"{nameof(validationResult.errorMessage)} : {validationResult.errorMessage}");
		return false;
	}

	// Some code for inserting the news in database.

	return true;

	(bool isValid, string errorMessage) RequestIsValid()
	{
		if (request == null)
		{
			throw new ArgumentNullException(nameof(request), $"The {nameof(request)} may not be null.");
		}

		var lsb = new Lazy<StringBuilder>();

		if (string.IsNullOrWhiteSpace(request.Headline))
		{
			lsb.Value.AppendLine($"The {nameof(request)}'s {nameof(request.Headline)} property may not be empty.");
		}

		if (string.IsNullOrWhiteSpace(request.Detail))
		{
			lsb.Value.AppendLine($"The {nameof(request)}'s {nameof(request.Detail)} property may not be empty.");
		}

		if (request.Id <= 0)
		{
			lsb.Value.AppendLine($"The {nameof(request)}'s {nameof(request.Id)} property may not be less than zero.");
		}

		if (lsb.IsValueCreated)
		{
			var errorMessage = lsb.Value.ToString();
			return (isValid: false, errorMessage: errorMessage);
		}

		return (isValid: true, errorMessage: string.Empty);
	}
}

B. Iterator functions

In case of iterators functions, non-iterator wrapper public function is commonly needed for eagerly checking the arguments at the time of the call. Local function as best suited in this use case.

public IEnumerable<Book> BooksAvailableForDisplay(IEnumerable<Book> booksAvailableInStock, Func<Book, bool> validate)
{
	if (booksAvailableInStock == null)
	{
		throw new ArgumentNullException(nameof(booksAvailableInStock));
	}

	if (validate == null)
	{
		throw new ArgumentNullException(nameof(validate));
	}

	return ValidBooksForDisplay();

	IEnumerable<Book> ValidBooksForDisplay()
	{
		foreach (var book in booksAvailableInStock)
		{
			if (validate(book))
			{
				yield return book;
			}
		}
	}
}

C. Methods that are called from only one place & only makes sense inside of a single method that uses it

In the below example,  LogInvalidDataMessages and HasValidValues local functions  are available inside IsValidInputDataWithLogging function. In this case, LogInvalidDataMessages and HasValidValues functions are only used by IsValidInputDataWithLogging function.

public bool IsValidInputDataWithLogging(Dictionary<string, string> inputData)
{
	var output = HasValidValues();
	var invalidData = output.Where(x => x.Value.Status== false);

	if (invalidData.Any() == false)
	{
		return true;
	}

	LogInvalidDataMessages();

	return false;

	void LogInvalidDataMessages() // LogInvalidDataMessages local function.
	{
		// Some Code.
	}

	Dictionary<string, OperationResult> HasValidValues() // HasValidValues local function.
	{
		// Some Code.
	}
}

Full sample code to explain the use of C# 7’s Local Function

Note the use of Local Functions:

  1. Parameter validation scenario (RequestIsValid local function inside InsertNews function)
  2. Iterator functions (ValidBooksForDisplay iterator function)
  3. Methods that are called from only one place & only makes sense inside of a single method that uses it (LogInvalidDataMessages and HasValidValues local functions inside IsValidInputDataWithLogging function)

in the below sample code:

/// <summary>
///     CSharp7Sample.
/// </summary>
public partial class CSharp7Sample
{
	/// <summary>
    ///     Checks Input Data is valid.
    /// </summary>
    /// <param name="inputData">Input data.</param>
    /// <returns>true or false.</returns>
    public bool IsValidInputDataWithLogging(Dictionary<string, string> inputData)
    {
        var output = HasValidValues();
        var invalidData = output.Where(x => x.Value.Status == false);

        if (invalidData.Any() == false)
        {
            return true;
        }

        LogInvalidDataMessages();

        return false;

        void LogInvalidDataMessages() // LogInvalidDataMessages local function.
        {
            var logMessages = new StringBuilder();

            foreach (var item in invalidData) // LogInvalidDataMessages can access invalidData defined in the enclosing scope.
            {
                var value = item.Value;
                logMessages.Append($"{nameof(item.Key)}: {item.Key}, {nameof(item.Value)}: {item.Value}, {nameof(value.ErrorCode)}: {value.ErrorCode}, {nameof(value.ErrorMessage)}: {value.ErrorMessage}");
                logMessages.AppendLine();
            }

            Console.Write(logMessages.ToString());
        }

        Dictionary<string, OperationResult> HasValidValues() // HasValidValues local function.
        {
            var result = new Dictionary<string, OperationResult>();

            foreach (var inputString in inputData) // HasValidValues can access inputData dictionary.
            {
                result.Add(inputString.Key, this.HasValue(inputString.Value));
            }

            return result;
        }
    }

	/// <summary>
	///     Insert News.
	/// </summary>
	/// <param name="request">Request.</param>
	/// <returns>Status: true or false.</returns>
	public bool InsertNews(News request)
	{
		var validationResult = RequestIsValid();
		if (validationResult.isValid == false)
		{
			Console.Write($"{nameof(validationResult.errorMessage)} : {validationResult.errorMessage}");
			return false;
		}

		// Some code for inserting the news in database.

		return true;

		(bool isValid, string errorMessage) RequestIsValid()
		{
			if (request == null)
			{
				throw new ArgumentNullException(nameof(request), $"The {nameof(request)} may not be null.");
			}

			var lsb = new Lazy<StringBuilder>();

			if (string.IsNullOrWhiteSpace(request.Headline))
			{
				lsb.Value.AppendLine($"The {nameof(request)}'s {nameof(request.Headline)} property may not be empty.");
			}

			if (string.IsNullOrWhiteSpace(request.Detail))
			{
				lsb.Value.AppendLine($"The {nameof(request)}'s {nameof(request.Detail)} property may not be empty.");
			}

			if (request.Id <= 0)
			{
				lsb.Value.AppendLine($"The {nameof(request)}'s {nameof(request.Id)} property may not be less than zero.");
			}

			if (lsb.IsValueCreated)
			{
				var errorMessage = lsb.Value.ToString();
				return (isValid: false, errorMessage: errorMessage);
			}

			return (isValid: true, errorMessage: string.Empty);
		}
	}

	/// <summary>
	///     Books available for display.
	/// </summary>
	/// <param name="booksAvailableInStock">Books available in stock.</param>
	/// <param name="validate">Validate function.</param>
	/// <returns>Valid books for display.</returns>
	public IEnumerable<Book> BooksAvailableForDisplay(IEnumerable<Book> booksAvailableInStock, Func<Book, bool> validate)
	{
		if (booksAvailableInStock == null)
		{
			throw new ArgumentNullException(nameof(booksAvailableInStock));
		}

		if (validate == null)
		{
			throw new ArgumentNullException(nameof(validate));
		}

		return ValidBooksForDisplay();

		IEnumerable<Book> ValidBooksForDisplay()
		{
			foreach (var book in booksAvailableInStock)
			{
				if (validate(book))
				{
					yield return book;
				}
			}
		}
	}

	/// <summary>
	///     Is valid book for display.
	/// </summary>
	/// <param name="book">Book.</param>
	/// <returns>Valid: true or false.</returns>
	public bool IsValidBookForDisplay(Book book)
	{
		if (book == null
			|| string.IsNullOrWhiteSpace(book.Publisher)
			|| string.IsNullOrWhiteSpace(book.BookCategory)
			|| string.IsNullOrWhiteSpace(book.Title)
			|| book.Id <= 0
			|| book.Price <= 0)
		{
			return false;
		}

		return true;
	}

	/// <summary>
    ///     Input string has value or not.
    /// </summary>
    /// <param name="inputString">Input string.</param>
    /// <returns>Operation Result.</returns>
    public OperationResult HasValue(string inputString)
    {
        var result = new OperationResult {};

        if (inputString == null)
        {
            result.ErrorCode = 1;
            result.ErrorMessage = "Input string is null";
        }
        else if (inputString.Equals(string.Empty))
        {
            result.ErrorCode = 2;
            result.ErrorMessage = "Input string is empty";
        }
        else if (inputString.Trim().Equals(string.Empty))
        {
            result.ErrorCode = 3;
            result.ErrorMessage = "Input string only whitespaces";
        }

        result.Status = string.IsNullOrEmpty(result.ErrorMessage);

        return result;
    }
}

/// <summary>
///     OperationResult.
/// </summary>
public class OperationResult
{
    /// <summary>
    ///     Gets or sets a value indicating whether status is true.
    /// </summary>
    public bool Status { get; set; }

    /// <summary>
    ///     Gets or sets the error code.
    /// </summary>
    public int ErrorCode { get; set; }

    /// <summary>
    ///     Gets or sets the error message.
    /// </summary>
    public string ErrorMessage { get; set; }
}

/// <summary>
///     Book class.
/// </summary>
public class Book
{
	public string Publisher { get; set; }
	public string BookCategory { get; set; }
	public int Id { get; set; }
	public string Title { get; set; }
	public decimal Price { get; set; }
}

/// <summary>
///     News class.
/// </summary>
public class News
{
	public int Id { get; set; }
	public string Headline { get; set; }
	public string Detail { get; set; }
}

Happy Coding !!!

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *