SQL Server How to update the property value (Database field) of T Entity (Database Table) from the DbContext? (save decrypted data back to the DbContext)

trnvg8h3  于 2023-10-15  发布在  其他
关注(0)|答案(2)|浏览(80)

How to iterate through DbContext and decrypt certain fields

I am using this StackOverflow answer as a guide: Database Table Export to CSV using EntityFramework DbContext .NET 6

We have very a legacy database (20+ years old) that used software encryption on certain columns. We are decrypting the encrypted columns so we can move to a more modern and efficient way of protecting the data.

I need to iterate through this old MS SQL Server database using EF Core and decrypt the columns with the software decryption. I have the code working to where I can decrypt the values, but I have not been able to figure out how to save them back to the DbContext (i.e. the database).

How do I set the `property value' (data field) of the Entity (data table) so that I may update the DbContext and save the decrypted changes to the database?

We have the GetPropertyValue() method in the TableDecrypt class, but have not been able to discover how to create the SetPropertyValue() method.

private static string GetPropertyValue(IProperty property, object record)
{
    var value = property.GetGetter().GetClrValue(record);
    if (value == null) return "";
    var strValue = value.ToString() ?? "";

    return strValue;
}

I have written the comment // The part I can't figure out next to the issue in the TableDecrypt class below.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Linq;
using System.Reflection;

namespace DatabaseTableExport
{
    public static class TableDecryptUtils
    {
        public static void DecryptTable<T>(DbContext context, IEntityType entityType, ITableDecrypt tableDecrypt) where T : class
        {
            // Export table to csv and get results
            var result = tableDecrypt.DecryptTable(context, context.Set<T>(), entityType);

            // TODO: report result here
        }

        private static readonly MethodInfo ProcessDecryption = typeof(TableDecryptUtils).GetMethod(nameof(DecryptTable))!;

        public static void Execute(DbContext context, ITableDecrypt tableDecrypt)
        {
            var entityTypes = context.Model.GetEntityTypes()
                .Where(et => !et.IsOwned())
                .ToList();

            foreach (var et in entityTypes)
            {
                // create and invoke generic method to get table data database and export to csv
                ProcessDecryption.MakeGenericMethod(et.ClrType).Invoke(null, new object[] { context, et, tableDecrypt });
            }
        }
    }

}
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace DatabaseTableExport
{
    public interface ITableDecrypt
    {        
        bool DecryptTable<T>(DbContext dbContext, IQueryable<T> table, IEntityType entityType) where T : class;
    }
}
using DatabaseTableExport.Services.Cryptography;
using DatabaseTableExport.Services.Cryptography.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Collections.Generic;
using System.Linq;

namespace DatabaseTableExport
{
    public class TableDecrypt : ITableDecrypt
    {
        // set RedactPii to true to redact Personally Identifiable Information (PII) from the export
        private bool RedactPii { get; set; }                
        private readonly string _encryptionKey;
        private readonly List<EncryptionHandler> _encryptionHandlers;

        public TableDecrypt(string encryptionKey, List<EncryptionHandler> encryptionHandlers, bool redactPii)
        {
            RedactPii = redactPii;                       
            _encryptionKey = encryptionKey;
            _encryptionHandlers = encryptionHandlers;
        }

        /// <summary>
        /// Returns the value of the given property as a string.
        /// </summary>
        /// <param name="property"></param>
        /// <param name="record"></param>
        /// <returns>String</returns>
        private static string GetPropertyValue(IProperty property, object record)
        {
            var value = property.GetGetter().GetClrValue(record);
            if (value == null) return "";
            var strValue = value.ToString() ?? "";

            return strValue;
        }

        /// <summary>
        /// Sets the value of the given property with the given value.
        /// </summary>
        /// <param name="property"></param>
        /// <param name="record"></param>        
        private static void SetPropertyValue(IProperty property, object record, string value)
        {
            // The part I can't figure out          
        }

        /// <summary>
        /// Decrypts set of records (table) from database context entity.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dbContext"></param>
        /// <param name="table"></param>
        /// <param name="entityType"></param>
        /// <returns>Returns true if successful, false if not successful.</returns>
        public bool DecryptTable<T>(DbContext dbContext, IQueryable<T> table, IEntityType entityType) where T : class
        {
            var tableRowsAsCsv = new List<string>();
            var properties = entityType.GetProperties().ToList();
            var tableName = entityType.Name.Split('.').Last();
            var encryption = new AesEncryption(_encryptionKey);

            try
            {
                // get Column Names from entity to compare with encryption handlers
                var columnNames = properties.Select(p => p.GetColumnName()).ToArray();                
                var columnIndex = 0;

                foreach (var record in table)
                {                   
                    foreach (var property in properties)
                    {
                        var columnName = columnNames[columnIndex];
                        try
                        {                            
                            // Decrypt, redact, or skip column value based on encryption handler 
                            // If table has encryption handler, decrypt column value based on handler action, else continue
                            if (_encryptionHandlers.Any(handler => handler.TableName == tableName && handler.ColumnName == columnName))
                            {
                                var handler = _encryptionHandlers.FirstOrDefault(h => h.TableName == tableName && h.ColumnName == columnName);
                                
                                if (property is not null)
                                {
                                    // Skip column value if handler action is Skip then continue
                                    if (handler.Action == "Skip")
                                    {
                                        columnIndex++;
                                        continue;
                                    }

                                    // Handle Decrypt or DecryptOrRedact
                                    if (handler.Action == "Decrypt" || handler.Action == "DecryptOrRedact")
                                    {
                                        // If RedactPii is true, redact column value instead of decrypting, else decrypt
                                        if (RedactPii)
                                        {                                            
                                            SetPropertyValue(property, record,"REDACTED");                                            
                                            continue;
                                        }
                                        else
                                        {
                                            SetPropertyValue(property, record, encryption.Decrypt(GetPropertyValue(property, record))); 
                                            continue;
                                        }
                                    }
                                } 
                                else // if null, continue
                                {
                                    columnIndex++;
                                    continue;
                                }
                            }        
                        }
                        catch 
                        {
                            throw;
                        }                        
                        columnIndex++;
                    }                                             
                    columnIndex = 0;
                }                
                dbContext.SaveChanges();                
            }
            catch
            {
                return false;                             
            }
            return true;
        }

    }
}
oug3syen

oug3syen1#

There is no need to create a method. You are using reflection to go through the columns. There are likely some control flow logic bugs in your code, such as the column index does not look like it would be incremented when it actually processes a value. I recommend using "fail fast" guard conditions rather than nesting conditions with if / else . The following should do what you are looking for:

foreach (var record in table)
{   
    int columnIndex = -1;             
    foreach (var property in properties)
    {
        columnIndex++;
        var columnName = columnNames[columnIndex];
        var handler = _encryptionHandlers.FirstOrDefault(h => h.TableName == tableName && h.ColumnName == columnName);

        if (handler is null || (handler.Action != "Decrypt" && handler.Action != "DecryptOrRedact"))
            continue;

        if (RedactPii)
            property.SetValue(record, property.GetValue(record) + "REDACTED");                                            
        else
        {
            string decrypted = encryption.Decrypt(property.GetValue(record));
            property.SetValue(record, decrypted);
        }
    }   
}

I have condensed the conditional logic and streamlined the column index incrementation so it's done only in one place to avoid issues where certain conditional flows risk not incrementing it. The logic around Skip vs. anything other than decrypt/redact got condensed down, though it is a bit of a code smell that you have a flag for Redact and a converter status distinction between decrypt vs. decryptOrRedact. There is no point having a catch block if it is just going to re-throw.

The core of the issue you had remaining is just using the property.SetValue() when you want to update the value of the property in the current row you are iterating over. "property" in this case is the PropertyInfo representing a property in the target class. To Set the value of a record, you call property.SetValue(record) which tells the PropertyInfo which row's property instance will be updated.

The other important detail here is that when you pass a DbSet in as IQueryable that you do not use AsNoTracking as we want the change tracking to be watching for changes on the entity properties. It looks like you had removed the AsNoTracking from the original question so that should be good to go.

dsf9zpds

dsf9zpds2#

Once you have tracked entity, it's much easier and convenient to use change tracking API services than model API services. i.e.

var entry = dbContext.Entry(entity);

where entity is either T or just object . Then you can use either CurrentValues / OriginalValues to get/set property values, e.g.

var value = entry.CurrentValues[property]; // get
entry.CurrentValues[property] = value; // set

or obtain PropertyEntry via Property method and use CurrentValue / OriginalValue for the same purpose, e.g.

var propertyEntry = entry.Property(property);
var value = propertyEntry.CurrentValue; // get
propertyEntry.CurrentValue = value; // set

For instance, the method in question can be implemented as follows (added few optimizations as a bonus):

public bool DecryptTable<T>(DbContext dbContext, IQueryable<T> table, IEntityType entityType) where T : class
{
    var tableName = entityType.GetTableName();
    if (tableName is null) return false;

    var decryptInfo = (
        from property in entityType.GetProperties()
            .Where(property => property.ClrType == typeof(string))
        join handler in _encryptionHandlers
            .Where(handler => handler.TableName == tableName
                && handler.Action is "Decrypt" or "DecryptOrRedact")
        on property.GetColumnName() equals handler.ColumnName
        select (property, redactPii: RedactPii/* && handler.Action is "DecryptOrRedact"*/)
    ).ToList();

    if (!decryptInfo.Any()) return false;

    var encryption = new AesEncryption(_encryptionKey);

    try
    {
        foreach (var entity in table.AsTracking())
        {
            var entry = dbContext.Entry(entity);
            foreach ((var property, var redactPii) in decryptInfo)
            {
                var propertyEntry = entry.Property(property);
                // Get property value
                var value = (string)propertyEntry.CurrentValue;
                if (string.IsNullOrEmpty(value)) continue;
                // Redact or decrypt
                if (redactPii)
                {
                    value += $"REDACTED";
                }
                else
                {
                    value = encryption.Decrypt(value);
                }
                // Set property value
                propertyEntry.CurrentValue = value;
            }
        }

        dbContext.SaveChanges();
        return true;
    }
    catch
    {
        return false;
    }
    finally
    {
        dbContext.ChangeTracker.Clear();
    }
}

相关问题