I'm using asp.net core on a project. (I'm fairly new to it)
I have a User Model. the code below is a simplified version:
public class User
{
public int id { get; set; }
// attribute declaration
public ICollection<User> friends { get; set; }
}
I'm using automapper service to map my api to this Model:
public class UserResource
{
public UserResource()
{
this.friendsId = new List<int>();
}
public int id { get; set; }
// attribute declaration
public ICollection<int> friendsId { get; set; }
}
consider a post request to UserController with the following body:
{
"id" : 1
"friendsId": [2,3,4],
}
I want to map integers in friendsId to id of each user in friends collection. but I can't figure out what to do. here's what I've got:
CreateMap<UserResource,User>()
.ForMember(u => u.friends,opt => opt.MapFrom(????);
is this the right approach? if so how should I implement it?
or should I change my database model to this:
public class User
{
public int id { get; set; }
// attribute declaration
public ICollection<int> friendsId { get; set; }
}
Thank you in advance.
You'll need to implement a custom value resolver. These can be injected into, so you can access things like your context inside:
public class FriendsResolver : IValueResolver<UserResource, User, ICollection<User>>
{
private readonly ApplicationDbContext _context;
public FriendsResolver(ApplicationDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public ICollection<User> Resolve(UserResource source, User destination, ICollection<User> destMember, ResolutionContext context)
{
var existingFriendIds = destMember.Select(x => x.Id);
var newFriendIds = source.friendsId.Except(existingFriendIds);
var removedFriendIds = existingFriendIds.Except(source.Friends);
destMember.RemoveAll(x => removedFriendIds.Contains(x.Id);
destMember.AddRange(_context.Users.Where(x => newFriendIds.Contains(x.Id).ToList());
return destMember;
}
}
Not sure if that's going to actually work as-is, as I just threw it together here, but it should be enough to get your going. The general idea is that you inject whatever you need into the value resolver and then use that to create the actual stuff you need to return. In this case, that means querying your context for the User entities with those ids. Then, in your CreateMap:
.ForMember(dest => dest.friends, opts => opts.ResolveUsing<FriendsResolver>());
This only covers one side of the relationship, though, so if you need to map the other way, you may need a custom resolver for that path as well. Here, I don't think you actually do. You should be able to just get by with:
.ForMember(dest => dest.friendsId, opts => opts.MapFrom(src => src.friends.Select(x => x.Id));
This would help
CreateMap<UserResource,User>()
.ForMember(u => u.friends,opt => opt.MapFrom(t => new User {FriendsId = t.friendsId);
public class User
{
...
public ICollection<User> friends { get; set; }
}
Where friends is ICollection<User> whereas UserResource class has ICollection<int>. There is type mismatch here. You need to map ICollection to ICollection that is why I casted new User ...
Related
I have a user class with a navigation property that acts as the join model for a many-to-many relationship.
As an example:
class User
{
public string Id { get; set; }
public ICollection<UserFoo> Foos { get; set; }
}
class UserFoo
{
public string UserId { get; set; }
public User User { get; set; }
public int FooId { get; set; }
public Foo Foo { get; set; }
}
class Foo
{
public int Id { get; set; }
public ICollection <UserFoo> Users { get; set; }
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<UserFoo>
.HasOne(uf => uf.User)
.WithMany(u => u.Foos)
.HasForeignKey(uf => uf.UserId);
builder.Entity<UserFoo>
.HasOne(uf => uf.Foo)
.WithMany(u => u.Users)
.HasForeignKey(uf => uf.FooId);
}
When updating a user with the UserManager class, it exhibits strange behaviour with the navigation property.
The first time I add some relationships to the user, it updates fine as you would expect.
Any subsequent time I make changes, that's when things get weird.
If a user has a link to FooA and FooB, adding a link to FooC will erase FooA and FooB, resulting in only FooC.
If a user has a link to FooA and FooB, removing either of them removes all links.
The weird state comes after calling UserManager.UpdateAsync - the user model passed to it is in the correct state before the call, and the wrong one after the call ends.
I've traced this down to the internal call to Store.UpdateAsync. I can't figure out where to go from there, other than switching to using a context to update the user.
The update method (snipped):
public IActionResult Update(string id, UserForm form)
{
// form in correct state
var user = userManager.Users.Include(u => u.Foos).FirstOrDefault(u => u.Id == id);
// user in correct state
var role = await roleManager.FindByIdAsync(form.RoleId);
mapper.Map(form, user); // AutoMapper
// user now in correct state after mapping
var result = await userManager.UpdateAsync(user, role, form.Password);
// user in incorrect state
}
I'm building an application which uses NHibernate mapping by code, and I am unable to map protected properties when I use a component mapping (equivalent to hbm composite-element mapping) for a collection of value objects.
I am able to map protected properties in entity and compoment mappings for single value objects, it is just protected properties do not appear to be supported when mapping collections of value objects.
public class MyEntity
{
public virtual int Id { get; protected set; }
protected virtual MyValueObject MyValueObject { get; set; }
}
public class MyValueObject
{
protected string SomeString { get; set; }
protected ISet<NestedValueObject> NestedValueObjects { get; set; }
// Constructor, Equals/GetHashcode overrides, etc.
}
public class NestedValueObject
{
public string Name { get; set; }
protected DateTime CreatedOn { get; set; } // Audit only property
// Constructor, Equals/GetHashcode overrides, etc.
}
public MyEntityMap()
{
Table("MyEntityTable");
Id(x => x.Id, map =>
{
map.Column("Id");
});
Component<MyValueObject>("MyValueObject", c =>
{
// Protected property maps correctly
c.Property("SomeString", map =>
{
map.NotNullable(true);
});
c.Set<NestedValueObject>("NestedValueObjects", map =>
{
map.Table("NestedValueObjectsTable");
map.Key(k => k.Column("MyEntityId"));
}, r => r.Component(n =>
{
// Public property maps correctly
n.Property(x => x.Name);
// Compilation fail - there is no method that supports protected properties in component mappings
n.Property<DateTime>("CreatedOn", map =>
{
map.NotNullable(true);
});
}));
});
}
This is because IMinimalPlainPropertyContainerMapper<TContainer> supports protected properties, while IComponentElementMapper<TComponent> doesn't.
Is there a reason for this? It seems reasonable that a value object should be allowed to have protected properties which are for auditing purposes only and do not form a part of its conceptual identity, and protected properties are supported with the component mapping for single value objects.
It looks like this is missing feature, rather than a design decision, and will be fixed in a future release of NHibernate:
https://nhibernate.jira.com/browse/NH-3993
As a workaround until this release, the alternatives would be to make the properties public or to map the value object as an entity with a composite id using a one-to-many mapping, since these support protected variables.
What is the best way to retrieve data for a logged in user in Api. I can retrieve data for all users just fine.
'ApplicationDbContext':
public DbSet<Category> Categories { get; set; }
'ApplicationUser':
public ICollection<Category> Categories { get; set; }
my Repository:
public class CategoryRepository : ICategoryRepository
{
private ApplicationDbContext _ctx;
public CategoryRepository(ApplicationDbContext ctx)
{
_ctx = ctx;
}
//to do
public IEnumerable<Category> GetAll(string username)
{
return _ctx.Users.First(...).Categories.Include(x => x.Tasks);
}
public IEnumerable<Category> GetAllForFromAllUsers()
{
return _ctx.Categories.Include(x => x.Tasks);
}
}
As you have noticed above, I can not use extension method 'Include' on ICollection.
What is the best way to retrieve data for a specific user?
Can you try this?
var users = _ctx.Users
.Include(user => user.Categories)
.ThenInclude(cat => cat.Tasks)
.Where(u => u.username == "xx")
.ToList();
From Loading Related Data
I have the following data model
public class Profile : Entity
{
public virtual string Name { get; set; }
public virtual int Sequence { get; set; }
public virtual string Description { get; set; }
public virtual IList<MapService> MapServices { get; set; }
}
public class MapService : Entity
{
public virtual string Name { get; set; }
public virtual string Url { get; set; }
public virtual int MaximumResolution { get; set; }
}
As you can see , a profile has many MapService(s).
And the relation is many to many.
I am building an ASP.NET Web API rest service that return profile data.
I have two calls, one to return all the profiles, and the second filtered by ID, with the following URL
http://myapp/api/profiles
http://myapp/api/profiles/:id
I am using NHibernate for data access in the API Controller.
the Web API controller looks like this
public class ProfilesController : ApiController
{
public ProfilesController()
{
}
public IEnumerator<Profile> GetAllProfiles()
{
using (Session = .. create nhibernate session )
{
return Session.Query<Profile>().GetEnumerator();
}
}
}
When I return the whole list, I don't want the details of the MapService(s), just the Name and Id
so, I thought I will do lazy loading.
So, I configured the nhibernate mapping using fluent nhibernate as follows
public class ProfileMapping : ClassMap<Profile>
{
public ProfileMapping()
{
Table("PROFILE");
Id(x => x.Id, "OBJECT_ID");
Map(x => x.Name, "PROFILE_NAME");
Map(x => x.Sequence, "SEQUENCE_NO");
Map(x => x.Description, "DESCR");
HasManyToMany<MapService>(x => x.MapServices).LazyLoad().
Table("PROFILE_MAP_SERVICE").ParentKeyColumn("PROFILE_ID").ChildKeyColumn("MAP_SERVICE_ID");
}
}
I thought by doing this, I will return only the profile data without the details of MapService List
But when I call the rest service to return the whole data like this
http://myapp/api/profiles
I get this error
"Message":"An error has occurred.","ExceptionMessage":"The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json
"Message":"An error has occurred.","ExceptionMessage":"Initializing[Domain.Profile#2]-failed to lazily initialize a collection of role: Domain.Profile.MapServices, no session or session was closed","ExceptionType":"NHibernate.LazyInitializationException","StackTrace":"
It seems that the nhibernate is returning the list of profiles without mapservices, and close the session.
But them somehow the web api service is trying to access the list of map service during serialization.
how to tell the web api service to ignore the map service list?
The simple approach here would be to introduce the DTO object:
public class ProfileDto
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Sequence { get; set; } // or Guid...
public virtual string Description { get; set; }
... // if more needed
}
and then adjust the API Controller method
public IEnumerable<ProfileDto> GetAllProfiles()
{
using (var session = ...)
{
return session.Query<Profile>()
.Select(entity => new ProfileDto
{
Id = entity.ID,
Name = entity.Name,
Sequence = entity.Sequence,
Description = entity.Description,
})
.ToList();
}
}
The most important here is the call .ToList() which will assure, that all the loads from DB server are done during the session life time (using clause). Automapper maybe could be next step to make it more easy (less code)...
Suppose I have only two classes: Group and User. User has groups and Group has members (instance of users)
public class User {
public virtual int id { set; get; }
public virtual string username { set; get; }
public virtual IList<Group> groups { set; get; }
public User()
{
groups = new List<Group>();
}
public virtual void joinGroup(Group group)
{
if (this.groups.Contains(group))
throw new AlreadyJoinedException();
group.members.Add(this);
this.groups.Add(group);
}
public class Group
{
public virtual int id { set; get; }
public virtual string name { set; get; }
public virtual User administrator { set; get; }
public virtual IList<User> members { set; get; }
public Group()
{
members = new List<User>();
}
As you can see the domain it's quite simple. I've already mapped both classes correctly using Fluent NHibernate,
public class UserMapping : ClassMap<User>
{
public UserMapping()
{
this.Id(user => user.id).GeneratedBy.Identity();
this.Map(user => user.username).Not.Nullable().Length(50).Not.LazyLoad();
this.HasManyToMany(user => user.groups).Table("MemberPerGroup").ParentKeyColumn("id_user").ChildKeyColumn("id_group").Not.LazyLoad();
}
}
public class GroupMapping : ClassMap<Group>
{
public GroupMapping()
{
this.Id(group => group.id).GeneratedBy.Identity();
this.Map(group => group.name).Not.Nullable().Length(50).Not.LazyLoad();
this.References(group => group.administrator).Not.Nullable().Not.LazyLoad();
this.HasManyToMany(group => group.members).Table("MemberPerGroup").ParentKeyColumn("id_group").ChildKeyColumn("id_user").Not.LazyLoad();
}
}
I'm progamming a web application using ASP MVC 4. My problem shows up when a user tries to join group. It doesn't break but it neither works fine (doesn't insert into the table the new row in MemberPerGroup). I'm doing something like it:
public void JoinGroup(User user,Group group){
this.userRepository.GetSessionFactory().TransactionalInterceptor(() =>
{
user.joinGroup(group);
});
}
Thanks in advance.
Ivan.
It seems your mapping has no cascading set?
this.HasManyToMany(group => group.members)
.Table("MemberPerGroup")
.ParentKeyColumn("id_group")
.ChildKeyColumn("id_user")
.Not.LazyLoad()
.Cascade.SaveUpdate();
I'm curious - why do you use GetSessionFactory()? our repositories take an ISession object in the constructor, (injected by autofac, but that's irrelevant) from which we start our queries:
// even better to use a transaction, but this is just a sample
_session.SaveOrUpdate(user);
_session.Flush();