NickStrupat / EntityFramework.Triggers

Adds events for entity inserting, inserted, updating, updated, deleting, and deleted
MIT License
373 stars 46 forks source link

Exception when accessing Original property #33

Closed DotNetFire closed 5 years ago

DotNetFire commented 6 years ago

The following code does not work, when Entity is a proxy:

           Triggers<EntityBase>.Updating += entity => {
                if (entity.Original.Version != entity.Entity.Version) { <== Exception
                    entity.Cancel = true;
                    // ReSharper disable once PossibleNullReferenceException
                    var entityName = entity.Entity.GetType().BaseType != null 
                        && entity.Entity.GetType().Name.StartsWith(entity.Entity.GetType().BaseType.Name)
                        // ReSharper disable once PossibleNullReferenceException
                        ? entity.Entity.GetType().BaseType.Name
                        : entity.Entity.GetType().Name;

                    throw new DbUpdateConcurrencyException(
                        $"Validation failed for entitiy {entityName} with id {entity.Entity.Id}. Version in Database in different from version in Entity.");
                }
                entity.Entity.Updated = DateTime.UtcNow;
                entity.Entity.Version += 1;
            };

Exception is:

System.TypeLoadException
  HResult=0x80131522
  Message=Could not load type 'Hotel_FEB49A97C767CB2D75BD11061E9CD3A5AC5E68203057224FA38195FCC1D93530__OriginalValuesWrapper' from assembly 'Hotel_FEB49A97C767CB2D75BD11061E9CD3A5AC5E68203057224FA38195FCC1D93530__OriginalValuesWrapperAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' because the parent type is sealed.
  Source=<Cannot evaluate the exception source>
  StackTrace:
<Cannot evaluate the exception stack trace>

I could solve the problem by changing the line

if (entity.Original.Version != entity.Entity.Version) {

to

if (entity.Context.Entry(entity.Entity).OriginalValues.ToObject() is EntityBase org && org.Version != entity.Entity.Version) {

It may be a problem of EntityFramework.TypedOriginalValues...

NickStrupat commented 6 years ago

Could you post your EntityBase class definition?

DotNetFire commented 6 years ago

Below is the complete EntityBase class:

using System;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity.Infrastructure;
using EntityFramework.Triggers;

namespace DotNetFire.Data.Entity {

    public abstract class EntityBase {

        static EntityBase() {
            Triggers<EntityBase>.Inserting += entity => {
                entity.Entity.Created = entity.Entity.Updated = DateTime.UtcNow;
                entity.Entity.Version = 1;
            };

            Triggers<EntityBase>.Updating += entity => {
                //if (entity.Original.Version != entity.Entity.Version) {
                if (entity.Context.Entry(entity.Entity).OriginalValues.ToObject() is EntityBase org && org.Version != entity.Entity.Version) {
                    entity.Cancel = true;
                    // ReSharper disable once PossibleNullReferenceException
                    var entityName = entity.Entity.GetType().BaseType != null 
                        && entity.Entity.GetType().Name.StartsWith(entity.Entity.GetType().BaseType.Name)
                        // ReSharper disable once PossibleNullReferenceException
                        ? entity.Entity.GetType().BaseType.Name
                        : entity.Entity.GetType().Name;

                    throw new DbUpdateConcurrencyException(
                        $"Validation failed for entitiy {entityName} with id {entity.Entity.Id}. Version in Database in different from version in Entity.");
                }
                entity.Entity.Updated = DateTime.UtcNow;
                entity.Entity.Version += 1;
            };

        }

        [Key]
        //TODO: Generate from Sequence with MandantId!!
        public long Id { get; set; }

        public int Version { get; set; } = 1;

        public DateTime Created { get; set; } = DateTime.UtcNow;

        public DateTime Updated { get; set; } = DateTime.UtcNow;

    }
}

Thx Stefan

NickStrupat commented 6 years ago

It appears that the original values cannot be retrieved using the proxy objects created by EF for lazy loading.

Swizzy commented 6 years ago

The solution appears to be mentioned here: https://github.com/NickStrupat/EntityFramework.Triggers/issues/24

Might want to leave this one open so you don't get the same question again, just mark it as solved or something...

Googling on the error only points towards this post, until you only search for "OriginalValuesWrapperAssembly", then you'll find the old closed ones aswell.

Swizzy commented 6 years ago

After further research, i found that your TypedOriginalValues project found here which is where the problem is.

The below is my fixed entity:

namespace ADDER.VAT.Data.Models
{
    public abstract class EntityBase
    {
        static EntityBase()
        {
            Triggers<EntityBase>.Inserting += OnInserting;
            Triggers<EntityBase>.Updating += OnUpdating;
        }

        private static void OnUpdating(IUpdatingEntry<EntityBase, DbContext> e)
        {
            // These make sure that the value of CreatedAt and CreatedById stay the same
            e.Entity.CreatedAt = (DateTime)e.Context.Entry(e.Entity).Property(nameof(CreatedAt)).OriginalValue;
            e.Entity.CreatedById = (string)e.Context.Entry(e.Entity).Property(nameof(CreatedById)).OriginalValue;
            // These update the UpdatedAt and UpdatedById to whoever is responsible for the current update
            e.Entity.UpdatedAt = DateTime.Now;
            e.Entity.UpdatedById = Thread.CurrentPrincipal.Identity.GetUserId();
        }

        private static void OnInserting(IInsertingEntry<EntityBase, DbContext> e)
        {
            e.Entity.CreatedAt = e.Entity.UpdatedAt = DateTime.Now;
            e.Entity.CreatedById = e.Entity.UpdatedById = Thread.CurrentPrincipal.Identity.GetUserId();
        }

        [Key]
        [Column("ID")]
        public virtual int Id { get; set; }

        public virtual DateTime CreatedAt { get; set; }

        [Required]
        public virtual string CreatedById { get; set; }

        public virtual DateTime UpdatedAt { get; set; }

        [Required]
        public virtual string UpdatedById { get; set; }

        // Navigation properties

        [ForeignKey(nameof(CreatedById))]
        public virtual ApplicationUser CreatedBy { get; set; }

        [ForeignKey(nameof(UpdatedById))]
        public virtual ApplicationUser UpdatedBy { get; set; }
    }
}

Originally i had these 2 for lines 22/23:

e.Entity.CreatedAt = e.Original.CreatedAt;
e.Entity.CreatedById = e.Original.CreatedById;