Upgrading from 1.x
Most 1.x code still compiles against 2.0. But some methods now return different results, two classes have new names, the target frameworks changed and the package no longer has dependencies. This page lists every breaking change by area, with the 1.x code next to the 2.0 code. Read it once when you move to version 2.0.
At a glance
| Method | What it does |
|---|---|
| Target frameworks | netstandard2.1 and net10.0 replace the 1.x builds |
| Dependencies | No third-party packages; System.Text.Json on netstandard2.1 only |
StringExtensions, EnumerableExtensions | The misspelled 1.x class names are fixed; static calls must be renamed |
ToPascalCase, ToCamelCase | Normalize every word, not just the first character |
ToSentenceCase | Capitalizes the first word |
ToCamelCase(bool, bool) | Obsolete; acronyms are always handled |
Contains, StartsWith, EndsWith, *IgnoreCase | Throw for a null value to look for; compare ordinally |
ContainsIgnoreCase(StringComparison, ...) | Obsolete; use ContainsAny(comparison, ...) |
EqualsIgnoreCase | Two null strings are equal |
Left | Throws ArgumentOutOfRangeException when the length is too large |
FirstCharToUpper | Returns "" for an empty string |
ReplaceFirst | Throws ArgumentException for an empty search value |
ToEnum<T> | The constraint is now where T : struct, Enum |
AddFirst, AddLast, InsertBefore, InsertAfter, Replace* | Accept null items |
MoveItem | Returns bool and works on any IList<T> |
ToObject<T> | Takes IDictionary<string, object?>, ignores unknown keys, converts values |
IDictionary.ToQueryString | Percent-encodes, uses the invariant culture and ISO 8601 dates |
NameValueCollection.ToQueryString | Writes spaces as %20; the pairs overload takes any IEnumerable |
object.ToQueryString | New collectionSeparator parameter; repeats keys by default |
ToJsonString | Uses System.Text.Json instead of Newtonsoft.Json |
Is(Type) | True for derived types too |
IsNumber | Returns false for null |
IsIn, If | Accept a null receiver |
GetPropertyValue, SetPropertyValue | Throw ArgumentException for unknown or read-only properties |
IsComplex | DateTimeOffset, TimeSpan, Uri, DateOnly, TimeOnly are simple values |
| Invalid arguments | Always ArgumentNullException / ArgumentOutOfRangeException with the parameter name |
Upgrade checklist
- Change the package version to
2.0.0. Check that your target framework is still supported (Target frameworks). - Build. Fix the compile errors from the class renames,
ToEnum<T>,object.ToQueryStringand any package you used through this one (Dependencies). - Fix the
CS0618obsolete warnings (ToCamelCase(bool, bool),ContainsIgnoreCase(StringComparison, ...)). If your project treats warnings as errors, these stop the build. - Search for the methods whose results changed. Check what depends on their output: stored data, URLs, JSON contracts and tests.
- Rebuild every library of yours that references the package. Assemblies compiled against 1.x are not binary compatible with 2.0.
This search finds most call sites that need a look:
git grep -nE "StringExtentions|EnumerableExtenstions|ToPascalCase|ToCamelCase|ToSentenceCase|ToJsonString|ToQueryString|ToObject<|MoveItem|IsComplex|ReplaceFirst|EqualsIgnoreCase"
In compiled code an extension method call is a static call on its class, such as
StringExtentions.ToKebabCase(name). A library built against 1.x still refers to the old class names and the old
MoveItem return type. Run against 2.0, it fails with TypeLoadException or MissingMethodException. Rebuild
such libraries against 2.0.
Package and platform
Target frameworks
The 1.x source targeted netstandard2.1, netcoreapp3.0 and net5.0, but the 1.0.0 package on nuget.org contains
a single netcoreapp3.1 build. 2.0 ships netstandard2.1 and net10.0.
NuGet picks the closest build for your app, so every .NET Core 3.0 or later app can still install the package:
| Your app targets | Build it uses |
|---|---|
| .NET Core 3.0, 3.1, .NET 5 to .NET 9 | netstandard2.1 |
| .NET 10 and later | net10.0 |
| .NET Framework | Not supported: it cannot run netstandard2.1 (the same as in 1.x) |
<PackageReference Include="Masterly.Extensions.Core" Version="1.0.0" />
<PackageReference Include="Masterly.Extensions.Core" Version="2.0.0" />
The net10.0 build is trim- and Native AOT-compatible. Reflection and JSON members are annotated, so the trim
analyzer warns where you call them. See Trimming and AOT.
Dependencies
1.x depended on Ardalis.GuardClauses, JetBrains.Annotations and Newtonsoft.Json. 2.0 has no third-party
dependencies. On netstandard2.1 it references System.Text.Json. On net10.0 it references nothing.
Did your code use one of those packages without referencing it? Then it compiled in 1.x only because the package
came in through this one. In 2.0 those using directives fail with CS0246. Add the package yourself:
<PackageReference Include="Masterly.Extensions.Core" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
Class names
Two 1.x classes had misspelled names: StringExtentions and EnumerableExtenstions. 2.0 names them
StringExtensions and EnumerableExtensions. They stay in the System and System.Collections.Generic
namespaces.
Extension-method calls such as name.ToKebabCase() compile without changes. Only code that names the class must
change: static calls, using static directives, and nameof or typeof expressions.
using static System.StringExtentions;
List<Order> orders = new List<Order>();
string slug = StringExtentions.ToKebabCase("OrderLineItem");
bool noOrders = EnumerableExtenstions.IsEmpty(orders);
using static System.StringExtensions;
List<Order> orders = new List<Order>();
string slug = StringExtensions.ToKebabCase("OrderLineItem"); // → "order-line-item"
bool noOrders = EnumerableExtensions.IsEmpty(orders); // → true
// Simpler: call them as extension methods, which never name the class.
string sameSlug = "OrderLineItem".ToKebabCase(); // → "order-line-item"
bool stillNoOrders = orders.IsEmpty(); // → true
String casing
ToPascalCase and ToCamelCase
In 1.x these methods changed only the first character. In 2.0 they split the text into words (at case changes,
digits, _, - and spaces) and rebuild it. Acronyms become normal words, and separators are removed.
string column = "customer_first_name".ToPascalCase(); // → "Customer_first_name"
string header = "HTTPResponse".ToPascalCase(); // → "HTTPResponse"
string field = "HTTPResponse".ToCamelCase(); // → "hTTPResponse"
string key = "CustomerID".ToCamelCase(); // → "customerID"
string column = "customer_first_name".ToPascalCase(); // → "CustomerFirstName"
string header = "HTTPResponse".ToPascalCase(); // → "HttpResponse"
string field = "HTTPResponse".ToCamelCase(); // → "httpResponse"
string key = "CustomerID".ToCamelCase(); // → "customerId"
Input that is already in the target style usually stays the same: "OrderId".ToCamelCase() returns "orderId" in
both versions. null, empty and white-space input is still returned as is.
Do you build JSON property names, column names, cache keys or URLs with these methods? Compare the old and the new
output. For example, "CustomerID".ToCamelCase() used to return "customerID" and now returns "customerId".
1.x ToPascalCase upper-cased the first character with the invariant culture. FirstCharToUpper() does the same
for a non-null string: "customer_first_name".FirstCharToUpper() returns "Customer_first_name". Unlike
ToPascalCase, it throws ArgumentNullException for null.
More examples are on String casing.
ToSentenceCase
In 1.x, ToSentenceCase kept the first character's case and split words only at a lower-case to upper-case
change. In 2.0 it uses the same word splitting as the other casing methods and capitalizes the first word.
string label = "orderStatus".ToSentenceCase(); // → "order status"
string title = "HTTPResponse".ToSentenceCase(); // → "HTTPResponse"
string text = "ThisIsSampleSentence".ToSentenceCase(); // → "This is sample sentence"
string label = "orderStatus".ToSentenceCase(); // → "Order status"
string title = "HTTPResponse".ToSentenceCase(); // → "Http response"
string text = "ThisIsSampleSentence".ToSentenceCase(); // → "This is sample sentence"
ToCamelCase(bool, bool)
1.x had ToCamelCase(useCurrentCulture = false, handleAbbreviations = false). With handleAbbreviations: true,
an all-upper-case string was lowered completely. 2.0 always handles acronyms. The three-argument overload is
marked [Obsolete] and ignores the flag.
string code = "HTTP".ToCamelCase(false, true); // → "http"
string sameCode = "HTTP".ToCamelCase(handleAbbreviations: true); // → "http"
string code = "HTTP".ToCamelCase(); // → "http"
string localCode = "HTTP".ToCamelCase(useCurrentCulture: true); // → "http"
"HTTP".ToCamelCase(false, true) still compiles in 2.0, with warning CS0618. A call that names only
handleAbbreviations: does not compile, because the obsolete overload has no default values. Remove the argument.
String predicates
Contains, StartsWith, EndsWith and the IgnoreCase variants
In 1.x every string predicate caught all exceptions and returned false. A null search value looked like
"no match". In 2.0 the rules are:
- A
nullstring (the receiver) still returnsfalse. - A
nullvalue to look for, or anullarray, throwsArgumentNullExceptionwithParamName"toCheck". - All comparisons are ordinal. In 1.x the
StartsWith(params string[])andEndsWith(params string[])overloads used a culture-sensitive comparison.
These rules cover Contains, ContainsIgnoreCase, StartsWith, StartsWithIgnoreCase, EndsWith and
EndsWithIgnoreCase.
string customerNote = null;
string couponCode = null;
bool flagged = customerNote.Contains("refund", "chargeback"); // → false
bool hasCoupon = "Invoice overdue".Contains("paid", couponCode); // → false (the exception was swallowed)
string? customerNote = null;
string? couponCode = null;
bool flagged = customerNote.Contains("refund", "chargeback"); // → false
bool hasCoupon = "Invoice overdue".Contains("paid", couponCode!); // throws ArgumentNullException ("toCheck")
text.Contains("paid") and text.StartsWith("INV") with a single string call the string instance methods,
not these extensions, in both versions. They throw NullReferenceException when text is null. To get the
extension and its null-receiver rule, pass two or more values, or use the IgnoreCase methods.
See String predicates for every overload.
ContainsIgnoreCase with a comparison
The overloads ContainsIgnoreCase(StringComparison, string) and
ContainsIgnoreCase(StringComparison, params string[]) are obsolete. The name says "ignore case", but the
argument can say otherwise. Use ContainsAny, which takes the same arguments.
string ticketTitle = "Customer requests a REFUND";
bool isRefund = ticketTitle.ContainsIgnoreCase(StringComparison.OrdinalIgnoreCase, "refund", "chargeback");
string ticketTitle = "Customer requests a REFUND";
bool isRefund = ticketTitle.ContainsAny(StringComparison.OrdinalIgnoreCase, "refund", "chargeback"); // → true
bool sameCheck = ticketTitle.ContainsIgnoreCase("refund", "chargeback"); // → true
The ContainsIgnoreCase overloads without a StringComparison are not obsolete.
EqualsIgnoreCase
In 1.x, EqualsIgnoreCase on a null string returned false, even when the other string was also null. In
2.0 it calls string.Equals(a, b, StringComparison.OrdinalIgnoreCase), so two null strings are equal.
string billingEmail = null;
string shippingEmail = null;
bool same = billingEmail.EqualsIgnoreCase(shippingEmail); // → false
string? billingEmail = null;
string? shippingEmail = null;
bool same = billingEmail.EqualsIgnoreCase(shippingEmail); // → true
bool oneMissing = billingEmail.EqualsIgnoreCase("amal@example.com"); // → false
String manipulation
Left
In 1.x, Left(len) with a length larger than the string threw ArgumentException. In 2.0 it throws
ArgumentOutOfRangeException with ParamName "len", and it also rejects a negative length. Right follows the
same rule.
string prefix;
try
{
prefix = "INV".Left(10);
}
catch (ArgumentException)
{
prefix = "INV";
}
string invoiceNumber = "INV-2024-0042";
string prefix = invoiceNumber.Left(3); // → "INV"
string tooLong = "INV".Left(10); // throws ArgumentOutOfRangeException ("len")
ArgumentOutOfRangeException derives from ArgumentException, so the 1.x catch block above still works. A
check for the exact type, such as ex.GetType() == typeof(ArgumentException), does not.
FirstCharToUpper
In 1.x, "".FirstCharToUpper() threw ArgumentException. In 2.0 it returns the empty string. A null input still
throws ArgumentNullException.
string city = "";
string display = city.Length == 0 ? city : city.FirstCharToUpper();
string city = "";
string display = city.FirstCharToUpper(); // → ""
string capital = "amman".FirstCharToUpper(); // → "Amman"
ReplaceFirst
In 1.x an empty search value matched at position 0, so the replacement was inserted at the start. 2.0 throws
ArgumentException with ParamName "search". A null replacement is now allowed and removes the match.
string sku = "order".ReplaceFirst("", "x"); // → "xorder"
string sku = "order".ReplaceFirst("", "x"); // throws ArgumentException ("search")
string reference = "order-123-123".ReplaceFirst("123", "456"); // → "order-456-123"
Parsing
ToEnum
1.x ToEnum<T> had the constraint where T : struct, so "5".ToEnum<int>() compiled and failed at run time. 2.0
uses where T : struct, Enum. Calls with a concrete enum type are unchanged. A generic method that passes on its
own T must add the Enum constraint.
public static T ParseStatus<T>(string text)
where T : struct
{
return text.ToEnum<T>(ignoreCase: true);
}
public static T ParseStatus<T>(string text)
where T : struct, Enum
{
return text.ToEnum<T>(ignoreCase: true);
}
OrderStatus status = ParseStatus<OrderStatus>("paid"); // → OrderStatus.Paid
Lists
Null items
In 1.x, AddFirst, AddLast, InsertBefore, InsertAfter, ReplaceWhile and ReplaceOne threw
ArgumentNullException for a null item, and for a null existing item. In 2.0 they accept null, as
IList<T>.Insert does. Only the list and the delegates are checked.
List<string> reviewers = new List<string> { "amal", "omar" };
reviewers.AddFirst(null); // throws ArgumentNullException ("item")
List<string?> reviewers = new List<string?> { "amal", "omar" };
reviewers.AddFirst(null);
reviewers.InsertAfter("amal", null);
// reviewers → [null, "amal", null, "omar"]
If you relied on the exception to keep null out of a list, check the item yourself before the call.
ReplaceOne(item, replaceWith) now finds the item by equality (IList<T>.IndexOf). In 1.x it used
Comparer<T>.Default, which throws for types that do not implement IComparable.
MoveItem
In 1.x, MoveItem returned void, was defined on List<T> only, and threw ArgumentOutOfRangeException when no
item matched. 2.0 returns bool: true when an item was found, false when nothing matched. It works on any
IList<T> that supports inserting and removing, such as Collection<T> or ObservableCollection<T>.
List<string> steps = new List<string> { "draft", "review", "publish" };
if (steps.Contains("archive"))
{
steps.MoveItem(step => step == "archive", 0);
}
List<string> steps = new List<string> { "draft", "review", "publish" };
bool moved = steps.MoveItem(step => step == "publish", 0); // → true
// steps → ["publish", "draft", "review"]
bool missing = steps.MoveItem(step => step == "archive", 0); // → false
A targetIndex outside the list still throws ArgumentOutOfRangeException. Code that ignores the result
compiles unchanged, but it must be recompiled (see the checklist). See
Lists and collections.
Dictionaries and query strings
The examples in this section and the next ones use these types:
public sealed class Invoice
{
public string? Number { get; set; }
public decimal Total { get; set; }
}
public sealed class CustomerFilter
{
public string? City { get; set; }
}
public sealed class OrderFilter
{
public string? Status { get; set; }
public int[]? Ids { get; set; }
public CustomerFilter? Customer { get; set; }
}
ToObject
ToObject<T> now extends IDictionary<string, object?> instead of IDictionary<string, object>. It also:
- accepts an empty dictionary and returns a new
Twith default values (1.x threwArgumentException); - ignores keys that match no public writable property (1.x threw
NullReferenceException); - converts each value to the property type with the invariant culture, so
"99.5"fills adecimal(1.x threwArgumentExceptionwhen the types did not match).
IDictionary<string, object> row = new Dictionary<string, object>
{
["Number"] = "INV-7",
["Total"] = 99.5m,
};
Invoice invoice = row.ToObject<Invoice>();
IDictionary<string, object?> row = new Dictionary<string, object?>
{
["Number"] = "INV-7",
["Total"] = "99.5",
["Source"] = "csv",
};
Invoice invoice = row.ToObject<Invoice>();
// invoice.Number → "INV-7"
// invoice.Total → 99.5
Invoice blank = new Dictionary<string, object?>().ToObject<Invoice>();
// blank.Number → null
A value that cannot be converted throws FormatException ("abc" for a decimal) or InvalidCastException (no
conversion exists, or null for a non-nullable value type). Declare your dictionaries with object? values to
match the new signature. See Dictionaries.
IDictionary.ToQueryString
1.x joined key=value pairs as they were: no encoding, numbers in the current culture, DateTime as
MM/dd/yyyy HH:mm:ss. An empty dictionary threw ArgumentException. 2.0:
- percent-encodes keys and values (spaces become
%20); - formats numbers with the invariant culture;
- writes
DateTimeandDateTimeOffsetin the ISO 8601 round-trip format; - returns
""for an empty dictionary; - writes a
nullvalue askey=, and repeats the key for each item of a collection value.
Dictionary<string, object> query = new Dictionary<string, object>
{
["customer"] = "John Doe",
["total"] = 1.5m,
["since"] = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc),
};
string queryString = query.ToQueryString();
// → "customer=John Doe&total=1,5&since=01/02/2024 03:04:05"
Dictionary<string, object?> query = new Dictionary<string, object?>
{
["customer"] = "John Doe",
["total"] = 1.5m,
["since"] = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc),
};
string queryString = query.ToQueryString();
// → "customer=John%20Doe&total=1.5&since=2024-01-02T03%3A04%3A05.0000000Z"
Dictionary<string, object?> tagged = new Dictionary<string, object?>
{
["tags"] = new[] { "urgent", "vip" },
["note"] = null,
};
string tags = tagged.ToQueryString(); // → "tags=urgent&tags=vip¬e="
If you called Uri.EscapeDataString on values before passing them in, remove that call. Otherwise the values are
encoded twice (%20 becomes %2520). Also check that the receiving server reads ISO 8601 dates. ASP.NET Core
model binding does.
NameValueCollection.ToQueryString
1.x encoded with HttpUtility.UrlEncode, which writes a space as +. 2.0 writes %20. The second overload took a
KeyValuePair<string, string>[] in 1.x. In 2.0 it takes any IEnumerable<KeyValuePair<string, string?>>, so a
Dictionary<string, string?> works directly.
NameValueCollection search = new NameValueCollection { { "q", "red shoes" } };
string fromCollection = search.ToQueryString(); // → "q=red+shoes"
KeyValuePair<string, string>[] pairs = { new KeyValuePair<string, string>("q", "red shoes") };
string fromPairs = pairs.ToQueryString(); // → "q=red+shoes"
NameValueCollection search = new NameValueCollection { { "q", "red shoes" } };
string fromCollection = search.ToQueryString(); // → "q=red%20shoes"
Dictionary<string, string?> pairs = new Dictionary<string, string?> { ["q"] = "red shoes" };
string fromPairs = pairs.ToQueryString(); // → "q=red%20shoes"
Servers decode both forms to the same text. The change matters when you compare query strings as text, for example in tests or signed URLs.
object.ToQueryString
The parameter separator (default "&") is now collectionSeparator (default null):
nullrepeats the key for each collection value:Ids=1&Ids=2. ASP.NET Core binds this to an array.- Any other value joins the collection into one encoded value, as 1.x did.
- Nested objects are written as
Parent.Child=..., and lists of objects asItems[0].Name=...(the brackets are percent-encoded:Items%5B0%5D.Name=...). - Values use the invariant culture and ISO 8601 dates. A graph deeper than 16 levels, usually a cycle, throws
InvalidOperationException.
OrderFilter filter = new OrderFilter
{
Status = "open",
Ids = new[] { 1, 2 },
Customer = new CustomerFilter { City = "Amman" },
};
string byDefault = filter.ToQueryString();
// → "Status=open&Ids=1%262&Customer.City=Amman"
string commaSeparated = filter.ToQueryString(separator: ",");
// → "Status=open&Ids=1%2C2&Customer.City=Amman"
OrderFilter filter = new OrderFilter
{
Status = "open",
Ids = new[] { 1, 2 },
Customer = new CustomerFilter { City = "Amman" },
};
string byDefault = filter.ToQueryString();
// → "Status=open&Ids=1&Ids=2&Customer.City=Amman"
string commaSeparated = filter.ToQueryString(collectionSeparator: ",");
// → "Status=open&Ids=1%2C2&Customer.City=Amman"
string asInVersion1 = filter.ToQueryString("&");
// → "Status=open&Ids=1%262&Customer.City=Amman"
A positional argument such as filter.ToQueryString(",") compiles in both versions. A named separator: argument
does not compile in 2.0 (CS1739). See Objects for the full format.
Objects
ToJsonString
1.x serialized with Newtonsoft.Json. 2.0 uses System.Text.Json and takes an optional JsonSerializerOptions. The
output can differ:
- HTML-sensitive characters (
<,>,&,',") and non-ASCII characters are escaped as\uXXXX. - Public fields are not serialized; only properties are.
- A reference cycle throws
JsonException. - Newtonsoft attributes such as
[JsonProperty]are ignored. Use[JsonPropertyName]and[JsonIgnore]fromSystem.Text.Json.Serialization.
object order = new { Name = "Café <Bistro>", Total = 12.5m };
string json = order.ToJsonString();
{"Name":"Café <Bistro>","Total":12.5}
using System.Text.Encodings.Web;
using System.Text.Json;
object order = new { Name = "Café <Bistro>", Total = 12.5m };
string json = order.ToJsonString();
JsonSerializerOptions relaxed = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
string readable = order.ToJsonString(relaxed);
{"Name":"Caf\u00E9 \u003CBistro\u003E","Total":12.5}
{"Name":"Café <Bistro>","Total":12.5}
The escaped and the unescaped forms are both valid JSON and parse to the same values. Use the relaxed encoder only
when the JSON is not written into HTML. A null object still returns "null". On net10.0, ToJsonString is
marked [RequiresUnreferencedCode] and [RequiresDynamicCode]. See Trimming and AOT
and JSON and safe logging.
Is(Type)
1.x obj.Is(type) compared the exact runtime type. 2.0 uses type.IsInstanceOfType(obj), the same rule as
Is<T>() and the is operator: derived types and implemented interfaces match.
object error = new ArgumentNullException("invoice");
bool isArgumentError = error.Is(typeof(ArgumentException)); // → false
object error = new ArgumentNullException("invoice");
bool isArgumentError = error.Is(typeof(ArgumentException)); // → true
bool isExactType = error.GetType() == typeof(ArgumentException); // → false
To keep the old exact-type check, compare GetType() as in the last line.
IsNumber
In 1.x, IsNumber() on a null object threw ArgumentNullException. In 2.0 it returns false. It also
recognizes Half, Int128 and UInt128 on runtimes that have them.
object discount = null;
bool numeric = discount != null && discount.IsNumber();
object? discount = null;
object total = 12.5m;
bool numeric = discount.IsNumber(); // → false
bool money = total.IsNumber(); // → true
IsIn and If
1.x rejected a null receiver with ArgumentNullException. In 2.0 the receiver can be null. Only the list and
the delegate are checked.
string couponCode = null;
bool known = couponCode.IsIn("SPRING10", "VIP20"); // throws ArgumentNullException ("item")
string? couponCode = null;
bool known = couponCode.IsIn("SPRING10", "VIP20"); // → false
string? label = couponCode.If(couponCode is null, _ => "none"); // → "none"
GetPropertyValue and SetPropertyValue
These methods live in the System.Reflection namespace. In 1.x an unknown property name threw
NullReferenceException. In 2.0 it throws ArgumentException with ParamName "propertyName", and the message
names the type and the property. SetPropertyValue on a read-only property also throws ArgumentException with
that parameter name.
using System.Reflection;
Invoice invoice = new Invoice { Number = "INV-7" };
object number = invoice.GetPropertyValue("Nubmer"); // throws NullReferenceException
using System.Reflection;
Invoice invoice = new Invoice { Number = "INV-7" };
object? number = invoice.GetPropertyValue("Number"); // → "INV-7"
object? typo = invoice.GetPropertyValue("Nubmer"); // throws ArgumentException ("propertyName")
Property names are case-sensitive in both versions. See Objects.
Types
IsComplex
IsComplex tells simple values (primitives, enums, strings, dates) apart from objects with properties of their
own. 1.x treated DateTimeOffset, TimeSpan and Uri as complex. 2.0 treats them as simple, together with
DateOnly, TimeOnly and the nullable versions of these types.
bool offset = typeof(DateTimeOffset).IsComplex(); // → true
bool duration = typeof(TimeSpan).IsComplex(); // → true
bool link = typeof(Uri).IsComplex(); // → true
bool offset = typeof(DateTimeOffset).IsComplex(); // → false
bool duration = typeof(TimeSpan?).IsComplex(); // → false
bool link = typeof(Uri).IsComplex(); // → false
bool invoice = typeof(Invoice).IsComplex(); // → true
If your own code walks object graphs and goes into "complex" properties, it now treats these types as single values. See Types and reflection.
Exceptions
Invalid arguments
1.x threw a mix of exception types for bad arguments, including NullReferenceException from inside a method. In
2.0 every method checks its arguments first. It throws ArgumentNullException or ArgumentOutOfRangeException,
with ParamName set to the parameter's name. The Conventions page describes the rule.
string preview = "Overdue invoice reminder".TruncateWithPostfix(10, null); // throws NullReferenceException
string? preview = "Overdue invoice reminder".TruncateWithPostfix(10); // → "Overdue..."
string? broken = "Overdue invoice reminder".TruncateWithPostfix(10, null!); // throws ArgumentNullException ("postfix")
Both exception types derive from ArgumentException, so a catch (ArgumentException) block catches them. A
catch (NullReferenceException) block around a library call no longer catches anything. Replace it with a check
before the call.
The changelog lists the bugs
fixed in 2.0 in its "Fixed" section. Some fixes change results too. For example, "HTMLParser".ToKebabCase() now
returns "html-parser", MatchAll compares item counts, and Split(0) throws instead of looping forever.
See also
- Installation: the package reference and supported target frameworks.
- Conventions: the null, culture and argument rules that 2.0 applies everywhere.
- String casing: the word-splitting rules behind the new casing methods.
- Objects:
ToQueryString,Is,IsIn,Ifand the reflection helpers in full.