40

I am learning ASP.NET Core MVC and my model is

namespace Joukyuu.Models
{
    public class Passage
    {
        public int PassageId { get; set; }
        public string Contents { get; set; }


        public DateTime CreatedDate { get; set; }
        public DateTime ModifiedDate { get; set; }
    }
}

The Passage table is used to save passages I wrote.

Scenario

  • Create view just has one field Contents to input a passage. CreatedDate and ModifiedDate must be automatically set equal by the server (using UTC format).

  • Edit view just has one field Contents to edit a passage. ModifiedDate must be automatically set by the server.

Question

What attributes I have to attach to the CreatedDate and ModifiedDate properties to make them automatically populated by the server based on the above scenario?

Second Person Shooter
  • 14,188
  • 21
  • 90
  • 165
  • 1
    *automatically set by the server* = database triggers – Gert Arnold Jul 04 '16 at 11:01
  • 1
    See http://stackoverflow.com/questions/26355486/entity-framework-6-audit-track-changes – haim770 Jul 04 '16 at 11:03
  • just before saving changes you can set date and time ``CreatedDate = DateTime.Now`` – Ehsan Sajjad Jul 04 '16 at 11:14
  • Would it be reasonable to modify your Model (i.e.DTO, POCO) Code file that Entity Framework maps to the Database table by coding it like so: public DateTime CreationDateTime { get; set; } = DateTime.Now; – crazyTech Jun 28 '22 at 14:52

7 Answers7

56

What attributes I have to attach to the CreatedDate and ModifiedDate properties to make them automatically populated by the server based on the above scenario?

Solution 1)

namespace Joukyuu.Models
{
    public class Passage
    {
        public int PassageId { get; set; }
        public string Contents { get; set; }


        public DateTime CreatedDate { get; set; }
        public DateTime ModifiedDate { get; set; }

       public Passage()
       {          
         this.CreatedDate  = DateTime.UtcNow;
         this.ModifiedDate = DateTime.UtcNow;
       }
    }
}

and by edit you have to change/update it by your self!

Solution 2)

Custom attribute:

[SqlDefaultValue(DefaultValue = "getutcdate()")]
public DateTime CreatedDate { get; set; }

Entity Framework 6 Code first Default value

Solution 3)

with help of Computed:

[Required, DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime CreatedUtc { get; set; 


  "dbo.Products",
            c => new
                {
                    ProductId = c.Int(nullable: false, identity: true),
                    Name = c.String(),
                    CreatedUtc = c.DateTime(nullable: false, defaultValueSql: "GETUTCDATE()"),
                })
            .PrimaryKey(t => t.ProductId);

https://andy.mehalick.com/2014/02/06/ef6-adding-a-created-datetime-column-automatically-with-code-first-migrations/

Solution 4) You can also do this with command interceptor by modifying manually the query.

Solution 5) Use Repository pattern to manage the data creation and set it by CreateNew This is my favour Solution!

https://msdn.microsoft.com/en-us/library/ff649690.aspx

Solution 6) just set it or get in in the UI or in your VM.


In Entity Framework Core 1.0 easy:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Passage>()
        .Property(b => b.CreatedDate )
        .HasDefaultValueSql("getdate()");
}
Community
  • 1
  • 1
Bassam Alugili
  • 16,345
  • 7
  • 52
  • 70
  • Can we insert `ModifiedDate=DateTime.UtcNow();` in `Contents` setter? – Second Person Shooter Jul 04 '16 at 12:02
  • no this is not a good idea because Entity Framework will use the setter later to fill the (materialized data = rows to Entity from SQL). that means you will get always your current set data and not from the database. – Bassam Alugili Jul 04 '16 at 12:12
  • 2
    About your code for EF Core 1 (at the most bottom), why did not you use `getutcdate()` instead of `getdate()`? – Second Person Shooter Jul 04 '16 at 18:42
  • What namespace/package is `HasDefaultValueSql()` in? – mellis481 Mar 14 '17 at 14:56
  • @im1dermike I do not know if even this method still exists check this out: https://github.com/aspnet/EntityFramework/issues/4501 – Bassam Alugili Mar 14 '17 at 15:05
  • 2
    It's a shame that 99% of the solutions available online propose a method that ties the code to a specific database engine. The `.HasValueGenerator()` property allows for the usage of .Net code as initializer. It won't fit all cases, but for most it will. – Guillermo Prandi Aug 05 '20 at 19:44
23

For those who are using the asynchronous system (SaveChangesAsync) and .NET Core, it's better to override the DbContext's SaveChangesAsync method:

public override Task<int> SaveChangesAsync(
    bool acceptAllChangesOnSuccess,
    CancellationToken cancellationToken = default(CancellationToken))
{
    var AddedEntities = ChangeTracker.Entries()
        .Where(E => E.State == EntityState.Added)
        .ToList();

    AddedEntities.ForEach(E =>
    {
        E.Property("CreationTime").CurrentValue = DateTime.Now;
    });

    var EditedEntities = ChangeTracker.Entries()
        .Where(E => E.State == EntityState.Modified)
        .ToList();

    EditedEntities.ForEach(E =>
    {
        E.Property("ModifiedDate").CurrentValue = DateTime.Now;
    });

    return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

Also, you can define a base class or an interface for your models with these properties:

public class SaveConfig
{
    public DateTime CreationTime { get; set; }
    public DateTime? ModifiedDate { get; set; }
}
Massimiliano Kraus
  • 3,638
  • 5
  • 27
  • 47
Arash
  • 1,692
  • 5
  • 21
  • 36
  • 2
    I think this will be a performance hit. Every time, it will iterate all entities. – Mukesh Kumar Nov 25 '18 at 10:56
  • No, its asynchronous – Arash Nov 27 '18 at 08:47
  • 3
    @MukeshKumar It iterated over saved and modified entries yes. Unless you're saving thousands of entries in a single request you're fine (even then it would be fine..). This is a good answer. -Arash Asyncfronous methods aren't magical. They still impact performance.. – Mikael Holmberg Jul 23 '19 at 21:03
  • Could you please elaborate on why this is a better method? – Guillermo Prandi Jul 31 '20 at 14:52
  • @GuillermoPrandi you should be able to setup the model configuration to get your DBMS to do this for you, much more performant. – Alex Hope O'Connor Sep 14 '20 at 20:33
  • @AlexHopeO'Connor But I don't want to, because I don't know what my DBMS will be. We use SQLite for testing, MSSQL for production and we may switch to other DBMS anytime. – Guillermo Prandi Sep 15 '20 at 16:29
  • @GuillermoPrandi its fine as long as your app stays small and your not updating 10k records at a time or something, and simply set up conditional compilation for your different environments... you could have this code disabled in production and modify the DBMS to do it for you, a much less lazy solution. – Alex Hope O'Connor Sep 15 '20 at 21:01
  • @AlexHopeO'Connor there's no chance de database is faster, however, if I'm already using EF for the inserts anyway. The real advantage of doing it on the database is that the times are correlated (i.e. no two application servers having slightly different clocks). But in my application I wanted to do all kind of initializations, not just date. Or set a date depending on the changes made to certain fields and not others. And it won't ever insert 10000 records at once. – Guillermo Prandi Sep 16 '20 at 18:04
  • @GuillermoPrandi the point is your adding another full iteration over whatever number of records, so the real advantage to not doing this is avoiding that. You can still go down the road of production optimization using stored procedures and triggers to initialize more complicated values. But for a small application that is not under stress this really won't be an issue, just thought I would warn people potentially dealing with larger datasets as to possible adverse effects on performance they may run into as I have recently had to refactor an application that was doing exactly this. – Alex Hope O'Connor Sep 17 '20 at 22:26
  • What's the best way to check if CreationTime / ModifiedDate exist on an Entity in case not all models inherit SaveConfig? – Thomas May 04 '21 at 13:03
  • Safer if you use `nameof(SaveConfig.CreationTime)` and `nameof(SaveConfig. ModifiedDate)` instead of strings – dystopiandev Nov 05 '22 at 16:05
  • It's true that `ChangeTracker.Entries()` will iterate over all entities in the change tracker that have been loaded into memory. And if you have a large number of entities loaded, even if they are not modified, this will have a performance hit. But it's also true that `base.SaveChangesAsync` will iterate over all entities in the change tracker as well. Personally I would define an interface for the `CreationTime` / `ModifiedDate` properties. Then you can `.OfType()...`. – Jeremy Lakeman Aug 28 '23 at 01:00
10

If you are using Code first you could try this

[Required, DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime CreatedDate { get; set; }

On Migration

AddColumn("Passage", "CreatedDate", n => n.DateTime(nullable: false, defaultValueSql: "GETDATE()"));

More reference here,similar answer

Or you can global override the saveChanges Note* This will affect on the entire model if you have the CreatedDate field

public override int SaveChanges()
{
  DateTime saveTime = DateTime.Now;
  foreach (var entry in this.ChangeTracker.Entries()
      .Where(e => e.State == (EntityState) System.Data.EntityState.Added))
   {
     if (entry.Property("CreatedDate").CurrentValue == null)
       entry.Property("CreatedDate").CurrentValue = saveTime;
   }
   return base.SaveChanges();  
}
pnizzle
  • 6,243
  • 4
  • 52
  • 81
Eldho
  • 7,795
  • 5
  • 40
  • 77
2

Solution for CreationDate in PostgreSQL:

builder.Property(e => e.CreationDate)
  .HasColumnType("timestamp without time zone")
  .HasDefaultValueSql("NOW()")
  .ValueGeneratedOnAdd();

via: https://www.npgsql.org/efcore/modeling/generated-properties.html#guiduuid-generation Unfortunately there is not solution for update event

GetoX
  • 4,225
  • 2
  • 33
  • 30
  • 1
    Link in your answer is currently broken, I think this is what you meant: https://www.npgsql.org/efcore/modeling/generated-properties.html#computed-columns-on-add-or-update Also your example is not showing a computed column, just a normal column with a default value. But your right the equivalent code for OnUpdate does not seem to work. – Alex Hope O'Connor Sep 14 '20 at 20:35
1

An alternative to using SaveChangesAsync is you can create database triggers for the respective database. I made a gist with some helper functions for mssql, postgress, sqlite, and mysql. Here is a paste of the code.

There is an interesting discussion here of having database logic vs application logic. Personally, having the database enforce the updated_date column update has less lines in my code. One argument they also made is that if it's your is changed using a different service these database integrity constraints will still apply at the database level.

I've been using this class below whenever I do ef core data migrations to add custom updated column triggers on table and column updates.

using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

namespace ProjectNamespace.Api.Utils
{
    public class DatabaseTools
    {
        public class DatabaseKeys
        {
            public string ValueGenerationStrategy;
            public object SerialColumn;
            public string DEFAULTDATE_CREATE;
            public string DEFAULTDATE_UPDATE;
            public string dateTime;
            public bool UpdateDateTrigger = true;
            public string ProviderName { get; set; }
        }
        /*
        Notes:
        can check if updateDateTrigger needed for dataprovider and add update triggers
        if (dbKeys.UpdateDateTrigger)
            {
                migrationBuilder.Sql(DatabaseTools.getUpdateDateTrigger(dbKeys.ProviderName, "TG_roles_updated_at", "roles", "updated_at"));
            }
        if postgress don't forget to drop the generated functions for each of the different named update columns (updated_at, modified_date) at the end of the migration E.g.
        protected override void Down(MigrationBuilder migrationBuilder){...
        if (dbKeys.ProviderName == "postgress")
            {
                migrationBuilder.Sql(@"
                    DROP FUNCTION update_" + "updated_at" + @"_column();
                    ");
            }
        postgres doesn't support parameters in triggers for new and old keywords.
         */
        /// <summary>
        /// needed for non mysql implementation to update a datetime on a record update
        /// </summary>
        /// <param name="databaseProvider"></param>
        /// <param name="name"></param>
        /// <param name="table"></param>
        /// <param name="column"></param>
        /// <param name="schema"></param>
        /// <param name="id_column"></param>
        /// <returns></returns>
        public static string getUpdateDateTrigger(string databaseProvider, string name, string table, string column, string schema = "", string id_column = "id")
        {
            string updateDateTrigger = null;
            if (!string.IsNullOrEmpty(schema))
                schema = schema + ".";
            switch (databaseProvider)
            {
                case "sqlite":
                    updateDateTrigger = @"
                    CREATE TRIGGER [" + name + @"]  
                        AFTER   
                        UPDATE  
                        ON " + table + @"
                        FOR EACH ROW   
                        WHEN NEW." + column + @" <= OLD." + column + @"  
                    BEGIN  
                        update " + table + @" set " + column + @"=CURRENT_TIMESTAMP where " + id_column + @"=OLD." + id_column + @";  
                    END  
                    ";
                    break;
                case "postgress":
                    updateDateTrigger = @"
                    CREATE OR REPLACE FUNCTION update_" + column + @"_column() RETURNS TRIGGER AS
                    $$
                    BEGIN
                        NEW.""" + column + @""" = now();
                        RETURN NEW;  
                    END;
                    $$ LANGUAGE plpgsql;
                    CREATE TRIGGER " + name + @" 
                    BEFORE UPDATE ON " + schema + @"""" + table + @"""
                    FOR EACH ROW EXECUTE PROCEDURE update_" + column + @"_column();
                    ";
                    break;
                case "sqlserver":
                    updateDateTrigger = @"
                    CREATE TRIGGER " + name + @" 
                    ON " + table + @"
                    AFTER UPDATE AS
                        UPDATE " + table + @"
                        SET " + column + @" = GETDATE()
                        WHERE " + id_column + @" IN (SELECT DISTINCT " + id_column + @" FROM Inserted);
                    ";
                    break;
            }

            return updateDateTrigger;
        }
        /*
        Notes:
        if in OnModelCreating can call as var dbKeys = DatabaseTools.getDatabaseDefaults(this.Database.ProviderName);
        if in a migration call as var dbKeys = DatabaseTools.getDatabaseDefaults(migrationBuilder.ActiveProvider);
        Then update any datetime columns for correct defaults
        created_at = table.Column<DateTime>(type: dbKeys.dateTime, nullable: true, defaultValueSql: dbKeys.DEFAULTDATE_CREATE),
        updated_at = table.Column<DateTime>(type: dbKeys.dateTime, nullable: true, defaultValueSql: dbKeys.DEFAULTDATE_UPDATE)
         */
        /// <summary>
        /// Can get specific formats for different databases. Supports mssql, postgress, sqlite, and mysql.
        /// </summary>
        /// <param name="databaseProvider"></param>
        /// <returns></returns>
        public static DatabaseKeys getDatabaseDefaults(string databaseProvider)
        {
            var dbKeys = new DatabaseKeys();
            dbKeys.dateTime = "datetime";

            switch (databaseProvider)
            {
                case "Microsoft.EntityFrameworkCore.Sqlite":
                case "sqlite":
                    dbKeys.DEFAULTDATE_UPDATE = "datetime('now')";
                    dbKeys.SerialColumn = true;
                    dbKeys.ProviderName = "sqlite";
                    dbKeys.ValueGenerationStrategy = "Sqlite:Autoincrement";
                    break;
                case "postgress":
                case "Npgsql.EntityFrameworkCore.PostgreSQL":
                    dbKeys.DEFAULTDATE_UPDATE = "CURRENT_TIMESTAMP";
                    dbKeys.SerialColumn = NpgsqlValueGenerationStrategy.SerialColumn;
                    dbKeys.ProviderName = "postgress";
                    dbKeys.ValueGenerationStrategy = "Npgsql:ValueGenerationStrategy";
                    dbKeys.dateTime = "timestamp";
                    break;
                case "mysql":
                case "MySql.Data.EntityFrameworkCore":
                    dbKeys.DEFAULTDATE_UPDATE = "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP";
                    dbKeys.DEFAULTDATE_CREATE = "CURRENT_TIMESTAMP";
                    dbKeys.UpdateDateTrigger = false;
                    dbKeys.SerialColumn = true;
                    dbKeys.ProviderName = "mysql";
                    dbKeys.ValueGenerationStrategy = "MySQL:AutoIncrement";
                    break;
                case "sqlserver":
                default:
                    dbKeys.DEFAULTDATE_UPDATE = "(getdate())";
                    dbKeys.SerialColumn = SqlServerValueGenerationStrategy.IdentityColumn;
                    dbKeys.ProviderName = "sqlserver";
                    dbKeys.ValueGenerationStrategy = "SqlServer:ValueGenerationStrategy";
                    break;
            }
            if (string.IsNullOrEmpty(dbKeys.DEFAULTDATE_CREATE))
                dbKeys.DEFAULTDATE_CREATE = dbKeys.DEFAULTDATE_UPDATE;
            return dbKeys;
        }
    }
}
lastlink
  • 1,505
  • 2
  • 19
  • 29
  • Adding triggers onto database is considered bad practise, as it is moving code to database and within the application it is not obvious. – cpoDesign Aug 24 '21 at 08:14
  • @cpoDesign I disagree. I consider these more database constraints and they've made maintaining and extending code easier by having these consistent triggers rather than having to remember to set the updated column value or doing some global application logic to update the values. – lastlink Aug 24 '21 at 15:25
0

You can do this by using interceptors in EF Core 5.0+:

public class SavingChangesInterceptor : SaveChangesInterceptor
{
    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
    {
        SetDateTimeValues(eventData.Context);
        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
    {
        SetDateTimeValues(eventData.Context);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private static void SetDateTimeValues(DbContext context)
    {
        foreach (var entry in context.ChangeTracker.Entries())
        {
            if (entry.Entity is BaseEntity entity)
            {
                var now = DateTimeOffset.UtcNow;

                if (entry.State == EntityState.Added)
                {
                    entity.CreatedDateUtc = now;
                    entity.ModifyDateUtc = now;
                }
                else if (entry.State == EntityState.Modified)
                {
                    entity.ModifyDateUtc = now;
                }
            }
        }
    }
}

Then to add the interceptor:

public class DatabaseContext : DbContext
{
    // ...
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.AddInterceptors(new SavingChangesInterceptor());
    }
}

More info:

Shleemypants
  • 2,081
  • 2
  • 14
  • 21
-1

Its too easy.Just you should do 2 step:

1.create model with these fields on top of createDate field:

 [Required, DatabaseGenerated(DatabaseGeneratedOption.Computed)]

2.After create migration and in its file befor update database edit column of this part and add defaultValueSql: "getdate()" like this:

 id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                CreatedDate = table.Column<DateTime>(defaultValueSql: "getdate()",nullable: false)
   
akbar
  • 625
  • 6
  • 12