Skip to main content

Objects and Conversion

These methods work on any object. Use them to check or cast a type in a fluent chain, to convert a value to another type with the invariant culture, to build a URL query string from a filter object, or to read and write a property when you only know its name at run time.

The methods live in the System namespace, so you need no using. The exceptions are GetPropertyValue and SetPropertyValue: they live in System.Reflection.

At a glance​

MethodWhat it does
Is<T> / Is(Type)Checks whether an object is a type, a derived type or an implementation of an interface.
As<T>Casts a reference to T and throws when it is not one.
To<T> / To(Type)Converts a value with the invariant culture: numbers, enums, Guid, TimeSpan, nullable targets and TypeConverter types.
IsInChecks whether a value equals one of a list of values.
IfApplies a function or runs an action only when a condition is true.
AsDictionaryCopies an object's properties into a dictionary keyed by property name.
ToQueryStringBuilds a query string from an object's properties, with nested objects and collections.
TrimStringPropertiesTrims every writable string property of an object, in place.
IsNumberChecks whether a boxed value is a numeric type.
GetPropertyValueReads a public property by name.
SetPropertyValueWrites a public property by name.

Types and conversion​

Is<T> / Is(Type)​

Is<T>() returns true when the object is a T, derives from T or implements T. It is the is operator in method form, so it fits at the end of a chain. Is(Type) does the same when the type is only known at run time.

Checking types
object payment = new CardPayment(); // CardPayment : Payment

bool isPayment = payment.Is<Payment>(); // → true
bool isCard = payment.Is<CardPayment>(); // → true
bool isRefund = payment.Is<Refund>(); // → false

object lines = new List<OrderLine>();
bool isSequence = lines.Is<IEnumerable<OrderLine>>(); // → true

Type expected = typeof(Payment);
bool matches = payment.Is(expected); // → true
bool isObject = payment.Is(typeof(object)); // → true

object? missing = null;
bool nullIsPayment = missing.Is<Payment>(); // → false
  • A null receiver returns false for both overloads, even for typeof(object).
  • Is(Type) throws ArgumentNullException (parameter type) when the type is null.
  • The receiver is marked [NotNullWhen(true)], so after if (value.Is<Payment>()) the compiler knows value is not null.
  • A boxed int is also an int?: ((object)5).Is<int?>() returns true, as the is operator does.

As<T>​

As<T>() casts the object to the reference type T and returns the same instance. It reads left to right in a chain where a (T) cast would need extra brackets.

Casting in a chain
object source = new CardPayment { CardLast4 = "4242" };

string last4 = source.As<CardPayment>().CardLast4; // → "4242"
Payment payment = source.As<Payment>(); // same instance, typed as Payment
  • Unlike the C# as operator, it does not return null on a mismatch. It throws InvalidCastException, like a (T) cast.
  • A null receiver throws ArgumentNullException (parameter obj).
  • T must be a reference type (where T : class). For value types, use To<T>.
warning
FluentAssertions also has As<T>()

FluentAssertions defines its own object.As<T>(). In a test project that imports both, the call is ambiguous. Call this one statically, System.ObjectExtensions.As<CardPayment>(source), or use a using alias.

To<T> / To(Type)​

To<T>() converts a value to the value type T. To(Type) converts to any type you pass at run time, including reference types and nullable types. Both always use the invariant culture, so "1.5" reads as one and a half on every server.

Use them when you already have a value and want it as another type: a setting, a spreadsheet cell, a value from a dictionary. To parse user input without exceptions, use TryParse<T> instead.

Converting values
int quantity = "42".To<int>(); // → 42
long total = 42.To<long>(); // → 42L
decimal price = "1.5".To<decimal>(); // → 1.5m (also when the culture is de-DE)

DayOfWeek byName = "Monday".To<DayOfWeek>(); // → DayOfWeek.Monday
DayOfWeek ignoreCase = " monday ".To<DayOfWeek>(); // → DayOfWeek.Monday
DayOfWeek byText = "1".To<DayOfWeek>(); // → DayOfWeek.Monday
DayOfWeek byNumber = 5L.To<DayOfWeek>(); // → DayOfWeek.Friday
int statusCode = OrderStatus.Shipped.To<int>(); // → 2 (enum OrderStatus { Draft, Pending, Shipped })

Guid orderId = "7c9e6679-7425-40de-944b-e07fc1f90ae7".To<Guid>();
// → new Guid("7c9e6679-7425-40de-944b-e07fc1f90ae7")
TimeSpan window = "01:30:00".To<TimeSpan>(); // → TimeSpan.FromMinutes(90)
DateTimeOffset placedAt = "2024-01-02T03:04:05+02:00".To<DateTimeOffset>();
// → new DateTimeOffset(2024, 1, 2, 3, 4, 5, TimeSpan.FromHours(2))

object retries = "5".To(typeof(int?)); // → 5
object version = "1.2.3".To(typeof(Version)); // → new Version(1, 2, 3)
object text = 1.5m.To(typeof(string)); // → "1.5"

The conversion follows these rules, in order:

  1. If the value already is the target type, the same instance comes back. order.To(typeof(object)) returns order.
  2. A nullable target (int?) converts to its underlying type (int).
  3. An enum target reads a string by name, ignoring case and surrounding white space, or by number ("1"). Any other value is converted to the enum's underlying number first.
  4. When the value and the target are both IConvertible (numbers, string, bool, DateTime, enums as the source), it calls Convert.ChangeType with the invariant culture.
  5. Otherwise it asks the target type's TypeConverter to read the value, then the source type's TypeConverter to write it. That covers Guid, TimeSpan, DateTimeOffset, Version, Uri, DateOnly and your own types marked with [TypeConverter].
Your own type with a TypeConverter
[TypeConverter(typeof(CelsiusConverter))]
public readonly struct Celsius
{
public Celsius(double degrees)
{
Degrees = degrees;
}

public double Degrees { get; }
}

// CelsiusConverter reads a Celsius from a string and writes a Celsius as a double.
Celsius reading = "18.5".To<Celsius>();
double readingDegrees = reading.Degrees; // → 18.5
double degrees = new Celsius(21.5).To<double>(); // → 21.5
SituationException
The receiver is nullArgumentNullException (parameter obj)
To(Type) gets a null typeArgumentNullException (parameter type)
The text has the wrong format: "abc".To<int>(), "1".To<bool>(), "".To(typeof(int?))FormatException
An unknown enum name: "Funday".To<DayOfWeek>()ArgumentException
The number does not fit: 300.To<byte>()OverflowException
No conversion exists, or a TypeConverter returns nullInvalidCastException
Rounding and enum range
  • Converting a double or decimal to an integer type rounds to the nearest even number, as Convert.ToInt32 does. 2.5.To<int>() returns 2 and 3.5.To<int>() returns 4. It does not truncate.
  • Enum conversion does not check that the value is defined. "99".To<DayOfWeek>() returns (DayOfWeek)99. When the input is untrusted, check it with IsDefined<T> on the Enums page.
note
To<T> needs a non-nullable value type

To<T> has the constraint where T : struct, so To<int?>() does not compile. Use To(typeof(int?)). An empty string converted to int? still throws FormatException. It does not become null.

Conditions in fluent code​

IsIn​

IsIn checks whether a value equals one of the values you list. It replaces chains of == and ||. One overload takes the values as params; the other takes any IEnumerable<T>.

Membership checks
OrderStatus status = OrderStatus.Pending;

bool canCancel = status.IsIn(OrderStatus.Draft, OrderStatus.Pending); // → true
bool isFinal = status.IsIn(OrderStatus.Shipped); // → false

IEnumerable<int> vipCustomerIds = new List<int> { 1, 2, 3 };
bool isVip = 2.IsIn(vipCustomerIds); // → true

string? coupon = null;
bool noCoupon = coupon.IsIn("WELCOME10", null); // → true

int? rating = null;
bool rated = rating.IsIn(1, 2, 3, 4, 5); // → false
  • The item may be null. It matches a null in the list.
  • The params overload uses EqualityComparer<T>.Default, so strings compare ordinally and case-sensitively: "Pending".IsIn("pending") returns false. The sequence overload uses the collection's own Contains, so a HashSet<T> built with a comparer follows that comparer: "pending".IsIn(new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Pending" }) returns true.
  • A null list throws ArgumentNullException: parameter list for the params overload, items for the sequence overload.

If​

If lets a fluent chain take a step only when a condition holds. Pass a Func<T, T> to replace the value, or an Action<T> to act on the object and keep it.

Optional steps in a query
IQueryable<Order> orders = db.Orders
.AsQueryable()
.If(onlyOpen, q => q.Where(o => o.Status == OrderStatus.Pending))
.If(sortByTotal, q => q.OrderBy(o => o.Total));

int memberPoints = 100.If(true, points => points * 2); // → 200
int guestPoints = 100.If(false, points => points * 2); // → 100

bool copyManager = true;
List<string> recipients = new List<string> { "billing@example.com" }
.If(copyManager, list => list.Add("manager@example.com"));
// → ["billing@example.com", "manager@example.com"]
  • With a Func<T, T>, the result is the function's return value when the condition is true, and the original object otherwise.
  • With an Action<T>, the result is always the same object.
  • T is the receiver's static type, so call AsQueryable() on an EF Core DbSet<Order> first, as above. Without it, q => q.Where(...) returns an IQueryable<Order>, not a DbSet<Order>, so the call binds to the Action<T> overload and the filter is silently dropped.
  • A null delegate throws ArgumentNullException (parameter func or action), even when the condition is false.
warning
Value types and Action<T>

On a struct, the action receives a copy. point.If(true, p => p.X = 5) changes the copy, and the method returns the original value with X unchanged. Use the Func<T, T> overload and return the new value instead.

Properties as data​

AsDictionary​

AsDictionary() reads every readable, non-indexer property of the object into an IDictionary<string, object?> keyed by property name. Use it for logging, templating, or an API that wants a dictionary.

Object to dictionary
Customer customer = new Customer { Name = "Ahmad", Email = null, Age = 30 };

IDictionary<string, object?> values = customer.AsDictionary();
// → { ["Name"] = "Ahmad", ["Email"] = null, ["Age"] = 30 }

object invoice = new { Id = 7, Total = 120.50m };
IDictionary<string, object?> invoiceValues = invoice.AsDictionary();
// → { ["Id"] = 7, ["Total"] = 120.50m }

The optional bindingAttr parameter chooses which properties to read. The default is BindingFlags.Public | BindingFlags.Instance.

Including private properties
using System.Reflection;

IDictionary<string, object?> all = customer.AsDictionary(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
// also contains private properties, such as ["PasswordHash"]
  • Properties whose value is null are included, with a null value.
  • Read-only properties are included. Indexers are skipped.
  • The copy is shallow. A nested object stays as one value; it is not flattened.
  • Keys compare case-sensitively: values.ContainsKey("name") returns false.
  • A null source throws ArgumentNullException (parameter source).

To go the other way, from a dictionary to an object, see ToObject<T> on the Dictionaries page.

ToQueryString​

ToQueryString() builds a URL query string, without the leading ?, from the object's public properties. The keys follow the format that ASP.NET Core model binding reads, so a filter class on the client maps back to the same class on the server.

A search filter as a query string
OrderSearch search = new OrderSearch { Term = "a b&c", Page = 2 };

string query = search.ToQueryString();
// → "Term=a%20b%26c&Page=2"

string fromAnonymous = new { q = "x", n = 3 }.ToQueryString();
// → "q=x&n=3"

Values are formatted with the invariant culture. Dates use ISO 8601, bool is true or false, and enums use their name. Keys and values are both percent-encoded.

Typed values
string typed = new
{
Price = 1.5m,
At = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc),
Day = DayOfWeek.Friday,
Active = true,
Id = new Guid("7c9e6679-7425-40de-944b-e07fc1f90ae7"),
}.ToQueryString();
// → "Price=1.5&At=2024-01-02T03%3A04%3A05.0000000Z&Day=Friday&Active=true&Id=7c9e6679-7425-40de-944b-e07fc1f90ae7"

string dates = new { From = new DateOnly(2024, 3, 1), Paid = false }.ToQueryString();
// → "From=2024-03-01&Paid=false"

Nested objects and collections​

  • A nested object adds its properties with a dotted key: Address.City=Amman.
  • A collection of simple values repeats the key: Ids=1&Ids=2.
  • A collection of objects uses an index: Items[0].Name=pen. The brackets are percent-encoded (Items%5B0%5D.Name), and ASP.NET Core decodes them.
Nested shapes
OrderSearch byCity = new OrderSearch { Page = 1, Address = new Address { City = "Amman", Zip = null } };
string cityQuery = byCity.ToQueryString();
// → "Page=1&Address.City=Amman"

OrderSearch byIds = new OrderSearch { Page = 1, Ids = new[] { 1, 2 } };
string idQuery = byIds.ToQueryString();
// → "Page=1&Ids=1&Ids=2"

OrderSearch byLines = new OrderSearch
{
Page = 1,
Items = new List<LineItem>
{
new LineItem { Name = "pen", Quantity = 2 },
new LineItem { Name = "ink", Quantity = 1 },
},
};
string lineQuery = byLines.ToQueryString();
// → "Page=1&Items%5B0%5D.Name=pen&Items%5B0%5D.Quantity=2&Items%5B1%5D.Name=ink&Items%5B1%5D.Quantity=1"
string readable = Uri.UnescapeDataString(lineQuery);
// → "Page=1&Items[0].Name=pen&Items[0].Quantity=2&Items[1].Name=ink&Items[1].Quantity=1"

"Simple" means a type that IsComplex() reports as not complex: primitives, enums, string, decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Uri and their nullable forms, plus DateOnly, TimeOnly, Half, Int128 and UInt128 on runtimes that have them. A Dictionary property is a collection of key/value objects, so it comes out as Map[0].Key=k&Map[0].Value=1 (brackets encoded).

Joining collection values​

Pass collectionSeparator to put every value of a simple collection into one pair instead of repeating the key. The joined value is encoded as a whole, so a comma becomes %2C.

One pair per collection
OrderSearch byIds = new OrderSearch { Page = 1, Ids = new[] { 1, 2 } };

string repeated = byIds.ToQueryString(); // → "Page=1&Ids=1&Ids=2"
string joined = byIds.ToQueryString(collectionSeparator: ","); // → "Page=1&Ids=1%2C2"
string piped = new { Ids = new[] { 1, 2 } }.ToQueryString("|"); // → "Ids=1%7C2"

The separator applies only to collections of simple values. Collections of objects always use indexed keys.

Null handling and edge cases:

  • A property that is null is skipped. An object whose properties are all null returns "".
  • null items inside a collection are skipped: Tags = ["a", null, "b"] gives Tags=a&Tags=b.
  • An empty collection adds nothing by default. With a separator, it adds an empty pair such as Tags=.
  • A value-type property is never null, so it always appears: an unset int gives Page=0, and an unset enum gives its default name.
  • Keys use the property names as declared, with their case.
  • A null source throws ArgumentNullException (parameter source).
  • A graph deeper than 16 levels throws InvalidOperationException. This usually means the object references itself.
Other query-string builders

IDictionary<string, object?> and NameValueCollection have their own ToQueryString with the same encoding rules; see Dictionaries. To add parameters to an existing URL, use WithQuery on Uri, described on the I/O and system page.

TrimStringProperties​

TrimStringProperties() trims leading and trailing white space from every public string property that has a setter, in place. Use it on a form model or an imported record before you validate or save it.

Cleaning a sign-up form
SignUpForm form = new SignUpForm
{
Name = " Ahmad ",
Email = null,
Age = 30,
};

form.TrimStringProperties();

string? name = form.Name; // → "Ahmad"
string? email = form.Email; // → null
  • Only the object's own string properties change. Nested objects, string[] items and other types are left alone.
  • null values stay null.
  • Read-only properties, such as public string Code => " X ";, are skipped.
  • A property with a private or init setter is written too, because reflection can call it.
  • A null source throws ArgumentNullException (parameter source).

IsNumber​

IsNumber() checks whether a boxed value has a numeric type: sbyte, byte, short, ushort, int, uint, long, ulong, float, double, decimal, and Half, Int128 or UInt128 on runtimes that have them. Use it when you format or validate values of unknown type, such as cells in a report.

Detecting numeric values
object quantity = 3;
object price = 19.99m;
object ratio = 0.5d;
object label = "42";

bool a = quantity.IsNumber(); // → true
bool b = price.IsNumber(); // → true
bool c = ratio.IsNumber(); // → true
bool d = label.IsNumber(); // → false
bool e = 'a'.IsNumber(); // → false
bool f = true.IsNumber(); // → false
bool g = DayOfWeek.Monday.IsNumber(); // → false

object? empty = null;
bool h = empty.IsNumber(); // → false
  • It checks the type, not the content. A string of digits returns false; use TryParse<T> to test text.
  • char, bool, enums, nint, nuint and BigInteger return false.
  • null returns false, and the receiver is marked [NotNullWhen(true)].

Properties by name​

These two methods are in the System.Reflection namespace. Add using System.Reflection; to call them. Both look up a public instance property by its exact, case-sensitive name.

GetPropertyValue​

GetPropertyValue(propertyName) returns the value of a property as object?. GetPropertyValue<T>(propertyName) casts it to T. Use them when the property name comes from configuration, a column mapping or a sort expression.

Reading a property by name
using System.Reflection;

Account account = new Account { Owner = "Ahmad", Balance = 100, Overdraft = null };

object? owner = account.GetPropertyValue("Owner"); // → "Ahmad"
int balance = account.GetPropertyValue<int>("Balance"); // → 100
int? overdraft = account.GetPropertyValue<int?>("Overdraft"); // → null
SituationException
target or propertyName is nullArgumentNullException
No public instance property has that name, including a different case such as "owner"ArgumentException (parameter propertyName)
GetPropertyValue<T>: the value is not a TInvalidCastException
It is a cast, not a conversion

GetPropertyValue<T> casts the boxed value. An int property read as long throws InvalidCastException. A null value read as a non-nullable value type such as int throws NullReferenceException, so ask for int? instead. To change the type, read the value and call To<T> on it.

SetPropertyValue​

SetPropertyValue(value, propertyName) writes a property by name. Note the order: the value comes first, the property name second.

Writing a property by name
using System.Reflection;

Account account = new Account { Owner = "Ahmad" };

account.SetPropertyValue(250, "Balance");
int balance = account.Balance; // → 250

account.SetPropertyValue(null, "Owner");
string? owner = account.Owner; // → null

string input = "300";
account.SetPropertyValue(input.To<int>(), "Balance");
int updated = account.Balance; // → 300
SituationException
target or propertyName is nullArgumentNullException
No public instance property has that nameArgumentException (parameter propertyName)
The property has no setterArgumentException (parameter propertyName)
The value has the wrong type, such as a string or a long for an int propertyArgumentException
  • The value is not converted. Convert it first with To<T>, as in the last example.
  • A public property with a private setter is written, because reflection can call it.
Trimming and Native AOT

AsDictionary, ToQueryString, TrimStringProperties, GetPropertyValue and SetPropertyValue reflect over the runtime type, so they carry [RequiresUnreferencedCode]. A trimmed or AOT-published app gets a warning at each call. See Trimming and AOT.

See also​