How to specify the shape of results with WebApi2, OData with $expand - entity-framework

I have a problem executing my AutoMapper mappings when using OData with specific $select or $expand values.
Using the WebApi Action:
public IQueryable<BookInRequestDto> Get(ODataQueryOptions<BookInRequest> query)
{
var results = query.ApplyTo(_context.BookInRequests) as IQueryable<BookInRequest>;
var mappedResults = Mapper.Map<IQueryable<BookInRequest>, IQueryable<BookInRequestDto>>(results);
return mappedResults;
}
When I query: api/Get, I get an appropriate response, but a Document's Properties is set to null response containing documents properties set to null.
When I query: api/Get?$expand=Documents/Properties, the response is an empty array.
As I understand, this is because Select/Expand changes the shape of the response, so it no longer matches an IQueryable of BookInRequest, and instead returns an IQueryable.
I'm happy to return that, except I need to be able to apply the AutoMapper Mappings. Is there anything that can be done to enforce the shape of the query results?
I have the following entities:
public class BookInRequest {
//...
public virtual ICollection<BookInDocument> Documents { get; set; }
}
public class BookInDocument {
public ICollection<BookInDocumentProperty> Properties { get; set; }
}
With Corresponding DTO's that are pretty much identical, except for the BookInDocumentDto:
public class BookInDocumentDto {
public dynamic Properties { get; set; }
}
My Mapping definition is as follows:
Mapper.CreateMap<BookInRequest, BookInRequestDto>();
Mapper.CreateMap<BookInDocument, BookInDocumentDto>()
.ForMember(x => x.Properties,
y => y.MapFrom(z =>
DynamicHelpers.PropertiesAsDynamic(z.Properties)));

Related

Retrieve child entities from CrudAppService in abp.io using .Net 5 EF

I'm using the latest version of ABP from abp.io and have two entities with a many-many relationship. These are:
public class GroupDto : AuditedEntityDto<Guid>
{
public GroupDto()
{
this.Students = new HashSet<Students.StudentDto>();
}
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Students.StudentDto> Students { get; set; }
}
and
public class StudentDto : AuditedEntityDto<Guid>
{
public StudentDto()
{
this.Groups = new HashSet<Groups.GroupDto>();
}
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Groups.GroupDto> Groups { get; set; }
}
I set up the following test to check that I am retrieving the related entities, and unfortunately the Students property is always empty.
public async Task Should_Get_List_Of_Groups()
{
//Act
var result = await _groupAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(g => g.Name == "13Ck" && g.Students.Any(s => s.Name == "Michael Studentman"));
}
The same is true of the equivalent test for a List of Students, the Groups property is always empty.
I found one single related answer for abp.io (which is not the same as ABP, it's a newer/different framework) https://stackoverflow.com/a/62913782/7801941 but unfortunately when I add an equivalent to my StudentAppService I get the error -
CS1061 'IRepository<Student, Guid>' does not contain a definition for
'Include' and no accessible extension method 'Include' accepting a
first argument of type 'IRepository<Student, Guid>' could be found
(are you missing a using directive or an assembly reference?)
The code for this is below, and the error is being thrown on the line that begins .Include
public class StudentAppService :
CrudAppService<
Student, //The Student entity
StudentDto, //Used to show students
Guid, //Primary key of the student entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateStudentDto>, //Used to create/update a student
IStudentAppService //implement the IStudentAppService
{
private readonly IRepository<Students.Student, Guid> _studentRepository;
public StudentAppService(IRepository<Student, Guid> repository)
: base(repository)
{
_studentRepository = repository;
}
protected override IQueryable<Student> CreateFilteredQuery(PagedAndSortedResultRequestDto input)
{
return _studentRepository
.Include(s => s.Groups);
}
}
This implements this interface
public interface IStudentAppService :
ICrudAppService< // Defines CRUD methods
StudentDto, // Used to show students
Guid, // Primary key of the student entity
PagedAndSortedResultRequestDto, // Used for paging/sorting
CreateUpdateStudentDto> // Used to create/update a student
{
//
}
Can anyone shed any light on how I should be accessing the related entities using the AppServices?
Edit: Thank you to those who have responded. To clarify, I am looking for a solution/explanation for how to access entities that have a many-many relationship using the AppService, not the repository.
To aid with this, I have uploaded a zip file of my whole source code, along with many of the changes I've tried in order to get this to work, here.
You can lazy load, eagerly load or configure default behaviour for the entity for sub-collections.
Default configuration:
Configure<AbpEntityOptions>(options =>
{
options.Entity<Student>(studentOptions =>
{
studentOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Groups);
});
});
Eager Load:
//Get a IQueryable<T> by including sub collections
var queryable = await _studentRepository.WithDetailsAsync(x => x.Groups);
//Apply additional LINQ extension methods
var query = queryable.Where(x => x.Id == id);
//Execute the query and get the result
var student = await AsyncExecuter.FirstOrDefaultAsync(query);
Or Lazy Load:
var student = await _studentRepository.GetAsync(id, includeDetails: false);
//student.Groups is empty on this stage
await _studentRepository.EnsureCollectionLoadedAsync(student, x => x.Groups);
//student.Groups is filled now
You can check docs for more information.
Edit:
You may have forgotten to add default repositories like:
services.AddAbpDbContext<MyDbContext>(options =>
{
options.AddDefaultRepositories();
});
Though I would like to suggest you to use custom repositories like
IStudentRepository:IRepository<Student,Guid>
So that you can scale your repository much better.

AutoMapper: How to map between Linq Expressions (Func<>)

I am using repository pattern, so my repository just know about DTOs. It has to query the database with some filters using Entity Framework.
My problem is that Entity Framework only knows about DB model classes, so I have to 'automap' the Expression before being able to use them in any query.
I have declared a method that accepts a Expression as a filter.
public interface IRepository
{
IEnumerable<ItemDTO> GetItemsWithFilter(Expression<Func<ItemDTO, bool>> filter)
{
var filterDb = Mapper.Map<Expression<Func<ItemDB, bool>>>(filter);
return dbContext.CONFIGURATIONS.Where(filterDb).Select(x => Mapper.Map<ItemDTO>(x));
}
}
public class ItemDTO
{
public int numero { get; set; }
public string name { get; set; }
}
public class ItemDB //they are both the same, just for testing purpose
{
public int numero { get; set; }
public string name { get; set; }
}
//failing code
Repository.GetItemsWithFilter(x => x.name=="a");
I followed tutorial that says it is possible to map between expressions but i get some errors:
"The specified type member 'name' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported."}
I solved it by including this extension method call:
Mapper.Initialize(cfg => {
cfg.AddExpressionMapping();
// Rest of your configuration
});
Remeber to install nuget package AutoMapper.Extensions.ExpressionMapping
Install-Package AutoMapper.Extensions.ExpressionMapping

Nested Tables Using a DTO

I need help getting my WebApi Controller to work.
I have a 3 table Models like this.
First Table
public class MainTable{
public int MainTableID { get; set; }
... Other Fields
public ICollection<SubTable> SubTables { get; set; }
}
Second Table
public class SubTable{
public int SubTableID { get; set; }
... Other Fields
public int MainTableID { get; set; }
[ForeignKey("MainTableID ")]
[JsonIgnore]
public virtual MainTable MainTable{ get; set; }
public ICollection<SubSubTable> SubSubTables { get; set; }
}
Third Table
public class SubSubTable{
public int SubSubTableID { get; set; }
... Other Fields
public int SubTableID { get; set; }
[ForeignKey("SubTableID")]
[JsonIgnore]
public virtual SubTable SubTable{ get; set; }
}
I need to flatten the first model because of other relationships not mentioned in this post so I am using a dto
DTO
public class TableDTO
{
public int MainTableID { get; set; }
... Other Fields (there is a lot of flattening happening here but I am going to skip it to keep this simple)
public ICollection<SubTable> SubTables { get; set; }
}
Now that I got all of that out of the way. To my question.. I am linking this all to a web api controller.
If I use the DTO and create a controller like this
Controller with DTO
public IQueryable<TableDTO> GetMainTable()
{
var mainTable = from b in db.MainTables
.Include(b => b.SubTable.Select(e => e.SubSubTable))
select new TableDTO()
{
MainTableID = b.MainTableID
eager mapping of all the fields,
SubTables = b.SubTables
};
return mainTable;
}
This works for everything except the SubSubTable which returns null. If I ditch the DTO and create a controller like this
Controller without DTO
public IQueryable<MainTable> GetMainTable()
{
return db.MainTables
.Include(c => c.SubTables)
.Include(c => c.SubTables.Select(b => b.SubSubTables));
}
This works perfect and the JSon returns everything I need, except that I lose the DTO which I desperately need for other aspects of my code. I have rewritten my code in every way I can think of but nothing works. I am pretty sure that this can be done with the DTO but I don't know what it would take to make it work, and as they say "You don't know what you don't know" so hopefully someone here knows.
In Entity Framework 6 (and lower), Include is always ignored when the query ends in a projection, because the Include path can't be applied to the end result. Stated differently, Include only works if it can be positioned at the very end of the LINQ statement. (EF-core is more versatile).
This doesn't help you, because you explicitly want to return DTOs. One way to achieve this is to do the projection after you materialize the entities into memory:
var mainTable = from b in db.MainTables
.Include(b => b.SubTable.Select(e => e.SubSubTable))
.AsEnumerable()
select new MessageDTO()
{
MainTableID = b.MainTableID ,
// eager mapping of all the fields,
SubTables = b.SubTables
};
The phrase, "eager mapping of all the fields" suggests that the projection isn't going to narrow down the SELECT clause anyway, so it won't make much of a difference.
Another way could be to load all SubSubTable objects into the context that you know will be in the MainTables you fetch from the database. EF will populate all SubTable.SubSubTables collections by relationship fixup.
If this works:
public IQueryable<MainTable> GetMainTable()
{
return db.MainTables
.Include(c => c.SubTables)
.Include(c => c.SubTables.Select(b => b.SubSubTables));
}
Then use this one and just add a Select() to the end with a ToList(). Note the IEnumerable in the return type:
public IEnumerable<MainTableDto> GetMainTable()
{
return db.MainTables
.Include(c => c.SubTables)
.Include(c => c.SubTables.Select(b => b.SubSubTables))
.Select(c=> new MainTableDto { SubTables=c.SubTables /* map your properties here */ })
.ToList();
}
Not sure about the types though (at one place you have MainTableDto, at another you mention MessageDto?).

Entity framework don't delete records but fill column

I would like to change the way EF works with deleting records.
Instead of deleting the row in the database it should fill a column (GCColumn or so).
When retrieving data it should always filter on GCColumn IS NULL + the filter you apply.
Anyone know if this is achievable and how ?
I addition to my answer above, consider the case in which many or even all of your entities have this GCColumn.
You could start with a base entity for these pseudo-deletable entities:
public abstract class PseudoDeletable
{
public DateTime GCColumn { get; set;}
}
and have entities defined as:
public class Order : PseudoDeletable
{
public int Id { get; set; }
public int ProductId { get; set; }
public DateTime OrderDate { get; set; }
// etc.
}
Then, you could create a generic base repository
public class RepositoryBase<TEntity> where TEntity : PseudoDeletable
{
protected IDbSet<TEntity> DbSet { get; }
public RepositoryBase()
{
DbSet = context.Set<TEntity>();
}
private Expression<Func<TEntity, bool>> RemoveDeleted
{
get { return e => e.GCColumn == null; }
}
public virtual IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> expression)
{
expression = expression.And(RemoveDeleted);
return DbSet.Where(expression).ToList();
}
}
and have derived repositories, like:
public class OrderRepository : RepositoryBase<Order>
{
}
The GetAll method can then be called like this:
new orderRepository().GetAll(x => x.ProductId == 1);
and it will just return orders that have not been deleted.
Please note that you'll have an issue with entity includes for related records: how to include only un-deleted related entities, but that is a consequence of you desire to keep 'deleted' records in the database.
In one project we use the repository pattern for database access and each entity has its own repository.
It is a multi-tenant database and we use the type of filter you are looking for to filter entities accessible to the current user, not to filter for a delete flag, but the method could be used analogously.
Each repository that needs filtering, gets a filter method:
private Expression<Func<Order, bool>> RemoveDeleted
{
get
{
return order => order.GCColumn == null;
}
}
Then, add an expression to each repository method, like:
public override IEnumerable<Order> GetAll(Expression<Func<Order, bool>> expression)
{
expression = expression.And(RemoveDeleted);
return DbSet.Where(expression).ToList();
}
(The extension method Add comes from a set of ExpressionExtensions.)
Now, you can use expressions like:
orderRepository.GetAll(x => x.ProductId == productId);
and
orderRepository.GetAll(x => x.OrderDate >= DateTime.Now.AddMonths(-1));
So now you business logic can have many methods using the same GetAll() methods, with different filters, but doesn't have to care about 'deleted' entities. But you are still responsible for creating a correct filter for each repository method.
If the delete flag is not in all entities, but the delete status is registered in another entity, you can do the following:
private Expression<Func<Order, bool>> RemoveDeleted
{
get
{
return orderLine => orderLine.Order.GCColumn == null;
}
}
In this example orders are deleted in whole, not individual lines in it.

XML data type in EF 4.1 Code First

I would like to use SQL Server xml type as a column type for an entity class.
According to this thread it's possible to map such a column to string type:
public class XmlEntity
{
public int Id { get; set; }
[Column(TypeName="xml")]
public string XmlValue { get; set; }
}
The table is correctly generated in the datebase by this definition. New XmlEntity objects are also can be created.
But then I try to get some entity from the database:
var entity = db.XmlEntities.Where(e => e.Id == 1).FirstOrDefault();
An error occurs:
One or more validation errors were detected during model generation
System.Data.Edm.EdmEntityType: EntityType 'XElement' has no key defined. Define the key for this EntityType.
The problem was with my wrapper property:
[NotMapped]
public XElement XmlValueWrapper
{
get { return XElement.Parse(XmlValue); }
set { XmlValue = value.ToString(); }
}
I didn't specified NotMapped attribute.
Just to be complete. Here's all code needed, in one part.
[Column(TypeName = "xml")]
public String XmlContent { get; set; }
[NotMapped]
public XElement InitializedXmlContent
{
get { return XElement.Parse(XmlContent); }
set { XmlContent = value.ToString(); }
}
This's how you do that in Data Annotations, if you want to use Fluent API (and use a mapping class) then:
public partial class XmlEntityMap : EntityTypeConfiguration<XmlEntity>
{
public FilterMap()
{
// ...
this.Property(c => c.XmlContent).HasColumnType("xml");
this.Ignore(c => c.XmlValueWrapper);
}
}
If you use Fluent API by overriding OnModelCreating on DbContext then just change those "this" with modelBuilder.Entity<XmlEntity>()