C# reference · Dates & times
C# Date & Time Handling
The C#/.NET answer to “how do I do dates?” in one place — the types and which to reach for, the format specifiers, parsing, time zones, UTC, arithmetic, and the foot-guns.
Which type do I reach for?
DateTimeOffset
An absolute moment in time (a timestamp, a log entry, “when it happened”). Carries an offset from UTC, so it's unambiguous. The default for an instant.
DateOnly / TimeOnly
A calendar date with no time (a birthday, an invoice date) or a time with no date (a store's opening time). .NET 6+. No zone, no Kind confusion.
DateTime
The classic type — but it silently carries a Kind (Utc/Local/Unspecified) that equality ignores. Fine when you control the Kind; the source of most date bugs when you don't.
TimeSpan
A duration or elapsed interval (“3 days, 4 hours”), and what you get when you subtract two dates. Not a time-of-day type — use TimeOnly for that.
Format specifiers
The most-searched date answer: “give me the format string.” Pass a specifier to
ToString(…) or an interpolation ($"{dt:yyyy-MM-dd}"). Two kinds:
standard specifiers are single letters that pick a
culture-defined pattern; custom tokens spell out a
pattern exactly. The load-bearing traps live in the case: MM is
month but mm is minute;
HH is 24-hour but hh is 12-hour.
0 of 0 rows
Example output is the .NET docs' canonical instant — Monday, June 15, 2009 1:45:30.617 PM at offset -07:00, formatted with the en-US culture. Standard-specifier output is culture-sensitive; custom-token output mostly isn't.
Standard specifiers
| Spec | Meaning | Example (en-US) |
|---|---|---|
| d | Short date | 6/15/2009 |
| D | Long date | Monday, June 15, 2009 |
| f | Long date + short time | Monday, June 15, 2009 1:45 PM |
| F | Long date + long time | Monday, June 15, 2009 1:45:30 PM |
| g | General — short date + short time | 6/15/2009 1:45 PM |
| G | General — short date + long time | 6/15/2009 1:45:30 PM |
| M / m | Month + day | June 15 |
| O / o | Round-trip (ISO 8601, preserves offset/Kind) — the one to persist | 2009-06-15T13:45:30.6170000-07:00 |
| R / r | RFC 1123 (HTTP headers) — converts to UTC, appends “GMT” | Mon, 15 Jun 2009 20:45:30 GMT |
| s | Sortable (ISO 8601, no offset) | 2009-06-15T13:45:30 |
| t | Short time | 1:45 PM |
| T | Long time (with seconds) | 1:45:30 PM |
| u | Universal sortable — converts to UTC, appends “Z” | 2009-06-15 20:45:30Z |
| U | Universal full — converts to UTC, long form (DateTime only) | Monday, June 15, 2009 8:45:30 PM |
| Y / y | Year + month | June 2009 |
Mind the UTC conversion. R and
u convert a DateTimeOffset to UTC before formatting — that’s why the
examples above read 20:45, not
13:45. But they do not convert a bare
DateTime: call .ToUniversalTime() first, or you’ll stamp a local time
with a “GMT”/“Z” that lies.
Custom tokens
| Token | Meaning | Example |
|---|---|---|
| yyyy | Year, four digits | 2009 |
| yy | Year, two digits | 09 |
| MMMM | Month, full name | June |
| MMM | Month, abbreviated name | Jun |
| MM | Month number, 2 digits — month, not minute | 06 |
| M | Month number, no leading zero | 6 |
| dddd | Day of week, full name | Monday |
| ddd | Day of week, abbreviated | Mon |
| dd | Day of month, 2 digits | 15 |
| d | Day of month, no leading zero | 15 |
| HH | Hour, 24-hour clock, 2 digits | 13 |
| H | Hour, 24-hour clock | 13 |
| hh | Hour, 12-hour clock, 2 digits | 01 |
| h | Hour, 12-hour clock | 1 |
| mm | Minutes, 2 digits — minute, not month | 45 |
| m | Minutes, no leading zero | 45 |
| ss | Seconds, 2 digits | 30 |
| s | Seconds, no leading zero | 30 |
| fff | Fractional seconds, 3 digits (milliseconds) | 617 |
| FFF | Fractional seconds, trims trailing zeros | 617 |
| tt | AM/PM designator | PM |
| zzz | Offset from UTC, hours + minutes | -07:00 |
| zz | Offset from UTC, hours (2 digits) | -07 |
| K | Kind — “Z” for UTC, the offset for Local, empty for Unspecified | -07:00 |
No tokens match that search. .
Culture matters: ToString("d") is 6/15/2009 in en-US but
15/06/2009 in en-GB. For machine-readable output that never depends on
the machine's locale, pass CultureInfo.InvariantCulture — or use the round-trip
("o") or sortable ("s") specifiers, which are invariant by design. Full grammar:
standard
and custom format strings.
JSON & API timestamps
That 2009-06-15T20:45:30.617Z shape in JSON payloads is ISO 8601 in UTC (the trailing Z, “Zulu,” is the +00:00 offset) — no single standard specifier prints it exactly.
Write — from a UTC value
// dtUtc: the instant, Kind = Utc dtUtc.ToString( "yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture); // 2009-06-15T20:45:30.617Z
The Z is a literal — the value must already be UTC.
Read — back to an instant
var dto = DateTimeOffset.Parse( "2009-06-15T20:45:30.617Z", CultureInfo.InvariantCulture); // offset 00:00; TryParse for untrusted input
Parse into DateTimeOffset — no Kind to guess.
DateTime.ParseconvertsZto local time unless you passDateTimeStyles.RoundtripKind.- Round-trip
"o"is closest, but writes seven fractional digits and printsZonly for aUtc-KindDateTime. - A
DateTimeOffsetserializes with its offset (+00:00), notZ. System.Text.Jsonreads & writes this shape automatically forDateTimeOffset/DateTimeproperties.
The styles that control parsing (RoundtripKind, AssumeUniversal, AdjustToUniversal) are covered under
parsing date and time strings.
Showing 0 of 0 answers
No answers match that topic. .
1 · The types & which to use
What are the date/time types, and which should I use?
Six BCL types cover almost everything. DateTimeOffset is a moment in time with an offset from UTC — the right default for a timestamp. DateTime is the older date-and-time type that carries a Kind flag instead of an offset. DateOnly and TimeOnly (.NET 6+) are a calendar date with no time and a time with no date. TimeSpan is a duration. TimeZoneInfo represents a zone and its DST rules.
DateTimeOffset when = DateTimeOffset.UtcNow; // an instant — preferred DateOnly dob = new(1990, 5, 20); // a wall date TimeOnly opens = new(9, 0); // a wall time TimeSpan ttl = TimeSpan.FromMinutes(30); // a duration
Rule of thumb: an instant (log line, “created at”) → DateTimeOffset; a wall date/time with no zone (a birthday, a daily opening time) → DateOnly/TimeOnly; a duration → TimeSpan. Reach for a bare DateTime only when you're deliberately in UTC or you fully control its Kind. DateOnly/TimeOnly arrived in C# 10 / .NET 6.
Source: .NET — Choose between DateTime, DateTimeOffset, DateOnly, and TimeOnly.
What is the DateTime.Kind problem, and why prefer DateTimeOffset?
A DateTime carries a DateTimeKind — Utc, Local, or Unspecified — but the value looks the same either way, and the default from most sources is Unspecified. So 2009-06-15 13:45 could mean UTC, local time in some zone, or “we don't know,” and nothing in the value tells you which. Worse, equality ignores Kind (see the comparison section), so a UTC value and a local value with the same digits compare equal. DateTimeOffset fixes this by pinning an explicit offset from UTC, so the value is an unambiguous point on the timeline.
DateTime a = new(2009, 6, 15, 13, 45, 0); // Kind = Unspecified // Is that UTC? Local? Nobody knows. This is the footgun. DateTimeOffset b = new(2009, 6, 15, 13, 45, 0, TimeSpan.FromHours(-7)); // Unambiguous: 13:45 at UTC-7 == 20:45 UTC.
Store and pass instants as DateTimeOffset (or as UTC DateTime if you're disciplined about it). Keep DateTime for wall-clock values you're about to hand to a time zone — but DateOnly/TimeOnly are usually the better fit for those now.
Source: DateTimeKind / choosing guidance.
Are the date/time types mutable? Are they thread-safe?
All of them are immutable value types (structs). Every “mutating” method — AddDays, ToUniversalTime, AddMonths — returns a new value and leaves the original untouched. That makes them safe to share across threads freely; there's no formatter-state to corrupt (contrast Java's old thread-unsafe SimpleDateFormat). The corollary is a common beginner bug: calling dt.AddDays(1) and ignoring the return value does nothing.
var d = new DateTime(2009, 6, 15); d.AddDays(1); // BUG: result discarded, d is unchanged d = d.AddDays(1); // correct: reassign the new value
Source: DateTime (“this type is immutable”).
2 · Getting “now”
Now vs UtcNow vs Today — which do I want?
For anything you'll store, compare, or send over a wire, use a UTC reading: DateTimeOffset.UtcNow (best) or DateTime.UtcNow. DateTime.Now gives local time with Kind = Local — fine for display, a trap for storage, because the server's zone leaks in and DST makes it non-monotonic. DateTime.Today is midnight local today (a DateTime at 00:00); for a pure date prefer DateOnly.FromDateTime(DateTime.Now).
DateTimeOffset.UtcNow; // instant, unambiguous — store this DateTime.UtcNow; // instant, Kind = Utc DateTime.Now; // local wall clock, Kind = Local — display only DateOnly.FromDateTime(DateTime.Now); // today's date, no time
Source: DateTime.UtcNow / DateTimeOffset.UtcNow.
How do I make “now” testable? (TimeProvider)
Calling DateTime.Now directly inside logic makes that logic impossible to test at a fixed instant. Since .NET 8, inject TimeProvider — an abstract clock — instead. Production passes TimeProvider.System; a test passes a fake that returns a frozen time (the FakeTimeProvider in Microsoft.Extensions.TimeProvider.Testing). It also abstracts timers, so you can advance time deterministically in tests.
class TokenService(TimeProvider clock) { public bool IsExpired(DateTimeOffset exp) => clock.GetUtcNow() > exp; } // prod: new TokenService(TimeProvider.System) // test: new TokenService(fakeClockFrozenAtNoon)
TimeProvider arrived in .NET 8 (Nov 2023). Before it, the pattern was a hand-rolled IClock interface.
Source: TimeProvider.
3 · Formatting
How do I format a date to a string?
Pass a specifier (see the table above) to ToString, or use it in an interpolated string after a colon. The one decision that bites: culture. With no culture argument you get the current thread's culture, so the same code prints differently on different machines. For output a machine will read back, pass CultureInfo.InvariantCulture.
var dt = new DateTime(2009, 6, 15, 13, 45, 30); dt.ToString("yyyy-MM-dd HH:mm"); // 2009-06-15 13:45 $"{dt:yyyy-MM-dd}"; // 2009-06-15 (interpolation) dt.ToString("d", CultureInfo.InvariantCulture); // 06/15/2009 dt.ToString("o"); // round-trip ISO 8601 — persist this
For machine-readable timestamps, prefer "o" (round-trip) or an explicit invariant pattern over locale-dependent output.
Source: Formatting types in .NET.
4 · Parsing
Parse vs TryParse vs ParseExact — which and when?
Use TryParse for anything that could be malformed (user input, external data): it returns a bool instead of throwing. Use ParseExact/TryParseExact when the input has a known fixed layout and you want to reject anything else. Plain Parse is the throw-on-failure flexible parser. Always parse machine data with CultureInfo.InvariantCulture so "06/15/2009" isn't misread as day-first — the classic month/day parsing bug.
if (DateTime.TryParse(input, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)) { /* dt is valid */ } // Strict, exact layout — rejects anything else: DateTime.TryParseExact("2009-06-15", "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var d); // Round-trip an "o"/"s" string back losslessly: var back = DateTimeOffset.Parse("2009-06-15T13:45:30.617-07:00");
DateTimeStyles controls whitespace, assumed-UTC-vs-local, and whether to keep the offset (RoundtripKind / AssumeUniversal). Parsing an "o"-formatted string with DateTimeOffset preserves the offset exactly.
5 · Time zones
How do I convert a time between zones?
Get a TimeZoneInfo by id with FindSystemTimeZoneById, then convert with TimeZoneInfo.ConvertTime. Prefer working from a DateTimeOffset so the source instant is unambiguous. The conversion applies that zone's DST rules for the given date automatically.
var pacific = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles"); var utc = DateTimeOffset.UtcNow; var local = TimeZoneInfo.ConvertTime(utc, pacific); // applies DST for that date
Source: TimeZoneInfo.ConvertTime.
Why does my time-zone id work on one OS but not another? (Windows vs IANA)
The cross-platform tripwire. Historically Windows used its own zone ids ("Pacific Standard Time") while Linux/macOS use IANA ids ("America/Los_Angeles"), so an id hard-coded for one OS threw TimeZoneNotFoundException on the other. Since .NET 6, the runtime uses ICU and TimeZoneInfo accepts both forms on both platforms and can translate between them with TryConvertIanaIdToWindowsId / TryConvertWindowsIdToIanaId. On older runtimes you needed a mapping library.
// .NET 6+: both of these resolve on Windows AND Linux/macOS TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles"); TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); TimeZoneInfo.TryConvertWindowsIdToIanaId("Pacific Standard Time", out var iana); // iana == "America/Los_Angeles"
Prefer IANA ids for new code; they're the cross-platform standard.
Source: Time zones overview / ID conversion helpers.
How do I handle DST edge cases (ambiguous / invalid local times)?
Around a DST transition a local wall-clock time can be ambiguous (the “fall back” hour happens twice) or invalid (the “spring forward” hour never happens). TimeZoneInfo exposes IsAmbiguousTime and IsInvalidTime so you can detect and decide, rather than getting a silently wrong instant.
var tz = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles"); var t = new DateTime(2009, 11, 1, 1, 30, 0); // falls in the repeated hour tz.IsAmbiguousTime(t); // true — occurs twice that day tz.IsInvalidTime(t); // false here; true for a skipped spring-forward time
This is exactly the ambiguity DateTimeOffset avoids for stored instants — the offset disambiguates which of the two occurrences you mean.
Source: IsAmbiguousTime / IsInvalidTime.
6 · Converting to/from UTC
How do I convert to and from UTC without losing information?
ToUniversalTime() and ToLocalTime() convert — but for a DateTime they rely on Kind, and a value with Kind = Unspecified gets assumed to be local, which is where round-trips lose information. SpecifyKind stamps a Kind without changing the digits (use it when you know where a value came from). The clean answer: hold instants as DateTimeOffset, whose .UtcDateTime and .ToOffset() convert with no Kind guessing.
var utc = DateTime.SpecifyKind(raw, DateTimeKind.Utc); // stamp, don't shift var local = utc.ToLocalTime(); // now correct // Ambiguity-free path: DateTimeOffset dto = DateTimeOffset.Now; DateTime asUtc = dto.UtcDateTime; // no Kind guessing
Round-tripping a bare DateTime through a string can drop the Kind unless you use the "o" specifier with DateTimeStyles.RoundtripKind.
Source: DateTime.SpecifyKind / ToUniversalTime.
7 · Arithmetic
How do I add and subtract time?
Use the Add* methods (each returns a new value) or add/subtract a TimeSpan with the operators. Two behaviors surprise people. AddMonths clamps to the end of the shorter month — Jan 31 + 1 month is Feb 28, not an error, and it isn't reversible. And a calendar day isn't always 24 hours across a DST boundary, so adding TimeSpan.FromDays(1) to a zone-aware value can land on a different wall-clock time.
var d = new DateTime(2009, 1, 31); d.AddMonths(1); // 2009-02-28 (clamped; NOT reversible) d.AddDays(2.5); // fractional units are fine d + TimeSpan.FromHours(6); // operator form
Source: DateTime.AddMonths.
8 · Differences between two dates
How do I get the difference between two dates?
Subtract them — the result is a TimeSpan. Read whole units with Days/Hours (the component parts) or fractional totals with TotalDays/TotalHours (the whole span expressed in that unit). The two are easy to mix up: for 36 hours, Days is 1 but TotalDays is 1.5.
TimeSpan span = end - start; span.Days; // whole days (component) span.TotalDays; // e.g. 1.5 — the whole span as days span.TotalHours; // e.g. 36.0
TimeSpan has no “months” or “years” — those aren't fixed lengths. For “whole months/years between,” compute from the components (see the age recipe) or use Noda Time's Period.
Source: TimeSpan.
9 · Comparison & equality
Why do two “equal” times compare unequal — or unequal ones compare equal?
Compare with ==, <, or CompareTo. The trap: DateTime equality compares ticks and ignores Kind. So a UTC value and a local value with the same digits compare equal even though they're different instants; and 13:45 UTC vs 13:45 UTC-7 (which are seven hours apart) also compare equal because the digits match. DateTimeOffset equality instead compares the actual instant, so two values with different offsets but the same moment are equal.
var u = new DateTime(2009, 6, 15, 13, 45, 0, DateTimeKind.Utc); var l = new DateTime(2009, 6, 15, 13, 45, 0, DateTimeKind.Local); u == l; // TRUE — Kind ignored (the footgun) DateTimeOffset a = new(2009,6,15,13,45,0, TimeSpan.Zero); DateTimeOffset b = new(2009,6,15, 6,45,0, TimeSpan.FromHours(-7)); a == b; // TRUE — same instant, compared correctly
Convert to a common Kind (or use DateTimeOffset) before comparing DateTime values.
Source: DateTime equality / DateTimeOffset.Equals.
10 · Min / max & “no value”
What are the min/max values, and how do I represent “no date”?
The range is DateTime.MinValue (0001-01-01) to DateTime.MaxValue (9999-12-31 23:59:59). A common antipattern is using MinValue as a stand-in for “not set” — it's indistinguishable from a real minimum date, and because default(DateTime) is MinValue, an unset field silently looks like a valid ancient date. Represent “maybe no date” with a nullable DateTime? instead, so “absent” is a real null.
DateTime.MinValue; // 0001-01-01 — also == default(DateTime) DateTime.MaxValue; // 9999-12-31 23:59:59 DateTime? completed = null; // "not completed" — the right way if (completed is { } when) { /* it has a value: 'when' */ }
Source: DateTime.MinValue / nullable value types.
11 · Unix / epoch time
How do I convert to/from a Unix timestamp? Is there a 2038 problem?
DateTimeOffset has the built-in conversions: FromUnixTimeSeconds/ToUnixTimeSeconds and the millisecond variants. No manual epoch math needed. And there's no year-2038 problem in .NET — the Unix methods use a 64-bit count, and internally a DateTime is 64-bit ticks (100-nanosecond units since 0001-01-01), so the range runs to the year 9999.
long secs = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // e.g. 1244497530 var dto = DateTimeOffset.FromUnixTimeSeconds(secs); // back to a DateTimeOffset DateTimeOffset.FromUnixTimeMilliseconds(1244497530617); // ms variant
A “tick” is 100 ns, so TimeSpan.TicksPerSecond is 10,000,000. That resolution is finer than most system clocks actually provide (see the next section).
Source: DateTimeOffset.FromUnixTimeSeconds.
12 · Precision & measuring elapsed time
How do I time an operation? (Why not DateTime.Now?)
Use Stopwatch, never DateTime.Now. The wall clock is not monotonic — NTP sync, DST, or a manual clock change can move it backward, giving you a negative or wildly wrong elapsed time. Stopwatch is backed by a high-resolution monotonic counter built for exactly this. DateTime.Now also has coarse resolution (often ~15 ms), so it can't measure short operations at all.
var sw = Stopwatch.StartNew(); DoWork(); sw.Stop(); sw.Elapsed; // a TimeSpan — monotonic, high-resolution sw.ElapsedMilliseconds; // e.g. 42
Rule: DateTime/DateTimeOffset answer “what time is it?”; Stopwatch answers “how long did that take?”
Source: Stopwatch.
13 · Serialization
How do dates round-trip through JSON (and databases)?
System.Text.Json writes DateTime/DateTimeOffset as ISO 8601 (the round-trip "o" shape) by default, which is the good outcome — a DateTimeOffset round-trips losslessly. The trap is the same Kind problem: a DateTime serialized without offset information can come back with a different Kind than it went out with, so prefer DateTimeOffset (or UTC) for anything you serialize. Newtonsoft.Json historically defaulted to converting to local time on read — a classic surprise — so check its DateTimeZoneHandling if you use it.
JsonSerializer.Serialize(DateTimeOffset.UtcNow); // "2009-06-15T20:45:30.617+00:00" — ISO 8601, round-trips cleanly
On the database side, map an instant to datetimeoffset (SQL Server) rather than datetime2 when you need the offset; EF Core preserves the CLR type you map to. This is a mention-and-link, not a deep dive.
Source: DateTime and DateTimeOffset support in System.Text.Json.
14 · Common recipes
Start of day, end of month, next Monday, age, is-weekend, is-leap-year?
The everyday one-liners, using built-in helpers where they exist (DateTime.DaysInMonth, DateTime.IsLeapYear, Date, DayOfWeek).
var startOfDay = dt.Date; // midnight (time stripped) var daysInMonth = DateTime.DaysInMonth(2009, 2); // 28 var endOfMonth = new DateTime(y, m, DateTime.DaysInMonth(y, m)); bool weekend = dt.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday; bool leap = DateTime.IsLeapYear(2024); // true // Next Monday (on or after tomorrow): int delta = ((int)DayOfWeek.Monday - (int)dt.DayOfWeek + 7) % 7; var nextMon = dt.AddDays(delta == 0 ? 7 : delta); // Age in whole years from a birthdate: int age = today.Year - dob.Year; if (dob.Date > today.AddYears(-age)) age--; // not had birthday yet
DateOnly (.NET 6+) is the better carrier for the date-only recipes — it has no time component to strip.
Source: DateTime.DaysInMonth / IsLeapYear.
15 · Noda Time (the external library)
What is Noda Time, and when should I reach for it?
Noda Time is a widely-used open-source date/time library (from Jon Skeet) that replaces the BCL's ambiguous model with distinct, purpose-named types: Instant (a moment on the timeline), LocalDate / LocalTime / LocalDateTime (wall values with no zone), ZonedDateTime (a local value plus a zone), Duration (elapsed time), Period (calendar amounts like “3 months”), and DateTimeZone. Because each concept is its own type, whole classes of DateTime.Kind bugs simply can't compile.
Instant now = SystemClock.Instance.GetCurrentInstant(); // unambiguous DateTimeZone la = DateTimeZoneProviders.Tzdb["America/Los_Angeles"]; ZonedDateTime local = now.InZone(la); Period p = Period.Between(start, end); // real months/years
When to use it: apps where correct time-zone and calendar handling is central (scheduling, calendaring, billing across zones), or where you want the type system to enforce the instant-vs-wall-time distinction. For straightforward UTC timestamps, the BCL's DateTimeOffset is enough — Noda Time earns its dependency when the domain is genuinely temporal. It's a recommendation with tradeoffs, not a blanket “always use this.”
Source: Noda Time user guide.
The foot-guns, in one place
DateTime.Kindis silentlyUnspecifiedfrom most sources, and equality ignores it — preferDateTimeOffset.MMis month,mmis minute;HHis 24-hour,hhis 12-hour. The case is the meaning.- Parse machine data with
CultureInfo.InvariantCulture, or"06/15"may be read day-first. AddMonthsclamps Jan 31 + 1 month to Feb 28, and it isn't reversible.- Windows zone ids (
"Pacific Standard Time") vs IANA ("America/Los_Angeles") — .NET 6+ accepts both; older runtimes don't. DateTime.MinValueas “no value” is indistinguishable from a real date — useDateTime?.==onDateTimeignoresKind, so a UTC and a local value with the same digits compare equal.- Time an operation with
Stopwatch, neverDateTime.Now— the wall clock isn't monotonic.
Every answer links its primary source inline — the
official .NET date, time, and time-zone documentation
and the API references for the types it covers. The
standard
and custom
format-string pages back the specifier tables, and the external-library section links the
Noda Time user guide.
Feature-arrival details (DateOnly/TimeOnly, TimeProvider) cross-link to the
C# version reference and the
.NET version reference. Last updated July 2026.
Mungomash LLC · More on C#