entity code first many-to-many relations saving - ef-code-first

I have spent a day with this problem. There are these entitites:
public class UserRole
{
[Key]
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ICollection<Privilege> Privileges { get; set; }
}
public class Privilege
{
[Key]
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ICollection<UserRole> Roles { get; set; }
}
And this configuration in context:
modelBuilder.Entity<UserRole>().HasMany<Privilege>(a => a.Privileges).WithMany(a => a.Roles)
.Map(m =>
{
m.MapLeftKey("RoleId");
m.MapRightKey("PrivilegeId");
m.ToTable("RolesPrivilegesMapping");
});
And this test code:
UserRole role;
//get role entity from context
//write privileges count
using (VaultContext context = new VaultContext(connectionString))
{
role = context.UserRoles.Where(a => a.Name == "Users").FirstOrDefault();
Console.WriteLine(role.Privileges.Count.ToString()); //writes 0
}
//make offline changes
//the privilege already exists in database
role.Privileges.Add(new Privilege() { Id = 1, Name = "Login" });
//save changes to database
using (VaultContext context = new VaultContext(connectionString))
{
List<Privilege> privileges = new List<Privilege>();
foreach (Privilege p in role.Privileges)
{ privileges.Add(p); }
foreach (Privilege p in privileges)
{ context.Privileges.Attach(p); }
role.Privileges.Clear();
context.UserRoles.Attach(role);
context.Entry(role).State = System.Data.EntityState.Modified;
foreach (Privilege p in privileges)
{ role.Privileges.Add(context.Privileges.Find(p.Id)); }
context.SaveChanges();
}
//reload the role entity
//write privileges count
using (VaultContext context = new VaultContext(connectionString))
{
Console.WriteLine(context.UserRoles.Where(a => a.Name == "Users").FirstOrDefault().Privileges.Count.ToString()); //writes 0
}
The Users role and Login privilege is already in database. So I try to add privilege Login to Users role.
But relations are not saved in database - the RolesPrivilegesMapping table doesn't contain record with id corresponding with Users role and Login privilege.
If I simply add directly - it works, but it is not my target:
new UserRole()
{
Name = "Administrators",
Privileges = new List<Privilege>()
{
new Privilege() { Name = "Login" }
}
}
Could anyone advice me how to add only many-to-many relations if all entities exist in database?
I'm using EF 5.0. I have tried it with EF 4.3 with same result.

If all your wanting to do is attach existing entries in your database, could you do somethign a bit simpler?
using( VaultContext context = new VaultContext( ) )
{
role = context.UserRoles.FirstOrDefault(a => a.Name == "Users");
Privilege p = context.Privileges.FirstOrDefault(pr => pr.Name == "Login");
role.Privileges.Add(p);
context.SaveChanges();
}

Thank you.
This is my solution: (role is changed offline)
UserRole dbRole = context.UserRoles.Find(role.Id);
dbRole.Privileges.Clear();
foreach (Privilege p in role.Privileges)
{
dbRole.Privileges.Add(context.Privileges.Find(p.Id));
}
context.Entry(dbRole).State = System.Data.EntityState.Modified;

Related

What would be the best/simplest solution to retrieve values from my database for comparison? - ASP.NET Core

I'm currently stuck on accessing all of the 'UserName' values from my database.
I am doing this so I can compare the user input for a username to check if it has been used before (I don't want two instances of the same username). This is on a .cshtml.cs page.
I am already able to access the database through my program, as create commands have been tested and do work.
My program is on ASP.NET 6.0 Core Web App.
I am a student with basic knowledge on ASP.NET Core, and on how to solve this issue, therefore as much simplified explanation would be very appreciated.
Here is my code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using AQA_A_Level_CS_NEA__Suvat_Solver_.Models;
using AQA_A_Level_CS_NEA__Suvat_Solver_.Data;
namespace AQA_A_Level_CS_NEA__Suvat_Solver_.Pages.UserLogin
{
[BindProperties(SupportsGet = true)]
public class RegisterPageModel : PageModel
{
public new TempUserLoginModel TempUser { get; set; }
public bool HasPassword { get; set; } = true;
public bool HasUsername { get; set; } = true;
public bool UniUsername { get; set; } = true;
public bool RegisterApproved { get; set; } = false;
public bool AQAPhys { get; set; } = false;
public bool AQAMaths { get; set; } = false;
public bool SubjectChosen { get; set; } = true;
private readonly ApplicationDbContext _context;
public RegisterPageModel(ApplicationDbContext context)
{
_context = context;
}
public List<User> UserList = new List<User>();
public void OnGet()
{
}
public IActionResult OnPost()
{
User User = new User();
HasPassword = true;
HasUsername = true;
UniUsername = true;
SubjectChosen = true;
UserList = _context.User.ToList();
if (!AQAMaths && !AQAPhys)
{
SubjectChosen = false;
}
if (string.IsNullOrWhiteSpace(TempUser.Password) || TempUser.Password.Length < 4)
{
HasPassword = false;
}
if (string.IsNullOrWhiteSpace(TempUser.Username) || TempUser.Username.Length < 4)
{
HasUsername = false;
}
if (TempUser.Username == //database UserName value here )
{
//Here would be where the Username is compared
//UniUsername = false;
}
if (!HasPassword || !HasUsername || !UniUsername || !SubjectChosen)
{
return RedirectToPage("/UserLogin/RegisterPage", new { HasPassword, HasUsername, UniUsername, SubjectChosen });
}
else
{
RegisterApproved = true;
User.UserName = TempUser.Username;
User.UserPass = TempUser.Password;
User.UserCorrectAnsw = 0;
User.UserTotalAnsw = 0;
_context.User.Add(User);
_context.SaveChanges();
return RedirectToPage("/UserLogin/LoginPage", new { RegisterApproved });
}
}
}
}
Many Thanks.
Probably the strongest method is to enforce the user name column to be unique at the database level using a Unique Constraint. That way if you try to add a user with a duplicate user name, the database will simply return an error.
This article shows how to create a Unique Constraint with Entity Framework
You can be sure that the database will not allow a user with a duplicate user name with this method. However, trying to add a duplicate user will create an error which you will have to either handle or prevent from occurring in the first place (which is what you are doing now)
So for the code you are using now, since you already have the users pulled from the database here:
UserList = _context.User.ToList();
We can use LINQ to check if any of the users Usernames in UserList matches the TempUser like this:
if (UserList.Any(x => x.Username == TempUser.Username))
{
//Here would be where the Username is compared
UniUsername = false;
}
Since you didn't share your User model, this assumes your User class has a property named Username.
Happy Coding

NHibernate - one-to-many just not working with SQLite

TL;DR;
NHibernate reverse relationship is working on Azure-SQL and MSSQL2012 but not with SQLite
Description:
I am currently Unittesting my Asp.Net MVC App and set up my Unittest with FluentMigrator on SQLite.
After creating the Database I set up some base entries I need.
One of those is a Product.
A Product has many ProductSuppliers and a ProductSupplier has many ProductSupplierPrices
public class Product
{
public virtual long Id { get; set; }
public virtual string Number { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
//more properties
public virtual IList<ProductSupplier> Suppliers { get; set; }
//more properties
}
public class ProductSupplier
{
public virtual long Id { get; set; }
public virtual Product Product { get; set; }
public virtual Supplier Supplier { get; set; }
public virtual IList<ProductSupplierPrice> Prices { get; set; }
}
public class ProductSupplierPrice : IHaveId
{
public virtual long Id { get; set; }
public virtual ProductSupplier ProductSupplier { get; set; }
public virtual decimal FromAmount { get; set; }
public virtual decimal Price { get; set; }
}
Setup:
Create Supplier
Create Product
Create ProductSupplier
Create ProductSupplierPrice
Test:
Product product = this.session.Load<Product>((long)1);
ProductSupplier productSupplier = product.Suppliers.First(); //<-- Suppliers are null; therefore throws an exception
If I load them seperately to check the relationships:
productSupplierPrice.ProductSupplier <--- Correct Supplier
productSupplier.Prices <-- Null
productSupplier.Product <-- Product with Id 1
product.Suppliers <-- Null
So to me it seems, that the many-to-one direction works correctely, but the one-to-many (reverse relation) is not working.
The Problem exists only in my Unittest (SQLite) the App itself runs on Azure-SQL and is working fine.
EDIT:
Mappings with FluentnHibernate
public class ProductMap : ClassMap<Product>
{
public ProductMap()
{
Id(x => x.Id);
HasMany(x => x.Suppliers).Inverse().Cascade.DeleteOrphan().BatchSize(20);
//many more mappings
}
}
public ProductSupplierMap()
{
Id(x => x.Id);
References(x => x.Product);
References(x => x.Supplier);
Map(x => x.IsMainSupplier);
Map(x => x.SupplierProductNumber);
Map(x => x.CopperSurcharge);
HasMany(x => x.Prices).Inverse().Cascade.DeleteOrphan().BatchSize(20);
}
public ProductSupplierPriceMap()
{
Id(x => x.Id);
References(x => x.ProductSupplier);
Map(x => x.FromAmount);
Map(x => x.Price);
}
Edit2 - Creating the DB-Entries:
Product product = new Product()
{
Type = ProductType.Purchase,
Dispatcher = session.Load<Employee>(employeeId),
Number = "100.10-1000",
Name = "Testproduct",
//Lots of Properties
Suppliers = new List<ProductSupplier>()
};
session.SaveOrUpdate(product);
ProductSupplier productSupplier = new ProductSupplier()
{
Product = product,
Supplier = session.Load<Supplier>((long)1),
IsMainSupplier = true,
SupplierProductNumber = "Artikel123456",
CopperSurcharge = CopperSurchargeType.DEL700,
Prices = new List<ProductSupplierPrice>()
};
session.Save(productSupplier);
ProductSupplierPrice productSupplierPrice = new ProductSupplierPrice()
{
ProductSupplier = productSupplier,
FromAmount = 1,
Price = 5
};
session.Save(productSupplierPrice);
EDIT 3.1:
public static ISession InitializeDatabase()
{
NHibernateSessionHolder.CreateSessionFactory();
session = NHibernateSessionHolder.OpenSession();
CreateBaseEntries(); //Creates Employees, Supplier, Customer etc
return session;
}
Based on the Ayende's article you need to clear the session between insert/update and querying:
session.Clear();
Seems to be a session management, I'm not sure why the session should be clean, but the session is providing your original instance (the same you provided for saving, stored on the session cache) instead a proxy for lazy-loading.
private long CreatePurchaseOrder()
{
session.Clear();
var product = this.session.Load<Product>((long)1);
var productSupplier = product.Suppliers.First();
var productSupplierPrice = productSupplier.Prices.First();
return 0;
}
Sorry for late reply
In your unit test, you are using same session for creating and fetching entities. This is not right as subsequent fetch returns entities from first level cache which do not have their graph set up properly.
So....either use different sessions OR as a quick fix, I have added "session.Clear()" in the method "InitializeDatabase()" of "DatabaseSetUpHelper". Clearing the session clears first level cache and force NH to fetch data from DB again and the resulting entities have their graph set up properly.
public static ISession InitializeDatabase()
{
NHibernateSessionHolder.CreateSessionFactory();
session = NHibernateSessionHolder.OpenSession();
CreateBaseEntries();
session.Clear(); // notice this!!! this clears first level cache of session, thus forcing fetching of data from DB
return session;
}
Note: My quick-fix is not final solution, it is there just show how session behaves. In proper solution, you must use different sessions.

Mapping signalr users to connections using SQL Server

I read this topic about mapping SignalR users to connections. Briefly, the topic explain four methods of mapping and I want to use the forth method (Permanent, external storage). The method uses a SQL Server database to store the ConnectionId when the client is connected (when OnConnected method is fired) and when the client closes the browser (when OnDisconnected method is fired) it just make the ConnectionId not valid.
Here is the code for database:
public class UserContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Connection> Connections { get; set; }
}
public class User
{
[Key]
public string UserName { get; set; }
public ICollection<Connection> Connections { get; set; }
}
public class Connection
{
public string ConnectionID { get; set; }
public string UserAgent { get; set; }
public bool Connected { get; set; }
}
And here is the code in the hub class:
[Authorize]
public class ChatHub : Hub
{
public void SendChatMessage(string who, string message)
{
var name = Context.User.Identity.Name;
using (var db = new UserContext())
{
var user = db.Users.Find(who);
if (user == null)
{
Clients.Caller.showErrorMessage("Could not find that user.");
}
else
{
db.Entry(user)
.Collection(u => u.Connections)
.Query()
.Where(c => c.Connected == true)
.Load();
if (user.Connections == null)
{
Clients.Caller.showErrorMessage("The user is no longer connected.");
}
else
{
foreach (var connection in user.Connections)
{
Clients.Client(connection.ConnectionID)
.addChatMessage(name + ": " + message);
}
}
}
}
}
public override Task OnConnected()
{
var name = Context.User.Identity.Name;
using (var db = new UserContext())
{
var user = db.Users
.Include(u => u.Connections)
.SingleOrDefault(u => u.UserName == name);
if (user == null)
{
user = new User
{
UserName = name,
Connections = new List<Connection>()
};
db.Users.Add(user);
}
user.Connections.Add(new Connection
{
ConnectionID = Context.ConnectionId,
UserAgent = Context.Request.Headers["User-Agent"],
Connected = true
});
db.SaveChanges();
}
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
using (var db = new UserContext())
{
var connection = db.Connections.Find(Context.ConnectionId);
connection.Connected = false;
db.SaveChanges();
}
return base.OnDisconnected(stopCalled);
}
}
I want to enhance this method because using this method will create many ConnectionIds that is not needed at all. Also, with this method the Connections table will be enlarged with time without any useful.
What about cleaning up the connectionIds instead of setting the Connected property to false? Your OnDisconnnected() method could then look something like this:
public override Task OnDisconnected(bool stopCalled)
{
using (var db = new UserContext())
{
var connection = db.Connections.Find(Context.ConnectionId);
db.Connections.Remove(connection);
db.SaveChanges();
}
return base.OnDisconnected(stopCalled);
}
This would stop the Connections table from growing infinitely. You could then remove the Connected property altogether and have the existence of a Connection row indicate that a connection is active.
Update
Another approach, if you don't want to delete records but re-use the ones already in the database, could look like this:
public override Task OnConnected()
{
var name = Context.User.Identity.Name;
using (var db = new UserContext())
{
var user = db.Users
.Include(u => u.Connections)
.SingleOrDefault(u => u.UserName == name);
if (user == null)
{
user = new User
{
UserName = name,
Connections = new List<Connection>()
};
db.Users.Add(user);
}
var connection = user.Connections.Where(c => c.Connected == false && UserAgent == Context.Request.Headers["User-Agent"]).FirstOrDefault();
if (connection == null)
{
connection = new Connection();
connection.UserAgent = Context.Request.Headers["User-Agent"];
user.Connections.Add(connection);
}
connection.ConnectionID = Context.ConnectionId;
connection.Connected = true;
db.SaveChanges();
}
return base.OnConnected();
}
Is this along the lines of what you had in mind?

Entity Framework - list AspNetUsers with roles on admin page

I am working on an admin page where someone can go in and make some changes in the database. I want them to be able to view all non-admin users and edit them as well. After digging around online, I have figured out how to list out the users by email address and I have a Roles column. But the value showing up under Roles is not quite correct. It is the UserID in the AspNetUserRoles table.
Here is my AdminController code that is grabbing the users and roles:
using (var db = new ApplicationDbContext())
{
model.AppUsers = db.Users
.Include("Roles")
.Select(u =>
new ManagerUserViewModel
{
UserID = u.Id,
UserName = u.UserName,
Email = u.Email,
Roles = u.Roles.ToList()
}
).ToList();
};
return View(model);
And here is my ManagerUserViewModel:
public class ManagerUserViewModel
{
public String UserID { get; set; }
public String Email { get; set; }
public String UserName { get; set; }
public IEnumerable<IdentityUserRole> Roles { get; set; }
}
Let me know if any other code is needed.
Here is what I am getting output on the page now:
Can you access the UserManager instance? If so it has a GetRoles function which takes a userId and returns a List of roles.
using (var db = new ApplicationDbContext())
{
model.AppUsers = db.Users
.Include("Roles")
.Select(u =>
new ManagerUserViewModel
{
UserID = u.Id,
UserName = u.UserName,
Email = u.Email
}
).ToList();
};
foreach(var user in model.AppUser){
user.Roles = userManager.GetRoles(user.UserId);
}
You'll need to update your viewmodel
public IEnumerable<String> Roles { get; set; }
If you don't have the UserManager you should be able to get it from the OwinContext
var userManager = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();

Entity Framework : adding record with related data

I have a very simple Situation with 2 tables
public class Movie
{
[Key]
public Guid ID { get; set; }
public string Name { get; set; }
public byte[] Hash { get; set; }
public int GenreID{ get; set; }
[ForeignKey("GenreID")]
public virtual Genre genre{ get; set; }
}
and
public class Genre
{
public int ID { get; set; }
public string Name { get; set; }
}
Now, in an import sequence I want to create new movies and link the Genre with the existing entries in the Genre table or create new Genre entries if they don't exist.
Movie m = new Movie();
m.ID = Guid.NewGuid();
IndexerContext db = new IndexerContext();
var genre = db.Genre.Where(g => g.Name== genreValue).FirstOrDefault();
if(genre!= null)
{
m.GenreID= genre.GenreID;
}
else
{
genre= new Genre();
genre.Name = genreValue;
db.Genres.Add(genre);
var genreCreated= db.Genre.Where(g => g.Name== genreValue).FirstOrDefault();
m.GenreID= genreCreated.GenreID;
}
Now the problem is, it doesn't work. The last line fails because genreCreated is null.
Plus I think I must doing it wrong - it can't be that difficult in Entity Framework.
can anyone help me?
db.Genres.Add(genre);
This does not send insert statement to database - this instructs entity framework that new record should be inserted when saving changes. Genre will be saved (and created id available) after you call db.SaveChanges(); As for now, you do not have save call, so genreCreated is null.
In your situation - fix is simple, you do not need to select genreCreated from db. Just setting m.Genre to new value should do the job
Movie m = new Movie();
m.ID = Guid.NewGuid();
IndexerContext db = new IndexerContext();
var genre = db.Genre.Where(g => g.Name== genreValue).FirstOrDefault();
if(genre! = null)
{
m.GenreID = genre.GenreID;
}
else
{
genre = new Genre();
genre.Name = genreValue;
m.Genre = genre;
}
db.SaveChanges(); //m.GenreID will automatically be set to newly inserted genre
After the add statement you need to save it:
Try
genre= new Genre();
genre.Name = genreValue;
db.Genres.Add(genre);
db.SaveChanges();

Resources