ICollection<string> ValueComparer results in "Argument types do not match" - entity-framework

Following the Collection of Primitives sample code, I've made my own variation:
public class AttachmentTagsConfiguration : IEntityTypeConfiguration<Attachment>
{
public void Configure(EntityTypeBuilder<Attachment> builder)
{
builder.Property(e => e.Tags).HasConversion(
v => string.Join( ' ', v.Select(x => x.ToLower()).Distinct() ),
v => v.Split( ' ', StringSplitOptions.RemoveEmptyEntries )
.Select( x => x.ToLower() )
.Distinct()
.ToList(),
new ValueComparer<ICollection<string>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToList()
)
);
}
}
Tags have no spaces, so we just join them together in the database to create a single string. This is what the schema looks like:
public class Attachment
{
...
public ICollection<string> Tags { get; set; }
}
If I leave the ValueComparer in there, I get the following error when loading a query that has an Attachment:
System.ArgumentException: 'Argument types do not match'
However, if I remove the ValueComparer it works fine. But then I assume change tracking won't work correctly?
I'm not sure what part of the comparer code could lead to an incorrect argument type. None of the records in the database have a NULL value for Tags, so I don't see how there could be any null values.
Attachments is an ancestor class - we have AssetAttachments, InspectionAttachments, UnitAttachments and DefectAttachments. Does this have an impact on the SequenceEqual comparison?

Oh I see! I removed the casts that were in the original code:
public void Configure(EntityTypeBuilder<Attachment> builder)
{
builder.Property( e => e.Tags ).HasConversion(
v => string.Join( ' ', v.Select( x => x.ToLower()).Distinct() ),
v => (ICollection<string>)v.Split( ' ', StringSplitOptions.RemoveEmptyEntries )
.Select( x => x.ToLower() )
.Distinct()
.ToList(),
new ValueComparer<ICollection<string>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (ICollection<string>)c.ToList()
)
);
}
Visual Studio was showing me this:
The cast is dimmed on the line highlighted! So I removed it when I shouldn't have!

Related

InvalidOperationException in where clause when attempting to create projection with Compile/Invoke

I've got a model and I want to build a simplified query model on top of it using projections.
Here we have 2 related 'derived' classes.
note - in the 2nd class ES_PROGRAMGUIDEDAYSCHEDULE, there are 2 implementations of of the projection, the uncommented explicit one works, the commented one out fails.
class ESP_CHANNEL
{
public string? name { get; set; }
public static Expression<Func<Channel, ESP_CHANNEL>> Projection
{
get
{
return x => new ESP_CHANNEL
{
name = x.LotIdLeafoftreepartNavigation.Leaf
};
}
}
}
class ES_PROGRAMGUIDEDAYSCHEDULE
{
public ESP_CHANNEL? ds_channel { get; set; }
public DateTime? ds_date { get; set; }
public static IQueryable<ES_PROGRAMGUIDEDAYSCHEDULE> Query(ModelContext context)
{
return context.Pgprogramguidedayschedules.Select(ES_PROGRAMGUIDEDAYSCHEDULE.Projection);
}
private static Expression<Func<Pgprogramguidedayschedule, ES_PROGRAMGUIDEDAYSCHEDULE>> Projection
{
get
{
return x => new ES_PROGRAMGUIDEDAYSCHEDULE
{
ds_date = x.PgdsIdDayscheduleNavigation.DsTxdate,
// If I explicitly instantiate an ESP_CHANNEL it works
ds_channel = new ESP_CHANNEL
{
name = x.PgdsIdDayscheduleNavigation.DsIdScheduleNavigation.SchIdChannelNavigation.LotIdLeafoftreepartNavigation.Leaf
}
// note THIS seemingly equivalent implementation via compile/invoke fails
//ds_channel = ESP_CHANNEL.Projection.Compile().Invoke(x.PgdsIdDayscheduleNavigation.DsIdScheduleNavigation.SchIdChannelNavigation)
};
}
}
}
If I then run this query like this.
var x =
(from schedule in ES_PROGRAMGUIDEDAYSCHEDULE.Query(ds)
where schedule.ds_channel.name == channel
&& schedule.ds_date == dt
select schedule).Take(1);
var ys = x.ToArray();
the explicit implementation in ES_PROGRAMGUIDEDAYSCHEDULE.Projection, works
ds_channel = new ESP_CHANNEL
{
name = x.PgdsIdDayscheduleNavigation.DsIdScheduleNavigation.SchIdChannelNavigation.LotIdLeafoftreepartNavigation.Leaf
}
whilst the seemingly equivalent implemention, where the instantiation of the ESP_CHANNEL is held in a the projection expression fails
ds_channel = ESP_CHANNEL.Projection.Compile().Invoke(x.PgdsIdDayscheduleNavigation.DsIdScheduleNavigation.SchIdChannelNavigation)
fails with
An unhandled exception of type 'System.InvalidOperationException' occurred in Microsoft.EntityFrameworkCore.dll
The LINQ expression 'DbSet<Pgprogramguidedayschedule>()
.LeftJoin(
inner: DbSet<Wondayschedule>(),
outerKeySelector: p => EF.Property<decimal?>(p, "PgdsIdDayschedule"),
innerKeySelector: w => EF.Property<decimal?>(w, "Oid"),
resultSelector: (o, i) => new TransparentIdentifier<Pgprogramguidedayschedule, Wondayschedule>(
Outer = o,
Inner = i
))
.LeftJoin(
inner: DbSet<Psischedule>(),
outerKeySelector: p => EF.Property<decimal?>(p.Inner, "DsIdSchedule"),
innerKeySelector: p0 => EF.Property<decimal?>(p0, "Oid"),
resultSelector: (o, i) => new TransparentIdentifier<TransparentIdentifier<Pgprogramguidedayschedule, Wondayschedule>, Psischedule>(
Outer = o,
Inner = i
))
.LeftJoin(
inner: DbSet<Channel>(),
outerKeySelector: p => EF.Property<decimal?>(p.Inner, "SchIdChannel"),
innerKeySelector: c => EF.Property<decimal?>(c, "LotIdLeafoftreepart"),
resultSelector: (o, i) => new TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<Pgprogramguidedayschedule, Wondayschedule>, Psischedule>, Channel>(
Outer = o,
Inner = i
))
.Where(p => __Compile_0.Invoke(p.Inner).name == __channel_1 && p.Outer.Outer.Inner.DsTxdate == __dt_2)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
any ideas?
(the problem appears to be that its the where clause that fails to interpret the invoke correctly).
EDIT 1
note his query DOES execute correctly.
var x =
(from schedule in ES_PROGRAMGUIDEDAYSCHEDULE.Query(ds)
//where schedule.ds_channel.name == channel
//&& schedule.ds_date == dt
select schedule).Take(1);
so it appears that it is the where clause navigating via ds_channel that is causing the issue.
You can use LINQKit to make this query working. It needs just configuring DbContextOptions:
builder
.UseSqlServer(connectionString) // or any other provider
.WithExpressionExpanding(); // enabling LINQKit extension
Then you can inject your projection using LINQKit's Invoke method (but possible your query will be corrected also)
ds_channel = ESP_CHANNEL.Projection.Invoke(x.PgdsIdDayscheduleNavigation.DsIdScheduleNavigation.SchIdChannelNavigation)
Also you may find helpful this answer. It shows how to hide expression magic from end user.

How can I achieve to implement the below query without the if statement (SUM/AVG)?

public class DailyAggregator : Aggregator
{
public override Dictionary<string, int?> Aggregate<T>(IQueryable<T> query, Expression<Func<T, DateTime>> groupByProperty, Expression<Func<T, double>> operationProperty = null)
{
if (operationProperty == null) // property of sum/avg (null = count)
operationProperty = x => 1;
if (_operationType.Equals(ReportAggregationOperation.Sum))
{
return query
.GroupBy(g => new
{
Day = TestableDbFunctions.TruncateTime(groupByProperty.Invoke(g))
})
.Select(x => new
{
Key = x.Key.Day.ToString().Substring(0, 10),
Value = (int?) x.Sum(operationProperty.Compile()),
})
.ToDictionary(t => t.Key, t => t.Value);
}
else
{
return query
.GroupBy(g => new
{
Day = DbFunctions.TruncateTime(groupByProperty.Invoke(g))
})
.Select(
x => new
{
Key = x.Key.Day.ToString().Substring(0, 10),
Value = (int?) x.Average(operationProperty.Compile()),
}).ToDictionary(t => t.Key, t => t.Value);
}
}
}
I'm using an IOC container for creating instances like dailyaggreagtor/monthlyaggregator...
But I wasn't able to build this group by expression, or to apply the right design pattern to eliminate the above if statement.
The compile and invoke functions comes from the LinqKit extension.
The classes using this aggregator(s) is querying the DB and collecting data for callcenter reports (ex. TotalCdrRecords report, ReachedCdrRecords report, CdrTalkTime report, CdrCallAfterworkTime etc., a CDR is a call data record, practically holding all information about a call). The type of the query (T) is specified in the report classes, but basically is an IReportableEntity, but of course the operationProperty it can be any property of the DB Entity which we can perfom the count/sum/avg operations, the groupByProperty is always a DateTime column.
It generates the following sql query:
SELECT
1 AS [C1],
SUBSTRING(CASE WHEN ([GroupBy1].[K1] IS NULL) THEN N'' ELSE CAST( [GroupBy1].[K1] AS nvarchar(max)) END, 0 + 1, 10) AS [C2],
CAST( [GroupBy1].[A1] AS int) AS [C3]
FROM ( SELECT
[Filter1].[K1] AS [K1],
SUM([Filter1].[A1]) AS [A1]
FROM ( SELECT
convert (datetime2, convert(varchar(255), [Extent1].[CreatedOn], 102) , 102) AS [K1],
cast(1 as float(53)) AS [A1]
FROM [dbo].[VCCCdr] AS [Extent1]
WHERE ([Extent1].[CreatedOn] >= #p__linq__0) AND ([Extent1].[CreatedOn] <= #p__linq__1) AND ([Extent1].[CompanyId] = #p__linq__2)
) AS [Filter1]
GROUP BY [K1]
) AS [GroupBy1]
Since you are using LINQKit, you can extract an expression for calling Sum / Average part and Invoke it inside the query.
For instance:
var aggregateFunc = _operationType.Equals(ReportAggregationOperation.Sum) ?
Linq.Expr((IEnumerable<double> source) => source.Sum()) :
Linq.Expr((IEnumerable<double> source) => source.Average());
return query
.GroupBy(g => new
{
Day = DbFunctions.TruncateTime(groupByProperty.Invoke(g))
})
.Select(x => new
{
Key = x.Key.Day.ToString().Substring(0, 10),
Value = (int?)aggregateFunc.Invoke(x.Select(operationProperty.Compile())),
})
.ToDictionary(t => t.Key, t => t.Value);

Error when implementing Soft Delete in EF (Map was called more than once for type)

i trying to implement soft delete in Entity Framework
I have this code in OnModelCreating
if (modelBuilder == null)
{
throw new ArgumentNullException("modelBuilder");
}
modelBuilder.Entity<IdentityUser>().ToTable("AspNetUsers");
EntityTypeConfiguration<ApplicationUser> table = modelBuilder.Entity<ApplicationUser>().ToTable("AspNetUsers");
table.Property((ApplicationUser u) => u.UserName).IsRequired();
modelBuilder.Entity<ApplicationUser>().HasMany((ApplicationUser u) => u.Roles);
modelBuilder.Entity<IdentityUserRole>().HasKey((IdentityUserRole r) => new { UserId = r.UserId, RoleId = r.RoleId }).ToTable("AspNetUserRoles");
modelBuilder.Entity<ApplicationUser>().HasMany((ApplicationUser u) => u.Groups);
EntityTypeConfiguration<ApplicationUserGroup> usergroup = modelBuilder.Entity<ApplicationUserGroup>().ToTable("ApplicationUserGroups");
modelBuilder.Entity<Group>().HasMany((Group g) => g.Roles);
modelBuilder.Entity<ApplicationRoleGroup>().HasKey((ApplicationRoleGroup gr) => new { RoleId = gr.RoleId, GroupId = gr.GroupId }).ToTable("ApplicationRoleGroups");
EntityTypeConfiguration<Group> groupsConfig = modelBuilder.Entity<Group>().ToTable("Groups");
groupsConfig.Property((Group r) => r.Name).IsRequired();
EntityTypeConfiguration<IdentityUserLogin> entityTypeConfiguration =
modelBuilder.Entity<IdentityUserLogin>().HasKey((IdentityUserLogin l) => new { UserId = l.UserId, LoginProvider = l.LoginProvider, ProviderKey = l.ProviderKey }).ToTable("AspNetUserLogins");
entityTypeConfiguration.HasRequired((IdentityUserLogin u) => u.User);
EntityTypeConfiguration<IdentityUserClaim> table1 = modelBuilder.Entity<IdentityUserClaim>().ToTable("AspNetUserClaims");
table1.HasRequired((IdentityUserClaim u) => u.User);
modelBuilder.Entity<IdentityRole>().ToTable("AspNetRoles");
EntityTypeConfiguration<ApplicationRole> entityTypeConfiguration1 = modelBuilder.Entity<ApplicationRole>().ToTable("AspNetRoles");
entityTypeConfiguration1.Property((ApplicationRole r) => r.Name).IsRequired();
modelBuilder.Entity<Group>().Map(m => m.Requires("IsDeleted").HasValue(false)).Ignore(m => m.IsDeleted);
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
when i try to Migrate that to Database it gives me this error
Map was called more than once for type 'Group' and at least one of the
calls didn't specify the target table name.
this error only occurs when i add this Line of code to OnModelCreating
modelBuilder.Entity<Group>().Map(m => m.Requires("IsDeleted").HasValue(false)).Ignore(m => m.IsDeleted);
Any Solution for that, i need to do that to get only the rows which are (!Is.Deleted)
this scenario will be for most of my Models,
Please Advice,

The Include path expression must refer to a navigation property defined on the type

I have the following Repository method:-
public AccountDefinition GetCustomer2(int id)
{
var c = entities.AccountDefinitions
.Where(p=>p.ORG_ID==id)
.Include(a => a.SDOrganization)
.Include(a2 => a2.SiteDefinitions)
.Include(a3 => a3.SDOrganization.AaaPostalAddresses)
.Include(a4 => a4.SiteDefinitions.SelectMany
(a5 => a5.DepartmentDefinitions.SelectMany
(a6 => a6.SDUsers.Select
(a7 => a7.AaaUser))))
.SingleOrDefault();
return c;
}
The the following action method which calls the above method:-
public ActionResult Details2(int id = 0)
{
AccountDefinition cd = repository.GetCustomer2(id);
return View("copy",cd);
}
but when i navigate to the Action Method , i get the following error on the repository class:-
The Include path expression must refer to a navigation property
defined on the type. Use dotted paths for reference navigation
properties and the Select operator for collection navigation
properties.
So what is wrong with my code?
I think you may want to do something like
public AccountDefinition GetCustomer2(int id)
{
var c = entities.AccountDefinitions.Where(p=>p.ORG_ID==id)
.Include(a => a.SDOrganization)
.Include(a2 => a2.SiteDefinitions)
.Include(a3 => a3.SDOrganization.AaaPostalAddresses)
.Include(a4 => a4.SiteDefinitions.Select(a5 => a5.DepartmentDefinitions.Select(a6 => a6.SDUsers.Select(a7 => a7.AaaUser))));
return c;
}

Unable to cast object of type 'System.Linq.Expressions.FieldExpression'

I am using LinqKit.dll to do Linq To Entity in my application, as follow :
qry = _articleRepo.GetItemsByCulture(Thread.CurrentThread.CurrentCulture.Name)
.AsExpandable().Where(x => x.Approved && isInCategory(x.CategoryID, category.CategoryID));
Func<string, int, bool> isInCategory = (x, y) =>
{
IQueryable<string> list = x.Split(',').AsQueryable();
//Except
//x.Except(_);
return list.Any(z => z == y.ToString());
};
it gives me error :
System.InvalidCastException: Unable to cast object of type
'System.Linq.Expressions.FieldExpression' to type
'System.Linq.Expressions.LambdaExpression'.
but removing isInCategory(x.CategoryID, category.CategoryID) causes the application to run without problem.
Would please help me ?
Ok, after looking at the code in comments, I can suggest following:
Replace string CategoryID on Article to public virtual ICollection<Category> Categories { get; set; }, so EF will create a foreign key. Then, in order to get articles, that are in particular category, you can use code similar to the following:
var articlesInCategory = context.Articles
.Where(x => x.Language == Thread.CurrentThread.CurrentCulture.Name)
.Where(x => x.Approved && x.Categories.Any(c => c.CategoryID == c1.CategoryID)).ToList();
And when you want to create new articles, you should use something like:
var c1 = context.Categories.OrderBy(c => c.Title).First();
var c2 = context.Categories.OrderBy(c => c.Title).Skip(1).First();
context.Articles.Add(new Article { Categories = new Collection<Category> { c1, c2 } });
context.SaveChanges();