Search code examples
asp.net-coreasp.net-identity

EF Code First ApplicationUser required DateTime not added



I have a problem registering new users with my new ASP.NET Core Application (Code First).
When registering i'm using an `ApplicationUser` inheriting from `IdentityUser`.
public class ApplicationUser : IdentityUser
{
    [Required]
    [DefaultValue(5)]
    public int ViewerRange { get; set; }
    [DefaultValue(false)]
    [Required]
    public bool IsTACAccepted { get; set; }
    public DateTime? TACAccepted { get; set; }
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public DateTime Created { get; set; }
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime Modified { get; set; }
    public string Role { get; set; }

    public ApplicationUser()
    {
        Modified = DateTime.Now;
        Created = DateTime.Now;
    }
}

Here´s the part of Register.cshtml.cs

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser
        {
            UserName = Input.Email,
            Email = Input.Email,
            Role = Input.Role,
            Created = DateTime.Now,
            Modified = DateTime.Now,
            IsTACAccepted = false,
            ViewerRange = 5
        };


        var result = await _userManager.CreateAsync(user, Input.Password);
        //[...]

When registering, the CreateAsync Method fails with the following error:

InvalidOperationException: The value for property 'ApplicationUser.Modified' cannot be set to null because its type is 'System.DateTime' which is not a nullable type.

I can't understand why this is failing.
Can someone explain in detail please?
Kind regards


Solution

  • The reason for the error is that [DatabaseGenerated(DatabaseGeneratedOption.Computed)] says its value is generated by the database, so any assigned value should be ignored. The error shows this isn't the case though.

    This isn't the only oddity in the code. [DatabaseGenerated(DatabaseGeneratedOption.Identity)] on a DateTime makes no sense. IDENTITY is used to autoincrement numeric fields. This has no meaning for a DateTime column.

    f you want to specify the values yourself, remove the DatabaseGenerated attributes and use them as normal fields. You can avoid the explicit assignment by specifying a default property value, eg

    public DateTime Created { get; set; } = DateTime.Now;
    public DateTime Modified { get; set; } = DateTime.Now;
    

    It's also possible to configure the database to generate those values. This is described in the docs, in the Generated Values article. Adding Created and Modified columns is described in the Date/time generation section.

    A default value constraint for the Created column can be specified with HasDefaultValueSql :

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Created)
            .HasDefaultValueSql("getdate()");
    }
    

    The Modified timestamp needs a trigger though, which can't be specified in a DbContext's model. This must be created in the database, either directly :

    CREATE TRIGGER [dbo].[Blogs_UPDATE] ON [dbo].[Blogs]
        AFTER UPDATE
    AS
    BEGIN
        SET NOCOUNT ON;
    
        IF ((SELECT TRIGGER_NESTLEVEL()) > 1) RETURN;
    
        DECLARE @Id INT
    
        SELECT @Id = INSERTED.BlogId
        FROM INSERTED
    
        UPDATE dbo.Blogs
        SET LastUpdated = GETDATE()
        WHERE BlogId = @Id
    END
    

    Or through a raw SQL migration that executes the same script :

    migrationBuilder.Sql(
    @"
        EXEC ('CREATE TRIGGER [dbo].[Blogs_UPDATE] ON [dbo].[Blogs]
        AFTER UPDATE
    AS
    BEGIN
        SET NOCOUNT ON;
    
        IF ((SELECT TRIGGER_NESTLEVEL()) > 1) RETURN;
    
        DECLARE @Id INT
    
        SELECT @Id = INSERTED.BlogId
        FROM INSERTED
    
        UPDATE dbo.Blogs
        SET LastUpdated = GETDATE()
        WHERE BlogId = @Id
    END')");