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)...
Related
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 ...
I am building a Web API and have two models: Task and Feature:
public class Feature
{
[Key]
public long FeatureId { get; set; }
public string Analyst_comment { get; set; }
public virtual ICollection<User_Task> Tasks { get; set; }
public Feature()
{
}
}
public class User_Task
{
[Key]
public long TaskId { get; set; }
public string What { get; set; }
[ForeignKey("FeatureId")]
public long? FeatureId { get; set; }
public User_Task()
{
}
}
I create Tasks first and then create a Feature that combines few of them. Task creation is successful, however while creating a Feature with existing Tasks, my controller throws an error saying the task already exists:
My FeatureController has following method:
//Create
[HttpPost]
public IActionResult Create([FromBody] Feature item)
{
if (item == null)
{
return BadRequest();
}
** It basically expects that I am creating a Feature with brand new tasks, so I guess I will need some logic here to tell EF Core that incoming tasks with this feature already exist **
_featureRepository.Add(item);
return CreatedAtRoute("GetFeature", new { id = item.FeatureId }, item);
}
How to tell EF core that incoming Feature has Tasks that already exist and it just needs to update the references instead of creating new ones?
My context:
public class WebAPIDataContext : DbContext
{
public WebAPIDataContext(DbContextOptions<WebAPIDataContext> options)
: base(options)
{
}
public DbSet<User_Task> User_Tasks { get; set; }
public DbSet<Feature> Features { get; set; }
}
And repo:
public void Add(Feature item)
{
_context.Features.Add(item);
_context.SaveChanges();
}
When calling Add on a DBSet with a model that was not loaded from EF, it thinks it is untracked and will always assume it is new.
Instead, you need to load the existing record from the dbcontext and map the properties from the data passed into the API to the existing record. Typically that is a manual map from parameter object to domain. Then if you return an object back, you would map that new domain object to a DTO. You can use services like AutoMapper to map the domain to a DTO. When you're done mapping, you only need to call SaveChanges.
Generally speaking, loading the record and mapping the fields is a good thing for the security of your API. You wouldn't want to assume that the passed in data is pristine and honest. When you give the calling code access to all the properties of the entity, you may not be expecting them to change all the fields, and some of those fields could be sensitive.
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();
it works. pull the data, but gives this error: illegal access to loading collection
public class Image : File
{
public virtual string ImagePath { get; set; }
}
public class Video : File
{
public virtual string VideoPath { get; set; }
public virtual string VideoType { get; set; }
}
public class Service : ContentBase
{
public virtual IList<Image> Images { get; set; }
public virtual IList<Video> Videos { get; set; }
}
public class ServiceMap:SubclassMap<Domain.Service>
{
public ServiceMap()
{
DiscriminatorValue("Service");
HasMany(x => x.Images).KeyColumn("ContentBase");
HasMany(x => x.Videos).KeyColumn("ContentBase");
}
}
public class ImageMap:SubclassMap<Image>
{
public ImageMap()
{
DiscriminatorValue("Image");
Map(x => x.ImagePath);
}
}
public class VideoMap:SubclassMap<Video>
{
public VideoMap()
{
DiscriminatorValue("Video");
Map(x => x.VideoPath);
}
}
it works. but it gives this error when I query. I think the same "keycolumn" gives this error to be. mapping'i How should I do?
Are you aware that joinalias wont eagerload the collections? that's what Fetch is for. Try this one instead
var service = UnitOfWork.CurrentSession.QueryOver<Service>()
.Fetch(x => x.Images).Eager
.Fetch(x => x.Videos).Eager
.Where(x => x.Id == serviceId)
.SingleOrDefault();
Update:
illegal access to loading collection could be thrown when
session is closed when accessing not initialized collection
mapping and collection type mismatch
Database doesn't match
have you inspected the sql generated to see if NH tries to access nonexistant columns or columns on the wrong table?
I'm using EF 4.1 Code first. I have a model of user and a model of setting.
Each time the repository returns a user the Setting is also loaded. I've marked the Setting as virtual all my access modifiers are public LazyLoadingEnabled and ProxyCreationEnabled are enabled by default.
What am I missing?
public class User : BaseEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual ICollection<Setting> Settings { get; set; }
}
public class Setting
{
public int UserID { get; set; }
public int SettingID { get; set; }
public string Value { get; set; }
}
The User might have several setting, so there is a one to many relationship with a foreign key in setting.
The user configuration is
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
HasKey(u => u.ID);
HasMany(u => u.Settings).WithOptional().HasForeignKey(u => u.UserID);
}
}
and the Setting configuration is:
public class SettingsConfiguration : EntityTypeConfiguration<Setting>
{
public SettingsConfiguration()
{
ToTable("UserSettings");
HasKey(s => new { s.UserID, s.SettingID });
}
}
Lazy loading means the opposite of what you think it means.
With lazy loading (virtual property and defaults)
Settings is not retrieved immediately when querying User
Settings is retrieved when it's accessed for the first time. The DbContext must be open at that time for this to happen; otherwise you'll get an exception
Without lazy loading (non-virtual property and/or explicitly disabled)
Settings is not retrieved immediately when querying User
Settings will never be retrieved automatically; it will return null (which, in my opinion, is a terrible design decision: null is a wrong value and you shouldn't be able to get it)
In both cases, you can load Settings eagerly by using .Include(x => x.Settings), or when needed, by calling context.Entry(user).Collection(x => x.Settings).Load()