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
| Method | What 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. |
IsIn | Checks whether a value equals one of a list of values. |
If | Applies a function or runs an action only when a condition is true. |
AsDictionary | Copies an object's properties into a dictionary keyed by property name. |
ToQueryString | Builds a query string from an object's properties, with nested objects and collections. |
TrimStringProperties | Trims every writable string property of an object, in place. |
IsNumber | Checks whether a boxed value is a numeric type. |
GetPropertyValue | Reads a public property by name. |
SetPropertyValue | Writes 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.
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
nullreceiver returnsfalsefor both overloads, even fortypeof(object). Is(Type)throwsArgumentNullException(parametertype) when the type isnull.- The receiver is marked
[NotNullWhen(true)], so afterif (value.Is<Payment>())the compiler knowsvalueis notnull. - A boxed
intis also anint?:((object)5).Is<int?>()returnstrue, as theisoperator 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.
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#
asoperator, it does not returnnullon a mismatch. It throwsInvalidCastException, like a(T)cast. - A
nullreceiver throwsArgumentNullException(parameterobj). Tmust be a reference type (where T : class). For value types, useTo<T>.
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.
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:
- If the value already is the target type, the same instance comes back.
order.To(typeof(object))returnsorder. - A nullable target (
int?) converts to its underlying type (int). - 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. - When the value and the target are both
IConvertible(numbers,string,bool,DateTime, enums as the source), it callsConvert.ChangeTypewith the invariant culture. - Otherwise it asks the target type's
TypeConverterto read the value, then the source type'sTypeConverterto write it. That coversGuid,TimeSpan,DateTimeOffset,Version,Uri,DateOnlyand your own types marked with[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
| Situation | Exception |
|---|---|
The receiver is null | ArgumentNullException (parameter obj) |
To(Type) gets a null type | ArgumentNullException (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 null | InvalidCastException |
- Converting a
doubleordecimalto an integer type rounds to the nearest even number, asConvert.ToInt32does.2.5.To<int>()returns2and3.5.To<int>()returns4. 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 withIsDefined<T>on the Enums page.
To<T> needs a non-nullable value typeTo<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>.
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 anullin the list. - The
paramsoverload usesEqualityComparer<T>.Default, so strings compare ordinally and case-sensitively:"Pending".IsIn("pending")returnsfalse. The sequence overload uses the collection's ownContains, so aHashSet<T>built with a comparer follows that comparer:"pending".IsIn(new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Pending" })returnstrue. - A
nulllist throwsArgumentNullException: parameterlistfor theparamsoverload,itemsfor 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.
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 istrue, and the original object otherwise. - With an
Action<T>, the result is always the same object. Tis the receiver's static type, so callAsQueryable()on an EF CoreDbSet<Order>first, as above. Without it,q => q.Where(...)returns anIQueryable<Order>, not aDbSet<Order>, so the call binds to theAction<T>overload and the filter is silently dropped.- A
nulldelegate throwsArgumentNullException(parameterfuncoraction), even when the condition isfalse.
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.
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.
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
nullare included, with anullvalue. - 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")returnsfalse. - A
nullsource throwsArgumentNullException(parametersource).
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.
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.
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.
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.
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
nullis skipped. An object whose properties are allnullreturns"". nullitems inside a collection are skipped:Tags = ["a", null, "b"]givesTags=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 unsetintgivesPage=0, and an unset enum gives its default name. - Keys use the property names as declared, with their case.
- A
nullsource throwsArgumentNullException(parametersource). - A graph deeper than 16 levels throws
InvalidOperationException. This usually means the object references itself.
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.
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
stringproperties change. Nested objects,string[]items and other types are left alone. nullvalues staynull.- Read-only properties, such as
public string Code => " X ";, are skipped. - A property with a
privateorinitsetter is written too, because reflection can call it. - A
nullsource throwsArgumentNullException(parametersource).
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.
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; useTryParse<T>to test text. char,bool, enums,nint,nuintandBigIntegerreturnfalse.nullreturnsfalse, 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.
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
| Situation | Exception |
|---|---|
target or propertyName is null | ArgumentNullException |
No public instance property has that name, including a different case such as "owner" | ArgumentException (parameter propertyName) |
GetPropertyValue<T>: the value is not a T | InvalidCastException |
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.
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
| Situation | Exception |
|---|---|
target or propertyName is null | ArgumentNullException |
| No public instance property has that name | ArgumentException (parameter propertyName) |
| The property has no setter | ArgumentException (parameter propertyName) |
The value has the wrong type, such as a string or a long for an int property | ArgumentException |
- The value is not converted. Convert it first with
To<T>, as in the last example. - A public property with a
privatesetter is written, because reflection can call it.
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
- JSON and safe logging:
ToJsonStringand other ways to serialize objects. - Compare, clone and map:
CompareProperties,DeepCloneandMapTo<T>. - Types and reflection:
IsComplex, friendly type names and otherTypehelpers. - Parsing what people type:
TryParse<T>for input that may be invalid.