mapping on one of two columns / many-to-many mapping with fluentnhibernate - join

I'm trying to map the following:
A user can be connected to another user. The user class has a 'Connections' property which a collection of UserConnection. A UserConnection has two references to User, one for the requester (he who initiated the connection) and one for the requestee (he who is the target of the connection).
Using FluentNHibernate I want to load all UserConnections for when loading the User. A User's connections are those where the user's id matches either the requesterId or the requesteeId.
public class User
{
//....
public virtual IList<UserConnection> Connections { get; set; }
}
public class UserConnection
{
//....
public virtual User Requester { get; set; }
public virtual User Requestee { get; set; }
}
This feels like a many-to-many relationship, but I can't quite wrap my head around it. Can anyone help?
EDIT:
My current User mapping (below) obviously only matches on one column...
HasMany(x => x.Connections).Inverse().Key(k => k.Columns.Add("Requestee_id")).Fetch.Select().Not.LazyLoad();
EDIT 2:
I have considered this approach but I would prefer to avoid that if possible...

Isn't it 1-many? user has a collection of Connections? But each connection has 1 RequesterUser or 1 RequesteeUser?
This would be a self referencing relation, mapping something like below.
public class UserMap : ClassMap<User>
{
public UserMap()
{
Table("Users");
DynamicUpdate();
References<User>(x => x.Requester)
.Cascade.None()
.NotFound.Ignore();
References<User>(x => x.Requestee)
.Cascade.None()
.NotFound.Ignore();
HasMany<User>(x => x.RequesterConnections)
.Cascade.AllDeleteOrphan()
.Inverse()
.KeyColumn("RequesterId");
HasMany<User>(x => x.RequesteeConnections)
.Cascade.AllDeleteOrphan()
.Inverse()
.KeyColumn("RequesteeId");
}
}
I could be totally wrong, not been using NH long but maybe it will spark some ideas.
I think this is how i would try and do it.
Obviously you don't have a single collection of Connections but you could get them both pretty easily.
Hope this helps.
Kohan

Related

How to include related entities when getting ApplicationUser from DB? [duplicate]

I've extended IdentityUser to include a navigation property for the user's address, however when getting the user with UserManager.FindByEmailAsync, the navigation property isn't populated. Does ASP.NET Identity Core have some way to populate navigation properties like Entity Framework's Include(), or do I have to do it manually?
I've set up the navigation property like this:
public class MyUser : IdentityUser
{
public int? AddressId { get; set; }
[ForeignKey(nameof(AddressId))]
public virtual Address Address { get; set; }
}
public class Address
{
[Key]
public int Id { get; set; }
public string Street { get; set; }
public string Town { get; set; }
public string Country { get; set; }
}
Unfortunately, you have to either do it manually or create your own IUserStore<IdentityUser> where you load related data in the FindByEmailAsync method:
public class MyStore : IUserStore<IdentityUser>, // the rest of the interfaces
{
// ... implement the dozens of methods
public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail, CancellationToken token)
{
return await context.Users
.Include(x => x.Address)
.SingleAsync(x => x.Email == normalizedEmail);
}
}
Of course, implementing the entire store just for this isn't the best option.
You can also query the store directly, though:
UserManager<IdentityUser> userManager; // DI injected
var user = await userManager.Users
.Include(x => x.Address)
.SingleAsync(x => x.NormalizedEmail == email);
The short answer: you can't. However, there's options:
Explicitly load the relation later:
await context.Entry(user).Reference(x => x.Address).LoadAsync();
This will require issuing an additional query of course, but you can continue to pull the user via UserManager.
Just use the context. You don't have to use UserManager. It just makes some things a little simpler. You can always fallback to querying directly via the context:
var user = context.Users.Include(x => x.Address).SingleOrDefaultAsync(x=> x.Id == User.Identity.GetUserId());
FWIW, you don't need virtual on your navigation property. That's for lazy-loading, which EF Core currently does not support. (Though, EF Core 2.1, currently in preview, will actually support lazy-loading.) Regardless, lazy-loading is a bad idea more often than not, so you should still stick to either eagerly or explicitly loading your relationships.
Update for .NET 6.0 with EF Core 6.0:
You can now configure the property to be automatically included on every query.
modelBuilder.Entity<MyUser>().Navigation(e => e.Address).AutoInclude();
For more info check out:
https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager#model-configuration-for-auto-including-navigations
I found it useful to write an extension on the UserManager class.
public static async Task<MyUser> FindByUserAsync(
this UserManager<MyUser> input,
ClaimsPrincipal user )
{
return await input.Users
.Include(x => x.InverseNavigationTable)
.SingleOrDefaultAsync(x => x.NormalizedUserName == user.Identity.Name.ToUpper());
}
Best Option in my case is to add a package reference to Microsoft.EntityFrameworkCore.Proxies and then in your services use the UseLazyLoadingProxies
.AddDbContext<YourDbContext>(
b => b.UseLazyLoadingProxies()
.UseSqlServer(myConnectionString));
More infos
https://learn.microsoft.com/de-de/ef/core/querying/related-data/lazy

Adding model to another's models list in MVC 5

So I'm having difficulties with my 'flight order management system' in MVC 5.
What I'm trying to do is to add an existing passenger to a flight using the requirements given in a specific questions.
Must use lists
this is my code:
Flight:
public class Flight
{
public int Id { get; set; }
public string Depart { get; set; }
public string Destination { get; set; }
public DateTime TakeOffTime { get; set; }
public DateTime LandingTime { get; set; }
public virtual List<Passenger> Passengers { get; set; }
}
Passenger:
public class Passagenger
{
public int Id { get; set; }
public string Name { get; set; }
public int Gender { get; set; }
[Range(0,130)]
public int Age { get; set; }
}
We need to make a redirect at the index page (showing all flights) and a redirect link with the details page, where an admin can add a passenger to a flight. He does this by using a dropdown of the existing passengers and a 'add' button.
However, after the 'add' button is pushed, a table needs to pop up showing all passenger on the flight that it redirected to.
How would I approach this? I have already the following controller:
public ActionResult Details()
{
ViewBag.PassengerId = new SelectList(db.Passengers, "Id", "Name");
return View();
}
[HttpPost]
public ActionResult Details(int passengerId)
{
Passenger = db.Passegers.Find(passengerId);
if(ModelState.IsValid)
{
//add the passenger to the selected flight that it redirected to, but how to get the id of it now?
}
//Get all passengers of the flight, how to get the id?
List<Passagier> Passagiers = db.Passagiers.ToList();
return View(Passagiers);
}
My details view:
#using (Html.BeginForm("Details", "Home", FormMethod.Post))
{
#Html.DropDownList("passengerId", (SelectList)ViewBag.PassengersId);
<input type="submit" value="Add" />
}
I can't use anything that the related answers are giving me (viewModel, ienumerable,...) hence why I'm asking this.
How would I be able to pass the current route id of the page (flightID) and the passengersID to the controllers action? What would be a good approach to this?
I am nearly there I think because the redirects work and so does my dropdown list showing all the passengers.
First, any requirements that prohibit good design should be fought against. Project owners pay developers because they do not have the requisite skills, themselves. It is your job as a developer to educate the project owner when there are requirements that do not make sense. If the project owner insists on doing something fundamentally wrong, in spite of your protests, then that's a sign to you to drop them like a hot rock. They will only be a source of problems for you. Don't be afraid to fire clients. There's plenty of fish in the sea, but a bad client will be an anchor around your neck.
That said, you may just be misunderstanding the requirements. While there's places where you can use List<T> and probably even should use List<T>, your database-persisted entity is not one of them. It must be ICollection<T>. Period. A property of List<T> will not be persisted by Entity Framework.
That's where view models come in, as your view model can and should use List<T>, and then you can map that back and forth to the ICollection<T> on your entity. Again, if the requirements prohibit the use of view models, then your client either is uninformed or stupid. Your duty is to make sure they are informed, and if it turns out they're just stupid, then drop them.
Next, whenever you have a list of something you're posting to, you need to use ListBoxFor, rather than DropDownListFor. The latter only allows a single selection, but here you'll need to persist multiple passengers. On your view model, you should have a property like:
public List<int> SelectedPassengerIds { get; set; }
And, then one to hold your passenger options:
public IEnumerable<SelectListItem> PassengerOptions { get; set; }
In your action, you will map the Flight entity to your view model as normal, but for your Passengers collection you would do:
model.SelectedPassengerIds = flight.Passengers.Select(m => m.Id).ToList();
Then, set PassengerOptions:
model.PassengerOptions = db.Passengers.Select(m => new SelectListItem { Value = m.Id.ToString(), Text = m.Name });
Finally, in your view, you'll have:
#Html.ListBoxFor(m => m.SelectedPassengerIds, Model.PassengerOptions)
On post, you need to map on the appropriate posted values from your view model back on to your Flight entity. For your Passengers collection, you will do:
// Remove deselected passengers
flight.Passengers.Where(m => !model.SelectedPassengerIds.Contains(m.Id)).ToList()
.ForEach(m => flight.Passengers.Remove(m));
// Add newly selected passengers
var newPassengerIds = model.SelectedPassengerIds.Except(flight.Passengers.Select(m => m.Id));
db.Passengers.Where(m => newPassengerIds.Contains(m.Id)).ToList()
.ForEach(m => flight.Passengers.Add(m));

Updating many-to-many relationship entity framework

I have problem with updating entites that have many-to many relationship. Below my User and category class:
public class User : IEntity
{
[Key]
public virtual long Id { get; set; }
private ICollection<Category> _availableCategories;
public virtual ICollection<Category> AvailableCategories
{
get { return _availableCategories ?? (_availableCategories = new List<Category>()); }
set { _availableCategories = value; }
}
}
public class Category : IEntity
{
[Key]
public long Id { get; set; }
/// <summary>
/// Full name or description of a category
/// </summary>
[StringLength(255)]
public string FullName { get; set; }
}
This is code snippet from my repository
public override void Edit(User user)
{
var dbUser = _context.Users.Include(x => x.AvailableCategories)
.Single(x => x.Id == user.Id);
var categories = _context.Categories;
dbUser.AvailableCategories.Clear();
foreach (var cat in user.AvailableCategories)
{
dbUser.AvailableCategories.Add(cat);
}
_context.Entry(dbUser).State = EntityState.Modified;
}
However the categories don't get updated. What EF does is insert empty rows into category table and sets relations to this new rows with user.
How can I update User so that I change only categories that already exist in the database?
User that I pass to Edit method has AvailableCategories with only Ids set (rest of properties are empty).
When you're doing something like posting back M2M relationships, you either must post the full object, as in every single property on those objects, or simply post a list of ids and then use those to query the associated objects back from the database. Otherwise, Entity Framework understands your purpose to be to update the properties on the objects as well, in this case with empty values.
Obviously the first option is quite unwieldy, so the second way is the preferred and standard way. Generally, for this, you'd want to use a view model so you could have a property like the following, that you would post into:
public List<long> SelectedCategories { get; set; }
But, if you insist on using the entity directly, you can get much the same result by simply doing:
var selectedCategories = user.AvailableCategories.Select(m => m.Id)
Once you have the ids:
var newAvailableCategories = _context.Categories.Where(m => selectedCategories.Contains(m.Id));
And then finally set that on your user:
dbUser.AvailableCategories = newAvailableCategories;
I notice you are also adding the user.AvailableCategories directly into dbUser.AvailableCategories. I've noticed when binding back complex objects from an MVC view that DB Entities are no longer attached to the DbContext. If you look at the entity, you can verify by checking dbContext.Entry(cat).State is "detached" (or something unexpected) I believe.
You must query those entities back out of the dbContext (possibly by using the returned cat.Id's). Or otherwise manually set the entities as "unchanged". And then add those "non-detached" items into dbUser.AvailableCategories. Please see Chris's answer as it shows with specific code how to get this done.
Also, I might use a linking entity. Possibly something like this:
public class UserCategory
{
public User User {get;set;}
public Category Category {get;set;}
}
And add it to DB context. Also, drop the linking lists in your current User and Category class. This way you can manipulate the UserCategory class (and DbSet) to manage your many-to-many relationship.

Unable to Add Controller (with scaffolding) ASP.NET MVC

This has been addressed in multiple other questions, but alas, I have tried all the solutions posted there with no success. I'm developing an ASP.NET MVC application, using Code-First EF. I am trying to take advantage of the scaffolding built in so that it can automatically create a Controller for me based off my Model and DbContext. However, I am getting the following error when I try to create a Controller in this way:
'Unable to retrieve metadata for Employer.' Using the same DbCompiledModel to create contexts against different types of database servers is not supported. Instead, create a separate DbCompiledModel for each type of server being used.
The code for my model, Employer, my DbContext, MyDataContext, and my web.config file follow:
//Employer.cs
public class Employer : Organization, IEmployer
{
public virtual PhysicalAddress Address { get; set; }
public virtual ICollection<ContactMethod> ContactMethods { get; set; }
public int FederalTaxID { get; set; }
public virtual Client Client { get; set; }
}
-
//MyDataContext.cs
[DbConfigurationType(typeof(MySqlEFConfiguration))]
public class MyDataContext : IdentityDbContext<ApplicationUser>
{
public MyDataContext()
: base("DataConnection")
{
}
static MyDataContext()
{
Database.SetInitializer(new DataInitializer());
}
public DbSet<Client> Clients { get; set; }
public DbSet<Organization> Organizations { get; set; } // Store Employer and OrgEntity
/// <summary>
/// Sets up unclear relationships between entities before the models are constructued in a database.
/// For example, models which extend other models must have their Ids mapped (because it is an
/// inherited member, and so is not found by Entity Framework by default)
/// </summary>
/// <param name="modelBuilder">the object responsible for constructing the database from code-first</param>
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//set up parent organization relationship
modelBuilder.Entity<Organization>()
.HasOptional(n => n.ParentOrganization)
.WithMany(o => o.ChildOrganizations)
.Map(m => m.MapKey("ParentOrganization_Id"));
//set up the organization-employee assignment
modelBuilder.Entity<Employee>()
.HasOptional(a => a.OrganizationAssignment)
.WithMany(a => a.Employees);
//used to integrate Identity stuff into same db as Clients, etc.
modelBuilder.Entity<IdentityUserLogin>().HasKey<string>(l => l.UserId);
modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id);
modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId });
}
}
-
<!--web.config-->
.
.
.
<connectionStrings>
<add name="DataConnection"
providerName="Mysql.Data.MysqlClient"
connectionString="server=localhost;user id=myid;password=mypass;persistsecurityinfo=True;database=app_db"/>
</connectionStrings>
.
.
.
From various other posts like this one , this one , and this one . I have tried many different things to get it to work. This includes commenting out my constructor for MyDbContext, changing the providerName attribute of my connection string to be "System.Data.SqlClient", changing the connection string name to be "DefaultConnection", removing my connection strings altogether, and combinations of all of these things. I make sure to rebuild between trying to add the Controller. However, after performing these different changes, I still receive the same error when I try to add a new Controller.
Generally, I can find all my questions already answered, but the answers don't seem to be working for me on this one. I think what might separate my case from the ones linked is that my DbContext is actually an instance of IdentityDbContext. I believe the constructor for IdentityDbContext just calls the base constructor for it, anyway, so I don't see how this could be much of an issue.
Any help is much appreciated. Thank you!
You didn't map your Employer entity to the model. That's why compiler is unable to retrieve metadata for Employer. Firstly, add DbSet as below and let me know, if does it work.
public DbSet<Client> Clients { get; set; }
public DbSet<Organization> Organizations { get; set; }
public DbSet<Employer> Employers { get; set; }

Error with relationship

I have a User table and an Avatar table. One User can have many avatars (or null). But I need to mark which avatar is the current, so I have an Avatar_Id in User table that is the current avatar. And a ForeignKey User_Id in Avatar to tell me which User is the owner.
Trying to do that is generating me a lot of errors and headaches when I try to populate some data in order to test the relationship.
public class User
{
[Key]
public int Id { get; set; }
public Avatar Avatar { get; set; }
public virtual ICollection<Avatar> Avatars { get; set; }
}
public class Avatar
{
[Key, ForeignKey("User")]
public int Id { get; set; }
public User User { get; set; }
}
Test part:
var user = new User();
var avatar = new Avatar()
{
User = user
};
// user.Avatar = avatar; // <- this gives [a circular] error; without this I have null.
db.Users.Add(user);
db.Avatars.Add(avatar);
db.SaveChanges();
This is resulting me with Avatar_Id = NULL within User table, and User_Id = NULL in Avatar table. I expected these fields filled (well, Avatar_Id can be null).
Its better to make boolean field 'IsDefault' in table with avatar and check while add/update avatars that no more default avatars for this user. Also you can add same property in avatar class.
#Fabricio I can't test this code before post, but I'm pretty convinced it will work.
public class User
{
[Key]
public int UserId { get; set; }
public int AvatarId { get; set; }
[ForeignKey("AvatarId")]
public Avatar Avatar { get; set; }
public ICollection<Avatar> Avatars { get; set; }
}
public class Avatar
{
[Key]
public int AvatarId { get; set; }
[ForeignKey("User")]
public int UserId { get; set; }
public User User { get; set; }
}
The problem is when you put two foreign keys merge like one. Now you have a foreign key in Avatar table and other in User table, each one represents one mode of relationship.
The foreign key "AvatarId" represents a special form of foreign key, a unique + foreign key, (a second form to build the one to one relationship). You can read more about this in here: http://weblogs.asp.net/manavi/archive/2011/05/01/associations-in-ef-4-1-code-first-part-5-one-to-one-foreign-key-associations.aspx
I've given this a bit of though because I modeled a similar case once and didn't mind to reevaluate the options.
Look closely at your premises:
One User can have many avatars (or null)
This short sentence implies that the one-to-one association User-Avatar must be optional both ways, because a User without Avatars can’t possibly refer to one its own avatars, and when a user has more than one avatar only one of them can refer to User as being the user's default. (They all refer to user as owner).
So you can only model it as a 0..1 – 0..1 association. So Avatar’s primary key can't be a foreign key to user. (It couldn't anyway, otherwise a user could only have one avatar).
Maybe this could have been done by Jonny Piazzi's model if this wouldn't throw the infamous "may cause cycles or multiple cascade paths" exception. Both user and Avatar refer to one another and you have to tell EF explicitly which of the FKs is not cascading. This can only be done by fluent mapping:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOptional(u => u.Avatar)
.WithOptionalDependent()
.Map(m => m.MapKey("AvatarId"))
.WillCascadeOnDelete(false);
...
}
This puts a nullable, non-cascading FK column AvatarId in User (that's why User is the dependent of Avatar).
Now your second issue, the chicken-eg problem when populating the model.
This can only be done when you call SaveChanges twice and wrap these calls in a transaction scope. For example:
using (var tran = new TransactionScope())
{
var user = new User();
var avatar = new Avatar();
user.Avatars = new HashSet<Avatar>();
user.Avatars.Add(avatar);
user.Avatars.Add(new Avatar());
user.Avatars.Add(new Avatar());
db.Users.Add(user);
db.SaveChanges();
user.Avatar = avatar; // set FK
db.SaveChanges();
tran.Complete();
}
Now EF can decide which key to generate first (User's) before referring to it by foreign keys. Subsequently you set the FK in User.
But... is this the best model?
Maybe, maybe not.
The issue is that your model does not enforce the business rule that a user can only have one of its own avatars as default avatar. User.AvatarId can refer to any avatar. So you have to write business logic to enforce the business rule.
With YD1m's solution (no User.AvatarId, but a column Avatar.IsDefault) this business rule is enforced implicitly. But now you have to write business logic to enforce that only one avatar is the default.
It's up to you to decide what you think is more feasible.
(for the record: way back, I took the latter option)

Resources