Parameters and Return Values — Passing Data In and Out

Parameters define the input a method needs to do its work; the return value is what the method gives back. C# gives you rich control over how parameters are passed and how results are returned. Optional parameters with default values eliminate the need for overloads in many cases. Named arguments make call sites self-documenting. Tuples allow returning multiple values without defining a dedicated class. These features are used constantly in ASP.NET Core controller and service methods.

Required Parameters

// Required parameters — all must be provided, in order
public string FormatName(string first, string last)
{
    return $"{last}, {first}";
}

string name = FormatName("Alice", "Smith");     // "Smith, Alice"

// Named arguments — pass in any order using parameter names
string name2 = FormatName(last: "Jones", first: "Bob");  // "Jones, Bob"

// Named arguments improve readability at complex call sites
var result = CreateUser(
    email:    "alice@example.com",
    password: "Password1!",
    role:     "Admin",
    isActive: true
);
Note: Named arguments are purely a caller-side convenience — they do not change the method signature or how the method works. They are particularly valuable when calling methods with multiple parameters of the same type (where it is easy to mix up the order) or when a method has many boolean parameters that are ambiguous without names. In ASP.NET Core, named arguments are common when calling service methods with many parameters from controller actions.
Tip: Prefer optional parameters over overloads when the variations are simple defaults. Use overloads when the different signatures have meaningfully different behaviour, different parameter types, or when the method is part of a public API where you cannot add optional parameters without a breaking change. Optional parameters that are reference types should default to null, not to a constructed object — constructing objects in default values runs at compile time and can cause surprises.
Warning: Optional parameters are evaluated at the call site (compile time), not inside the method. This means public void Log(string msg, DateTime when = DateTime.Now) is a compile error because DateTime.Now is not a compile-time constant. Use null as the default and check inside: public void Log(string msg, DateTime? when = null) { var time = when ?? DateTime.Now; }. This is also the correct pattern when the default should be the current time of the call, not the time the assembly was compiled.

Optional Parameters and Default Values

// Optional parameters must come AFTER required parameters
public string BuildConnectionString(
    string server,
    string database,
    int    port        = 1433,       // SQL Server default port
    bool   useSSL      = true,
    int    timeout     = 30)
{
    return $"Server={server},{port};Database={database};Encrypt={useSSL};Timeout={timeout}";
}

// Call with only required parameters
string cs1 = BuildConnectionString("localhost", "BlogDb");

// Override specific optional parameters using named arguments
string cs2 = BuildConnectionString("prod-server", "BlogDb", timeout: 60);

// Override all optional parameters positionally
string cs3 = BuildConnectionString("prod-server", "BlogDb", 5433, false, 60);

Params — Variable-Length Arguments

// params allows calling a method with any number of arguments
public int Sum(params int[] numbers)
{
    int total = 0;
    foreach (int n in numbers) total += n;
    return total;
}

// Callers pass any number of arguments directly
int a = Sum(1, 2, 3);          // 6
int b = Sum(10, 20, 30, 40);   // 100
int c = Sum();                 // 0 — zero arguments is valid
int d = Sum(new[] { 5, 6 });   // can also pass an array directly

// Used in Console.WriteLine(string format, params object[] args)
Console.WriteLine("Name: {0}, Age: {1}", "Alice", 30);

Returning Multiple Values with Tuples

// Named tuple return type — no wrapper class needed
public (bool IsValid, string ErrorMessage) ValidateAge(int age)
{
    if (age < 0)   return (false, "Age cannot be negative");
    if (age > 150) return (false, "Age seems unrealistically high");
    return (true, string.Empty);
}

// Caller can use named fields or deconstruct
var result = ValidateAge(25);
Console.WriteLine(result.IsValid);      // true
Console.WriteLine(result.ErrorMessage); // ""

// Deconstruct into named variables
var (isValid, error) = ValidateAge(-5);
if (!isValid) Console.WriteLine($"Error: {error}");  // "Error: Age cannot be negative"

// Discard parts you don't need with _
var (valid, _) = ValidateAge(30);   // only need valid, ignore error

Common Mistakes

Mistake 1 — Optional parameter before required parameter (compile error)

❌ Wrong — optional parameters must follow required ones:

public void Send(string subject = "No subject", string body)   // compile error!

✅ Correct:

public void Send(string body, string subject = "No subject")   // ✓

Mistake 2 — Using a mutable object as a default parameter value

❌ Wrong — compile error: default values must be compile-time constants:

public void Process(List<int> items = new List<int>())   // compile error!

✅ Correct — use null and create inside the method:

public void Process(List<int>? items = null)
{
    items ??= new List<int>();   // ✓
}

🧠 Test Yourself

A method needs to return both a success flag and a message. What are two clean C# approaches and which is preferred for simple cases?