This content originally appeared on DEV Community and was authored by Karen Payne
Introduction
Learn about basic usage of partial classes, properties, and methods. By no means does a developer need to use partial classes, properties, and methods unless the usage of partial classes, properties, and methods is necessary, without causing maintenance issues or confusion.
Although partial interfaces are supported, they are not touched on here, while there is an example in the supplied source code.
Note the use of preview in the project file.
<LangVersion>preview</LangVersion>
Conventional classes
Consider a class which implements an interface for change notification as shown below.
public enum Gender
{
Male,
Female,
Other
}
public class Client : INotifyPropertyChanged
{
public int Id { get; set => SetField(ref field, value); }
public string FirstName
{
get;
set => SetField(ref field, value.CapitalizeFirstLetter());
}
public string LastName
{
get;
set => SetField(ref field, value.CapitalizeFirstLetter());
}
public Gender? Gender { get; set => SetField(ref field, value); }
public override string ToString() => $"{Id,-4}{FirstName,-10} {LastName,-14} ({Gender})";
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new(propertyName));
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
Let’s refactor the Client class, which has the class defined as partial, and create a secondary file for the setters and getters by defining properties as partial. In the project provided, the Directory.Build.targets provide MS Build instructions to nest the class files.
Note
Interfaces are used in source code as several classes share the same properties and have nothing to do with partial classes and partial properties.
Client.cs
public partial class Client : INotifyPropertyChanged, IPerson
{
public partial int Id { get; set; }
public partial string FirstName { get; set; }
public partial string LastName { get; set; }
public partial Gender? Gender { get; set; }
}
Client.Sets.cs
public partial class Client
{
public partial int Id { get; set => SetField(ref field, value); }
public partial string FirstName
{
get;
set => SetField(ref field, value.CapitalizeFirstLetter());
}
public partial string LastName
{
get;
set => SetField(ref field, value.CapitalizeFirstLetter());
}
public partial Gender? Gender { get; set => SetField(ref field, value); }
public override string ToString() => $"{Id,-4}{FirstName, -10} {LastName, -14} ({Gender})";
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new(propertyName));
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
Partial extensions
Like conventional classes and methods, extension methods can be implemented partial. One reason is to hide an extension body of code and when done this way use file nesting.
In the following example, the extension for masking an SSN has a decent amount of code that has been tested and only needs to be viewed for an inquiring developer. The other extensions are set as partial for consistency.
Extensions.cs (base file)
public static partial class Extensions
{
/// <summary>
/// Mask SSN
/// </summary>
/// <param name="ssn">Valid SSN</param>
/// <param name="digitsToShow">How many digits to show on right which defaults to 4</param>
/// <param name="maskCharacter">Character to mask with which defaults to X</param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static partial string MaskSsn(this string ssn, int digitsToShow = 4, char maskCharacter = 'X');
/// <summary>
/// Capitalizes the first letter of the given string.
/// </summary>
/// <param name="input">The string to capitalize.</param>
/// <returns>A new string with the first letter capitalized. If the input is null or empty, the original string is returned.</returns>
public static partial string CapitalizeFirstLetter(this string? input);
public static partial bool IsInteger(this string sender);
public static partial bool IsNotInteger(this string sender);
}
Extensions.Code.cs (sub class)
public partial class Extensions
{
public static partial string MaskSsn(this string ssn, int digitsToShow, char maskCharacter)
{
if (string.IsNullOrWhiteSpace(ssn)) return string.Empty;
if (ssn.Contains('-')) ssn = ssn.Replace("-", string.Empty);
if (ssn.Length != 9) throw new ArgumentException("SSN invalid length");
if (ssn.IsNotInteger()) throw new ArgumentException("SSN not valid");
const int ssnLength = 9;
const string separator = "-";
int maskLength = ssnLength - digitsToShow;
int output = int.Parse(ssn.Replace(separator, string.Empty).Substring(maskLength, digitsToShow));
string format = string.Empty;
for (int index = 0; index < maskLength; index++) format += maskCharacter;
for (int index = 0; index < digitsToShow; index++) format += "0";
format = format.Insert(3, separator).Insert(6, separator);
format = $"{{0:{format}}}";
return string.Format(format, output);
}
public static partial string? CapitalizeFirstLetter(this string? input)
=> string.IsNullOrWhiteSpace(input) ?
input : char.ToUpper(input[0]) + input.AsSpan(1).ToString();
public static partial bool IsInteger(this string sender)
{
foreach (var c in sender)
if (c is < '0' or > '9') return false;
return true;
}
public static partial bool IsNotInteger(this string sender)
=> sender.IsInteger() == false;
}
Asynchronous partial methods
At the current time, we can set the return type as a Task, but we can not return a Task with a type. This means the following can not be set as a partial method.
internal class FileHelpers
{
const int ErrorSharingViolation = 32;
const int ErrorLockViolation = 33;
public static async Task<bool> CanReadFile(string fileName)
{
static bool IsFileLocked(Exception exception)
{
var errorCode = Marshal.GetHRForException(exception) & (1 << 16) - 1;
return errorCode is ErrorSharingViolation or ErrorLockViolation;
}
try
{
await using var fileStream = File.Open(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
fileStream?.Close();
}
catch (IOException ex)
{
if (IsFileLocked(ex))
{
return false;
}
}
await Task.Delay(50);
return true;
}
}
Summary
Basic and intermediate uses for partial classes and methods have been presented to clean up developer code. Make sure to check out the provided source code.
This content originally appeared on DEV Community and was authored by Karen Payne