4

Update

So it looks like this is replicateable in .NET 4.7.2 as well:

https://dotnetfiddle.net/NI8H1n


Original

I have been having an issue with a DateTime? in a razor template in a netcoreapp3.1. The template is being run through:

Engine.Razor.RunCompile("templateName", "templateKey" model: model);

The model being passed consists of the property:

public DateTime? IssueDate { get; set; }

The razor line that is throwing a null ref error inside the template is:

<td>Issue date: @(result.IssueDate.HasValue ? result.IssueDate.Value.ToString("dd/MM/yyyy") : "")</td>

The error:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: Cannot perform runtime binding on a null reference

So after some debugging, I found that if I change the problematic line in the template to:

<td>Issue date: @(result.IssueDate != null? result.IssueDate.ToString("dd/MM/yyyy") : "")</td>

Then we are good to go.

So the question I have is why doesn't the initial line work?

Zze
  • 18,229
  • 13
  • 85
  • 118
  • A bug, I'm guessing. Consider `string s = null; var l = s.Length;`. Since `s` is null, the call get the length will throw. Now consider `bool? b = null; if (b.HasValue) {/*code*/}`. In the second case, the `null`-ness of `Nullable` isn't the same as the `null`-ness of string. A `bool?` has three possible values, `true`, `false` and `null`. All of them are full on values of the type. For reference types, `null` isn't a _value_ for the type, it's a possible value for a variable of that type. My guess is that that subtlety was lost on the MSFT Razor dev who coded up the code for this. – Flydog57 Aug 18 '21 at 22:04
  • 1
    C# dynamic (RuntimeBinder) doesn't like nullable types: https://stackoverflow.com/questions/3728752/c-sharp-4-dynamic-and-nullable and webpages generator makes heavy use of it, undercovers. Try to specify @model to avoid dynamic implicit usage: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-5.0#model – Simon Mourier Aug 22 '21 at 18:01
  • The problem is very simple to resolve: define a default value in property, like `public DateTime? IssueDate { get; set; } = default(DateTime)`, about this topic, see at [C#, EF Core and Scaffold-DbContext, How to not passing null for a default valued column in the database?](https://stackoverflow.com/questions/68846270/c-ef-core-and-scaffold-dbcontext-how-to-not-passing-null-for-a-default-valued/68846366#68846366) – Antonio Leonardo Aug 27 '21 at 19:46

1 Answers1

6

Since your view - copied here below from your Fiddle - does not contain a @model declaration, the type dynamic is being used for the viewmodel.

@(Model.SomeDate.HasValue ? Model.SomeDate.Value.ToString("dd/MM/yyyy") : "No Value")  

Once compiled, the above statement looks like below - notice the use of dynamic.

public override async Task ExecuteAsync()
{
    this.Write((((dynamic)base.Model).SomeDate.HasValue) 
        ? ((dynamic)base.Model).SomeDate.Value.ToString("dd/MM/yyyy") 
        : "No Value"
        );
}

Consider the information from the documentation about dynamic

  1. Variables of type dynamic are compiled into variables of type object. Therefore, type dynamic exists only at compile time, not at run time.
  2. any non-null expression can be converted to the dynamic type

This makes that the type dynamic must evaluate its operations and data types at runtime.


public class SampleViewModel
{
    public DateTime? SomeDate {get;set;}
}

Given your model here above, where SomeDate is of type Nullable<DateTime>.

When that SomeDate property would have a value null, you get the Cannot perform runtime binding on a null reference exception, since the code tries to access .HasValue on a null, instead on something that could have been converted to a dynamic (bullet point 2).

When that SomeDate would have a DateTime.Now value as shown in your Fiddle, the dynamic runtime binding concludes that SomeDate must be of type System.DateTime because DateTime.Now is not nullable and only a non-null expression can be converted to the dynamic type (bullet point 2).
Therefore you get the 'System.DateTime' does not contain a definition for 'HasValue' exception, since a DateTime does not contain a HasValue property.


One way to make the HasValue check work, is by including the model declaration in the view, as it avoids having to use the type dynamic - see updated Fiddle.

@model HelloWorldMvcApp.SampleViewModel
@(Model.SomeDate.HasValue ? Model.SomeDate.Value.ToString("dd/MM/yyyy") : "No Value")
pfx
  • 20,323
  • 43
  • 37
  • 57