Defining a resolver as follows:
public string Resolve(AppUser source, MemberDto destination, string destMember, ResolutionContext context)
{
if (source.Photos.Count > 0)
{
if (source.Photos.FirstOrDefault(x => x.IsMain) != null)
{
var urlStart = (source.Photos.FirstOrDefault(x => x.IsMain).Url.ToString()).Substring(0, 4);
if (urlStart == "http")
{
return source.Photos.FirstOrDefault(x => x.IsMain).Url;
}
else
{
return _config["ApiUrl"] + source.Photos.FirstOrDefault(x => x.IsMain).Url;
}
}
}
return null;
}
and mapping the property as follows:
CreateMap<AppUser, MemberDto>()
.ForMember(d => d.Image, o => o.MapFrom<MemberAvatarResolver>())
and returning the result as follows:
public async Task<PagedList<MemberDto>> GetMembersAsync(UserParams userParams)
{
var query = _context.Users
.Include(p => p.Photos)
.AsQueryable();
var mappedEntity = query.ProjectTo<MemberDto>(_mapper
.ConfigurationProvider, new { Image = query.Select(u => u.Photos.FirstOrDefault(x => x.IsMain).Url)});
return await PagedList<MemberDto>.CreateAsync(mappedEntity,
userParams.PageNumber, userParams.PageSize);
}
But I am getting a mapping exception:
Unable to create a map expression
Type Map configuration: AppUser -> MemberDto
Destination Member: Image
I've found this solution but it uses an anonymous type instead of DTO/Resolver
Any help on how to implement this case ?
I see you are using: "ProjectTo". And that only works around Expressions.
What you want is not supported.
See documentation on: https://docs.automapper.org/en/latest/Queryable-Extensions.html#supported-mapping-options
Supported mapping options
Not all mapping options can be supported ........
Not supported:
MapFrom (Func-based)
.......
Related
TargetFramework: netstandard2.0
EntityFrameworkCore: 2.2.6
I have the following code in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SlotOrder>(entity =>
{
entity.Property(e => e.Id).HasConversion(
v => v.ToString(),
v => new Guid(v));
});
modelBuilder.Entity<SlotOrderDetail>(entity =>
{
entity.Property(e => e.Id).HasConversion(
v => v.ToString(),
v => new Guid(v));
entity.HasOne<SlotOrder>()
.WithMany()
.HasForeignKey(c => c.SlotOrderId);
});
}
I do not use navigation properties and need to load all relationships of a particular entity in SaveChangesAsync. In my case if the entity is SlotOrder I need to determine that it has a child entity SlotOrderDetail:
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ChangeTracker.DetectChanges();
var utcNow = DateTime.UtcNow;
var added = ChangeTracker.Entries()
.Where(t => t.State == EntityState.Added)
.Select(t => t.Entity)
.ToList();
added.ForEach(entity =>
{
if (entity is IAuditable auditable)
{
auditable.CreatedAt = utcNow;
auditable.UpdatedAt = utcNow;
}
// var relationships = ...
});
return base.SaveChangesAsync(cancellationToken);
}
Any clue how to do that?
The relationship metadata is provided by IForeignKey interface.
Given an IEntityType, there are two methods that you can use to obtain information for entity relationships - GetForeignKeys which returns the relationships where the entity is the dependent, and GetReferencingForeignKeys which return the relationships where the entity is the principal.
In your case, don't select the .Entity property, use the EntityEntry which gives you access to the IEntityType via Metadata property, e.g.
var addedEntries = ChangeTracker.Entries()
.Where(t => t.State == EntityState.Added)
.ToList();
addedEntries.ForEach(entry =>
{
if (entry.Entity is IAuditable auditable)
{
auditable.CreatedAt = utcNow;
auditable.UpdatedAt = utcNow;
}
var foreignKeys = entry.Metadata.GetForeignKeys();
var referencingForeignKeys = entry.Metadata.GetReferencingForeignKeys();
});
I don't think it's possible to do that but I just got another idea, something like this. Create new function to do SaveChanges then load all.
In any class you create you like.
public IQueryable<T> CommitLoad<T>() where T : class
{
db.SaveChanges();
var list = db.Set<T>().AsQueryable();
var key = db.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties.FirstOrDefault();
var foreignkeys = key.GetContainingPrimaryKey().GetReferencingForeignKeys();
if (foreignkeys.Count() > 0)
{
foreach (var item in foreignkeys)
list = list.Include<T>(item.DeclaringEntityType.DisplayName());
}
return list;
}
Any class or page
public IQueryable<SlotOrder> GetTest()
{
//Save record to table
//After saving record, savechanges + load all
var list = CommitLoad<SlotOrder>();
return list;
}
Here is result screenshot
I am using AutoMapper and I need to ignore members where an Attribute is not defined. Then, if the Member is not being ignored, I need to map only where values are defined. I have managed to achieve these two separately, but ForAllMembers/ForAllOtherMembers seems to be overriding the first rule.
Let's say I have this class:
public class Foo
{
[MyCustomAttribute]
public string Name { get; set; }
public string IgnoreMe { get; set; }
[MyCustomAttribute]
public int? DontIgnoreNumber { get; set; }
}
I want to ignore IgnoreMe regardless. Then, for Name and DontIgnoreNumber, I want to map them only if they have a value. How can I achieve this?
I have tried this:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Foo, Foo>()
.IgnoreAllNonAttributedEntities()
.ForAllOtherMembers(opts =>
{
opts.Condition((src, dest, srcMember) =>
{
// Check if source is a default value
return srcMember != null;
});
});
});
I have checked that the ForAllOtherMembers rule is working. And I, separately, have checked that the IgnoreAllNonAttributedEntities is working. When I try to combine them, the ForAllOtherMembers seems to be taking priority.
IgnoreAllNonAttributedEntities is defined as:
public static IMappingExpression<TSource, TDestination> IgnoreAllNonAttributedEntities<TSource, TDestination>
(this IMappingExpression<TSource, TDestination> expression)
{
var flags = BindingFlags.Public | BindingFlags.Instance;
//var sourceType = typeof(TSource);
var destinationType = typeof(TDestination);
foreach(var prop in destinationType.GetProperties(flags))
{
var attr = ReflectionHelpers.GetAttribute<MyCustomAttribute>(prop);
if (attr == null)
{
expression.ForMember(prop.Name, opt => opt.Ignore());
}
}
return expression;
}
I've just run your code and it works as expected. However, maybe what bothers you is the default value of value types in c# (cuz you only check for nulls). Here is my fix for value types:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Foo, Foo>()
.IgnoreAllNonAttributedEntities()
.ForAllOtherMembers(opts =>
{
opts.Condition((src, dest, srcMember) =>
{
var srcType = srcMember?.GetType();
if (srcType is null)
{
return false;
}
return (srcType.IsClass && srcMember != null)
|| (srcType.IsValueType
&& !srcMember.Equals(Activator.CreateInstance(srcType)));
});
});
});
I've recreated your scenerio using latest version of automapper available on NuGet (8.0.0.0).
I'm currently using an extension method to generically mock DbSets as a list:
public static DbSet<T> AsDbSet<T>(this List<T> sourceList) where T : class
{
var queryable = sourceList.AsQueryable();
var mockDbSet = new Mock<DbSet<T>>();
mockDbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
mockDbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
mockDbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
mockDbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator());
mockDbSet.Setup(x => x.Add(It.IsAny<T>())).Callback<T>(sourceList.Add);
mockDbSet.Setup(x => x.Remove(It.IsAny<T>())).Returns<T>(x => { if (sourceList.Remove(x)) return x; else return null; } );
return mockDbSet.Object;
}
However, I can't figure out a way to mock the Find method, which searches based on the table's primary key. I could do it at a specific level for each table because I can inspect the database, get the PK, and then just mock the Find method for that field. But then I can't use the generic method.
I suppose I could also go add to the partial classes that EF auto-generated to mark which field is the PK with an attribute or something. But we have over 100 tables and it makes the code more difficult to manage if you're relying on people to manually maintain this.
Does EF6 provide any way of finding the primary key, or does it only know dynamically after it's connected to the database?
After pondering this for awhile, I think I've found the "best" solution currently available. I just have a series of if statements that directly checks the type in the extension method. Then I cast to the type I need to set the find behavior and cast it back to generic when I'm done. It's only pseudo-generic, but I can't think of anything else better.
if (typeof(T) == typeof(MyFirstSet))
{
mockDbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns<object[]>(x => (sourceList as List<MyFirstSet>).FirstOrDefault(y => y.MyFirstSetKey == (Guid)x[0]) as T);
}
else if (typeof(T) == typeof(MySecondSet))
{
mockDbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns<object[]>(x => (sourceList as List<MySecondSet>).FirstOrDefault(y => y.MySecondSetKey == (Guid)x[0]) as T);
}
...
As far as I can tell, there is no 'best practice' answer to this question, but here's how I've approached it. I've added an optional parameter to the AsDbSet method which identifies the primary key, then the Find method can be mocked up easily.
public static DbSet<T> AsDbSet<T>(this List<T> sourceList, Func<T, object> primaryKey = null) where T : class
{
//all your other stuff still goes here
if (primaryKey != null)
{
mockSet.Setup(set => set.Find(It.IsAny<object[]>())).Returns((object[] input) => sourceList.SingleOrDefault(x => (Guid)primaryKey(x) == (Guid)input.First()));
}
...
}
I've written this on the assumption of a single guid being used as primary key as that seemed to be how you're working, but the principle should be easy enough to adapt if you need more flexibility for composite keys, etc.
I ended in the following class:
public static class DbSetMocking
{
#region methods
public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>( this IReturns<TContext, DbSet<TEntity>> setup, ICollection<TEntity> entities, Func<object[], TEntity> find = null )
where TEntity : class where TContext : DbContext
{
return setup.Returns( CreateMockSet( entities, find ).Object );
}
private static Mock<DbSet<T>> CreateMockSet<T>( ICollection<T> data, Func<object[], T> find )
where T : class
{
var queryableData = data.AsQueryable();
var mockSet = new Mock<DbSet<T>>();
mockSet.As<IQueryable<T>>().Setup( m => m.Provider ).Returns( queryableData.Provider );
mockSet.As<IQueryable<T>>().Setup( m => m.Expression ).Returns( queryableData.Expression );
mockSet.As<IQueryable<T>>().Setup( m => m.ElementType ).Returns( queryableData.ElementType );
mockSet.As<IQueryable<T>>().Setup( m => m.GetEnumerator() ).Returns( queryableData.GetEnumerator() );
mockSet.SetupData( data, find );
return mockSet;
}
#endregion
}
Which can be used:
private static MyRepository SetupRepository( ICollection<Type1> type1s, ICollection<Type2> type2s )
{
var mockContext = new Mock<MyDbContext>();
mockContext.Setup( x => x.Type1s ).ReturnsDbSet( type1s, o => type1s.SingleOrDefault( s => s.Secret == ( Guid ) o[ 0 ] ) );
mockContext.Setup( x => x.Type2s ).ReturnsDbSet( type2s, o => type2s.SingleOrDefault( s => s.Id == ( int ) o[ 0 ] ) );
return new MyRepository( mockContext.Object );
}
I'm using now Entity Framework Core 2, and this solution works fine to me.
At first, I will find the primary key by using the class name with the suffix “Id”. (If you follow other convention you must change it to fit your necessity.)
//Find primary key. Here the PK must follow the convention "Class Name" + "Id"
Type type = typeof(T);
string colName = type.Name + "Id";
var pk = type.GetProperty(colName);
if (pk == null)
{
colName = type.Name + "ID";
pk = type.GetProperty(colName);
}
Now that you know the Pk, you can support the Find with the following code
dbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns((object[] id) =>
{
var param = Expression.Parameter(type, "t");
var col = Expression.Property(param, colName);
var body = Expression.Equal(col, Expression.Constant(id[0]));
var lambda = Expression.Lambda<Func<T, bool>>(body, param);
return queryable.FirstOrDefault(lambda);
});
So, the complete code for generically mock supporting DbSet.Find you can see below:
public static DbSet<T> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class
{
var queryable = sourceList.AsQueryable();
var dbSet = new Mock<DbSet<T>>();
dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>((s) => sourceList.Add(s));
//Find primary key. Here the PK must follow the convention "Class Name" + "Id"
Type type = typeof(T);
string colName = type.Name + "Id";
var pk = type.GetProperty(colName);
if (pk == null)
{
colName = type.Name + "ID";
pk = type.GetProperty(colName);
}
dbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns((object[] id) =>
{
var param = Expression.Parameter(type, "t");
var col = Expression.Property(param, colName);
var body = Expression.Equal(col, Expression.Constant(id[0]));
var lambda = Expression.Lambda<Func<T, bool>>(body, param);
return queryable.FirstOrDefault(lambda);
});
return dbSet.Object;
} //GetQueryableMockDbSet
My solution was to add a parameter to specify the key of the entity:
public static Mock<DbSet<TEntity>> Setup<TContext, TEntity, TKey>(this Mock<TContext> mockContext,
Expression<Func<TContext, DbSet<TEntity>>> expression, List<TEntity> sourceList,
Func<TEntity, TKey> id)
where TEntity : class
where TContext : DbContext
{
IQueryable<TEntity> data = sourceList.AsQueryable();
Mock<DbSet<TEntity>> mock = data.BuildMockDbSet();
// make adding to and searching the list work
mock.Setup(d => d.Add(It.IsAny<TEntity>())).Callback(add(sourceList));
mock.Setup(d => d.Find(It.IsAny<object[]>())).Returns<object[]>(s => find(sourceList, id, s));
// make context.Add() and Find() work
mockContext.Setup(x => x.Add(It.IsAny<TEntity>())).Callback(add(sourceList));
mockContext.Setup(x => x.Find<TEntity>(It.IsAny<object[]>()))
.Returns<object[]>(s => find(sourceList, id, s));
mockContext.Setup(x => x.Find(typeof(TEntity), It.IsAny<object[]>()))
.Returns<Type, object[]>((t, s) => find(sourceList, id, s));
mockContext.Setup(expression).Returns(mock.Object);
return mock;
}
private static Action<TEntity> add<TEntity>(IList<TEntity> sourceList)
where TEntity : class
{
return s => sourceList.Add(s);
}
private static TEntity find<TEntity, TKey>(IList<TEntity> sourceList, Func<TEntity, TKey> id, object[] s) where TEntity : class
{
return sourceList.SingleOrDefault(e => id(e).Equals(s[0]));
}
You can use it as
mockContext.Setup(m => m.Users, users, x => x.UsedId);
The BuildMockDbSet comes from the MockQueryable library (available from NuGet).
Edit: By the way, if you really do not want to specify the key everytime you call the above function, and you know most of your keys are of type int, you can create another overload like:
public static Mock<DbSet<TEntity>> Setup<TContext, TEntity>(this Mock<TContext> mockContext,
Expression<Func<TContext, DbSet<TEntity>>> expression, List<TEntity> sourceList)
where TEntity : class
where TContext : DbContext
{
return Setup(mockContext, expression, sourceList, x => x.GetKey<int>());
}
where GetKey is implemented by the extension methods:
public static object? GetKey(this object entity)
{
PropertyInfo keyInfo = entity.GetType().GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(KeyAttribute))).SingleOrDefault();
if (keyInfo == null)
return null;
return keyInfo.GetValue(entity);
}
public static TKey GetKey<TKey>(this object entity)
{
return (TKey)GetKey(entity);
}
So now you can call it simply as
var mockUsers = mockContext.Setup(m => m.Users, users);
I am working on a simple mapping EntityFramework <> DTO's , it's working perfecly excepto for the deferred execution , I have the following code :
public abstract class Assembler<TDto, TEntity> : IAssembler<TDto, TEntity>
where TEntity : EntityBase , new ()
where TDto : DtoBase, new ()
{
public abstract TDto Assemble(TEntity domainEntity);
public abstract TEntity Assemble(TEntity entity, TDto dto);
public virtual IQueryable<TDto> Assemble(IQueryable<TEntity> domainEntityList)
{
List<TDto> dtos = Activator.CreateInstance<List<TDto>>();
foreach (TEntity domainEntity in domainEntityList)
{
dtos.Add(Assemble(domainEntity));
}
return dtos.AsQueryable();
}
public virtual IQueryable<TEntity> Assemble(IQueryable<TDto> dtoList)
{
List<TEntity> domainEntities = Activator.CreateInstance<List<TEntity>>();
foreach (TDto dto in dtoList)
{
domainEntities.Add(Assemble(null, dto));
}
return domainEntities.AsQueryable();
}
}
Sample Assembler :
public partial class BlogEntryAssembler : Assembler<BlogEntryDto, BlogEntry>, IBlogEntryAssembler
{
public override BlogEntry Assemble(BlogEntry entity, BlogEntryDto dto)
{
if (entity == null)
{
entity = new BlogEntry();
}
/*
entity.Id = dto.Id;
entity.Created = dto.Created;
entity.Modified = dto.Modified;
entity.Header = dto.Header;
*/
base.MapPrimitiveProperties(entity, dto);
this.OnEntityAssembled(entity);
return entity;
}
public override BlogEntryDto Assemble(BlogEntry entity)
{
BlogEntryDto dto = new BlogEntryDto();
//dto.Id = entity.Id;
//dto.Modified = entity.Modified;
//dto.Created = entity.Created;
//dto.Header = entity.Header;
base.MapPrimitiveProperties(dto, entity);
dto.CategoryName = entity.Category.Name;
dto.AuthorUsername = entity.User.Username;
dto.AuthorFirstName = entity.User.FirstName;
dto.AuthorLastName = entity.User.LastName;
dto.TagNames = entity.Tags.Select(t => t.Name)
.ToArray();
dto.TagIds = entity.Tags.Select(t => t.Id)
.ToArray();
dto.VotedUpUsernames = entity.BlogEntryVotes.Where(v => v.Vote > 0)
.Select(t => t.User.Username)
.ToArray();
dto.VotedDownUsernames = entity.BlogEntryVotes.Where(v => v.Vote < 0)
.Select(t => t.User.Username)
.ToArray();
// Unmapped
dto.FileCount = entity.BlogEntryFiles.Count();
dto.CommentCount = entity.BlogEntryComments.Count();
dto.VisitCount = entity.BlogEntryVisits.Count();
dto.VoteCount = entity.BlogEntryVotes.Count();
dto.VoteUpCount = entity.BlogEntryVotes.Count(v => v.Vote.Equals(1));
dto.VoteDownCount = entity.BlogEntryVotes.Count(v => v.Vote.Equals(-1));
dto.VotePuntuation = entity.BlogEntryVotes.Sum(v => v.Vote);
dto.Published = entity.Visible && entity.PublishDate <= DateTime.Now;
this.OnDTOAssembled(dto);
return dto;
}
}
my service class :
public virtual PagedResult<BlogEntryDto> GetAll(bool includeInvisibleEntries, string tag, string search, string category, Paging paging)
{
var entries = this.Repository.GetQuery()
.Include(b => b.Tags)
.Include(b => b.User)
.Include(b => b.Category)
.Include(b => b.BlogEntryFiles)
.Include(b => b.BlogEntryComments)
.Include(b => b.BlogEntryPingbacks)
.Include(b => b.BlogEntryVisits)
.Include(b => b.BlogEntryVotes)
.Include(b => b.BlogEntryImages)
.AsNoTracking();
if (!includeInvisibleEntries)
{
entries = entries.Where(e => e.Visible);
}
if (!string.IsNullOrEmpty(category))
{
entries = entries.Where(e => e.Category.Name.Equals(category, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrEmpty(tag))
{
entries = entries.Where(e => e.Tags.Count(t => t.Name.Equals(tag, StringComparison.OrdinalIgnoreCase)) > 0);
}
if (!string.IsNullOrEmpty(search))
{
foreach (var item in search.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
{
entries = entries.Where(e => e.Header.Contains(item));
}
}
return this.Assembler.Assemble(entries).GetPagedResult(paging);
}
When I call the GetAll method it returns and converts all the entities in the table to Dto's and only then it pages the resulting collection, of course that's not what I was expecting.. I would like to execute the code inside my Assemble method once the paging has been done, any idea?
PS : I know I could use Automapper, just trying to learn the internals.
In your assembler you add the projected DTOs to a List<TDto>. That's detrimental in two ways. First, it is forced execution, because the list is filled and then returned. Second, at that moment you switch from LINQ to Entities to LINQ to objects and there is no way back. You can convert the list to IQueryable again by AsQueryable, but that does not re-inject the EF query provider. In fact, the conversion is useless.
That's why AutoMapper's ProjectTo<T> statement is so cool. It transfers expressions that come after the To all the way back to the original IQueryable and, hence, its query provider. If these expressions contain paging statements (Skip/Take) these will be translated into SQL. So I think that you'll quickly come to the conclusion you better use AutoMapper after all.
public virtual IQueryable<TDto> Assemble(IQueryable<TEntity> domainEntityList)
{
List<TDto> dtos = Activator.CreateInstance<List<TDto>>();
foreach (TEntity domainEntity in domainEntityList)
{
dtos.Add(Assemble(domainEntity));
}
return dtos.AsQueryable();
}
The foreach loop in the above code is the point at which you're executing the query on the database server.
As you can see from this line:
return this.Assembler.Assemble(entries).GetPagedResult(paging);
This method is getting called before GetPagedResult(paging).. so that is the reason paging happens on the full result set.
You should understand that enumerating a query (foreach) requires the query to run. Your foreach loop processes each and every record returned by that query.. it's too late at this point for a Paging method to do anything to stop it!
I have this method:
public CampaignCreative GetCampaignCreativeById(int id)
{
using (var db = GetContext())
{
return db.CampaignCreatives
.Include("Placement")
.Include("CreativeType")
.Include("Campaign")
.Include("Campaign.Handshake")
.Include("Campaign.Handshake.Agency")
.Include("Campaign.Product")
.AsNoTracking()
.Where(x => x.Id.Equals(id)).FirstOrDefault();
}
}
I would like to make the list of Includes dynamic. I tried:
public CampaignCreative GetCampaignCreativeById(int id, string[] includes)
{
using (var db = GetContext())
{
var query = db.CampaignCreatives;
foreach (string include in includes)
{
query = query.Include(include);
}
return query.AsNoTracking()
.Where(x => x.Id.Equals(id)).FirstOrDefault();
}
}
But it didn't compile. I got this error:
Cannot implicitly convert type 'System.Data.Entity.Infrastructure.DbQuery' to 'System.Data.Entity.DbSet'. An explicit conversion exists (are you missing a cast?)
Does anyone know how to make the list of Includes dynamic?
Thanks
I am more fond of the non-string expressive way of defining includes. Mainly because it doesn't rely on magic strings.
For the example code, it would look something like this:
public CampaignCreative GetCampaignCreativeById(int id) {
using (var db = GetContext()) {
return db.CampaignCreatives
.Include(cc => cc.Placement)
.Include(cc => cc.CreativeType)
.Include(cc => cc.Campaign.Select(c =>
c.Handshake.Select(h => h.Agency)))
.Include(cc => cc.Campaign.Select(c => c.Product)
.AsNoTracking()
.Where(x => x.Id.Equals(id))
.FirstOrDefault();
}
}
And to make those dynamic, this is how you do that:
public CampaignCreative GetCampaignCreativeById(
int id,
params Expression<Func<T, object>>[] includes
) {
using (var db = GetContext()) {
var query = db.CampaignCreatives;
return includes
.Aggregate(
query.AsQueryable(),
(current, include) => current.Include(include)
)
.FirstOrDefault(e => e.Id == id);
}
}
Which is used like this:
var c = dataService.GetCampaignCreativeById(
1,
cc => cc.Placement,
cc => cc.CreativeType,
cc => cc.Campaign.Select(c => c.Handshake.Select(h => h.Agency)),
cc => cc.Campaign.Select(c => c.Product
);
Make the query variable queryable:
public CampaignCreative GetCampaignCreativeById(int id, string[] includes)
{
using (var db = GetContext())
{
var query = db.CampaignCreatives.AsQueryable();
foreach (string include in includes)
{
query = query.Include(include);
}
return query
.AsNoTracking()
.Where(x => x.Id.Equals(id))
.FirstOrDefault();
}
}
Giving the compiler a hint by using IQueryable<CampaignCreative> instead of var will work too.
IQueryable<CampaignCreative> query = db.CampaignCreatives;
// or
DbQuery<CampaignCreative> query = db.CampaignCreatives;
When using var the compiler infers DbSet<T> for query which is more specific than the type returned by Include (which is DbQuery<T> (=base class of DbSet<T>) implementing IQueryable<T>), so you can't assign the result to the query variable anymore. Hence the compiler error on the query = query.Include(include) line.
I wrote this method to retrieve any set of entity dynamically based on their types. I used the IDbEntity interface to provide a valid key to search the userId in all the classes.
The Util.GetInverseProperties<T>() method is used to get the properties needed in the Include statement.
public IEnumerable<T> GetItems<T>(string userId) where T : class, IDbEntity
{
var query = db.Set<T>().Where(l => l.UserId==userId);
var props = Util.GetInverseProperties<T>();
foreach (var include in props)
query = query.Include(include.Name);
return query
.AsNoTracking()
.ToList();
}
public interface IDbEntity
{
public string UserId { get; set; }
}
public static List<PropertyInfo> GetInverseProperties<T>()
{
return typeof(T)
.GetProperties()
.Where(p => Attribute.IsDefined(p, typeof(InversePropertyAttribute)))
.ToList();
}