How can I configure Entity Framework to automatically trim values retrieved for specific columns mapped to char(N) fields?

怎甘沉沦 提交于 2019-11-27 18:33:16
Stuart Grassie

Rowan Miller (program manager for Entity Framework at Microsoft) recently posted a good solution to this which uses Interceptors. Admittedly this is only valid in EF 6.1+. His post is about trailing strings in joins, but basically, the solution as applied neatly removes trailing strings from all of the string properties in your models, automatically, without noticeably affecting performance.

Original blog post: Working around trailing blanks issue in string joins

The relevant code is reposted here, but I encourage you to read his blog post. (Also if you use EF, you should read his blog anyway).

using System.Data.Entity.Core.Common.CommandTrees;
using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure.Interception;
using System.Linq;

namespace FixedLengthDemo
{
    public class StringTrimmerInterceptor : IDbCommandTreeInterceptor
    {
        public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
        {
            if (interceptionContext.OriginalResult.DataSpace == DataSpace.SSpace)
            {
                var queryCommand = interceptionContext.Result as DbQueryCommandTree;
                if (queryCommand != null)
                {
                    var newQuery = queryCommand.Query.Accept(new StringTrimmerQueryVisitor());
                    interceptionContext.Result = new DbQueryCommandTree(
                        queryCommand.MetadataWorkspace,
                        queryCommand.DataSpace,
                        newQuery);
                }
            }
        }

        private class StringTrimmerQueryVisitor : DefaultExpressionVisitor
        {
            private static readonly string[] _typesToTrim = { "nvarchar", "varchar", "char", "nchar" };

            public override DbExpression Visit(DbNewInstanceExpression expression)
            {
                var arguments = expression.Arguments.Select(a =>
                {
                    var propertyArg = a as DbPropertyExpression;
                    if (propertyArg != null && _typesToTrim.Contains(propertyArg.Property.TypeUsage.EdmType.Name))
                    {
                        return EdmFunctions.Trim(a);
                    }

                    return a;
                });

                return DbExpressionBuilder.New(expression.ResultType, arguments);
            }
        }
    }
}

Rowan continues: "Now that we have an interceptor, we need to tell EF to use it. This is best done via Code-Based Configuration. We can just drop the following class in the same assembly/project as our context and EF will pick it up."

using System.Data.Entity;

namespace FixedLengthDemo
{
    public class MyConfiguration : DbConfiguration
    {
        public MyConfiguration()
        {
            AddInterceptor(new StringTrimmerInterceptor());
        }
    }
}

Use properties with backing fields instead of automatic properties on your entities.

Add the "Trim()" in the property setter, like so:

    protected string _name;
    public String Name
    {
        get { return this._name; }
        set { this._name = (value == null ? value : value.Trim()); }
    }

I wrote my own POCO generator that just does this automatically, but if you don't have an option like that, ReSharper can add backing fields to automatic properties in like two keystrokes. Just do it for strings, and you can do a global (at the file scope) find/replace for " = value;" with "= value.Trim();".

Entity Framework does not supply hooks to change the way it composes SQL statements, so you can't tell it to fetch and Trim string fields from the database.

It would be possible to trim string properties in the ObjectContext.ObjectMaterialized event, but I think this would greatly affect performance. Also, it would take a lot of if-else or switch code to do this for specific properties (as you intend to do). But it could be worth a try if you want to do this for nearly all properties (except the keys, for instance).

Otherwise I would go for the additional properties.

I had the same problem. And I resolved it by this simple way in DbContext:

public partial class MyDbContext : DbContext
{
    public override int SaveChanges()
    {
        foreach (var entity in this.ChangeTracker.Entries())
        {
            foreach (PropertyEntry property in entity.Properties.ToList().Where(o => !o.Metadata.IsKey()))
                TrimFieldValue(property);
        }

        return base.SaveChanges();
    }

    private void TrimFieldValue(PropertyEntry property)
    {
        var metaData = property.Metadata;
        var currentValue = property.CurrentValue == null ? null : property.CurrentValue.ToString();
        var maxLength = metaData.GetMaxLength();

        if (!maxLength.HasValue || currentValue == null) return;

        if (currentValue.Length > maxLength.Value)
            property.CurrentValue = currentValue.Substring(0, maxLength.Value);
    }       
}

I used the approach given by Stuart Grassie but it didn't work at first because the column type only contained the "char","varchar" etc. The columns are actually "char(30)", "varchar(10)", etc. Once I changed the line that follows it worked like a charm!

from: if (propertyArg != null && _typesToTrim.Contains(propertyArg.Property.TypeUsage.EdmType.Name))

to: if (propertyArg != null && _typesToTrim.Any(t => propertyArg.Property.TypeUsage.EdmType.Name.Contains(t)))

Thanks Stuart!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!