I want my union LINQ query to be evaluated on server side with EF Core.
There're entities:
public class Entity1
{
public int Id { get; set; }
public List<StringWithStyle> Names { get; set; } = new List<StringWithStyle>();
}
public class Entity2
{
public int Id { get; set; }
public StringWithStyle Name { get; set; }
}
public class StringWithStyle
{
public string Text { get; set; }
public bool IsBold { get; set; }
public bool IsItalic { get; set; }
public bool IsUpperCase { get; set; }
}
Their properties are stored in DbContext as json string using Value conversion:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Entity1>()
.HasKey(e => e.Id);
modelBuilder.Entity<Entity1>()
.Property(e => e.Names)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<StringWithStyle>>(v, (JsonSerializerOptions)null)
,
new ValueComparer<List<StringWithStyle>>(
(arr1, arr2) => arr1.Count() == arr2.Count() && !arr1.Except(arr2).Any(),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())))
);
modelBuilder.Entity<Entity2>()
.HasKey(e => e.Id);
modelBuilder.Entity<Entity2>()
.Property(e => e.Name)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<StringWithStyle>(v, (JsonSerializerOptions)null)
,
new ValueComparer<StringWithStyle>(
(val1, val2) => val1.Equals(val2),
c => c.GetHashCode())
);
}
I need to show both entities in one grid. So, I use such a query:
var entities1 = from e1 in dbContext.Set<Entity1>()
select new GridModel
{
Id = e1.Id,
IsFirst = true,
Names = e1.Names,
Name = default
};
var entities2 = from e2 in dbContext.Set<Entity2>()
select new GridModel
{
Id = e2.Id,
IsFirst = false,
Name = e2.Name,
Names = default
};
var grid = entities1.Union(entities2).ToList();
And it throws an Exception:
System.InvalidOperationException : Unable to translate set operation after client projection has been applied. Consider moving the set operation before the last 'Select' call.
Is it possible to to get such a query that is evaluating on server side?
*** UPDATE ***
There's GridModel class:
public class GridModel
{
public int Id { get; set; }
public bool IsFirst { get; set; }
public List<StringWithStyle> Names { get; set; }
public StringWithStyle Name { get; set; }
}
Related
I have a base class with some properties and 10 derived types, 2 of them have a navigation property to another table, both of them are configured the same way, one is working and the other is not.
public abstract class StepBase : FullAuditedEntity<Guid>
{
public StepType StepType { get; set; }
public bool IsDone { get; protected set; }
public string Description { get; set; }
}
public class TimerStep : StepBase
{
public string Label { get; set; }
**public UpdateField UpdateField { get; set; }**
public class UpdateStep : StepBase
{
public UpdateField UpdateField { get; set; }
}
builder.Entity<StepBase>(b =>
{
//Configure table & schema name
b.ConfigureBaseEntityProperties(tableName: "Steps");
//Properties
b.Property(p => p.Title).HasMaxLength(100).IsUnicode(false).IsUnicode(false).IsRequired();
b.Property(p => p.Order).IsRequired(true);
b.Property(p => p.IsDone).HasDefaultValue(false);
b.Property(p => p.Description).HasMaxLength(2048).IsUnicode(false);
b.HasMany(p => p.SubSteps).WithOne();
b.HasDiscriminator(p => p.StepType)
.HasValue<ChangePCountStep>(StepType.ChangePCount)
.HasValue<ExperimentStep>(StepType.Experiment)
.HasValue<GeneticModificationStep>(StepType.GeneticModification)
.HasValue<MyOwnStep>(StepType.MyOwn)
.HasValue<StartDateStep>(StepType.StartDate)
.HasValue<StoredSampleStep>(StepType.StoredSample)
.HasValue<TimerStep>(StepType.Timer)
.HasValue<TreatmentStep>(StepType.Treatment)
.HasValue<UpdateStep>(StepType.Update)
.HasValue<DestinationVesselStep>(StepType.DestinationVessel);
});
private static void ConfigureTimerStep(ModelBuilder builder)
{
builder.Entity<TimerStep>(b =>
{
//Properties
b.Property(p => p.Label).HasMaxLength(256).IsUnicode(false).IsRequired(false);
b.Property(p => p.StartDate).IsRequired();
b.Property(p => p.ReleaseIn).IsRequired();
b.Property(p => p.ReleaseInTimeLapse).IsRequired();
b.HasOne(a => a.UpdateField)
.WithOne(b => (TimerStep)b.Step)
.HasForeignKey<UpdateField>(b => b.StepId);
});
}
private static void ConfigureUpdateField(ModelBuilder builder)
{
builder.Entity<UpdateField>(b =>
{
//Configure table & schema name
b.ConfigureBaseEntityProperties(tableName: "UpdateField");
//Properties
b.Property(p => p.UpdatedDate).IsRequired();
b.Property(p => p.UpdateType).IsRequired();
b.Property(p => p.Field).HasMaxLength(2048).IsUnicode(false).IsRequired();
});
}
private static void ConfigureUpdateStep(ModelBuilder builder)
{
builder.Entity<UpdateStep>(b =>
{
//Properties
b.HasOne(a => a.UpdateField)
.WithOne(b => (UpdateStep)b.Step)
.HasForeignKey<UpdateField>(b => b.StepId);
});
}
UpdateStep is the one working fine and TimerStep is not, when I try to insert one it throws a
---- Microsoft.Data.Sqlite.SqliteException : SQLite Error 19: 'FOREIGN KEY constraint failed'.
the insert method are excactly the same also
TimerStep step = new(newId)
{
ReleaseInTimeLapse = input.ReleaseInTimeLapse,
StartDate = input.StartDate,
Label = input.Label,
ReleaseIn = input.ReleaseIn,
UpdateField = new(GuidGenerator.Create())
{
Field = input.UpdateField.Field,
UpdateType = input.UpdateField.UpdateType,
UpdatedDate = input.UpdateField.UpdatedDate,
},
};
UpdateStep step = new(GuidGenerator.Create())
{
UpdateField = new(GuidGenerator.Create())
{
Field = input.Field,
UpdateType = input.UpdateType,
UpdatedDate = input.UpdatedDate,
}
};
Both the entities should be working the same way
OData url:
https://{{baseUrl}}/api/vehicles?$expand=OwnershipHistories
I want to return a vehicle(s) and than $expand the OwnershipHistories as a List of owner ship histories.
But I only get one (1) Ownershiphistorie.
Question: Why can I not get all the ownership histories???
I have this on my controller:
[HttpGet]
[EnableQuery(PageSize = 10, MaxExpansionDepth = 20)]
[ODataRoute("vehicles")]
I use this url: {{baseUrl}}/api/vehicles?$expand=OwnershipHistories
I use the following c# code ():
public async Task<IQueryable<VehicleEntity>> GetVehicles()
{
var vehicles = _context.Vehicles; // .Include(v => v.OwnershipHistories) => ??? Is not working...
return vehicles;
}
The result is:
{
"#odata.context": "https://localhost:5001/api/$metadata#Vehicles(OwnershipHistories())",
"value": [
{
"id": 1,
"registrationNumber": "10STNV",
"vehicleIdentificationNumber": "JF1BP9LLA6G052053",
// more fields
"**OwnershipHistories**": [ ?????? Only **one** record, not the **8** records I expect ?????)
{
"VweID": 1,
"registrationNumber": "10STNV",
// more fields
}
]
},
}
This is the query for the database:
I have two entities and they are views in the sql database:
OwnershipHistoryEntity:
[Table("EigenaarsHistorie", Schema = "Voertuig")]
public class OwnershipHistoryEntity
{
[Key]
public int VweID { get; set; } // VDNI
// more fields
public virtual VehicleEntity Vehicle { get; set; }
}
VehicleEntity:
namespace VWE.MijnVWE.Vehicle.Api.DAL.Entities
{
[Table("VoertuigInformatie", Schema = "Voertuig")]
public class VehicleEntity
{
[Key]
public int VweID { get; set; } // VDNI
public string Kenteken { get; set; }
public string VoertuigIdentificatieNummer { get; set; }
// more fields
[ForeignKey("VweID")]
public ICollection<OwnershipHistoryEntity> OwnershipHistories { get; set; } = new List<OwnershipHistoryEntity>();
}
}
EdmModel builder:
using Microsoft.AspNet.OData.Builder;
using Microsoft.OData.Edm;
using VWE.MijnVWE.Vehicle.Api.DAL.Entities;
namespace VWE.MijnVWE.Vehicle.Api.BLL.Builders
{
public static class ModelBuilder
{
private static IEdmModel _edmModel;
public static IEdmModel GetEdmModel()
{
return GetExplicitEdmModel();
}
static IEdmModel GetExplicitEdmModel()
{
if (_edmModel == null)
{
var modelBuilder = new ODataConventionModelBuilder();
var vehicles = modelBuilder.EntitySet<VehicleEntity>("Vehicles");
var ownershipHistories = modelBuilder.EntitySet<OwnershipHistoryEntity>("OwnershipHistories");
modelBuilder.Namespace = "vwe.mijnvwe.vehicle";
modelBuilder.ContainerName = "vehicleApiContainer";
vehicles.EntityType.Name = "vehicles";
vehicles.EntityType.HasKey(k => k.VweID);
vehicles.EntityType.HasMany(v => v.OwnershipHistories);
vehicles.HasManyBinding(v => v.OwnershipHistories, "OwnershipHistories");
ownershipHistories.EntityType.Name = "ownershipHistories";
ownershipHistories.EntityType.HasKey(k => k.VweID);
ownershipHistories.EntityType.HasRequired(o => o.Vehicle, (o, t) => o.VweID == t.VweID);
vehicles.EntityType.Property(p => p.VweID).Name = "id";
vehicles.EntityType.Property(p => p.Kenteken).Name = "registrationNumber";
vehicles.EntityType.Property(p => p.VoertuigIdentificatieNummer).Name = "vehicleIdentificationNumber";
// more fields
ownershipHistories.EntityType.Property(p => p.Kenteken).Name = "registrationNumber";
ownershipHistories.EntityType.Property(p => p.EventDatum).Name = "eventDate";
ownershipHistories.EntityType.Property(p => p.SoortAansprReferentieCode).Name = "liabilityReferenceCode";
ownershipHistories.EntityType.Property(p => p.RegistratieDatumAansprakelijkheid).Name = "from";
ownershipHistories.EntityType.Property(p => p.RegistratieDatumAansprakelijkheidTot).Name = "to";
ownershipHistories.EntityType.Property(p => p.DagenInBezit).Name = "numberOfDays";
ownershipHistories.EntityType.Property(p => p.DatumGewijzigd).Name = "changedDateTime";
_edmModel = modelBuilder.GetEdmModel();
}
return _edmModel;
}
}
}
Ef core modelBuilder:
public class VehicleDbContext : DbContext
{
public VehicleDbContext(DbContextOptions<VehicleDbContext> options)
: base(options)
{ }
public DbSet<VehicleEntity> Vehicles { get; set; }
public DbSet<OwnershipHistoryEntity> OwnershipHistories { get; set; }
// more fields
protected override void OnModelCreating(ModelBuilder builder)
{
// ...
base.OnModelCreating(builder);
}
}
This is the Select query:
SELECT [t].[VweID], [t].[AandrijvingOmschrijving],
//more fields.. [t0].[SoortAansprReferentieCode], [t0].[c0]
FROM (
SELECT TOP(#__TypedProperty_3) [v].[VweID], [v].[AandrijvingOmschrijving], [v].[AantalAangedrevenAssen],
// more fields
[v].[Werkingsbeginsel], [v].[Wielbasis]
FROM [Voertuig].[VoertuigInformatie] AS [v]
ORDER BY [v].[VweID]
) AS [t]
OUTER APPLY (
SELECT TOP(#__TypedProperty_1) #__TypedProperty_2 AS [c], [e].[VweID], [e].[DagenInBezit], [e].[DatumGewijzigd], [e].[EventDatum], [e].[Kenteken], [e].[RegistratieDatumAansprakelijkheid], [e].[RegistratieDatumAansprakelijkheidTot], [e].[SoortAansprReferentieCode], CAST(1 AS bit) AS [c0]
FROM [Voertuig].[EigenaarsHistorie] AS [e]
WHERE [t].[VweID] = [e].[VweID]
ORDER BY [e].[VweID]
) AS [t0]
ORDER BY [t].[VweID], [t0].[VweID]
It is quit similar to the following question:
Same question about washington school odata question
Washington school code odata example
I'm trying to map a parent/child to another parent/child where the child.id need to be read from a key/value dictionary.
I could do a foreach loop to map the children but I'm interrested to see if there is another way. The code is a simplified version of my model and the remoteDict is read async so using a IValueResolver seems unusable?
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<ParentFrom, ParentTo>()
.ForMember(d => d.Children, o => o.MapFrom(s => s.Children));
cfg.CreateMap<(ChildFrom child, Dictionary<string, int> dict), ChildTo>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.dict.GetValueOrDefault(s.child.ExternalId)));
});
var mapper = config.CreateMapper();
Dictionary<string, int> remoteDict = new Dictionary<string, int>();
remoteDict.Add("A1", 1);
remoteDict.Add("B1", 1);
ParentFrom p = new ParentFrom() { Id = 1001 };
p.Children.Add(new ChildFrom() { ExternalId = "A1" });
p.Children.Add(new ChildFrom() { ExternalId = "B1" });
ParentTo p2 = mapper.Map<ParentTo>(p);
/*
Missing type map configuration or unsupported mapping.
Mapping types:
ChildFrom -> ChildTo
mapping.ChildFrom -> mapping.ChildTo
*/
}
}
public class ParentFrom
{
public int Id { get; set; }
public List<ChildFrom> Children { get; set; } = new List<ChildFrom>();
}
public class ChildFrom
{
public string ExternalId { get; set; }
}
public class ParentTo
{
public int Id { get; set; }
public List<ChildTo> Children { get; set; }
}
public class ChildTo
{
public int Id { get; set; }
}
UPDATE
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<ParentFrom, ParentTo>()
.ForMember(d => d.Children, o => o.MapFrom(s => s.Children));
cfg.CreateMap<ChildFrom, ChildTo>()
.ForMember(d => d.Id, o => o.MapFrom<FromRemote>());
});
var mapper = config.CreateMapper();
Dictionary<string, int> remoteDict = new Dictionary<string, int>();
remoteDict.Add("A1", 1);
remoteDict.Add("B1", 1);
ParentFrom p = new ParentFrom() { Id = 1001 };
p.Children.Add(new ChildFrom() { ExternalId = "A1" });
p.Children.Add(new ChildFrom() { ExternalId = "B1" });
ParentTo p2 = mapper.Map<ParentTo>(p, opt => opt.Items["dict"] = remoteDict);
}
}
public class FromRemote : IValueResolver<ChildFrom, ChildTo, int>
{
public int Resolve(ChildFrom source, ChildTo destination, int destMember, ResolutionContext context)
{
var dict = context.Items["dict"] as Dictionary<string, int>;
return dict.GetValueOrDefault(source.ExternalId);
}
}
public class ParentFrom
{
public int Id { get; set; }
public List<ChildFrom> Children { get; set; } = new List<ChildFrom>();
}
public class ChildFrom
{
public string ExternalId { get; set; }
}
public class ParentTo
{
public int Id { get; set; }
public List<ChildTo> Children { get; set; }
}
public class ChildTo
{
public int Id { get; set; }
}
I have the following LINQ statement:
var repoActivityRowsTest = appManager.GetRepository<ActivityRow>();
var activityRowsTest = await repoActivityRowsTest.Search(f => f.ExcelReport.uploadPhase == RPToolConstants.Phase_Planning, includeProperties: "PlanningInfo")
.Where(f => iso3Alpha3List.Contains(f.ExcelReport.countryOfficeIso3Alpha3))
.SelectMany(sm => sm.PlanningInfo).Select(s => new { s.Year, s.Count, s.ActivityRow.UnitCost })
.GroupBy(g=>new { g.Year }).Select(sg=>new { sg.Key.Year, Total = sg.Sum(sum => sum.UnitCost * sum.Count) })
.ToListAsync();
Which uses the repository pattern. The search function is the one below:
public IQueryable<TEntity> Search(Expression<Func<TEntity, bool>> filter = null,
string includeProperties = "", bool trackChanges = false)
{
IQueryable<TEntity> query = context.Set<TEntity>();
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty.Trim());
}
if (!trackChanges)
{
query = query.AsNoTracking();
}
return query;
}
When I inspect the command that arrives in SQL Server I see that the query is translated in the following SQL:
SELECT [a0].[Year], SUM([a1].[UnitCost] * CAST([a0].[Count] AS decimal(18,2))) AS [Total]
FROM [ActivityRows] AS [a]
INNER JOIN [ExcelReports] AS [e] ON [a].[ExcelReportId] = [e].[Id]
INNER JOIN [ActivityRowPlanningInfo] AS [a0] ON [a].[Id] = [a0].[ActivityRowId]
INNER JOIN [ActivityRows] AS [a1] ON [a0].[ActivityRowId] = [a1].[Id]
WHERE ([e].[uploadPhase] = N'planning')
AND [e].[countryOfficeIso3Alpha3] IN (N'AFG', N'DZA', N'AGO', N'ARM', N'BGD')
GROUP BY [a0].[Year]
It works perfectly, but why there is an inner join duplicated:
INNER JOIN [ActivityRows] AS [a1] ON [a0].[ActivityRowId] = [a1].[Id]
is a non-sense to me!
If I remove it from the SQL it works as before. Is there any issue in my LINQ query that causes this strange SQL?
here is the definition of the entities:
public class ActivityRow : Entity<int>
{
public string Description { get; set; }
public int ExcelReportId { get; set; }
[ForeignKey("ExcelReportId")]
public virtual ExcelReport ExcelReport { get; set; }
public int ActivitySubTypeId { get; set; }
[ForeignKey("ActivitySubTypeId")]
public virtual ActivitySubType ActivitySubType { get; set; }
public int? ActivityCategoryId { get; set; }
[ForeignKey("ActivityCategoryId")]
public virtual ActivityCategory ActivityCategory { get; set; }
public string ResponsibleEntity { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal UnitCost { get; set; }
public string Notes { get; set; }
public virtual ICollection<ActivityRowReportingInfo> ReportingInfo { get; set; }
public virtual ICollection<ActivityRowPlanningInfo> PlanningInfo { get; set; }
}
public class ActivityRowPlanningInfo : Entity<int>
{
public int ActivityRowId { get; set; }
[ForeignKey("ActivityRowId")]
public virtual ActivityRow ActivityRow { get; set; }
public int Year { get; set; }
public int Quarter { get; set; }
public int Count { get; set; }
}
and here the definition of the relationships with fluent API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//activities
modelBuilder.Entity<ActivityRow>()
.HasMany(b => b.ReportingInfo)
.WithOne(t => t.ActivityRow)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ActivityRow>()
.HasMany(b => b.PlanningInfo)
.WithOne(t => t.ActivityRow)
.OnDelete(DeleteBehavior.Cascade);
...etc.
}
Rewrite query via LINQ Query syntax and you can simplify your query with ease.
The following query do not create non wanted joins:
var repoActivityRowsTest = appManager.GetRepository<ActivityRow>();
var activityRows = repoActivityRowsTest
.Search(f => true);
var resultQuery =
from ar in activityRows
where
ar.ExcelReport.uploadPhase == RPToolConstants.Phase_Planning
&& iso3Alpha3List.Contains(ar.ExcelReport.countryOfficeIso3Alpha3)
from pi in ar.PlanningInfo
group new { ar, pi } by new { pi.Year } into g
select new
{
g.Key.Year,
Total = g.Sum(x => x.ar.UnitCost * x.pi.Count)
};
var result = await resultQuery.ToListAsync();
I have code that works, but I worked around a 'Join' in Linq to Entities, because I could not figure it out.
Could you please show me how to succesfully apply it to my code?
My desired result is a dictionary:
Dictionary<string, SelectedCorffData> dataSelectedForDeletion = new Dictionary<string, SelectedCorffData>();
The above mentioned class:
public class SelectedCorffData
{
public long CorffId { get; set; }
public string ReportNumber { get; set; }
public DateTime CorffSubmittedDateTime { get; set; }
}
Please note the 'intersectResult' I am looping through is just a string collection.
Here is my code:
DateTime dateToCompare = DateTime.Now.Date;
Dictionary<string, SelectedCorffData> dataSelectedForDeletion = new Dictionary<string, SelectedCorffData>();
foreach (var mafId in intersectResult)
{
var corffIdsPerMaf = context
.Mafs
.Where(m => m.MafId == mafId)
.Select(m => m.CorffId);
var corffIdForMaf = context
.Corffs
.Where(c => corffIdsPerMaf.Contains(c.Id))
.OrderByDescending(c => c.CorffSubmittedDateTime)
.Select(c => c.Id)
.First();
//Selected close-out forms, whose MAF's may be up for deletion, based on date.
var corffData = context
.Corffs
.Where(c => c.Id == corffIdForMaf && System.Data.Entity.DbFunctions.AddYears(c.CorffSubmittedDateTime, 1).Value > dateToCompare)
.Select(c => new SelectedCorffData () { CorffId = c.Id, ReportNumber = c.ReportNumber, CorffSubmittedDateTime = c.CorffSubmittedDateTime })
.FirstOrDefault();
if(corffData != null)
{
dataSelectedForDeletion.Add(mafId, corffData);
}
}
Please note: this is not just a simple join. If it can't be simplified, please tell me. Also please explain why.
The code below I don't think is exactly right but it is close to what you need. I simulated the database so I could get the syntax correct.
namespace System
{
namespace Data
{
namespace Entity
{
public class DbFunctions
{
public static Data AddYears(DateTime submittedTime, int i)
{
return new Data();
}
public class Data
{
public int Value { get; set; }
}
}
}
}
}
namespace ConsoleApplication23
{
class Program
{
static void Main(string[] args)
{
Context context = new Context();
int dateToCompare = DateTime.Now.Year;
var corffIdsPerMaf = context.Mafs.Select(m => new { id = m.CorffId, mafs = m}).ToList();
var corffIdForMaf = context.Corffs
.Where(c => System.Data.Entity.DbFunctions.AddYears(c.CorffSubmittedDateTime, 1).Value > dateToCompare)
.OrderByDescending(c => c.CorffSubmittedDateTime).Select(c => new { id = c.Id, corff = c}).ToList();
var intersectResult = from p in corffIdsPerMaf
join f in corffIdForMaf on p.id equals f.id
select new SelectedCorffData() { CorffId = p.id, ReportNumber = f.corff.ReportNumber, CorffSubmittedDateTime = f.corff.CorffSubmittedDateTime };
Dictionary<string, SelectedCorffData> dataSelectedForDeletion = intersectResult.GroupBy(x => x.ReportNumber, y => y).ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
}
public class Context
{
public List<cMafs> Mafs { get; set;}
public List<cCorffs> Corffs { get; set;}
}
public class cMafs
{
public int CorffId { get; set; }
}
public class cCorffs
{
public DateTime CorffSubmittedDateTime { get; set; }
public int Id { get; set; }
public string ReportNumber { get; set; }
}
public class Test
{
}
public class SelectedCorffData
{
public long CorffId { get; set; }
public string ReportNumber { get; set; }
public DateTime CorffSubmittedDateTime { get; set; }
}
}