Here is my query with Entity Framework :
var transportlist = await _context.Set<Transport>()
.Include(transport => transport.TransportState)
.Select(transport =>
new
{
Transport = _mapper.Map<TransportDto>(transport),
TransportState = _mapper.Map<TransportStateForTransportDto>(transport.TransportState),
}
)
.Take(limitNumber)
.ToListAsync();
return transportlist;
Here is my TransportDto :
public class TransportDto
{
public int TransportId { get; set; }
public TransportStateForTransportDto TransportState { get; set; }
public bool Locked { get; set; }
}
Here is my TransportStateForTransportDto :
public class TransportStateForTransportDto
{
public string Label { get; set; }
}
I would like to have something like that as result :
[
{
"transportId": 123456,
"transportState" : {
"Label": "Deleted"
},
"locked" : false
}
]
But, I have this one (and it is a bit logical...) :
[
{
"transport": {
"transportId": 123456,
"transportState" : {
"Label": "Deleted"
},
"locked" : false
},
"transportstate": {
"Label": "Deleted"
}
}
]
I thought about doing like this :
var transportlist = await _context.Set<Transport>()
.Include(transport => transport.TransportState)
.Select(transport =>
new
{
_mapper.Map<TransportDto>(transport),
}
)
.Take(limitNumber)
.ToListAsync();
return transportlist;
But, if I do, the property transportstate in JSON equals NULL.
So I am trying to do nested mapping where Transport should be mapped with its object properties mapped too. Note that all the properties have exactly the same name.
I wrote a Profile for mapping which is :
public class TransportsProfile : Profile
{
public TransportsProfile()
{
CreateMap<TransportState, TransportStateForTransportDto>();
CreateMap<Transport, TransportDto>();
}
}
Thank you in advance for your help.
If you are using Automapper 8 or above you should leverage ProjectTo rather than Map to translate entities into DTOs.
var transportlist = await _context.Set<Transport>()
.ProjectTo<TransportDto>(_mapper.ConfigurationProvider)
.Take(limitNumber)
.ToListAsync();
Edit: had _mapper.Configuration above, looks like the config is provided by ConfigurationProvider.
Given that your TransportDTO contains a reference to the related TransportState DTO, automapper should be able to tell EF exactly what fields are needed from the related entities to ensure that they are loaded properly. The issue with Map is that this is really geared towards loaded objects, not expressions that EF could otherwise turn into SQL.
With using Take you should also have an Order By clause to ensure the items returned are predictable when more than that limit are present.
Like using Select for projection, there is no need to eager load any of the related details or disable lazy loading. EF will simply compose a query to pull the required fields provided your DTOs are purely simple data containers and don't contain references to entities within them.
Related
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.
I have a one-to-many relationship between two entities: Maps and MapNodes. When I eager load MapNodes with Maps (via an Include()), I get Map, and I get related MapNodes, but in the related MapNodes, I also get the full parent Map again. That Map then has associated MapNodes (again), and so on...
I need to limit the contents of the query to one level (i.e MapNodes) and not have the eager loading go deeper.
[Table("maps")]
public partial class Maps
{
public Maps()
{
MapNodes = new HashSet<MapNodes>();
}
<...>
[InverseProperty("Map")]
public virtual ICollection<MapNodes> MapNodes { get; set; }
}
[Table("map_nodes")]
public partial class MapNodes
{
public MapNodes()
{
}
<...>
[Column("map_id", TypeName = "int(10) unsigned")]
public uint MapId { get; set; }
public Maps Map { get; set; }
}
When I execute the following query:
var map = await context.Maps.Include( x => x.MapNodes).FirstOrDefaultAsync( x => x.Id == id);
I get an infinate eager loading:
{
"id": 1063,
"mapNodes": [
{
"id": 25784,
"mapId": 1063,
"map": {
"id": 1063,
"mapNodes": [
{
"id": 25784,
"mapId": 1063,
"map": {
"id": 1063,
"mapNodes": [
...
Microsoft replaced Microsoft.AspNetCore.Mvc.NewtonsoftJson in ASP.NET Core 3 with their own implementation which is System.Text.Json but it doesn't support Reference Loop Handling handling yet.
So in order to configure Reference Loop Handling you need to add the nuget package for Microsoft.AspNetCore.Mvc.NewtonsoftJson then configure it this way:
services.AddControllersWithViews().AddNewtonsoftJson(options => {
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
I've seen all the questions out there for tracking changes to an entity, but none of those solutions included navigation properties. Is this even possible? I don't need some SUPER generic solution that just logs everything. I only want things tracked for specific "main" entities. And the audit log is going to be a human readable custom one, so not just some table that has oldData newData like I have seen in the examples on here.
I've gotten it almost to a good point except tracking the navigation properties, I have an Interface on the POCO's service that determines whether I want to track changes, then in my entityService.Save method I have this check:
var trackChangesService = this as ITrackChangesService<TClass>;
if (trackChangesService != null)
{
TClass oldEntity = Repository.GetOldEntity(entity);
trackChangesService.SaveChanges(entity, oldEntity);
}
The save changes method is what the ITrackChanges interface exposes and will have custom logic on a per entity basis (this includes email notifications, etc.), but the problem is I can't propertly get the oldEntity. Currently the method looks like this:
public TClass GetOldEntity(TClass entity)
{
//DbEntityEntry<TClass> oldEntry = Context.Entry(entity);
////oldEntry.CurrentValues.SetValues(oldEntry.OriginalValues);
//var values = oldEntry.OriginalValues;
//return (TClass)oldEntry.GetDatabaseValues().ToObject();
return Context.Set<TClass>().AsNoTracking().FirstOrDefault(x => x.Id == entity.Id);
}
You can see the code I have been playing with. I can get the regular properties old and new values fine, but the navigation properties do not carry over. But I need this information, say for instance I am editing a user and on the edit user screen, groups can be removed and added. When the save button is clicked, it performs those updates.
So my update view look like this:
private UserSaveModel Update(Guid id, UserSaveModel userSaveModel)
{
User user = _userService.GetOne(x => x.Id == id);
user = _userService.ConvertFromSaveModel(userSaveModel, user);
_userService.Save(user);
return userSaveModel;
}
I guess one way to look at the problem is, I am adjusting the values on the entity on the context in .ConvertFromSaveeModel() on the existing user record, including the navigation properties, so once I call save, that old navigation property data is gone.
Is there anyway to get original navigation property information for an entity using this workflow? If not, how can I change the workflow to make this possible? If I don't query the user object at the beginning and instead make one from scratch, some data that may not be exposed in the viewModel won't pass over, so I am not sure that will work. Any other suggestions? Maybe clone the entity and use that as original before I made changes? Does that carry over navigation properties?
Unfortunately, there is no easy way to detect Navigation Properties changes on EF SaveChanges, specially for the EF 7 (Core) version. Check this and this MSDN article.
So, if you don't have a relationship entity, and your entities are related like this:
public class User
{
[Key]
public int UserId { get; set; }
public ICollection<Department> Departments { get; set; }
}
public class Department
{
[Key]
public int DepartmentId { get; set; }
public ICollection<User> Users { get; set; }
}
and you only change a Navigation Property, say for example you add a Department to a User, like this:
using (var ctx = new MyContext())
{
var user1 = ctx.Users.Include(u => u.Departments).First(u => u.Id == 1);
var dept3 = ctx.Departments.First(d => d.Id == 3);
user1.Departments.Add(dept3);
ctx.SaveChanges();
}
The library Audit.EntityFramework will not detect the change, since you did a Relationship change, but not an Entity change.
You can avoid this problem creating an entity for your relation, i.e. a UserDepartment class. So you will see the changes to the relation as changes to that entity.
So far, this is the only way I found to obtain the relationship changes in case you don't have the relation entity. But... Only works for EF 6 so I didn't included that on my library.
Another workaround is to make sure to change another property value of the entity (i.e. a LastUpdatedDate column), or to explicitly mark the entity as Modified before the SaveChanges, so the library can detect the change and, at least, you'll see the entity current navigation properties.
For example:
using (var ctx = new MyContext())
{
var user1 = ctx.Users.Include(u => u.Departments).First(u => u.Id == 1);
var dept3 = ctx.Departments.First(d => d.Id == 3);
user1.Departments.Add(dept3);
ctx.Entry(user1).State = EntityState.Modified; // <-- Here
ctx.SaveChanges();
}
Now the library will detect the change, and it will include some data about the change.
This is how the output (as JSON) would be like:
{
"EventType": "MyContext",
//...
"EntityFrameworkEvent": {
"Database": "Users",
"Entries": [{
"Table": "User",
"Action": "Update",
"PrimaryKey": {
"UserId": 1
},
"Entity": {
"UserId": 1,
"Username": "federico",
"Departments": [{
"DepartmentId": 3,
"DepartmentName": "Dept3",
"Users": []
}]
},
"Changes": [],
"ColumnValues": {
"UserId": 1,
"Username": "federico"
},
"Valid": true,
"ValidationResults": []
}],
"Result": 1,
"Success": true
}
}
You need your context to inherits from AuditDbContext and set the IncludeEntityObjects to true:
[AuditDbContext(IncludeEntityObjects = true)]
public class MyContext : AuditDbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Department> Departments { get; set; }
}
I know this is not a solution for your problem, but it could probably help.
I have a simple class which has an array of documents in it. The C# objects and mappings are below.
For some reason I the MongoDB driver isn't returning the embedded collection of documents when I find a user. I can edit the collection using AddToSet and Pull.
I don't really understand why I need to explicitly map the Groups property either. I assumed it'd just work if I called cm.AutoMap().
Here's the code I'm using to load the user...
var collection = Database.GetCollection<User>(Constants.UsersCollectionName);
var builder = Builders<User>.Filter;
FilterDefinition<User> filter = builder.Eq(x => x.Id, UserId);
var result = await collection.Find(filter)
.ToListAsync();
return result;
I've tried to add an explicit include projection for the groups property but all that lead to was me removing all the other properties and still having an empty collection.
Here's the class...
public class User
{
public User()
{
Groups = new HashSet<UserGroup>();
}
public string Id { get; set; }
public string Email { get; set; }
public IEnumerable<UserGroup> Groups { get; }
}
public class UserGroup
{
public string GroupId { get; set; }
public string TenantId { get; set; }
}
And the mapping...
BsonClassMap.RegisterClassMap<UserGroup>(cm =>
{
cm.AutoMap();
});
BsonClassMap.RegisterClassMap<User>(cm =>
{
cm.AutoMap();
cm.MapIdProperty(c => c.Id)
.SetIdGenerator(StringObjectIdGenerator.Instance)
.SetSerializer(new StringSerializer(BsonType.ObjectId));
cm.MapProperty(x => x.Groups);
});
and the json from the DB...
{
"_id" : ObjectId("556717bb10e5c4a3f831dfca"),
"Email" : "test#test.com",
"Groups" : [
{
"GroupId" : "5641c6da134b9006dcdc0bff",
"TenantId" : "5567179f10e5c4a3f831dfc8"
}
]
}
Edit:
I've just tried it as a list of strings and it still doesn't return the result. I've also removed the explicit mapping and it's throwing an exception.
Element 'Groups' does not match any field or property of class AssetStream.Model.Admin.User.
Ahh, I bet it's because there's no public setter.
I feel like I'm asking a lot of questions, but I keep getting stuck. I am developing an OData service, and I want an entity that can have several user-designated name-value pairs associated, which can then be searched against. I am using EF4.3, DataServiceVersion 3.0. I am using a custom Metadata, Query, and Update provider.
So let's say that I have an entity Person:
public class Person : EntityBase
{
public virtual IList<Property> PropertySet { get; set; }
}
(EntityBase is a common POCO that all my entities come from; it has only a Guid ID property.) Now let's define our Property:
public abstract class Property : EntityBase
{
public string Name { get; set; }
public Person Person { get; set; }
public Guid PersonId { get; set; }
}
public class IntProperty : Property
{
public int? IValue { get; set; }
}
public class StringProperty : Property
{
public string SValue { get; set; }
}
So far, so good. In my configuration, I am using Table Per Hierarchy for inheritance.
Now, I am able to add a Property to my Person, and when I make a request like this:
GET /Service/People(guid'THE_ID')?$expand=PropertySet
It works:
{"d": {
"__metadata": {...},
"PropertySet": {
"results": [{
"__metadata": {...},
"Id": "PROP_1_ID",
"Name": "Number",
"IValue": 1234
},{
"__metadata": {...},
"Id": "PROP_2_ID",
"Name": "EmailAddress",
"SValue": "AAAA"
}]
},
"Id": "THE_ID",
}
}
If I query for a Person that has a property named 'EmailAddress', that works:
GET /Service/People?$expand=PropertySet&$filter=PropertySet/any(x: x/Name eq 'EmailAddress')
But even for that, I had to pull a few tricks. I implemented an expression visitor, and whacked out a few comparisons that Linq To Entities didn't seem to like:
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == ExpressionType.Equal)
{
Expression left = Visit(node.Left);
Expression right = Visit(node.Right);
ConstantExpression rightConstant = right as ConstantExpression;
if (null != rightConstant && rightConstant.Value == null)
{
if (left.Type == typeof(IList<Property>))
{
return Expression.Constant(false, typeof(bool));
}
}
}
return base.VisitBinary(node);
}
protected override Expression VisitConditional(ConditionalExpression node)
{
Expression visitedTest = Visit(node.Test);
Expression visitedIfTrue = Visit(node.IfTrue);
Expression visitedIfFalse = Visit(node.IfFalse);
ConstantExpression constantTest = visitedTest as ConstantExpression;
if (null != constantTest && constantTest.Value is bool)
{
return ((bool)constantTest.Value) ? visitedIfTrue : visitedIfFalse;
}
return Expression.Condition(visitedTest, visitedIfTrue, visitedIfFalse);
}
The gist is with the first override my query gets expressions such as "it.PropertySet == null", which I know will always be untrue. (In my spike, the only thing that has PropertySet is a Person, and a Person always has a PropertySet.) In the second override, I'm looking at expressions such as "IIF((it.PropertySet == null), Empty(), it.PropertySet)", and I know that "it" will always have a PropertySet. This prevents errors comparing an IList against null.
Now, the problem.
Merely searching for the presence of a Property isn't enough. I would like to check it's value:
GET /Service/People?$expand=PropertySet&$filter=PropertySet/any(x: x/Name eq 'EmailAddress' and cast(x, 'InheritedPropertyTest.Entities.StringProperty')/SValue eq 'AAAA')
And this is the resulting query:
value(System.Data.Objects.ObjectQuery`1[InheritedPropertyTest.Entities.Person])
.MergeAs(AppendOnly)
.Where(it => it.PropertySet.Any(x => ((x.Name == "EmailAddress") AndAlso (IIF((Convert(x) == null), null, Convert(x).SValue) == "AAAA"))))
.OrderBy(p => p.Id)
.Take(100)
.Select(p => new ExpandedWrapper`2() {ExpandedElement = p, Description = "PropertySet", ReferenceDescription = "", ProjectedProperty0 = p.PropertySet.OrderBy(p => p.Id) .Take(100)})
But I get this error: "Unable to cast the type 'InheritedPropertyTest.Entities.Property' to type 'InheritedPropertyTest.Entities.StringProperty'. LINQ to Entities only supports casting Entity Data Model primitive types." So... now I'm stuck beating my head against the wall again. Perhaps my inheritance isn't set up correctly? Do I need to overload some other Expression Visitor method to make the Convert work? How do I convince Linq To Entities to work with inherited properties?
Thanks!
Instead of cast, use the type segment. So for example x/InheritedPropertyTest.Entities.StringProperty/SValue
That should be translated to a TypeOf which EF should be able to handle.
Note though that custom provider over EF will have lot of problems anyway. One way to simplify the expressions is to turn of null propagation (IDataServiceQueryProvider.IsNullPropagationRequired = false). That should get rid of the IFF(i==null, null, something).
But you will still run into problems especially when you start using projections ($select).