EF Core Value Converter does not run when DB value is null - entity-framework-core

We have a custom value converter for being able to store a JsonBlob. This works if value in the DB is a string / json string but if the value is null the Deserialize function is never hit. Is there a way to change this? This causes an issue because we would prefer to init an empty collection if there is no value.
The Converter
public class JsonStringConvertor<T> : ValueConverter<T, string> where T : class, new()
{
public JsonStringConvertor(ConverterMappingHints mappingHints = null)
: base(convertToProviderExpression, convertFromProviderExpression, mappingHints) { }
//Work around to dynamically create a ValueComparer<T>. Called using reflection
public ValueComparer<T> CreateValueComparer()
{
var comparer = new ValueComparer<T>
(
(l, r) => Serialize(l) == Serialize(r),
v => v == null ? 0 : Serialize(v).GetHashCode(),
v => Deserialize<T>(Serialize(v))
);
return comparer;
}
private static readonly Expression<Func<T, string>> convertToProviderExpression = v => Serialize(v);
private static readonly Expression<Func<string, T>> convertFromProviderExpression = v => Deserialize<T>(v) ?? new T();
private static string Serialize<TObj>(TObj t) => JsonConvert.SerializeObject(t, EntityFrameworkJson.SerializerSettings);
private static TVal Deserialize<TVal>(string s) => JsonConvert.DeserializeObject<TVal>(s, EntityFrameworkJson.SerializerSettings);
}
Usage
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Model
.GetEntityTypes()
.SelectMany(t => t.ClrType.GetProperties()
.Select(p => new
{
DeclaringEntityType = t.ClrType,
Property = p
}))
.Where(t => GetAttribute<JsonDataAttribute>(t.Property) != null)
.ForEach(t =>
{
//had to use reflection api to get all JsonData attributes, so now we reconstruct the EF PropertyBuilder
var propBuilder = modelBuilder.Entity(t.DeclaringEntityType).Property(t.Property.Name);
//Use old EF6 Naming Convention _Json
var identifier = StoreObjectIdentifier.Create(propBuilder.Metadata.DeclaringEntityType, StoreObjectType.Table);
if (identifier != null)
{
var propName = propBuilder.Metadata.GetColumnName(identifier.Value);
propBuilder.Metadata.SetColumnName(propName + "_Json");
}
//Setup Converter and Comparer
var converterType = typeof(JsonStringConvertor<>).MakeGenericType(t.Property.PropertyType);
var converter = (ValueConverter) Activator.CreateInstance(converterType, (object) null);
var comparerFunc = converter?.GetType().GetMethod("CreateValueComparer");
if (comparerFunc is {})
{
var comparer = (ValueComparer) comparerFunc.Invoke(converter, null);
propBuilder.Metadata.SetValueComparer(comparer);
}
propBuilder.HasConversion(converter);
propBuilder.Metadata.SetValueConverter(converter);
});
Thought it might be the comparer but it still doesn't work when not using the comparer. What i found during testing is that if the value in the database is null then the converter does not run. If i update it to an empty string it works.
we're using Entity Framework Core 5.0.1

Related

ef core 2 apply HasQueryFilter for all entity

Is there any way to apply "HasQueryFilter" globaly to all my entity ? I don't want
to add in modelbuilder one by one ?
modelBuilder.Entity<Manufacturer>().HasQueryFilter(p => p.IsActive);
In case you have base class or interface defining the IsActive property, you could use the approach from Filter all queries (trying to achieve soft delete).
Otherwise you could iterate entity types, and for each type having bool IsActive property build dynamically filter expression using Expression class methods:
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var isActiveProperty = entityType.FindProperty("IsActive");
if (isActiveProperty != null && isActiveProperty.ClrType == typeof(bool))
{
var parameter = Expression.Parameter(entityType.ClrType, "p");
var filter = Expression.Lambda(Expression.Property(parameter, isActiveProperty.PropertyInfo), parameter);
entityType.QueryFilter = filter;
}
}
Update (EF Core 3.0): Due to public metadata API breaking change (replacing many properties with Get / Set extension methods), the last line becomes
entityType.SetQueryFilter(filter);
For those looking to implement Ivan's answer in EF Core 3.0, note the necessary change in the last line:
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var isActiveProperty = entityType.FindProperty("IsActive");
if (isActiveProperty != null && isActiveProperty.ClrType == typeof(bool))
{
var parameter = Expression.Parameter(entityType.ClrType, "p");
var filter = Expression.Lambda(Expression.Property(parameter, isActiveProperty.PropertyInfo), parameter);
MutableEntityTypeExtensions.SetQueryFilter(entityType, filter);
}
}
Here is extention method for EF Core version 6
public static void ApplySoftDeleteQueryFilter(this ModelBuilder modelBuilder)
{
var entityTypes = modelBuilder.Model
.GetEntityTypes();
foreach (var entityType in entityTypes)
{
var isActiveProperty = entityType.FindProperty("IsActive");
if (isActiveProperty != null && isActiveProperty.ClrType == typeof(bool))
{
var entityBuilder = modelBuilder.Entity(entityType.ClrType);
var parameter = Expression.Parameter(entityType.ClrType, "e");
var methodInfo = typeof(EF).GetMethod(nameof(EF.Property))!.MakeGenericMethod(typeof(bool))!;
var efPropertyCall = Expression.Call(null, methodInfo, parameter, Expression.Constant("IsActive"));
var body = Expression.MakeBinary(ExpressionType.Equal, efPropertyCall, Expression.Constant(true));
var expression = Expression.Lambda(body, parameter);
entityBuilder.HasQueryFilter(expression);
}
}
}

Entity framework ordering using reflection

I have an extension method for ordering, the sortExpression can be something like "Description" or "Description DESC", it is working perfectly for columns at the same table:
public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string sortExpression)
{
if (source == null)
throw new ArgumentNullException("source");
if (string.IsNullOrEmpty(sortExpression))
return source;
var parts = sortExpression.Split(' ');
var isDescending = false;
var propertyName = "";
var type = typeof(T);
if (parts.Length > 0 && parts[0] != "")
{
propertyName = parts[0];
if (parts.Length > 1)
isDescending = parts[1].ToLower().Contains("esc");
var prop = type.GetProperty(propertyName);
if (prop == null)
throw new ArgumentException(string.Format("No property '{0}' on type '{1}'", propertyName, type.Name));
var funcType = typeof(Func<,>)
.MakeGenericType(type, prop.PropertyType);
var lambdaBuilder = typeof(Expression)
.GetMethods()
.First(x => x.Name == "Lambda" && x.ContainsGenericParameters && x.GetParameters().Length == 2)
.MakeGenericMethod(funcType);
var parameter = Expression.Parameter(type);
var propExpress = Expression.Property(parameter, prop);
var sortLambda = lambdaBuilder
.Invoke(null, new object[] { propExpress, new ParameterExpression[] { parameter } });
var sorter = typeof(Queryable)
.GetMethods()
.FirstOrDefault(x => x.Name == (isDescending ? "OrderByDescending" : "OrderBy") && x.GetParameters().Length == 2)
.MakeGenericMethod(new[] { type, prop.PropertyType });
var result = (IQueryable<T>)sorter
.Invoke(null, new object[] { source, sortLambda });
return result;
}
return source;
}
Working Example:
var query = db.Audit.Include("AccessLevel").AsQueryable();
query = query.OrderBy("Description");
Please note that the "Description" column exists at the same table "Audit".
What I'm trying to do is to sort by a column in a relation table:
Like the following
var query = db.Audit.Include("AccessLevel").AsQueryable();
query = query.OrderBy("AccessLevel.Name");
Which is equivalent to:
query = query.OrderBy(o => o.AccessLevel.Name);
What is the required modification on my extension method ?
I solved it by using the following code:
public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, parts[0], "OrderBy");
}
public static IQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "OrderByDescending");
}
public static IQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenBy");
}
public static IQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenByDescending");
}
static IQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
{
if (source == null)
throw new ArgumentNullException("source");
if (string.IsNullOrEmpty(property))
return source;
string[] props = property.Split('.');
Type type = typeof(T);
ParameterExpression arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach (string prop in props)
{
// use reflection (not ComponentModel) to mirror LINQ
PropertyInfo pi = type.GetProperty(prop);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
object result = typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { source, lambda });
return (IQueryable<T>)result;
}

Can I convert this extension method to use IDbSet<> instead of DbContext?

Currently I use the below method like this: db.GetProjectsAllowed(profileId, profOrgList, projectList). I would like to convert this to use IDbSet<Project>, but I'm not sure how to get the second LINQ query.
public static IQueryable<Project> GetProjectsAllowed
(
this IMkpContext db,
Guid profileId,
List<Guid> profOrgIds = null,
List<Guid> projectIds = null
)
{
var projects =
(
from p in db.Project
.Include(p => p.Proposals)
.Include(p => p.RoleAssignments)
.Include("RoleAssignments.AssigneeSnapshot")
where p.IsActive
select p);
if (profOrgIds != null && profOrgIds.Any())
{
var profileIds = db.ProfileOrganization
.Where(po => po.IsActive && profOrgIds.Contains(po.OrganizationId))
.Select(po => po.ProfileId);
projects = projects.Where(p => profileIds.Contains(p.CreatedById));
}
if (projectIds != null && projectIds.Any())
projects = projects.Where(proj => projectIds.Contains(proj.ProjectId));
return projects;//.ToList();
}
Can I convert this to use IDbSet<Project> or not?
Here, why not split this into two extension methods? This makes your GetProjectsAllowed extension method more cohesive and single responsible.
First:
public static IEnumerable<Guid> GetProfileIds(
this IDbSet<ProfileOrganization> profileOrganizations,
IEnumerable<Guid> profOrgIds = null)
{
return profOrgIds == null ? null :
from po in profileOrganizations
where po.IsActive
where profOrgIds.Contains(po.OrganizationId)
select po.OrganizationId;
}
And second:
public static IQueryable<Project> GetProjectsAllowed(
this IDbSet<Project> projects,
IEnumerable<Guid> profileIds,
IEnumerable<Guid> projectIds = null)
{
var activeProjects =
from project in projects
//.Include(..
where project.IsActive
select project;
if (profileIds != null && profileIds.Any())
{
activeProjects = activeProjects.Where(p => profileIds.Contains(p.CreatedById));
}
if (projectIds != null && projectIds.Any())
{
activeProjects = activeProjects.Where(proj => projectIds.Contains(proj.ProjectId));
}
return activeProjects;//.ToList();
}
And then the consumer can call it like this:
var profileIds = db.ProfileOrganization.GetProfileIds(profOrgIds);
var projectsAllowed = db.Projects.GetProjectsAllowed(profileIds, projectIds);

What is the equivalent of DbSet<TEntity>.Local in the ObjectSet<TEntity>?

I'm trying to get an entity that has already been loaded into the context. It's a legacy system so I cannot change the context creation (so that I can access it through DbSet) and have to work through ObjectSet API.
What is the best equivalent of DbSet.Local property in the ObjectSet API?
It looks like I could use something like objectContext.GetObjectByKey(key) or objectContext.ObjectStateManager.GetObjectStateEntry(key) but the creation of the EntityKey seems to involve hardcoding type and property names as strings:
var key = new EntityKey("MyEntities.Employees", "EmployeeID", 1);
Is there a better way?
Or the above as an extension method:
public static IEnumerable<T> Local<T>(this ObjectSet<T> ObjectSet) where T : class
{
var localObjects = ObjectSet.Context.ObjectStateManager
.GetObjectStateEntries(EntityState.Added | EntityState.Modified | EntityState.Unchanged)
.Where(e => e.Entity is T)
.Select(e => e.Entity as T);
return localObjects;
}
Usage:
Row row = Context.MyObjectSet.Where(mos => mos.Key == key).FirstOrDefault();
if (row == null) // no row exists
{
// see if the row exists in the Entity Cache
row = Context.MyObjectSet.Local<MyObjectType>().Where(mos => mos.Key == key).FirstOrDefault();
if (row == null)
{
row = new MyObjectType(); // no, so create a new one
Context.MyObjectSet.AddObject(row);
}
}
Based on Julie Lerman's blog post linked by Gert and her book (“Programming Entity Framework”) it looks like I'm going with this helper method:
public class MyUnitOfWork : MyEntities
{
public IEnumerable<T> GetLocalObjects<T>() where T : class
{
var localObjects = ObjectStateManager
.GetObjectStateEntries(EntityState.Added | EntityState.Modified | EntityState.Unchanged)
.Where(e => e.Entity is T)
.Select(e => e.Entity as T);
return localObjects;
}
}
And then using it in the following way:
_myUnitOfWork.GetLocalObjects<Employee>().First(x => x.EmployeeID == 1);

Get the EntitySet name from an EntityType in EF

Given an EntityType, such as "Contact", how can I derive from it the name of the EntitySet it would belong to, i.e. the pluralization such as "Contacts"?
If you already have an attached entity (obviously you don't need the first line, just use your existing entity):
Contact c = context.Contacts.Where(x => x.blah).FirstOrDefault();
string setName = c.EntityKey.EntitySetName;
Or if you don't:
string className = typeof(Contact).Name
var container =
context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, DataSpace.CSpace);
string setName = (from meta in container.BaseEntitySets
where meta.ElementType.Name == className
select meta.Name).First();
This extension may be useful
public static class MyExtensions
{
public static string GetEntitySetName<T>(this ObjectContext context)
{
string className = typeof(T).Name;
var container = context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, DataSpace.CSpace);
string entitySetName = (from meta in container.BaseEntitySets
where meta.ElementType.Name == className
select meta.Name).First();
return entitySetName;
}
}
And use it like:
db.AttachTo(db.GetEntitySetName<MyEntityType>(), myEntityInstance);
Here is a method that works similar to the accepted answer except that this supports a) proxy types (for example, if you dynamically get the type of an EF6 entity it could be of type "Contact_1A2B3C4D5E" instead of "Contact") and b) inheritance (table-per-type, table-per-hierarchy).
private static string GetEntitySetName(ObjectContext objectContext, Type type)
{
if (objectContext == null) throw new ArgumentNullException(nameof(objectContext));
if (type == null) throw new ArgumentNullException(nameof(type));
EntityContainer container = objectContext.MetadataWorkspace.GetEntityContainer(objectContext.DefaultContainerName, DataSpace.CSpace);
return container.BaseEntitySets
.Where(x =>
(x.ElementType.Name == type.Name) ||
(x.ElementType.Name == type.BaseType?.Name) ||
(x.ElementType.Name == type.BaseType?.BaseType?.Name)
)
.Select(x => x.Name)
.FirstOrDefault() ?? throw new ArgumentException($"Specified type is not an entity type.", nameof(type));
}