PROBLEM:
I am very new to EF and to LINQ, so please bear with me.
I am trying to create a EF6 model using the database first approach. Simply speaking, I have 2 database tables tblUser and tblMilkMan which have a foreign key relationship on the UserID column.
To avoid cyclic references and to shape the entity data I have created DTO classes for both the models.
I made the MilkManDTO class contain a reference to a UserDTO instance.(This is probably stupid, if so, please guide me to the right way).My aim is to be able to load a milkmen and the related User data
Anyway in my API call, when I try to load a MilkMan by ID, I do not know how to load the related UserDTO. I found examples online on how to load related Entities but not related DTOs.
DB Schema:
Models:
MilkMan Model and DTO:
namespace MilkMan.Models
{
using System;
using System.Collections.Generic;
public partial class tblMilkMan
{
public int RecordID { get; set; }
public int UserID { get; set; }
public bool IsMyTurn { get; set; }
public int RoundRobinOrder { get; set; }
public virtual tblUser tblUser { get; set; }
}
public class MilkManDTO
{
public int RecordID { get; set; }
public int UserID { get; set; }
public bool IsMyTurn { get; set; }
public int RoundRobinOrder { get; set; }
public virtual UserDTO User { get; set; }
}
}
User Model and DTO:
public partial class tblUser
{
public tblUser()
{
this.tblMilkMen = new HashSet<tblMilkMan>();
}
public int UserID { get; set; }
public string LogonName { get; set; }
public string Password { get; set; }
public int PasswordExpiresAfter { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
:
// more fields
:
public virtual ICollection<tblMilkMan> tblMilkMen { get; set; }
}
public class UserDTO
{
public int UserID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Web API Controller Method:
// GET api/MilkMan/5
[ResponseType(typeof(MilkManDTO))]
public async Task<IHttpActionResult> GettblMilkMan(int id)
{
//tblMilkMan tblmilkman = await db.tblMilkMen.FindAsync(id);
MilkManDTO milkMan = await db.tblMilkMen.Select(b => new MilkManDTO()
{
RecordID = b.RecordID,
UserID = b.UserID,
IsMyTurn = b.IsMyTurn,
RoundRobinOrder = b.RoundRobinOrder,
User = //???? Error//
}).SingleOrDefaultAsync(b => b.RecordID == id);
if (milkMan == null)
{
return NotFound();
}
return Ok(milkMan);
}
You can nest a new UserDTO and use the same initialization list technique.
MilkManDTO milkMan = await db.tblMilkMen.Select(b => new MilkManDTO()
{
RecordID = b.RecordID,
UserID = b.UserID,
IsMyTurn = b.IsMyTurn,
RoundRobinOrder = b.RoundRobinOrder,
User = new UserDTO {
UserID = b.User.UserID,
FirstName = b.User.FirstName,
LastName = b.User.LastName,
}
}).SingleOrDefaultAsync(b => b.RecordID == id);
This code may throw a null reference exception on b.User.UserID if there is not associated User and thus User could be null. You would need to deal with this with either a ?? coalesce, ternary (b.User == null ? "DefaultFirstName" : b.User.FirstName) or omit the entire reference User = (b.User == null ? (UserDTO)null : new UserDTO { ... }). null's make this kind of thing fun.
With C# 6 we have null reference operator .? that makes this much more succinct.
Related
In an ASP.NET core (dotnet 6) web application, we have a small user management functionality in which a user can be assigned to a specific site (physical location) with certain rights. A user can be assigned to one or more sites and a site can also be assigned to one or more users, thus the many to many relationship. We have the following classes:
public class User
{
public int ID { get; set; }
public string SamAccountName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int GroupID { get; set; }
public bool IsEnabled { get; set; }
public ICollection<UserSite> UserSites { get; set; }
}
public class Site
{
public int ID { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public ICollection<UserSite> UserSites { get; set; }
}
The many to many relationship is described in a join table with fluent API:
public class UserSite
{
public int UserID { get; set; }
public User User { get; set; }
public int SiteID { get; set; }
public Site Site { get; set; }
}
// Fluent API for UserSite table
builder.HasKey(us => new { us.UserID, us.SiteID });
builder.HasOne(us => us.User)
.WithMany(us => us.UserSites)
.HasForeignKey(us => us.UserID)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(us => us.Site)
.WithMany(us => us.UserSites)
.HasForeignKey(us => us.SiteID)
.OnDelete(DeleteBehavior.Cascade);
These classes result in the following DTO's:
public class UserDTO
{
public int ID { get; set; }
public string SamAccountName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int GroupID { get; set; }
public bool IsEnabled { get; set; }
public IList<SiteDTO> Sites { get; set; }
}
public class SiteDTO
{
public int ID { get; set; }
public string Name { get; set; }
public string Code { get; set; }
}
What we want to know is, what is the best way to create a new user or update an existing user? Just to clarify, the data in the Sites table exists already and is static. What I mean by that is that when creating a new user or updating an existing one, a user can be assigned to one or more existing sites, but no new site is created in the process.
We already tried some things that did work but we are not sure if this is really the best way to this or if there is a far better way to do it. Here is what we tried:
The create method
// Create method
public IActionResult CreateUser(UserDTO userDTO)
{
if (ModelState.IsValid)
{
User user = _mapper.Map<User>(userDTO);
user.IsEnabled = true;
AttachSitesToUser(user.ID, user.UserSites);
_context.Users.Add(user);
_context.SaveChanges();
userViewModel = _mapper.Map<UserViewModel>(user);
}
return new JsonResult(new[] { userDTO });
}
The update method:
// Update method
public IActionResult UpdateUser(UserDTO userDTO)
{
if (ModelState.IsValid)
{
User user = _context.Users
.Include(u => u.Sites)
.ThenInclude(s => s.Site)
.Single(u => u.ID == userDTO.ID);
_mapper.Map(userViewModel, user);
AttachSitesToUser(user.ID, user.UserSites);
_context.Entry(user).State = EntityState.Modified;
_context.SaveChanges();
}
return new JsonResult(new[] { userDTO });
}
The helper method AttachSitesToUser:
// Helper method that adds the user ID and attaches the site(s) to the context
private void AttachSitesToUser(int userID, IEnumerable<UserSite> userSites)
{
foreach (UserSite userSite in userSites)
{
userSite.UserID = userID;
if (Context.Entry<Site>(userSite.Site).State == EntityState.Detached)
{
Context.Sites.Attach(userSite.Site);
Context.Entry<Site>(userSite.Site).State = EntityState.Unchanged;
}
}
}
As you can see, for the moment we have to loop through the UserSites list in the User entity and attach the sites the user was assigned to manually to the context. Is there no better way to do this or is this the official best practice?
I have created classes using EF Code First that have collections of each other.
Entities:
public class Field
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<AppUser> Teachers { get; set; }
public Field()
{
Teachers = new List<AppUser>();
}
}
public class AppUser
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
public virtual List<Field> Fields { get; set; }
public AppUser()
{
Fields = new List<FieldDTO>();
}
}
DTOs:
public class FieldDTO
{
public int Id { get; set; }
public string Name { get; set; }
public List<AppUserDTO> Teachers { get; set; }
public FieldDTO()
{
Teachers = new List<AppUserDTO>();
}
}
public class AppUserDTO
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
public List<FieldDTO> Fields { get; set; }
public AppUserDTO()
{
Fields = new List<FieldDTO>();
}
}
Mappings:
Mapper.CreateMap<Field, FieldDTO>();
Mapper.CreateMap<FieldDTO, Field>();
Mapper.CreateMap<AppUserDTO, AppUser>();
Mapper.CreateMap<AppUser, AppUserDTO>();
And I am getting StackOverflowException when calling this code (Context is my dbContext):
protected override IQueryable<FieldDTO> GetQueryable()
{
IQueryable<Field> query = Context.Fields;
return query.ProjectTo<FieldDTO>();//exception thrown here
}
I guess this happens because it loops in Lists calling each other endlessly. But I do not understand why this happens. Are my mappings wrong?
You have self-referencing entities AND self-referencing DTOs. Generally speaking self-referencing DTOs are a bad idea. Especially when doing a projection - EF does not know how to join together and join together and join together a hierarchy of items.
You have two choices.
First, you can force a specific depth of hierarchy by explicitly modeling your DTOs with a hierarchy in mind:
public class FieldDTO
{
public int Id { get; set; }
public string Name { get; set; }
public List<TeacherDTO> Teachers { get; set; }
public FieldDTO()
{
Teachers = new List<TeacherDTO>();
}
}
public class TeacherDTO
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
}
public class AppUserDTO : TeacherDTO
{
public List<FieldDTO> Fields { get; set; }
public AppUserDTO()
{
Fields = new List<FieldDTO>();
}
}
This is the preferred way, as it's the most obvious and explicit.
The less obvious, less explicit way is to configure AutoMapper to have a maximum depth it will go to traverse hierarchical relationships:
CreateMap<AppUser, AppUserDTO>().MaxDepth(3);
I prefer to go #1 because it's the most easily understood, but #2 works as well.
Other option is using PreserveReferences() method.
CreateMap<AppUser, AppUserDTO>().PreserveReferences();
I use this generic method:
public static TTarget Convert<TSource, TTarget>(TSource sourceItem)
{
if (null == sourceItem)
{
return default(TTarget);
}
var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
var serializedObject = JsonConvert.SerializeObject(sourceItem, deserializeSettings);
return JsonConvert.DeserializeObject<TTarget>(serializedObject);
}
...
MapperConfiguration(cfg =>
{
cfg.ForAllMaps((map, exp) => exp.MaxDepth(1));
...
When you giving 1 navigation_property to 2nd entity and visa-versa it go in an infinite loop state. So, the compiler automatically throws a Stackoverflow exception.
So, to avoid that, you just need to remove one navigation_property from any of the entities.
I have a annoying problem with my code.
My model :
public class Option
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Conference> Conference { set; get; }
}
public partial class Conference
{
[Key, ForeignKey("User")]
public int UserId { get; set; }
public virtual Option Option { set; get; }
public virtual User User { get; set; }
}
public partial class User
{
public int Id {get; set; }
public string Name { get; set; }
public virtual Conference Conference { get; set; }
}
And now i`m getting Option object from Db by dbSet.Find(id) (RepositoryFactory) and what i want to do is to save newly created User, but with selected Option.
If i do like that:
var option = dbSet.Find(id);
var user = new User()
{
Name = "Name",
Conference = new Conference
{
Option = option
}
};
//...
context.SaveChanges();
I`m getting an exception:
An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
What I`m doing wrong?
Edit: I Tried to create Mapping, but this doesn`t seems to work too.
modelBuilder.Entity<Conference>()
.HasKey(x => x.UserId)
.HasRequired(x => x.User)
.WithOptional(user => user.Conference);
modelBuilder.Entity<Option>()
.HasMany(option => option.Conferences)
.WithRequired(conference => conference.Option)
.HasForeignKey(conference => conference.UserId);
Are you trying to achieve a 1:1 relationship between User and Conference? If so, you need to add an Id (key) property to User. Please see the comments I added to the code sample below regarding the 1:1 relationship. I would advise further evaluation of your domain layer to ensure this is what you are trying to achieve...
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
namespace Stackoverflow
{
public class EntityContext : DbContext
{
public IDbSet<Conference> Conferences { get; set; }
public IDbSet<Option> Options { get; set; }
public IDbSet<User> Users { get; set; }
}
public class Option
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Conference> Conference { set; get; }
}
public class Conference
{
// In one-to-one relation one end must be principal and second end must be dependent.
// User is the one which will be inserted first and which can exist without the dependent one.
// Conference end is the one which must be inserted after the principal because it has foreign key to the principal.
[Key, ForeignKey("User")]
public int UserId { get; set; }
public int OptionId { get; set; }
public virtual Option Option { set; get; }
public virtual User User { get; set; }
}
public class User
{
// user requires a key
public int Id { get; set; }
public string Name { get; set; }
public virtual Conference Conference { get; set; }
}
class Program
{
static void Main(string[] args)
{
using (var entityContext = new EntityContext())
{
// added to facilitate your example
var newOption = new Option {Name = "SomeOptionName"};
entityContext.Options.Add(newOption);
entityContext.SaveChanges();
var option = entityContext.Options.Find(newOption.Id);
var user = new User
{
Name = "Name",
Conference = new Conference
{
Option = option
}
};
// need to add the user
entityContext.Users.Add(user);
//...
entityContext.SaveChanges();
}
}
}
}
I have made simple model for example.
public class Publisher
{
public int Id { get; set; }
public string Title { get; set; }
public Address Location { get; set; }
public virtual ICollection<Book> Books { get; set; }
}
public class Address
{
public string Country { get; set; }
public string City { get; set; }
public string Street { get; set; }
public string HouseNumber { get; set; }
}
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public int LanguageId { get; set; }
public int? PublisherId { get; set; }
}
I need to get publishers with related books. I know how to do it using linq to entities. Is it possible to solve a problem using entity sql?
public class CatalogContext : DbContext {...}
public List<Publisher> GetByCity(string city)
{
var result = new List<Publisher>();
string queryString;
queryString = String.Format(#"SELECT VALUE row(a,b)
FROM CatalogContext.Publishers AS a
join CatalogContext.Books AS b on a.Id = b.PublisherId
WHERE a.Location.City = '{0}'", city);
var rows = ((IObjectContextAdapter)_context).ObjectContext.CreateQuery<DbDataRecord>(queryString).ToList();
return ???
}
Query returns required data but it's List<DbDataRecord> - list of pairs <publisher, book>. How to translate it to list of publishers with filled navigation property "Books"?
Is it possible to write query which directly returns List<Publisher>?
you can do the following:
var result = ObjectContext.Publishers.Include("Books").Include("Locations")
.Where(c => c.Location.City = "SOME_CITY").Select(c => c);
Include - basically joins the table.
Then you can drill down to books by doing the following:
var test = result[0].Books;
Why are you using direct sql command instead of Entity Framework code style?
UPDATED: I understood I should not use a DbSet so I changed the implementation to an ICollection as suggested by Erenga
Please consider the following classes:
[Table("Tenant")]
public class Tenant : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
[Key]
public string Guid { get; set; }
public virtual ICollection<User> Users { get; set; }
}
[Table("User")]
public class User : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
public string Password { get; set; }
}
The first test creates a new Tenant and a new User and stores them in the appropriate tables.
[Test]
public void CreateNewUserForNewTenant()
{
var user = _applicationContext.Users.Create();
user.Name = "barney";
user.EmailAddress = "barney#flinstone.com";
var tenant = _applicationContext.Tenants.Create();
tenant.Name = "localhost";
tenant.Guid = Guid.NewGuid().ToString();
tenant.Users.Add(user); // NullReferenceException, I expected the EF would LazyLoad the reference to Users?!
_tenantRepository.Add(tenant);
_applicationContext.SaveChanges();
}
This test will fail on a NullReferenceException since the property Users is not initialized.
How should I change my code that I can rely on LazyLoading provided with EF?
There are 2 problems I see here.
As #SimonWhitehead mentioned, reference types are initialized as null by default. Lazy loading works only on entities created by EF. These are actually sub classes of your class that contain addtional logic to lazy load.
DbSet is not a collection type that is supported on entities. You need to change the type to ICollection, ISet, or IList.
Here's a working example
[Table("Tenant")]
public class Tenant : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
[Key]
public string Guid { get; set; }
public virtual ICollection<User> Users { get; set; }
}
[Table("User")]
public class User : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
public string Password { get; set; }
}
[Test]
public void CreateNewUserForNewTenant()
{
var user = _applicationContext.Users.Create();
user.Name = "barney";
user.EmailAddress = "barney#flinstone.com";
var tenant = _applicationContext.Tenents.Create();
tenant.Name = "localhost";
tenant.Guid = Guid.NewGuid().ToString();
tenant.Users = new List<User> { user };
_tenantRepository.Add(tenant);
_applicationContext.SaveChanges();
}
var tenant = new Tenant
{
Name = "localhost",
Guid = Guid.NewGuid().ToString(),
Users = new List<User> { user }
};
I think you were expecting something like this (not threadsafe):
[Table("Tenant")]
public class Tenant : IEntity
{
private DbSet<User> _users;
public int Id { get; set; }
public string Name { get; set; }
[Key]
public string Guid { get; set; }
public virtual ICollection<User> Users
{
get
{
if (_users == null)
_users = new List<Users>();
return _users;
}
set { _users = value; }
}
}
I'm sure the Lazy<T> class could be utilised somehow too but I am not familiar with that class.