C# 14 marks a thoughtful evolution in the language, focusing on expressiveness, modularity, and developer productivity. While not a radical overhaul, this release introduces subtle yet powerful enhancements that make everyday coding more intuitive and maintainable. Whether you're building high-performance systems or crafting elegant APIs, C# 14 helps you write less boilerplate and more meaningful logic.
In this guide, we'll explore the most impactful features of C# 14, with practical examples and insights into how they improve real-world development.
๐งญ C# 14 Feature Highlights
Here are the key features we'll be diving into:
- Extension Members — Add properties and indexers to existing types via extension blocks.
- Field Keyword in Properties — Access auto-generated backing fields directly with
field.
- Null-Conditional Assignment (
??=) — Assign only when the target is null.
- Unbound Generic Types in
nameof — Use open generic types for cleaner diagnostics.
- Implicit Span Conversions — Seamless transitions between
Span<T> and ReadOnlySpan<T>.
- Lambda Parameter Modifiers — Use
ref, in, or out in lambda expressions.
- Partial Events and Constructors — Modularize event and constructor logic across files.
- User-Defined Compound Assignment Operators — Customize behavior for
+=, -=, etc.
- Improved Interpolated Strings — Enhanced formatting and performance for string interpolation.
- Collection Expressions (Preview) — Concise syntax for initializing collections.
1. Extension Members
๐ง What Are Extension Members?
Traditionally, C# extension methods let you "add" methods to existing types without modifying them. But with C# 14, you can now define:
- Properties
- Indexers
- Events
- Even operators
…all inside an extension block scoped to a specific type.
๐งช Example: Extension Methods for IEnumerable<T>
public static class EnumerableExtensions
{
public static bool IsEmpty<T>(this IEnumerable<T> source)
=> !source.Any();
public static T? ElementAtOrDefaultSafe<T>(this IEnumerable<T> source, int index)
=> source.Skip(index).DefaultIfEmpty(default!).FirstOrDefault();
public static T? LastOrDefaultSafe<T>(this IEnumerable<T> source)
=> source.LastOrDefault();
}
๐ก Usage Example
var numbers = new[] { 1, 2, 3, 4, 5 };
if (!numbers.IsEmpty())
{
Console.WriteLine($"Third element: {numbers.ElementAtOrDefaultSafe(2)}");
Console.WriteLine($"Last element: {numbers.LastOrDefaultSafe()}");
}
๐ก Key Benefits:
- Cleaner API surface — Access properties and indexers naturally without method calls
- Better IntelliSense — IDE suggestions now include extension properties and indexers
- Type safety — Compile-time checks ensure proper usage
2. Field Keyword in Properties
๐ Direct Access to Backing Fields
The new field keyword allows you to access the auto-generated backing field directly in property accessors, eliminating the need to manually declare private fields.
๐งช Example: Smart Properties with Validation
public class Product
{
public string Name { get; set; }
public decimal Price
{
get => field;
set
{
if (value < 0)
throw new ArgumentException("Price cannot be negative");
field = value;
}
}
public int Quantity
{
get => field;
set
{
field = Math.Max(0, value);
Console.WriteLine($"Stock updated: {field} units");
}
}
}
๐ก Why This Matters: Before C# 14, you had to declare a separate backing field and manually manage it. Now, you can add validation and logging to auto-properties without the boilerplate!
3. Null-Conditional Assignment (??=)
๐ฏ Assign Only When Null
The ??= operator assigns a value to a variable only if it's currently null. This simplifies lazy initialization patterns.
private Logger? _logger;
public Logger Logger
{
get
{
_logger ??= new Logger();
return _logger;
}
}
Comparison with traditional approach:
if (_logger == null)
{
_logger = new Logger();
}
_logger ??= new Logger();
4. Unbound Generic Types in nameof
๐ What Are Unbound Generic Types?
Prior to C# 14, you couldn't use nameof with open generic types directly. You had to specify type parameters even when you just wanted the type's name. Now, you can reference unbound generic types cleanly.
๐งช Example: Cleaner Type Name References
string typeName = nameof(Dictionary<string, int>);
string typeName = nameof(Dictionary<,>);
string listName = nameof(List<>);
๐ก Real-World Usage: Generic Factory with Logging
public class GenericFactory<T> where T : new()
{
public T Create()
{
var factoryName = nameof(GenericFactory<>);
Console.WriteLine($"Creating instance in {factoryName}");
return new T();
}
}
public void ProcessRepository<TRepo, TEntity>()
{
try
{
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Failed in {nameof(ProcessRepository<,>)}", ex);
}
}
๐ก Key Benefits:
- Cleaner code — No need to provide arbitrary type arguments just to get a name
- Better refactoring — Generic type names update automatically when renamed
- Improved diagnostics — More readable error messages and logs
- API documentation — Reference generic types clearly in generated docs
๐ Use Cases
- Logging: Log generic type names without concrete type parameters
- Diagnostics: Create meaningful exception messages referencing generic methods
- Reflection: Build type metadata utilities more elegantly
- Code generation: Generate code based on generic type names
5. Implicit Span Conversions
⚡ Seamless Span Type Transitions
C# 14 introduces implicit conversions between Span<T> and ReadOnlySpan<T>, making it much easier to work with high-performance memory operations. You can now pass Span<T> to methods expecting ReadOnlySpan<T> without explicit casting.
๐งช Example: Before vs. After
void ProcessData(ReadOnlySpan<byte> data)
{
Console.WriteLine($"Processing {data.Length} bytes");
}
Span<byte> buffer = new byte[1024];
ProcessData(buffer);
ProcessData((ReadOnlySpan<byte>)buffer);
Span<byte> buffer = new byte[1024];
ProcessData(buffer);
๐ก Real-World Example: Stream Processing
public class DataProcessor
{
public int CalculateChecksum(ReadOnlySpan<byte> data)
{
int checksum = 0;
foreach (var b in data)
{
checksum ^= b;
}
return checksum;
}
public void EncryptData(Span<byte> data, byte key)
{
for (int i = 0; i < data.Length; i++)
{
data[i] ^= key;
}
}
public void ProcessStream()
{
Span<byte> buffer = stackalloc byte[512];
var checksum = CalculateChecksum(buffer);
EncryptData(buffer, 0x5A);
var newChecksum = CalculateChecksum(buffer);
Console.WriteLine($"Checksums: {checksum} → {newChecksum}");
}
}
๐ฏ Performance Benefits
public static bool ValidateData(ReadOnlySpan<char> input)
{
return input.Length > 0 && input[0] == '{' && input[^1] == '}';
}
string json = "{\"name\":\"John\"}";
if (ValidateData(json))
{
Console.WriteLine("Valid JSON structure");
}
Span<char> buffer = stackalloc char[100];
int length = FormatData(buffer);
if (ValidateData(buffer.Slice(0, length)))
{
Console.WriteLine("Buffer validated");
}
๐ก Key Benefits:
- Zero-copy operations — Pass data without allocations or copying
- Cleaner API design — Methods can accept ReadOnlySpan for immutability guarantees
- Better performance — Stack-allocated buffers with minimal overhead
- Improved interop — Easier integration with native code and unsafe contexts
- Type safety — Compiler enforces read-only vs. mutable semantics
๐ Common Patterns
public static int ParseNumber(ReadOnlySpan<char> text)
{
return int.Parse(text);
}
string input = "12345";
var number = ParseNumber(input);
void ProcessSegments(ReadOnlySpan<char> data)
{
var firstPart = data.Slice(0, 10);
var secondPart = data.Slice(10);
}
void SafeReadBuffer()
{
Span<byte> writeBuffer = new byte[100];
FillBuffer(writeBuffer);
VerifyBuffer(writeBuffer);
}
6. Lambda Parameter Modifiers
๐ Enhanced Lambda Expressions
C# 14 allows you to use parameter modifiers (ref, in, out) in lambda expressions, enabling more efficient parameter passing and better performance in high-throughput scenarios.
๐งช Example: Using ref in Lambdas
delegate void RefAction<T>(ref T value);
void ProcessWithRef(RefAction<int> action)
{
int value = 10;
action(ref value);
Console.WriteLine(value);
}
var incrementer = (ref int x) => x++;
int number = 5;
incrementer(ref number);
Console.WriteLine(number);
๐ก Real-World Example: High-Performance Data Processing
public class DataTransformer
{
public void TransformInPlace<T>(
List<T> items,
RefAction<T> transformer) where T : struct
{
for (int i = 0; i < items.Count; i++)
{
var temp = items[i];
transformer(ref temp);
items[i] = temp;
}
}
}
struct Point
{
public double X, Y;
}
var points = new List<Point> { new() { X = 1, Y = 2 } };
var transformer = new DataTransformer();
transformer.TransformInPlace(points, (ref Point p) =>
{
p.X *= 2;
p.Y *= 2;
});
๐ Using in for Read-Only References
struct Matrix4x4
{
public double[,] Data;
}
public delegate double InFunc<T>(in T value);
InFunc<Matrix4x4> calculateDeterminant =
(in Matrix4x4 matrix) =>
{
return matrix.Data[0, 0] * matrix.Data[1, 1];
};
var matrix = new Matrix4x4();
var det = calculateDeterminant(in matrix);
๐ฏ Using out for Output Parameters
public delegate bool TryParseFunc<T>(string input, out T result);
TryParseFunc<int> tryParse =
(string input, out int result) =>
{
return int.TryParse(input, out result);
};
if (tryParse("42", out var value))
{
Console.WriteLine($"Parsed: {value}");
}
public delegate bool TryValidate(string input, out string errorMessage);
TryValidate validateEmail =
(string email, out string errorMessage) =>
{
if (string.IsNullOrEmpty(email))
{
errorMessage = "Email cannot be empty";
return false;
}
if (!email.Contains("@"))
{
errorMessage = "Invalid email format";
return false;
}
errorMessage = string.Empty;
return true;
};
๐ก Key Benefits:
- Better performance — Avoid copying large structs
- More expressive — Write functional code with side effects when needed
- Cleaner APIs — Use lambdas in more scenarios without wrapper methods
- Type safety — Compiler enforces parameter semantics
7. Partial Events and Constructors
๐ Modular Code Organization
C# 14 extends the partial keyword to events and constructors, allowing you to split their implementation across multiple files. This is especially useful for generated code scenarios and large class implementations.
๐งช Example: Partial Constructors
public partial class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public partial Person(string firstName, string lastName);
}
public partial class Person
{
public partial Person(string firstName, string lastName)
{
if (string.IsNullOrWhiteSpace(firstName))
throw new ArgumentException(nameof(firstName));
if (string.IsNullOrWhiteSpace(lastName))
throw new ArgumentException(nameof(lastName));
FirstName = firstName;
LastName = lastName;
Age = 0;
InitializeGeneratedProperties();
}
partial void InitializeGeneratedProperties();
}
๐ก Example: Partial Events
public partial class DataService
{
public partial event EventHandler<DataChangedEventArgs> DataChanged;
public void UpdateData(string newData)
{
ProcessData(newData);
DataChanged?.Invoke(this, new DataChangedEventArgs(newData));
}
private void ProcessData(string data) { }
}
public partial class DataService
{
public partial event EventHandler<DataChangedEventArgs> DataChanged
{
add
{
Console.WriteLine($"Subscriber added to DataChanged event");
field += value;
}
remove
{
Console.WriteLine($"Subscriber removed from DataChanged event");
field -= value;
}
}
}
๐ฏ Real-World Use Case: Separating Generated Code
public partial class UserViewModel
{
private string _username;
public partial event PropertyChangedEventHandler PropertyChanged;
public string Username
{
get => _username;
set
{
if (_username != value)
{
_username = value;
OnPropertyChanged(nameof(Username));
}
}
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public partial class UserViewModel
{
public partial UserViewModel(
ILogger logger,
IDataService dataService);
public partial UserViewModel(
ILogger logger,
IDataService dataService)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
_logger.LogInformation("UserViewModel created");
}
public partial event PropertyChangedEventHandler PropertyChanged
{
add { _weakEventManager.AddHandler(value); }
remove { _weakEventManager.RemoveHandler(value); }
}
private readonly WeakEventManager _weakEventManager = new();
private readonly ILogger _logger;
private readonly IDataService _dataService;
}
๐ก Key Benefits:
- Clean separation — Keep generated code separate from hand-written code
- Better maintainability — Organize large classes across multiple files
- Source generators — Perfect for code generation scenarios
- Team collaboration — Different team members can work on different aspects
8. User-Defined Compound Assignment Operators
๐ง Customize Compound Operations
C# 14 allows you to define custom behavior for compound assignment operators like +=, -=, *=, etc. This enables more intuitive APIs for custom types.
๐งช Example: Custom Vector Type
public struct Vector3
{
public double X, Y, Z;
public Vector3(double x, double y, double z)
{
X = x; Y = y; Z = z;
}
public static Vector3 operator +(Vector3 a, Vector3 b)
=> new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vector3 operator *(Vector3 v, double scalar)
=> new(v.X * scalar, v.Y * scalar, v.Z * scalar);
}
var velocity = new Vector3(1, 2, 3);
var acceleration = new Vector3(0.1, 0.2, 0.3);
velocity += acceleration;
velocity *= 0.5;
๐ก Example: StringBuilder-like Builder Pattern
public class QueryBuilder
{
private StringBuilder _query = new();
public static QueryBuilder operator +(QueryBuilder builder, string clause)
{
if (builder._query.Length > 0)
builder._query.Append(" ");
builder._query.Append(clause);
return builder;
}
public static QueryBuilder operator &(QueryBuilder builder, string condition)
{
if (!builder._query.ToString().Contains("WHERE"))
builder._query.Append(" WHERE ");
else
builder._query.Append(" AND ");
builder._query.Append(condition);
return builder;
}
public override string ToString() => _query.ToString();
}
var query = new QueryBuilder();
query += "SELECT * FROM Users";
query &= "Age > 18";
query &= "IsActive = 1";
query += "ORDER BY Name";
Console.WriteLine(query);
๐ฏ Example: Collection Builder with Validation
public class ValidatedCollection<T>
{
private List<T> _items = new();
private Func<T, bool> _validator;
public ValidatedCollection(Func<T, bool> validator)
{
_validator = validator;
}
public static ValidatedCollection<T> operator +(
ValidatedCollection<T> collection, T item)
{
if (collection._validator(item))
{
collection._items.Add(item);
Console.WriteLine($"✓ Added: {item}");
}
else
{
Console.WriteLine($"✗ Rejected: {item}");
}
return collection;
}
public static ValidatedCollection<T> operator -(
ValidatedCollection<T> collection, T item)
{
collection._items.Remove(item);
return collection;
}
public int Count => _items.Count;
}
var numbers = new ValidatedCollection<int>(x => x > 0);
numbers += 5;
numbers += -3;
numbers += 10;
๐ก Key Benefits:
- Intuitive syntax — Make custom types behave like built-in types
- Performance — Optimize in-place operations without temporary allocations
- Fluent APIs — Create chainable, readable code
- Domain modeling — Express domain concepts naturally
9. Improved Interpolated Strings
๐ Enhanced String Formatting
C# 14 brings significant improvements to string interpolation, including better performance, enhanced formatting options, and support for custom interpolated string handlers.
๐งช Example: Advanced Formatting
var price = 1234.567;
var date = new DateTime(2025, 11, 3);
var formatted = $"Price: {price,15:C2} | Date: {date:yyyy-MM-dd}";
var stock = 5;
var message = $"Stock: {stock} ({(stock > 0 ? \"Available\" : \"Out of stock\")})";
var items = new[] { "Apple", "Banana", "Cherry" };
var list = $"Items: {string.Join(", ", items.Select(x => $"'{x}'"))}";
๐ก Performance: Custom Interpolated String Handlers
[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct SqlInterpolatedStringHandler
{
private System.Text.StringBuilder _builder;
private System.Collections.Generic.List<object> _parameters;
public SqlInterpolatedStringHandler(int literalLength, int formattedCount)
{
_builder = new System.Text.StringBuilder(literalLength);
_parameters = new System.Collections.Generic.List<object>(formattedCount);
}
public void AppendLiteral(string s) => _builder.Append(s);
public void AppendFormatted<T>(T value)
{
_parameters.Add(value);
_builder.Append($"@p{_parameters.Count - 1}");
}
public (string Query, object[] Parameters) GetResult()
=> (_builder.ToString(), _parameters.ToArray());
}
public static class SqlQuery
{
public static (string Query, object[] Parameters) Create(
SqlInterpolatedStringHandler handler)
{
return handler.GetResult();
}
}
var userId = 42;
var userName = "John";
var (query, parameters) = SqlQuery.Create(
$"SELECT * FROM Users WHERE Id = {userId} AND Name = {userName}");
Console.WriteLine(query);
Console.WriteLine(string.Join(", ", parameters));
๐ฏ Example: Logging with Conditional Evaluation
public enum LogLevel { Debug = 1, Info = 2, Warn = 3, Error = 4 }
public class SmartLogger
{
private LogLevel _currentLevel = LogLevel.Info;
public void Log(
LogLevel level,
[System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute("this", "level")]
LogInterpolatedStringHandler handler)
{
if (level >= _currentLevel)
{
Console.WriteLine(handler.ToString());
}
}
}
[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct LogInterpolatedStringHandler
{
private System.Text.StringBuilder? _builder;
private bool _enabled;
public LogInterpolatedStringHandler(
int literalLength,
int formattedCount,
SmartLogger logger,
LogLevel level,
out bool isEnabled)
{
_enabled = level >= logger._currentLevel;
isEnabled = _enabled;
_builder = _enabled ? new System.Text.StringBuilder(literalLength) : null;
}
public void AppendLiteral(string s)
{
if (_enabled) _builder!.Append(s);
}
public void AppendFormatted<T>(T value)
{
if (_enabled) _builder!.Append(value);
}
public override string ToString() => _builder?.ToString() ?? string.Empty;
}
var logger = new SmartLogger();
var data = new List<int> { 1, 2, 3 };
string ExpensiveOperation(IEnumerable<int> items)
{
return string.Join(",", items.Select(i => (i * i).ToString()));
}
logger.Log(LogLevel.Debug,
$"Processing {data.Count} items, details: {ExpensiveOperation(data)}");
๐ Real-World Example: URL Builder
public class UrlBuilder
{
public static string Build(UrlInterpolatedStringHandler handler)
=> handler.ToString();
}
[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct UrlInterpolatedStringHandler
{
private System.Text.StringBuilder _builder;
public UrlInterpolatedStringHandler(int literalLength, int formattedCount)
{
_builder = new System.Text.StringBuilder(literalLength);
}
public void AppendLiteral(string s) => _builder.Append(s);
public void AppendFormatted(string value)
{
_builder.Append(Uri.EscapeDataString(value ?? string.Empty));
}
public override string ToString() => _builder.ToString();
}
var searchTerm = "C# programming";
var category = "Books & Media";
var url = UrlBuilder.Build(
$"https://example.com/search?q={searchTerm}&category={category}");
Console.WriteLine(url);
๐ก Key Benefits:
- Zero-allocation — Custom handlers can optimize memory usage
- Conditional evaluation — Skip expensive operations when not needed
- Type safety — Enforce formatting rules at compile time
- Domain-specific — Create specialized string builders (SQL, URLs, etc.)
- Better performance — Reduced allocations in hot paths
10. Collection Expressions (Preview)
๐ฆ Concise Collection Initialization
C# 14 introduces collection expressions, providing a unified, concise syntax for creating and initializing collections of any type. This feature simplifies code and makes collection creation more intuitive.
๐งช Example: Basic Collection Expressions
var oldList = new List<int> { 1, 2, 3, 4, 5 };
var oldArray = new int[] { 1, 2, 3, 4, 5 };
List<int> list = [1, 2, 3, 4, 5];
int[] array = [1, 2, 3, 4, 5];
Span<int> span = [1, 2, 3, 4, 5];
List<string> empty = [];
int[] emptyArray = [];
๐ก Example: Spread Operator
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] third = [7, 8, 9];
int[] combined = [..first, ..second, ..third];
int[] mixed = [0, ..first, 10, ..second, 20];
var numbers = [1, 2, 3, 4, 5];
var doubled = [..numbers.Select(x => x * 2)];
๐ฏ Example: Pattern Matching with Collections
string DescribeCollection(int[] numbers) => numbers switch
{
[] => "Empty collection",
[var single] => $"Single element: {single}",
[var first, var second] => $"Two elements: {first}, {second}",
[var first, .., var last] => $"First: {first}, Last: {last}",
_ => $"Collection with {numbers.Length} elements"
};
Console.WriteLine(DescribeCollection([]));
Console.WriteLine(DescribeCollection([42]));
Console.WriteLine(DescribeCollection([1, 2]));
Console.WriteLine(DescribeCollection([1, 2, 3, 4]));
๐ฅ Real-World Example: API Response Building
public record ApiResponse(string Status, object[] Data);
public class UserService
{
public ApiResponse GetUsers(bool includeAdmin, bool includeGuests)
{
var regularUsers = GetRegularUsers();
var adminUsers = includeAdmin ? GetAdminUsers() : [];
var guestUsers = includeGuests ? GetGuestUsers() : [];
return new ApiResponse(
"success",
[
..regularUsers,
..adminUsers,
..guestUsers
]
);
}
private object[] GetRegularUsers() => [
new { Id = 1, Name = "Alice", Role = "User" },
new { Id = 2, Name = "Bob", Role = "User" }
];
private object[] GetAdminUsers() => [
new { Id = 100, Name = "Admin", Role = "Admin" }
];
private object[] GetGuestUsers() => [
new { Id = 200, Name = "Guest", Role = "Guest" }
];
}
๐ Example: Building Test Data
public class Product
{
public string Name { get; set; } = string.Empty;
public int Price { get; set; }
}
public static class TestDataBuilder
{
public static List<Product> CreateTestProducts()
{
var electronics = CreateElectronics();
var books = CreateBooks();
var featured = new Product { Name = "Featured Item", Price = 999 };
return [featured, ..electronics, ..books];
}
private static Product[] CreateElectronics() => [
new Product { Name = "Laptop", Price = 1200 },
new Product { Name = "Phone", Price = 800 }
];
private static Product[] CreateBooks() => [
new Product { Name = "C# Guide", Price = 45 },
new Product { Name = "Design Patterns", Price = 55 }
];
}
int Sum(int[] numbers) => numbers.Sum();
Console.WriteLine(Sum([1,2,3]));
Console.WriteLine(Sum([10,20,30]));
⚡ Performance Example: Stack Allocation
public static void ProcessData()
{
Span<int> numbers = [1, 2, 3, 4, 5];
foreach (var num in numbers)
{
Console.WriteLine(num * 2);
}
Span<byte> buffer = stackalloc byte[256];
Span<byte> header = [0xFF, 0xFE, 0xFD];
header.CopyTo(buffer);
}
๐ก Key Benefits:
- Unified syntax — Same syntax for arrays, lists, spans, and more
- More readable — Cleaner, less verbose collection initialization
- Spread operator — Easy collection combination and manipulation
- Pattern matching — Powerful collection destructuring
- Performance — Works with stack-allocated spans
- Type inference — Compiler figures out the target type
๐ Conclusion
C# 14 continues the language's tradition of thoughtful, developer-friendly improvements. From extension members that extend types more naturally, to the field keyword that eliminates boilerplate, these features help you write code that's both more expressive and maintainable.
The addition of lambda parameter modifiers enables more efficient functional programming patterns, while partial events and constructors improve code organization in large projects. User-defined compound assignment operators make custom types feel more natural, and improved interpolated strings provide both performance and flexibility.
Finally, collection expressions bring a modern, unified syntax for working with collections, making code more concise and readable. As you adopt C# 14 in your projects, you'll find these small enhancements add up to significant productivity gains. The language continues to evolve in ways that respect existing code while opening new possibilities for elegant solutions.
๐ Next Steps:
- Experiment with these features in your own projects
- Explore the official C# 14 documentation for more details
- Share your experiences and patterns with the community
- Stay tuned for more advanced use cases and best practices
Happy coding with C# 14! ๐