2013-07-08 2 views
7

Possiedo un framework che implementa le eliminazioni software nel database (DateTime Nullable denominato DeletedDate). Sto usando un repository per gestire le richieste principali entità in questo modo:Utilizzare ExpressionVisitor per escludere i record eliminati in join in join

/// <summary> 
/// Returns a Linq Queryable instance of the entity collection. 
/// </summary> 
public IQueryable<T> All 
{ 
    get { return Context.Set<T>().Where(e => e.DeletedDate == null); } 
} 

Questa grande opera, ma il problema che sto avendo è quando si includono le proprietà di navigazione, e come per assicurarsi che solo i record attivi vengono interrogati. Il metodo repository in questione inizia così:

/// <summary> 
/// Returns a Linq Queryable instance of the entity collection, allowing connected objects to be loaded. 
/// </summary> 
/// <param name="includeProperties">Connected objects to be included in the result set.</param> 
/// <returns>An IQueryable collection of entity.</returns> 
public IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) 
{ 
    IQueryable<T> query = Context.Set<T>().Where(e => e.DeletedDate == null); 

    foreach (var includeProperty in includeProperties) 
    { 
     query = query.Include(includeProperty); 
    } 

    return query; 
} 

Quindi, se il repository è in uso da un chiamato Parent ente che ha una proprietà di navigazione denominata bambini, il metodo AllIncluding sarebbe correttamente filtrare i record morbide Capogruppo cancellati, ma i record dei bambini cancellati con soft sarebbero comunque inclusi.

Guardando la query inviata al database, sembra che tutto ciò che deve essere fatto sia aggiungere alla clausola sql join "AND Children.DeletedDate IS NULL" e la query restituirebbe i risultati corretti.

Durante la mia ricerca, ho trovato il this post che sembra essere esattamente ciò di cui ho bisogno, tuttavia la mia implementazione non ottiene gli stessi risultati del poster. Passando attraverso il codice, nulla sembra accadere alla parte Bambini della query.

Ecco il mio attuale relativo codice (Nota: l'uso QueryInterceptor da NuGet):

BaseClass:

using System; 
using System.ComponentModel.DataAnnotations; 
using System.ComponentModel.DataAnnotations.Schema; 

namespace DomainClasses 
{ 
    /// <summary> 
    /// Serves as the Base Class for All Data Model Classes 
    /// </summary> 
    public class BaseClass 
    { 
     /// <summary> 
     /// Default constructor, sets EntityState to Unchanged. 
     /// </summary> 
     public BaseClass() 
     { 
      this.StateOfEntity = DomainClasses.StateOfEntity.Unchanged; 
     } 

     /// <summary> 
     /// Indicates the current state of the entity. Not mapped to Database. 
     /// </summary> 
     [NotMapped] 
     public StateOfEntity StateOfEntity { get; set; } 

     /// <summary> 
     /// The entity primary key. 
     /// </summary> 
     [Key, Column(Order = 0), ScaffoldColumn(false)] 
     [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] 
     public int Id { get; set; } 

     /// <summary> 
     /// The date the entity record was created. Updated in InsightDb.SaveChanges() method 
     /// </summary> 
     [Column(Order = 1, TypeName = "datetime2"), ScaffoldColumn(false)] 
     public DateTime AddDate { get; set; } 

     /// <summary> 
     /// The UserName of the User who created the entity record. Updated in InsightDb.SaveChanges() method 
     /// </summary> 
     [StringLength(56), Column(Order = 2), ScaffoldColumn(false)] 
     public string AddUser { get; set; } 

     /// <summary> 
     /// The date the entity record was modified. Updated in InsightDb.SaveChanges() method 
     /// </summary> 
     [Column(Order = 3, TypeName = "datetime2"), ScaffoldColumn(false)] 
     public DateTime ModDate { get; set; } 

     /// <summary> 
     /// The UserName of the User who modified the entity record. 
     /// </summary> 
     [StringLength(56), Column(Order = 4), ScaffoldColumn(false)] 
     public string ModUser { get; set; } 

     /// <summary> 
     /// Allows for Soft Delete of records. 
     /// </summary> 
     [Column(Order = 5, TypeName = "datetime2"), ScaffoldColumn(false)] 
     public DateTime? DeletedDate { get; set; } 
    } 
} 

Parent Classe:

using System.Collections.Generic; 
using System.ComponentModel.DataAnnotations; 

namespace DomainClasses 
{ 
    /// <summary> 
    /// The Parent Entity. 
    /// </summary> 
    public class Parent : BaseClass 
    { 
     /// <summary> 
     /// Instantiates a new instance of Parent, initializes the virtual sets. 
     /// </summary> 
     public Parent() 
     { 
      this.Children = new HashSet<Child>(); 
     } 

     #region Properties 

     /// <summary> 
     /// The Parent's Name 
     /// </summary> 
     [StringLength(50), Required, Display(Name="Parent Name")] 
     public string Name { get; set; } 

     #endregion 

     #region Relationships 
     /// <summary> 
     /// Relationship to Child, 1 Parent = Many Children. 
     /// </summary> 
     public virtual ICollection<Child> Children { get; set; } 

     #endregion 
    } 
} 

Bambino Classe:

using System.Collections.Generic; 
using System.ComponentModel.DataAnnotations; 
using System.ComponentModel.DataAnnotations.Schema; 

namespace DomainClasses 
{ 
    /// <summary> 
    /// The Child entity. One Parent = Many Children 
    /// </summary> 
    public class Child : BaseClass 
    { 
     #region Properties 

     /// <summary> 
     /// Child Name. 
     /// </summary> 
     [Required, StringLength(50), Display(Name="Child Name")] 
     public string Name { get; set; } 

     #endregion 

     #region Relationships 
     /// <summary> 
     /// Parent Relationship. 1 Parent = Many Children. 
     /// </summary> 
     public virtual Parent Parent { get; set; } 

     #endregion 
    } 
} 

Contesto Classe:

using DomainClasses; 
using System; 
using System.Data; 
using System.Data.Entity; 
using System.Linq; 

namespace DataLayer 
{ 
    public class DemoContext : DbContext, IDemoContext 
    { 
     /// <summary> 
     /// ActiveSession object of the user performing the action. 
     /// </summary> 
     public ActiveSession ActiveSession { get; private set; } 

     public DemoContext(ActiveSession activeSession) 
      : base("name=DemoDb") 
     { 
      ActiveSession = activeSession; 
      this.Configuration.LazyLoadingEnabled = false; 
     } 

     #region Db Mappings 

     public IDbSet<Child> Children { get; set; } 
     public IDbSet<Parent> Parents { get; set; } 

     #endregion 

     public override int SaveChanges() 
     { 
      var changeSet = ChangeTracker.Entries<BaseClass>(); 

      if (changeSet != null) 
      { 
       foreach (var entry in changeSet.Where(c => c.State != EntityState.Unchanged)) 
       { 
        entry.Entity.ModDate = DateTime.UtcNow; 
        entry.Entity.ModUser = ActiveSession.UserName; 

        if (entry.State == EntityState.Added) 
        { 
         entry.Entity.AddDate = DateTime.UtcNow; 
         entry.Entity.AddUser = ActiveSession.UserName; 
        } 
        else if (entry.State == EntityState.Deleted) 
        { 
         entry.State = EntityState.Modified; 
         entry.Entity.DeletedDate = DateTime.UtcNow; 
        } 
       } 
      } 

      return base.SaveChanges(); 
     } 

     public new IDbSet<T> Set<T>() where T : BaseClass 
     { 
      return ((DbContext)this).Set<T>(); 
     } 
    } 
} 

Repository Classe:

using DomainClasses; 
using QueryInterceptor; 
using System; 
using System.Data.Entity; 
using System.Linq; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    /// <summary> 
    /// Entity Repository to be used in Business Layer. 
    /// </summary> 
    public class EntityRepository<T> : IEntityRepository<T> where T : BaseClass 
    { 
     public IDemoContext Context { get; private set; } 

     /// <summary> 
     /// Main Constructor for Repository. Creates an instance of DemoContext (derives from DbContext). 
     /// </summary> 
     /// <param name="activeSession">UserName of the User performing the action.</param> 
     public EntityRepository(ActiveSession activeSession) 
      : this(new DemoContext(activeSession)) 
     { 
     } 

     /// <summary> 
     /// Constructor for Repository. Allows a context (i.e. FakeDemoContext) to be passed in for testing. 
     /// </summary> 
     /// <param name="context">IDemoContext to be used in the repository. I.e. FakeDemoContext.</param> 
     public EntityRepository(IDemoContext context) 
     { 
      Context = context; 
     } 

     /// <summary> 
     /// Returns a Linq Queryable instance of the entity collection. 
     /// </summary> 
     public IQueryable<T> All 
     { 
      get { return Context.Set<T>().Where(e => e.DeletedDate == null); } 
     } 

     /// <summary> 
     /// Returns a Linq Queryable instance of the entity collection, allowing connected objects to be loaded. 
     /// </summary> 
     /// <param name="includeProperties">Connected objects to be included in the result set.</param> 
     /// <returns>An IQueryable collection of entity.</returns> 
     public IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) 
     { 
      IQueryable<T> query = Context.Set<T>().Where(e => e.DeletedDate == null); 

      InjectConditionVisitor icv = new InjectConditionVisitor(); 

      foreach (var includeProperty in includeProperties) 
      { 
       query = query.Include(includeProperty); 
      } 

      return query.InterceptWith(icv); 
     } 

     /// <summary> 
     /// Finds a single instance of the entity by the Id. 
     /// </summary> 
     /// <param name="id">The primary key for the entity.</param> 
     /// <returns>An instance of the entity.</returns> 
     public T Find(int id) 
     { 
      return Context.Set<T>().Where(e => e.DeletedDate == null).SingleOrDefault(e => e.Id == id); 
     } 

     /// <summary> 
     /// Takes a single entity or entity graph and reads the explicit state, then applies the necessary State changes to Update or Add the entities. 
     /// </summary> 
     /// <param name="entity">The entity object.</param> 
     public void InsertOrUpdate(T entity) 
     { 
      if (entity.StateOfEntity == StateOfEntity.Added) 
      { 
       Context.Set<T>().Add(entity); 
      } 
      else 
      { 
       Context.Set<T>().Add(entity); 
       Context.ApplyStateChanges(); 
      } 
     } 

     /// <summary> 
     /// Deletes the instance of the entity. 
     /// </summary> 
     /// <param name="id">The primary key of the entity.</param> 
     public void Delete(int id) 
     { 
      var entity = Context.Set<T>().Where(e => e.DeletedDate == null).SingleOrDefault(e => e.Id == id); 
      entity.StateOfEntity = StateOfEntity.Deleted; 
      Context.Set<T>().Remove(entity); 
     } 

     /// <summary> 
     /// Saves the transaction. 
     /// </summary> 
     public void Save() 
     { 
      Context.SaveChanges(); 
     } 

     /// <summary> 
     /// Disposes the Repository. 
     /// </summary> 
     public void Dispose() 
     { 
      Context.Dispose(); 
     } 
    } 
} 

InjectConditionVisitor Classe:

using System; 
using System.Linq; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    public class InjectConditionVisitor : ExpressionVisitor 
    { 
     private QueryConditional queryCondition; 

     public InjectConditionVisitor(QueryConditional condition) 
     { 
      queryCondition = condition; 
     } 

     public InjectConditionVisitor() 
     { 
      queryCondition = new QueryConditional(x => x.DeletedDate == null); 
     } 

     protected override Expression VisitMember(MemberExpression ex) 
     { 
      // Only change generic types = Navigation Properties 
      // else just execute the normal code. 
      return !ex.Type.IsGenericType ? base.VisitMember(ex) : CreateWhereExpression(queryCondition, ex) ?? base.VisitMember(ex); 
     } 

     /// <summary> 
     /// Create the where expression with the adapted QueryConditional 
     /// </summary> 
     /// <param name="condition">The condition to use</param> 
     /// <param name="ex">The MemberExpression we're visiting</param> 
     /// <returns></returns> 
     private Expression CreateWhereExpression(QueryConditional condition, Expression ex) 
     { 
      var type = ex.Type;//.GetGenericArguments().First(); 
      var test = CreateExpression(condition, type); 
      if (test == null) 
       return null; 
      var listType = typeof(IQueryable<>).MakeGenericType(type); 
      return Expression.Convert(Expression.Call(typeof(Enumerable), "Where", new Type[] { type }, (Expression)ex, test), listType); 
     } 

     /// <summary> 
     /// Adapt a QueryConditional to the member we're currently visiting. 
     /// </summary> 
     /// <param name="condition">The condition to adapt</param> 
     /// <param name="type">The type of the current member (=Navigation property)</param> 
     /// <returns>The adapted QueryConditional</returns> 
     private LambdaExpression CreateExpression(QueryConditional condition, Type type) 
     { 
      var lambda = (LambdaExpression)condition.Conditional; 
      var conditionType = condition.Conditional.GetType().GetGenericArguments().FirstOrDefault(); 
      // Only continue when the condition is applicable to the Type of the member 
      if (conditionType == null) 
       return null; 
      if (!conditionType.IsAssignableFrom(type)) 
       return null; 

      var newParams = new[] { Expression.Parameter(type, "bo") }; 
      var paramMap = lambda.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement); 
      var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, lambda.Body); 
      lambda = Expression.Lambda(fixedBody, newParams); 

      return lambda; 
     } 
    } 
} 

QueryConditional Classe:

using DomainClasses; 
using System; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    public class QueryConditional 
    { 
     public QueryConditional(Expression<Func<BaseClass, bool>> ex) 
     { 
      Conditional = ex; 
     } 

     public Expression<Func<BaseClass, bool>> Conditional { get; set; } 
    } 
} 

ParameterRebinder Classe:

using System.Collections.Generic; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    public class ParameterRebinder : ExpressionVisitor 
    { 
     private readonly Dictionary<ParameterExpression, ParameterExpression> map; 

     public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map) 
     { 
      this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>(); 
     } 

     public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp) 
     { 
      return new ParameterRebinder(map).Visit(exp); 
     } 

     protected override Expression VisitParameter(ParameterExpression node) 
     { 
      ParameterExpression replacement; 

      if (map.TryGetValue(node, out replacement)) 
       node = replacement; 

      return base.VisitParameter(node); 
     } 
    } 
} 

IEntityRepository Interfaccia:

using System; 
using System.Linq; 
using System.Linq.Expressions; 

namespace DataLayer 
{ 
    public interface IEntityRepository<T> : IDisposable 
    { 
     IQueryable<T> All { get; } 
     IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties); 
     T Find(int id); 
     void InsertOrUpdate(T entity); 
     void Delete(int id); 
     void Save(); 
    } 
} 

IDemoContext Interfaccia:

using DomainClasses; 
using System; 
using System.Data.Entity; 

namespace DataLayer 
{ 
    public interface IDemoContext : IDisposable 
    { 
     ActiveSession ActiveSession { get; } 

     IDbSet<Child> Children { get; } 
     IDbSet<Parent> Parents { get; } 

     int SaveChanges(); 

     IDbSet<T> Set<T>() where T : BaseClass; 
    } 
} 
+0

Ho bisogno di fare lo stesso nel mio progetto. Hai trovato una soluzione a questo? – Colin

+0

Non ancora, ho iniziato a esaminare l'espressione in modo dinamico, ma sono stato spostato su un altro progetto. Mi sento come se ci fosse un modo per farlo, non ho ancora familiarità con le espressioni e la classe ExpressionVisitor. –

+1

A seconda della versione di SQL Server, è possibile che sia più semplice interagire con EF con Views, che hanno la clausola non cancellata. Quindi puoi semplicemente aggiungere alcuni invece dei trigger e tutto dovrebbe funzionare. – Aron

risposta

0

Non sono mai riuscito a capire il visitatore dell'espressione e avevo già trascorso abbastanza tempo. Così ho finito per gestire questo in un Table Trigger eliminando il record se l'DeletedDate non era nullo.

Lo scopo originale dell'eliminazione software consisteva nel tenere traccia di chi ha eliminato il record nell'applicazione. Stavo impostando l'utente Mod nel contesto delle modifiche di salvataggio, ma su una cancellazione questo non viene aggiornato, quindi non c'è un controllo su chi ha effettuato la cancellazione.

Avevo già un trigger "Dopo l'aggiornamento" e "Dopo l'eliminazione" per ogni tabella che stavo verificando e una tabella di controllo associata per ogni tabella. I trigger inseriscono fondamentalmente il vecchio record nella tabella di controllo ogni volta che c'è un aggiornamento o eliminazione. Le tabelle di controllo e trigger vengono creati tramite una stored procedure:

CREATE PROCEDURE [dbo].[CreateAuditTable](
    @TableName NVARCHAR(100), 
    @SchemaName NVARCHAR(50) 
) 
as 
/* 
----------------------------------------------------------------------------------------------------- 
* Procedure Name : dbo.CreateAuditTable 
* Author   : Josh Jay 
* Date    : 03/15/2013 
* Description  : Creates an Audit table from an existing table. 
----------------------------------------------------------------------------------------------------- 
Sl No  Date Modified  Modified By   Changes 
-------  -------------  ----------------- ------------------------------------------------- 
    1   07/01/2013   Josh Jay   Removed the table alias parameter and replaced usage with table name. 
    2   08/28/2013   Josh Jay   Modified the Update Statement to Delete the Row if it is a Soft Delete. 
----------------------------------------------------------------------------------------------------- 

Ex: 
EXEC dbo.CreateAuditTable 
    @TableName = 'Product', 
    @SchemaName = 'dbo' 

*/ 
BEGIN 
DECLARE @IssueCount INT = 0, 
     @IssueList NVARCHAR(MAX) = NULL, 
     @LineBreak NVARCHAR(50) = REPLICATE('-',50), 
     @CreateTableScript NVARCHAR(MAX) = NULL, 
     @CreateDeleteScript NVARCHAR(MAX) = NULL, 
     @CreateUpdateScript NVARCHAR(MAX) = NULL, 
     @ColumnNamesSection NVARCHAR(MAX) = NULL, 
     @TableObjectId INT, 
     @msg varchar(1024); 

--1) Check if table exists 
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @SchemaName AND TABLE_NAME = @TableName) 
     BEGIN 
      SET @IssueCount = @IssueCount + 1; 
      SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') The table ' + @SchemaName + '.' + @Tablename + ' does not exist.'; 
     END; 

--2) Check if audit table exists 
    IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @SchemaName AND TABLE_NAME = @TableName + '_Audit') 
     BEGIN 
      SET @IssueCount = @IssueCount + 1; 
      SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') The audit table ' + @SchemaName + '.' + @Tablename + '_Audit already exists. To recreate the audit table, please drop the existing audit table and try again.'; 
     END; 

--3) Check for existing triggers 
    IF EXISTS (SELECT 1 FROM sys.triggers tr INNER JOIN sys.tables t on tr.parent_id = t.object_id 
       WHERE t.schema_id = SCHEMA_ID(@SchemaName) AND t.name = @TableName AND tr.name LIKE 'tg_%Audit_%') 
     BEGIN 
      SET @IssueCount = @IssueCount + 1; 
      SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') At least one audit trigger exists on the ' + @SchemaName + '.' + @Tablename + ' table. To recreate the audit table, please drop the audit triggers.'; 
     END; 

--4) Print errors if there are any 
    IF @IssueCount > 0 
     BEGIN 
      PRINT('There were ' + CONVERT(VARCHAR,@IssueCount) + ' issues found when attempting to create the audit table. Please correct the issues below before trying again.'); 
      PRINT(@LineBreak); 
      PRINT(@IssueList); 
      RETURN; 
     END; 

--5) Build Scripts 
    select 
     @CreateTableScript = 
      'CREATE TABLE [' + SS.name + '].[' + ST.name + '_Audit]' + CHAR(10) + 
      '(' + CHAR(10) + 
      CHAR(9) + '[AuditId] INT IDENTITY(1,1) NOT NULL CONSTRAINT [pk_' + @SchemaName + '.' + @Tablename + '_Audit_AuditId] PRIMARY KEY,' + CHAR(10) + 
      CHAR(9) + '[AuditDate] DATETIME NOT NULL CONSTRAINT [df_' + @SchemaName + '.' + @Tablename + '_Audit_AuditDate] DEFAULT (getutcdate()),' + CHAR(10) + 
      CHAR(9) + '[AuditIsDelete] BIT NOT NULL CONSTRAINT [df_' + @SchemaName + '.' + @Tablename + '_Audit_AuditIsDelete] DEFAULT ((0))', 
     @CreateDeleteScript = 
      'CREATE TRIGGER [dbo].[tg_' + @SchemaName + '.' + @Tablename + '_Audit_Delete]' + CHAR(10) + 
      'ON [' + SS.name + '].[' + ST.name + ']' + CHAR(10) + 
      'After Delete' + CHAR(10) + 
      'As Begin' + CHAR(10) + 
      CHAR(9) + 'IF TRIGGER_NESTLEVEL() > 1' + CHAR(10) + 
      CHAR(9) + CHAR(9) + 'Return' + CHAR(10) + 
      CHAR(10) + 
      CHAR(9) + 'INSERT INTO' + CHAR(10) + 
      CHAR(9) + CHAR(9) + '[' + SS.name + '].[' + ST.name + '_Audit] (' + CHAR(10) + 
      CHAR(9) + CHAR(9) + CHAR(9) + '[AuditIsDelete]', 
     @CreateUpdateScript = 
      'CREATE TRIGGER [dbo].[tg_' + @SchemaName + '.' + @Tablename + '_Audit_Update]' + CHAR(10) + 
      'ON [' + SS.name + '].[' + ST.name + ']' + CHAR(10) + 
      'After Update' + CHAR(10) + 
      'As Begin' + CHAR(10) + 
      CHAR(9) + 'IF TRIGGER_NESTLEVEL() > 1' + CHAR(10) + 
      CHAR(9) + CHAR(9) + 'Return' + CHAR(10) + 
      CHAR(10) + 
      CHAR(9) + 'INSERT INTO' + CHAR(10) + 
      CHAR(9) + CHAR(9) + '[' + SS.name + '].[' + ST.name + '_Audit] (' + CHAR(10) + 
      CHAR(9) + CHAR(9) + CHAR(9) + '[AuditIsDelete]' 
    from 
     sys.tables ST 
     INNER JOIN 
     sys.schemas SS ON ST.schema_id = SS.schema_id 
    WHERE 
     ST.name = @TableName AND 
     ST.type = 'U' AND 
     SS.name = @SchemaName 

    SELECT 
     @CreateTableScript = @CreateTableScript + ',' + CHAR(10) + CHAR(9) + '[' + ISC.COLUMN_NAME + '] ' + ISC.DATA_TYPE + CASE WHEN ISC.CHARACTER_MAXIMUM_LENGTH IS NOT NULL AND ISC.DATA_TYPE <> 'xml' THEN '(' + CASE WHEN ISC.CHARACTER_MAXIMUM_LENGTH = -1 THEN 'MAX' ELSE CONVERT(varchar,ISC.CHARACTER_MAXIMUM_LENGTH) END + ')' ELSE '' END + ' NULL', 
     @ColumnNamesSection = ISNULL(@ColumnNamesSection,'') + ',' + CHAR(10) + CHAR(9) + CHAR(9) + CHAR(9) + '[' + ISC.COLUMN_NAME + ']' 
    FROM 
     INFORMATION_SCHEMA.COLUMNS ISC 
    WHERE 
     ISC.TABLE_NAME = @TableName AND 
     ISC.TABLE_SCHEMA = @SchemaName 
    ORDER BY 
     ISC.ORDINAL_POSITION ASC 

    SET @CreateTableScript = @CreateTableScript + CHAR(10) + ');' 

    SET @CreateDeleteScript = @CreateDeleteScript + @ColumnNamesSection + CHAR(10) + 
     CHAR(9) + CHAR(9) + ')' + CHAR(10) + 
     CHAR(9) + CHAR(9) + 'SELECT' + CHAR(10) + 
     CHAR(9) + CHAR(9) + CHAR(9) + '1 as [AuditIsDelete]' + 
     @ColumnNamesSection + CHAR(10) + 
     CHAR(9) + CHAR(9) + 'FROM' + CHAR(10) + 
     CHAR(9) + CHAR(9) + CHAR(9) + 'deleted' + CHAR(10) + 
     'End;' 

    SET @CreateUpdateScript = @CreateUpdateScript + @ColumnNamesSection + CHAR(10) + 
     CHAR(9) + CHAR(9) + ')' + CHAR(10) + 
     CHAR(9) + CHAR(9) + 'SELECT' + CHAR(10) + 
     CHAR(9) + CHAR(9) + CHAR(9) + '0 as [AuditIsDelete]' + 
     @ColumnNamesSection + CHAR(10) + 
     CHAR(9) + CHAR(9) + 'FROM' + CHAR(10) + 
     CHAR(9) + CHAR(9) + CHAR(9) + 'deleted' + CHAR(10) + 
     'declare @SoftDelete bit, 
      @Id int 

    select 
     @SoftDelete = case when i.DeletedDate is not null then 1 else 0 end, 
     @Id = i.Id 
    from 
     inserted i; 

    if @SoftDelete = 1 
     begin 
      INSERT INTO 
       [' + @SchemaName + '].[' + @TableName + '_Audit] (
        [AuditIsDelete] 
        ' + @ColumnNamesSection + ' 
       ) 
       SELECT 
        1 as [AuditIsDelete] 
        ' + @ColumnNamesSection + ' 
       FROM 
        inserted 

      delete from ' + @SchemaName + '.' + @TableName + ' where Id = @Id 
     end;' + CHAR(10) + 

     'End;' 

--6) Print and Run Scripts 
    BEGIN TRY 
     BEGIN TRANSACTION; 

     EXEC(@CreateTableScript); 

     EXEC(@CreateDeleteScript); 

     EXEC(@CreateUpdateScript); 

     --Test Try Catch: 
     --SELECT 1/0 

     COMMIT TRANSACTION; 

     PRINT('The audit table was successfully created.') 
    END TRY 
    BEGIN CATCH 
     ROLLBACK TRANSACTION; 

     set @msg = 
      'db_name()=' + isnull(db_name(), 'NULL') + '; ERROR_MESSAGE()=' + 
      isnull(ERROR_MESSAGE(), 'NULL') + 
      '; ERROR_PROCEDURE()=' + isnull(ERROR_PROCEDURE(), 'NULL') + 
      '; ERROR_LINE()=' + isnull(CONVERT(varchar(10), ERROR_LINE()), 'NULL') +  
      '; ERROR_NUMBER()=' + isnull(CONVERT(varchar(10), ERROR_NUMBER()), 'NULL') + 
      '; ERROR_SEVERITY()=' + isnull(CONVERT(varchar(10), ERROR_SEVERITY()), 'NULL') + 
      '; ERROR_STATE()=' + isnull(CONVERT(varchar(10), ERROR_STATE()), 'NULL'); 

     PRINT(CHAR(10) + 'Create Audit Table Script:'); 
     PRINT(@LineBreak); 
     PRINT(@CreateTableScript); 
     PRINT(@LineBreak); 

     PRINT(CHAR(10) + 'Create Audit Delete Trigger Script:'); 
     PRINT(@LineBreak); 
     PRINT(@CreateDeleteScript); 
     PRINT(@LineBreak); 

     PRINT(CHAR(10) + 'Create Audit Update Trigger Script:'); 
     PRINT(@LineBreak); 
     PRINT(@CreateUpdateScript); 
     PRINT(@LineBreak); 

     raiserror (@msg, 18, 1); 
    END CATCH 
END; 

Mentre i trigger non sono l'ideale, compiono gli obiettivi della revisione dell'utente che ha cancellato e non ho più bisogno di preoccuparsi per i record eliminati morbide.

0

Il problema è che si desidera aggiungere alla conditition utilizzando l'istruzione include() nella tua AllIncluding metodo. Il pacchetto queryinterceptor non supporta i metodi Include(). L'unica soluzione per ottenere questo funzionamento non è l'utilizzo dell'istruzione Includi.

Tutto funziona quando si fa qualcosa di simile al seguente:

Articles.Select(x => new { 
Vat = x.VatTypes 
}) 
.InterceptWith(Visitor); 

Così, quando il sopra si traduce per sql si vedrà che una in cui viene aggiunto alla query VatTypes.IsDeleted = 0.

È davvero necessario disporre di un metodo includeAll, questo mi sembra un ENORME sovraccarico dal punto di vista delle prestazioni perché si sta caricando tutto dal database.

MODIFICA: dopo aver letto di nuovo alcuni post precedenti, sembra che sia effettivamente possibile utilizzare il metodo InterceptWith con l'istruzione Include(). Forse è l'ExpressionVisitor che ha problemi con Include(). Se trovo un po 'di tempo, proverò e tornerò da te.

-1

Personalmente odio il modello di progettazione per cui si aggiunge una colonna "IsDeleted" a un tavolo. Le ragioni sono numerose.

  1. Il modello produce una piattaforma interna, si dispone di un database all'interno di un database.
  2. personalizzato API necessario per accedere al database interno (select * from table where IsDeleted = 0) e (delete from table becomes update table set IsDeleted = 1)
  3. dati extra nella tabella riduce le prestazioni
  4. dati extra non è utile a fini di controllo, se si vuole il controllo, farlo correttamente.

Il punto di dolore che hai incontrato è 2. API personalizzata. Entity Framework è stato creato per funzionare con i database SQL, non con qualche strano data-store che esiste all'interno del database SQL.

La soluzione che ho scoperto a questo problema è di utilizzare SQL Server Views. MS SQL Server supporta Views, che puoi filtrare su righe con la tua soft delete attiva. Vorrei quindi aggiungere un TRIGGER INSTEAD OF INSERT,UPDATE, DELETE alla vista per mappare i tuoi inserti/aggiornamenti/cancella le azioni corrette sul tuo database.

Tuttavia, quando si utilizza qualsiasi forma di astrazione, le prestazioni diminuiscono. In questo caso il principale trade off è SELECT. Con SQL Server Enterprise Edition è possibile aggiungere un indice alla vista (e utilizzare SQL Server in modo automatico per l'indice) per velocizzare tutte le selezioni, a scapito dell'accesso in scrittura. Che si occupa di punto 3.

quanto riguarda il punto 4. preferisco invece di una colonna IsDeleted di utilizzare il seguente schema ...

  • ValidFrom DateTime NOT NULL
  • ValidTo DateTime NULL
  • EditedBy VARCHAR NOT NULL

Quando si crea una nuova riga, si imposta ValidFrom su UTCNOW() e Modificato su CURRENTUSER(). Quando aggiorni una riga, imposta ValidTo della vecchia riga su UTCNOW() e crea una nuova riga con i valori corretti. Quando si cancella, si imposta ValidTo della vecchia riga su UTCNOW().

Questo schema consente di avere una visualizzazione storica completa della tabella in QUALSIASI MOMENTO IN TEMPO. Auditing completo.:)

+0

Anche a me non piace l'implementazione Soft Delete, ma per il il controllo dei modi è implementato è necessario tenere traccia di chi ha cancellato il record. Il controllo è impostato con i trigger che inseriscono il vecchio record in una tabella di controllo. L'utente di modifica/eliminazione è impostato nel codice C# in modo che non funzioni per utilizzare SQL CURRENTUSER() poiché questo sarebbe l'utente che l'applicazione impersonerà e sarebbe lo stesso per tutte le azioni. Potrei potenzialmente utilizzare le viste, oppure potrei fare in modo che il trigger di cancellazione cancelli il record una volta che i dati necessari sono stati registrati nella tabella di controllo. Grazie per i tuoi suggerimenti, dovrò indagare. –