I have a Repository pattern for product entity and I have a method that retrieves a product by id. Each product has a category, among other complex properties. In some cases I want to retrieve the products without the category (lazy loading) but in some cases I want to return both entities (products and categories). Is there any better option that having two methods? this is what I coded:
Product GetById(int id)
{
// without includes
...
}
Product GetByIdFull(int id)
{
// with includes
...
}
You can do something like this:
public Product Get(int id, params Expression<Func<TEntity, object>>[] propertiesToInclude)
{
var query = context.Products;
foreach (var expression in propertiesToInclude)
{
query = query.Include(expression);
}
return query.SingleOrDefault(p => p.id == id);
}
The calling code could optionally specify the properties to be included using a lambda like so:
var justProduct = repo.Get(productId);
var productAndCategory = repo.Get(productId, p => p.Category);
Related
I faced with a strange behaviour when entity framework core throwing away my Includes.
I need to make some generic methods to filter and combine my queries and those methods must receive some parameters and IQueryable filter subquery and return combined IQueryable result for further composition.
I simplified my code and made an example when you can see what I do mean:
public IQueryable<Tuple<TResult, TFilter>> Method1<TResult, TFilter>(IQueryable<TFilter> filters)
where TResult : ResultEntity
where TFilter : FilterEntity
{
var q = from state in _dbContext.Set<TResult>()
join f in filters on state.ID_Result equals f.ID
where ....
select new Tuple<TResult, TFilter>(state, f);
return q;
}
public void GetResult(int pageIndex, int pageSize)
{
IQueryable<Car> results = _dbContext.Cars.Include(x => x.Images);
// 1) All right, it has the images
var q1 = Method1<CarState, Car>(results).ToList();
// 2) Wrong, there is no images. And SQL query doesn't contain any joins to Images table
var q2 = Method1<CarState, Car>(results).Skip(pageIndex).Take(pageSize).ToList();
// 3) Without the method and with anonymous type it's ok.
var tmp = from state in _dbContext.Set<CarState>()
join f in results on state.ID_Result equals f.ID
where ....
select new { state, f };
var q3 = tmp.Skip(pageIndex).Take(pageSize).ToList();
}
What did I do wrong?
Do not use Builder methods or constructors if you want to use query later. It is because LINQ translator cannot trace back fields to generate correct SQL.
This part of query is problematic
new Tuple<TResult, TFilter>(state, f);
Better to create new class(es)
public class MTuple<T1, T2>
{
public T1 Item1 { get; set; }
public T2 Item2 { get; set; }
}
And use them in your methods (similar to anonymous classes usage)
public IQueryable<MTuple<TResult, TFilter>> Method1<TResult, TFilter>(IQueryable<TFilter> filters)
where TResult : ResultEntity
where TFilter : FilterEntity
{
var q = from state in _dbContext.Set<TResult>()
join f in filters on state.ID_Result equals f.ID
where ....
select new MTuple<TResult, TFilter>
{
Item1 = state,
Item2 = f
};
return q;
}
SUGGESTION
Such classes can be generated by T4 template, or just copy generated code
So I partially followed from an SO answer on how to store a property with array datatype in Entity Framework. What I didn't follow on that answer is setting the string InternalData to be private instead of public as I find it a code smell if it is set to public (not enough reputation to comment there yet).
I also managed to map the private property in entity framework from this blog.
When I perform CR (create, read) from that entity, all goes well. However, when my LINQ query has a where clause using that property with array datatype, it says that "System.NotSupportedException: 'The specified type member is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.'".
How to work around on this? Here are the relevant code blocks:
public class ReminderSettings
{
[Key]
public string UserID { get; set; }
[Column("RemindForPaymentStatus")]
private string _remindForPaymentStatusCSV { get; set; }
private Status[] _remindForPaymentStatus;
[NotMapped]
public Status[] RemindForPaymentStatus
{
get
{
return Array.ConvertAll(_remindForPaymentStatusCSV.Split(','), e => (Status)Enum.Parse(typeof(Status), e));
}
set
{
_remindForPaymentStatus = value;
_remindForPaymentStatusCSV = String.Join(",", _remindForPaymentStatus.Select(x => x.ToString()).ToArray());
}
}
public static readonly Expression<Func<ReminderSettings, string>> RemindForPaymentStatusExpression = p => p._remindForPaymentStatusCSV;
}
public enum Status
{
NotPaid = 0,
PartiallyPaid = 1,
FullyPaid = 2,
Overpaid = 3
}
protected override void OnModelCreating(DbModelBuuilder modelBuilder)
{
modelBuilder.Entity<ReminderSettings>().Property(ReminderSettings.RemindForPaymentStatusExpression);
}
//This query will cause the error
public IEnumerable<ReminderSettings> GetReminderSettingsByPaymentStatus(Status[] statusArray)
{
var query = ApplicationDbContext.ReminderSettings.Where(x => x.RemindForPaymentStatus.Intersect(statusArray).Any());
return query.ToList(); //System.NotSupportedException: 'The specified type member 'RemindForPaymentStatus' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.'
}
Entity Framework can not translate LINQ expressions to SQL if they access a property that is annotated as [NotMapped]. (It also can not translate if the property contains custom C# code in its getter/setter).
As a quick (but potentially low performance) workaround, you can execute the part of the query that does not cause problems, then apply additional filtering in-memory.
// execute query on DB server and fetch items into memory
var reminders = dbContext.ReminderSettings.ToList();
// now that we work in-memory, LINQ does not need to translate our custom code to SQL anymore
var filtered = reminders.Where(r => r.RemindForPaymentStatus.Contains(Status.NotPaid));
If this causes performance problems, you have to make the backing field of your NotMapped property public and work directly with it.
var filtered = dbContext.ReminderSettings
.Where(r => r._remindForPaymentStatusCSV.Contains(Status.NotPaid.ToString("D"));
Edit
To handle multiple Status as query parameters, you can attach Where clauses in a loop (which behaves like an AND). This works as long as your Status enum values are distinguishable (i.e. there is no Status "11" if there is also a Status "1").
var query = dbContext.ReminderSettings.Select(r => r);
foreach(var statusParam in queryParams.Status) {
var statusString = statusParam.ToString("D");
query = query.Where(r => r._remindForPaymentStatusCSV.Contains(statusString));
}
var result = query.ToArray();
I have a structure of classes that follow logic similar to this...
DBContext1
public class Parent {
public int Id
public ICollection<Child> child
}
public class Child {
public int Id
public string Item
public ICollection<Action> action
}
public class Action {
public int Id
}
This is a one to many to many relationship. I created these tables using entity framework and everything is working well.
My problem is that I want to do a left join from my class child to an object in a different dbContext. Most of the examples I have seen all do the left join at the parent class level.
For example in DBContext2 I have a class that contains item information. I want to perform a left join on the item property from the child class in DBContext1.
DBContext2
public class Item {
public string Item
public string Description
}
This below gets me my Parent and Child objects joined together but how do I expand it to do a left join on the item class object
using (var dbcontext1 = new DBContext1())
{
using (var dbcontext2 = new DBContext2())
{
var parent = dbcontext1.Parent
.Include(ch => ch.Child)
//maybe do groupjoin here... would join parent.Child.item = dbcontext2.item.item
.ToList();
}
}
I have tried doing a GroupJoin but it does not seem to work. I want to use the Fluent pattern instead of using the "from p in Parent" syntax. Is there a way to do this?
Edit 1:
using (var dbcontext1 = new DBContext1())
{
using (var dbcontext2 = new DBContext2())
{
var parent = dbcontext1.Parent
.Include(ch => ch.Child)
.ToList();
var item = dbcontext2.Item.ToList()
var query = from p in parent
join i in item on p.Child.Item == i.item //how do I do the join correctly?
select {
//select whatever here
}
}
}
Comments say to do this in two SQL statements. Running with this approach but struggling how to do the join from the Parent.Child.Item to Item.Item
I have the following method automatically generated from the scaffold template with repository:-
public Group Find(int id)
{
return context.Groups.Find(id);
}
But since the Groups object has two navigation properties which I need , so I wanted to include the .Include, so I replace the .find with .where :-
public Group Find(int id)
{
return context.Groups.Where(c=>c.GroupID==id)
.Include(a => a.UserGroups)
.Include(a2 => a2.SecurityRoles)
.SingleOrDefault();
}
But my question is how can I apply the .Include with the .find() instead of using .Where()?
I was just thinking about what find actually does. #lazyberezovsky is right include and find cant be used in conjunction with each other. I think this is quite deliberate and here's why:
The Find method on DbSet uses the primary key value to attempt to find
an entity tracked by the context. If the entity is not found in the
context then a query will be sent to the database to find the entity
there. Null is returned if the entity is not found in the context or
in the database.
Find is different from using a query in two significant ways:
A round-trip to the database will only be made if the entity with the given key is not found in the context.
Find will return entities that are in the Added state. That is, Find will return entities that have been added to the context but have
not yet been saved to the database.
(from http://msdn.microsoft.com/en-us/data/jj573936.aspx)
Because find is an optimised method it can avoid needing a trip to the server. This is great if you have the entity already tracked, as EF can return it faster.
However if its not just this entity which we are after (eg we want to include some extra data) there is no way of knowing if this data has already been loaded from the server. While EF could probably make this optimisation in conjunction with a join it would be prone to errors as it is making assumptions about the database state.
I imagine that include and find not being able to be used together is a very deliberate decision to ensure data integrity and unnecessary complexity. It is far cleaner and simpler
when you are wanting to do a join to always go to the database to perform that join.
You can't. Find method defined on DbSet<T> type and it returns entity. You can't call Include on entity, so the only possible option is calling Find after Include. You need DbSet<T> type for that, but Include("UserGroups") will return DbQuery<T>, and Include(g => g.UserGroups) will also return DbQuery<T>:
public static IQueryable<T> Include<T>(this IQueryable<T> source, string path)
where T: class
{
RuntimeFailureMethods.Requires(source != null, null, "source != null");
DbQuery<T> query = source as DbQuery<T>;
if (query != null)
return query.Include(path); // your case
// ...
}
DbQuery<T> is not a child of DbSet<T> thus method Find is not available. Also keep in mind, that Find first looks for entity in local objects. How would it include some referenced entities, if they don't loaded yet?
You can try to do this:
public static class DbContextExtention
{
public static TEntity FirstOfDefaultIdEquals<TEntity, TKey>(
this IQueryable<TEntity> source, TKey otherKeyValue)
where TEntity : class
{
var parameter = Expression.Parameter(typeof(TEntity), "x");
var property = Expression.Property(parameter, "ID");
var equal = Expression.Equal(property, Expression.Constant(otherKeyValue));
var lambda = Expression.Lambda<Func<TEntity, bool>>(equal, parameter);
return source.FirstOrDefault(lambda);
}
public static TEntity FirstOfDefaultIdEquals<TEntity>(
this ObservableCollection<TEntity> source, TEntity enity)
where TEntity : class
{
var value = (int)enity.GetType().GetProperty("ID").GetValue(enity, null);
var parameter = Expression.Parameter(typeof(TEntity), "x");
var property = Expression.Property(parameter, "ID");
var equal = Expression.Equal(property, Expression.Constant(value));
var lambda = Expression.Lambda<Func<TEntity, bool>>(equal, parameter);
var queryableList = new List<TEntity>(source).AsQueryable();
return queryableList.FirstOrDefault(lambda);
}
}
GetById:
public virtual TEntity GetByIdInclude(TId id, params Expression<Func<TEntity, object>>[] includes)
{
var entry = Include(includes).FirstOfDefaultIdEquals(id);
return entry;
}
Method include EntityFramework Core (look here(EF6 and EF Core)):
protected IQueryable<TEntity> Include(params Expression<Func<TEntity, object>>[] includes)
{
IIncludableQueryable<TEntity, object> query = null;
if (includes.Length > 0)
{
query = DbSet.Include(includes[0]);
}
for (int queryIndex = 1; queryIndex < includes.Length; ++queryIndex)
{
query = query.Include(includes[queryIndex]);
}
return query == null ? DbSet : (IQueryable<TEntity>)query;
}
Is there anyway to do a comparison between objects for equality generically without objects having an ID?
I am trying to do a typical generic update, for which I have seen many examples of online, but they all usually look something like this:
public void Update(TClass entity)
{
TClass oldEntity = _context.Set<TClass>().Find(entity.Id);
foreach (var prop in typeof(TClass).GetProperties())
{
prop.SetValue(oldEntity, prop.GetValue(entity, null), null);
}
}
or something similar.The problem with my system is that not every class has a property named Id, depending on the class the Id can be ClassnameId. So is there anyway for me to check for the existence of and return such an entity via LINQ without supplying any properties generically?
Try
public void Update(TClass entity)
{
var oldEntry = _context.Entry<TClass>(oldEntity);
if (oldEntry.State == EntityState.Detached)
{
_context.Set<TClass>().Attach(oldEntity);
}
oldEntry.CurrentValues.SetValues(entity);
oldEntry.State = EntityState.Modified;
}