Until now I have always worked with my own DAL for SQL Server.
In a new project I decided to work with Entity in a MVC project and Identity.
I use to work with bridge tables.
Here is my IdentityModels (simplified)
ApplicationUser
public class ApplicationUser : IdentityUser
{
[Required]
public string Surname { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Group> Groups { get; set; }
}
Group
public class Group
{
[Key]
public int Id { get; set; }
[Display(Name = "Nom du Groupe")]
[Required]
[CustomRemoteValidation("IsGroupNameExist", "Groups", AdditionalFields =
"Id")]
public string Name { get; set; }
public virtual ICollection<ApplicationUser> ApplicationUsers { get; set;
}
And DbContext
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
public DbSet<Group> Groups { get; set; }
}
All tables I need are created and seems well created (ApplicationUser Group and ApplicationUserGroups).
The trouble is :
I have 3 groups (A, B, C) with Id 1,2,3. I"m adding a user in table ApplicationUser with 3 groups in the Groups property.
First part is OK, it adds the good values in the bridge table (ApplicationUsersGroup) BUT It adds groups A, B, C again, with Id 4,5,6 in Group table.
The CreateAsync method of UserManageris not the point (It's the same with just an Add).
If I have a look in the debugger, I can see that when I pass to the add method the user object, in the Groupsproperty, I have a ApplicationUsers property with inside the Groups property. For me, it could be the reason, but if I remove the Groups property from ApplicationUser, code first doesn't create the ApplicationUserGroups.
I'm wrong with something, but what? How can I have a user without an additional entry in Grouptable?
Thank you for your help.
UPDATE
Ok, now I understood why duplicates are added, but in my case, how to avoid that?
Here is the involved part of the Register method:
List<Group> selectedItems = new List<Group>();
foreach (GroupTableViewModel item in model.SelectedGroups)
{
if (item.Selected == true) selectedItems.Add(new Group { Id = item.Id, Name = item.GroupName });
}
var user = new ApplicationUser { Name = model.Name, Surname = model.Surname, UserName = model.Surname + "." + model.Name, Email = model.Email,Groups=selectedItems};
string password = RandomPassword.Generate(8, 8);
var result = await UserManager.CreateAsync(user, password);
CreateAsync() is the identity method. I don't understand how it adds the user (I don't see any Add() or 'SaveChanges() inside with JustDecompile).
Maybe I'm wrong again but if I want to attach an entity to the context I have to create a new context, which will be different from the context used by the CreateAsync() method.
So help needed...
This is a common issue that people unfamiliar with EF face. Because of the disconnected state of entities in the object context, EF will attempt to insert the entities in the relationships, even though they already exist. In order to solve, you need to tell EF that the entities are not new by setting their state to Unchanged. Take a look at this article from Julie Lerman and the related SO question/answer.
https://msdn.microsoft.com/en-us/magazine/dn166926.aspx
Entityframework duplicating when calling savechanges
Related
I'm trying to defined a lambda query in a property of my code first EF model as seen below as, GetLatestTransaction :
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual List<TransactionModel> Transactions { get; set; }
public TransactionModel GetLatestTransaction {
get {
return Transactions.OrderByDescending(x => x.Created).FirstOrDefault();
}
}
}
The reason for this is that I don't want to have to retype this query in many places and by having it in one place reduce the chances of a bug.
I want to use this in a query like this:
var user = _DB.Users
.Select(u => new UserDetailsView()
{
Id = u.Id,
FirstName= u.FirstName,
LastName= u.LastName,
Balance = u.GetLatestTransaction.ValueResult
}).FirstOrDefault(x => x.Id == userId);
This is however resulting in this error:
System.NotSupportedException: 'The specified type member 'GetLatestTransaction' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.'
Is there some way to achieve this without storing another relation to the latest transaction on the user and having to update it every time there is a new transaction?
Edit: I would also like to do it as above to avoid making another query to the database, I want it all in one go to improve performance.
Your ApplicationUser class represents the table in the database. It does not represent the usage of the data in the table.
Quite a lot of people think it is good practice to separate the database structure from the usage of the data. This separation is quite often done using the repository pattern. The repository is an abstraction from the internal datastructure of the database. It allows you to add functionality to your classes without demanding this functionality in the control classes that communicate with the database.
There are numerous articles about the repository. This one helped me to understand what functionality I should put in my entity framework classes and which in the repository.
So you'll need a class that represents the elements in your database table and one that represents the applicationUsers with only their LatestTransaction
The class that represents the database table:
class ApplicationUser : IdentityUser
{
public int Id {get; set;}
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual List<TransactionModel> Transactions { get; set; }
}
ApplicationUser with the latest transaction
class AppicationUserExt : <base class needed?>
{
public int Id {get; set;}
public string FirstName { get; set; }
public string LastName { get; set; }
public TransactionModel LatestTransaction { get; set; }
}
The function to get your extended ApplicationUser is an extension function of your ApplicationUser. Input: IQueryable<ApplicationUser output: IQueryable<ApplicationUserExt>
static class MyDbContextExtensions
{
// returns ne ApplicationUserExt for every ApplicationUser
public IQueryable<ApplicationUserExt> ToExtendedUsers(this IQueryable<ApplicationUser> applicationUsers)
{
return applicationUsers
.Select(user => new ApplicationUserExt()
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
LatestTransaction = user.Trnasactions
.OrderByDescenting(transaction => transaction.CreationDate)
.FirstOrDefault(),
}
}
}
}
So whenever you have a query with the ApplicationUsers you want, you can use ToExtendedUsers() to get the extended suers
using (var dbContext = new MyDbContext(...))
{
// you wanted to have a query like:
var result dbContext.ApplicationUsers
.Where(user => user.FirstName = "John"
&& user.LastName = "Doe");
// you'll have to add ToExtendedUsers:
var result = dbContext.ApplicationUsers
.Where(user => user.FirstName = "John"
&& user.LastName = "Doe");
.ToExtendedUsers();
}
As the result is still an IQueryable, no query has been done yet. You can still add LINQ statements before the query is done:
var result2 = result
.Where(user.LatestTransaction.Year == 2018)
.GroupBy(user => user.LatestTransaction.Date)
.OrderBy(group => group.Key)
.Take(10)
.ToList();
You see, that you can still do all kinds of LINQ stuff as long as it is an ApplicationUser. As soon as you need the LatestTransaction you convert it to an ApplicationUserExt and continue concatenating your linq statements.
There must be an easy solution for such a generic question, so I apologize upfront for my ignorance:
I have a multi-user Web-app (Asp.net MVC5 with EF6) that a.o. allows users to view and/or modify their relevant data stored in several related tables (Company, Csearch, Candidate). (for more details see below). They should NOT see any other data (e.g. by tampering with the URL).
I use Asp.net Identity 2.0 for authentication and would like to use it for the mentioned authorization as well. Userdata is stored in the standard AspNetUser Table. I use only one context for both Identity and my Business Tables.
I guess I have to either use Roles or maybe Claims to solve this, but I cannot find any guidance on how to do that. Can anyone point me in the right direction?
I have currently solved it (for the Company Model) by adding a LINQ condition to the CompanyController, but this does not appear to be a very secure and proper way of solving the problem.
public ActionResult Index(int? id, int? csearchid)
{
var companies = db.Companies
.OrderBy(i => i.CompanyName)
.Where(t => t.UserName == User.Identity.Name);
return View(companies);
My DataModel is straightforward and I had it scaffolded using Visual Studio 2017
Through EF6 Code first I have constructed a Relational Datamodel which is roughly as follows:
a COMPANY can have multiple SEARCHES (one to many).
Each Search can have multiple CANDIDATES (one to many).
A COMPANY can have multiple USERS logging in.
Users are save in the AspNetUsers table genberated by ASP.Net Identity.
My Company model looks as follows:
public class Company
{
public int CompanyID { get; set; }
// Link naar de Userid in Identity: AspNetUsers.Id
[Display(Name = "Username")]
public string UserName { get; set; }
public string CompanyName { get; set;}
public string CompanyContactName { get; set; }
[DataType(DataType.EmailAddress)]
public string CompanyEmail { get; set; }
public string CompanyPhone { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
//One to Many Navigatie links
public virtual ICollection<Csearch> Csearches { get; set; }
Once the user is identified, you can make sure the user can only access its own data. You cannot use roles for that, since that will only define the level of access. But you can use claims.
Out-of-the-box there is a seperation of concerns. Maintain this seperation. You are not meant to query the Identity tables directly. Use the userManager for that. Also never use an Identity object as ViewModel. You may expose more than you mean to. If you keep this seperation, you'll see that it is in fact much easier.
The identity context contains all data to identify the user, the business context contains all business information, including user information. You may think that this is redundant, but the login user has really nothing in common with the business user. The login emailaddress may differ from the business.user.emailaddress (what is the meaning of the emailaddress in both cases?). Also consider the possibility to have users that cannot login (anymore).
As a rule of thumb always consider if the information is part of the identity or part of the business.
When do you need the ApplicationUser? Only for the current user or when managing users. When you query users, always use the business.user. Because all the information you need should be available there.
For the current user, add claims with the information you need. The advantage of claims is that you won't have to query the database on each call to retrieve this information, like the corresponding UserId and the (display)UserName.
How to add claims
You can, without having to extend the ApplicationUser class, add a claim to the user by adding a row to the AspNetUserClaims table. Something like:
userManager.AddClaim(id, new Claim("UserId", UserId));
On login the claim will be automatically added to the ClaimsIdentity.
You can also add claims for properties that extend the ApplicationUser:
public class ApplicationUser : IdentityUser
{
public int UserId { get; set; }
public string DisplayUserName { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
userIdentity.AddClaim(new Claim("UserId", UserId));
userIdentity.AddClaim(new Claim("DisplayUserName", DisplayUserName));
return userIdentity;
}
}
How to read claims
In the controller you can read the claim with code like this:
var user = (System.Security.Claims.ClaimsIdentity)User.Identity;
var userId = user.FindFirstValue("UserId");
You can use userId in your queries to filter the data for the current user or even use business.users as the only entry to retrieve data. Like db.Users(u => u.Id == userId).Companies.ToList();
Please note, the code is just an example. I didn't test all of it. It is just to give you an idea. In case something isn't clear, please let me know.
It's pretty simple really. To illustrate with the example Company you provided. Note that you should use UserId to join rather than UserName since UserName can change, but UserId will always be unique.)
Instead of having UserName in your Company table, you need to change that to UserId. Then you join the AspNetUsers table with your Company table on UserId.
For example (I prefer to use the query syntax rather than the fluent syntax):
var companies = from c in db.Companies join u in db.AspNetUsers
on c.UserId equals u.UserId
orderby c.CompanyName
where u.UserName = User.Identity.Name
select c;
If you need the username as well, then include that in your select
select new { Company = c, User = u.UserName };
However, this model does not work if you want to have multiple users per company. You either need to add CompanyId to the users table (assuming a user can't be a member of more than one company) or create a many-to-many join if a user can be a member of multiple companies.
So rather than linking the user to the company, you link the company to the user. Your current model only allows one user per company.
Another thing I see wrong here is the use of DisplayName in your entity object. That seems to indicate you are using the entity in your MVC view, which you shouldn't do. You should create a separate ViewModel.
Here is how it should look like for multiple users per company:
public class Company
{
public int CompanyID { get; set; }
// Link naar de Userid in Identity: AspNetUsers.Id
// [Display(Name = "Username")] <-- Get rid of these
// public string UserName { get; set; } <-- get rid of these
...
}
public class ApplicationUser : IdentityUser
{
public int CompanyId { get; set; }
}
Then change your query to:
var companies = from c in db.Companies join u in db.AspNetUsers
on c.CompanyId equals u.CompanyId // <-- Change to this
orderby c.CompanyName
where u.UserName = User.Identity.Name
select c;
I made it in the following way:
I added UserId property to the Company class. (It is string type because at SQL it is NVARCHAR type)
public class Company
{
public string UserId { get; set; }
public int CompanyID { get; set; }
// Link naar de Userid in Identity: AspNetUsers.Id
[Display(Name = "Username")]
public string UserName { get; set; }
public string CompanyName { get; set;}
public string CompanyContactName { get; set; }
[DataType(DataType.EmailAddress)]
public string CompanyEmail { get; set; }
public string CompanyPhone { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
//One to Many Navigatie links
public virtual ICollection<Csearch> Csearches { get; set; }
}
In the Create controller for getting current logged in user id I used How to get the current logged in user ID in ASP.NET Core? post. In brief UserId = User.FindFirstValue(ClaimTypes.NameIdentifier)
public class CompanyController : Controller
{
private readonly ApplicationDbContext _context;
private readonly IWebHostEnvironment webHostEnvironment;
public CompanyController (ApplicationDbContext context, IWebHostEnvironment hostEnvironment)
{
_context = context;
webHostEnvironment = hostEnvironment;
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(RecordViewModel model)
{
if (ModelState.IsValid)
{
Company company = new Company
{
UserId = User.FindFirstValue(ClaimTypes.NameIdentifier),
FirstName = model.FirstName,
CompanyName = model.CompanyName,
CompanyContactName = model.CompanyContactName,
CompanyEmail = model.CompanyEmail,
CompanyPhone = model.CompanyPhone
};
_context.Add(company);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(model);
}
}
And for the displaying only records of the current logged in user I use following action:
public async Task<IActionResult> Index()
{
var companyLoggedInUser = from c in _context.Company
where c.UserId ==
User.FindFirstValue(ClaimTypes.NameIdentifier)
select c;
return View(companyLoggedInUser);
}
I have problem with copying entities with many to many relationship.
I have three entities Company, Role and User defined like this:
Company:
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public virtual IList<User> Users { get; set; }
}
User:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public virtual IList<Role> Roles { get; set; }
}
Role:
public class Role
{
public int Id { get; set; }
public string Name { get; set; }
public virtual IList<User> Users { get; set; }
}
Also, I defined many to many relationship between users and roles:
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
ToTable("TUser");
HasKey(x => x.Id);
HasMany(x => x.Roles).WithMany(x => x.Users).Map(m =>
{
m.MapLeftKey("UserId");
m.MapRightKey("RoleId");
m.ToTable("TUserRole");
});
}
}
I used migrations to create tables in db and obviously EF created table TUserRole (so far everything good).
And now, I would like to create copy of company and users but without copying roles (so I want to create new records at the tables TCompany, TUser and TUserRole, but no new records at the TRole).
I thought that something like this would work but I'm getting exception:
Context context = new Context();
var company = context.Companies.Include(x => x.Users.Select(u => u.Roles)).AsNoTracking().SingleOrDefault();
context.Companies.Add(company);
foreach (var user in company.Users)
{
foreach (var role in user.Roles)
{
context.Entry(role).State = EntityState.Unchanged;
}
}
context.SaveChanges();
And the exception is Saving or accepting changes failed because more than one entity of type 'Mackas.EF.Model.Role' have the same primary key value.
I understand why I'm getting this (because there is more than one role with the same ID), but I don't know what should be my approach.
Any suggestions?
I'm using EF 6.1.3.
Using AsNoTracking generally is a good idea to obtain a graph of entities that aren't attached to a context. As you know, adding the root entity (company) to a new context will mark all entities in the graph as Added and copying entities is a piece of cake.
But there's one bummer. AsNoTracking causes EF to materialize a new object for each entity in the result set, because it has no way to track that an entity has already been materialized. So you're OK as long as the object graph only diverges off the root entity. I.e. as long as all associations are 1 - 0..n. It that is true, all entities in the graph will represent exactly one "real" entity.
However, in your case, there's a m - n association between User and Roles. The graph converges. If some users have the same roles, EF will create duplicate Role objects when using AsNoTracking.
[By the way, contrary to EF6, EF-core manages to create unique entities even with AsNoTracking]
The way to go here is to query the object graph by one context, as POCOs, not proxies, and then add/attach it to a second context:
Company company;
using (Context context = new Context())
{
context.Configuration.ProxyCreationEnabled = false;
company = context.Companies.Include(x => x.Users.Select(u => u.Roles))
.SingleOrDefault();
}
using (Context context = new Context())
{
context.Companies.Add(company);
foreach (var user in company.Users.SelectMany(u => u.Roles)
.Distinct())
{
context.Entry(role).State = EntityState.Unchanged;
}
context.SaveChanges();
}
Proxies have a reference to the context they were created by, so you can't attach them to a second context.
I have two EF models/classes that have relation between them: Member and MembershipSeason. One Member can have several MembershipSeasons. One MembershipSeason model has a foreign key reference (MemberID) to Member model in db.
Member.cs
public class Member
{
public int MemberID { get; set; }
//some properties left out
public virtual ICollection<MembershipSeason> MembershipSeasons { get; set; }
}
MembershipSeason.cs
public class MembershipSeason
{
[Key]
public int MembershipSeasonID { get; set; }
//some properties left out
public int MemberID { get; set; }
public virtual Member Member { get; set; }
}
I experimented to post those two models to the same Create method together in the same time. I discovered that EF tracks those two models and saves them into db as new models. It also links those two models by setting MemberID of the new Member model as foreign key to the new MembershipSeason model in db. I guess this is planned behaviour? – I mean the fact EF sets foreign key to the related models automatically seems to be expected behaviour – how things should work. Therefore I guess I don’t need to save Member model first, obtain it’s MemberID and use it for MembershipSeason and save it separately in the Create method? (because EF does the work for you)
db.Members.Add(member);
db.MembershipSeasons.Add(membershipSeason);
await db.SaveChangesAsync();
The above and the below Create method works in the way that no MemberID property is needed to be set directly to MembershipSeason model, because EF does it automatically.
MemberController.cs
public class MemberController : Controller
{
private MembersContext db = new MembersContext();
//some code left out
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "MemberNumber,FirstName,LastName")] Member member,
[Bind(Include = "HasPaidMembership,SeasonId")] MembershipSeason membershipSeason)
{
try
{
if (ModelState.IsValid)
{
db.Members.Add(member);
db.MembershipSeasons.Add(membershipSeason);
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
}
return View(member);
}
}
I am quite new with EF and ASP.NET MVC, so I am trying to figure these things out. Any help appreciated – thanks.
It also links those two models by setting MemberID of the new Member model as foreign key to the new MembershipSeason model in db. I guess this is planned behaviour?
TL;DR: Yes
Yes, it has to be the required behavior. Lets start with reads:
public class Organization
{
public Guid Id { get; set; }
}
public class Employee
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public Organization Organization { get; set; }
}
public Employee GetEmployeeWithOrganization(guid id)
{
var result = _context.Employees
.Include(e => e.Organization)
.FirstOrDefault(e => e.Id = id);
}
Basically when you ask EF to include the navigation property you'd get an object graph kinda like:
Employee
- Id : <guid1>
- OrganizationId : <guid2>
- Organization : object
- Id : <guid2>
It would be common sense to assume that because EF should keep track of entities because what happens if you do this:
var employee = GetEmployeeWithOrganization(<guid1>)
var org = new Organization { id = guid.NewGuid() }; //<guid3>
employee.Organization = org;
_context.SaveChanges();
Which one of these is a valid object:
A:
Employee
- Id : <guid1>
- OrganizationId : <guid2> // <-- difference
- Organization : object
- Id : <guid3>
B:
Employee
- Id : <guid1>
- OrganizationId : <guid3> // <-- difference
- Organization : object
- Id : <guid3>
A isn't valid, because you can't rely on the values and programming against that object would not only be a completely nightmare, but at the database level doesn't make sense. B is valid, it is data you can rely on.
This also means you can precache items and EF will write them up automagically. Consider:
var org = GetOrganization(<guid3>);
var emp = GetEmployee(<guid1>);
Assert.That(emp.Organization, Is.Not.Null); // passes
This happens because EF is tracking org and because EF is configured with org as a FK to employee.
Lets say I added a couple of additional properties to the default User when using asp.net identity:
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
I am aware that in my asp.net MVC controller I can simply do the following to get the current logged in user's name:
User.Identity.Name
So when saving to the database I can simply pass User.Identity.Name to my repository along with the object I am saving so the CreatedBy field can be populated.
Now lets say I am retrieving items from the database which have a field of CreatedBy that contains a string of the username, but I want to display Created by : FirstName + LastName in the View.
How do I obtain this extra information? If I was using pure SQL I would do an INNER JOIN on the AspNetUsers table where CreatedBy=Username and simply retrieve the FirstName and LastName in a custom column called CreatedByFullName.
Since I am using Entity Framework now along with the latest version of ASP.NET Identity I am a bit confused at how we are expected to retrieve user information to display in the View of our pages. Is it a matter of doing a join with linq in my repository or simply adding an object to each of my properties called ApplicationUser or is there better ways?
Assumptions:
You have a single tabled called ApplicationUser that contains all your users.
This table has an Id column(int) that you are reusing to store lookups in other tables.
Other classes (what I call uni-directional navigation properties):
public class BookContext : DbContext
{
public DbSet<Book> Books { get; set; }
public Dbset<ApplicationUser> Users { get; set; }
public overridee OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasRequired(b => b.CreatedByUser)
.WithMany()
.HasForeignKey(b => b.CreatedBy);
}
}
public class Book
{
public int CreatedBy { get; set; }
public virtual ApplicationUser CreatedByUser { get; set; }
}
Then you'd simply
using (var bookContext = new BookContext())
{
var firstBookWithRelatedUser bookContext.Books
.Include(b => b.CreatedByUser)
.First();
}
Something like that. I recommend reading the Entity Framework Documentation. Granted the above code I pretty much just wrote off the top of my head so I may not be exactly right.
If you wanted, what I call, Bi-Directional navigation properties:
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<Book> Books { get; set; }
}
then
public overridee OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasRequired(b => b.CreatedByUser)
.WithMany(u => u.Books)
.HasForeignKey(b => b.CreatedBy);
}
Then you'd simply
using (var bookContext = new BookContext())
{
var firstUserWithAllRelatedBooks = bookContext.Users
.Include(u => u.Books)
.First();
}
It really just depends on your needs. But becareful, you can end up with a Giant God DbContext that is aware of all relationships...
Sample EF query will look like below -
var result = (from tab in db.YourTable
join user in db.AspNetUsers on user.username equals tab.CreatedBy
select new {YourTableObj = tab, CreatedByFullName = user.FirstName + " " + user.LastName).ToList();