I'm new to mongo and I'm looking for some guidance on best practices when dealing with referenced relationships and modeling them in .net core.
Yes, it's the usual "joins in mongodb??" question. But I haven't found a good answer to this.
To make this simple, lets say I've got a simple API i'm building out with controllers to manage users and accounts.
In mongo there are two collections, Accounts and Users. Users belong to their parent Account. I don't want to go the embedded document route in this case, so each User document will have an AccountID in it to link the user back to their parent Account.
My current entities in .net are:
public class User
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
[BsonElement("firstName")]
public string FirstName { get; set; }
[BsonElement("lastName")]
public string LastName { get; set; }
[BsonElement("email")]
public string Email { get; set; }
[BsonElement("status")]
public string Status { get; set; }
[BsonElement("type")]
public string Type { get; set; }
[BsonElement("createdDateTime")]
public DateTime CreatedDateTime { get; set; }
[BsonElement("modifiedDateTime")]
public DateTime ModifiedDateTime { get; set; }
[BsonRepresentation(BsonType.ObjectId)]
[BsonElement("accountId")]
public string AccountId { get; set; }
}
public class Account
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
[BsonElement("name")]
public string Name { get; set; }
[BsonElement("status")]
public string Status { get; set; }
[BsonElement("type")]
public string Type { get; set; }
[BsonElement("createdDateTime")]
public DateTime CreatedDateTime { get; set; }
[BsonElement("modifiedDateTime")]
public DateTime ModifiedDateTime { get; set; }
}
Those are then mapped to models using AutoMapper in the controller
public class UserModel
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Email { get; set; }
[Required]
public string Status { get; set; }
[Required]
public string Type { get; set; }
[Required]
public DateTime CreatedDateTime { get; set; }
[Required]
public DateTime ModifiedDateTime { get; set; }
[Required]
public string AccountId { get; set; }
}
public class AccountModel
{
[Required]
public string Name { get; set; }
[Required]
public string Status { get; set; }
[Required]
public string Type { get; set; }
[Required]
public DateTime CreatedDateTime { get; set; }
[Required]
public DateTime ModifiedDateTime { get; set; }
}
And an example of a controller method where the mapper is used:
[HttpGet]
public async Task<ActionResult<List<AccountModel>>> Get()
{
try
{
var results = await _repository.Get();
return _mapper.Map < List < AccountModel >>(results);
}
catch (Exception ex)
{
return this.StatusCode(StatusCodes.Status500InternalServerError, "Database Failure");
}
}
All that works just fine. I can call the controller methods, get the data, and it gets mapped from entity to model and then returned from the controller method.
The issue is this: I'd like to return the user data with information from the account (example: account name). So just a simple join.
I think I have an handle on how to do the join itself, using one of the methods outlined in this answer. But my question is, is there a best practice on how to set up my entities and models to make storing this as clean as possible?
I was thinking of adding a property to the User entity to store the related account. Tagged with the [BsonIgnore] attribute so that it stays out of the db.
[BsonIgnore]
public Account Account { get; set; }
The property
[BsonRepresentation(BsonType.ObjectId)]
[BsonElement("accountId")]
public string AccountId { get; set; }
would still remain in the user entity, so the reference is preserved.
Then, the User model could have properties like
public string AccountName { get; set; }
They get populated using the mapper.
Is this the best way to set this up when you want to reference related objects rather then embed them? Is there some gotcha here I'm missing?
have a look at the code below. it uses my library MongoDB.Entities which has built-in support for one-to-one, one-to-many and many-to-many relationships between entities.
using MongoDB.Entities;
using System.Linq;
namespace StackOverflow
{
public class Program
{
public class Account : Entity
{
public string Name { get; set; }
public Many<User> Users { get; set; }
public Account() => this.InitOneToMany(() => Users);
}
public class User : Entity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public One<Account> Account { get; set; }
[Ignore]
public string AccountName { get; set; }
}
private static void Main(string[] args)
{
new DB("test");
var account = new Account { Name = "parent account" };
account.Save();
var user = new User
{
FirstName = "dave",
LastName = "mathews",
Account = account.ToReference()
};
user.Save();
account.Users.Add(user);
//find parent by ID
var parent = DB.Find<Account>().One(account.ID);
//get first user of parent
var dave = parent.Users.ChildrenQueryable()
.FirstOrDefault();
//get dave's account
var davesAccount = dave.Account.ToEntity();
//get dave with account name filled in by a single mongo query
var daveExtra = (from u in DB.Queryable<User>().Where(u => u.ID == dave.ID)
join a in DB.Queryable<Account>() on u.Account.ID equals a.ID
select new User
{
ID = u.ID,
FirstName = u.FirstName,
LastName = u.LastName,
AccountName = a.Name
}).SingleOrDefault();
}
}
}
Related
I am working on Asp.net core 5 Web-api, I am using many to many relationship between User entity and Permission entity, and I need to load users permission from the database using eager loading but the data is returning a looped information including users full information but I only load the permissions. I am using a Dto to return the Json data. Here is my models and my code
Permission Model
public class Permission
{
public int Id { get; set; }
public string ClaimName { get; set; }
public IList<UserPermission> UserPermissions { get; set; }
}
UserPermission model
public class UserPermission
{
public int UserId { get; set; }
public AppUser AppUser { get; set; }
public int PermissionId { get; set; }
public Permission Permission { get; set; }
}
User model
public class AppUser
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<Photo> Photos { get; set; }
public DateTime Created { get; set; } = DateTime.Now;
public DateTime LastActive { get; set; }
public IList<UserPermission> UserPermissions { get; set; }
public bool Status { get; set; }
}
My user Dto
public class UserDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<Photo> Photos { get; set; }
public ICollection<UserPermission> UserPermissions { get; set; }
public string Token { get; set; }
}
My function
var sss = _context.UserPermissions
.Include(l => l.Permission)
.Where(v => v.UserId == user.Id)
.ToList();
Use [JsonIgnore] on the properties that we want to exclude.
note that we should use "using Newtonsoft.Json" name space not using "System.Text.Json.Serialization"
My project is throwing error
Invalid column name 'InitialsID_InitialsID'.
public User ValidateUser(string Username, string Password)
{
return uow.UserRepository.GetQueryable().Where(x => x.UserName == Username && x.Password == Password).FirstOrDefault();
}
Even there's no column by that name in the whole solution.
I had generated a migration script and it had that name in it but I changed that to InitialsID but still it asks for the same name.
How do I resolve it? I tried putting ForeignKey attribute etc but nothing works.
User.cs
public class User
{
public int UserID { get; set; }
public int UserGroupID { get; set; }
public UserGroup UserGroup { get; set; }
public string UserName { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string Designation { get; set; }
public string Password { get; set; }
public bool? PasswordExpire { get; set; }
public DateTime? ExpiryDate { get; set; }
public bool Status { get; set; }
public DateTime? CreatedOn { get; set; }
public string CreatedBy { get; set; }
public DateTime? ModifiedOn { get; set; }
public string ModifiedBy { get; set; }
public string Office { get; set; }
public string Station { get; set; }
public Initials InitialsID { get; set; }
}
Initials.cs
public class Initials
{
public short InitialsID { get; set; }
public string Code { get; set; }
}
I am using Code First approach.
Problem is in your foreign key configuration. When you are referencing navigation property public Initials InitialsID { get; set; }, EF is adding an implicit foreign key for this navigation property and it is by convention navigationPropery_navigationPropertyPrimaryKey and hence it is InitialsID_InitialsID
If you really want public Initials Initials { get; set; } navigaation property in User model class then write your foreign key configuration as follows:
public class User
{
public int UserID { get; set; }
public int UserGroupID { get; set; }
......................
public short InitialsId { get; set; }
public Initials Initials { get; set; }
}
It seems your database not migrated with new model or new models property,
Pleas migrate your database.
If you are using dotnet core, in cmd on the project folder type:
dotnet ef migrations add [name]
and then:
dotnet ef databae update
I am looking for a good way of getting a list of recent conversations.
I have made an inbox ui, where I am looking to implement a proper way of getting a list of conversations.
I have the following tables set up, with a relation to the Message table
public class Message
{
public int Id { get; set; }
public string Content { get; set; }
public string UserId { get; set; }
public ApplicationUser User { get; set; }
public DateTime CreateDate { get; set; }
public List<MessageReceived> MessagesReceived { get; set; }
public List<MessageSent> MessagesSent { get; set; }
}
public class MessageReceived
{
public int Id { get; set; }
public int MessageId { get; set; }
public virtual Message Message { get; set; }
public string FromUserId { get; set; }
public ApplicationUser FromUser { get; set; }
public string ToUserId { get; set; }
public ApplicationUser ToUser { get; set; }
public DateTime DateRecieved { get; set; }
public DateTime DateRead { get; set; }
public bool Deleted { get; set; }
}
And for now i have this in my controller
public ActionResult Index()
{
var userId = User.Identity.GetUserId();
var messages = db.MessageReceived.Where(p => p.ToUserId == userId).GroupBy(p => p.FromUser.UserName);
return View(messages);
}
This will return the list of Usernames that the user has received a message from, including all the rows that are created when receiving a message. Which is not what I am aiming for. It could end bad when querying for multiple long conversations
Is there a more efficient way to query the list of conversations, than using group by?
If all you need are the list of names that a user has received messages from, a more efficient way to query this is:
var names = db.MessageRecieved.Where(p => p.ToUserId == userId).Select(p => p.FromUser.UserName).Distinct().ToList();
I am trying to create my first app using ASP.NET MVC framework and Entity Framework 6.
I chose to use code first approach and I started by defining my Models.
I have a model called Client with an identity attribute called Id. I have multiple Models that has an attribute called ClientId. The ClientId attribute should have virtual link to the Clients Model.
Here is how my Client model looks like
[Table("clients")]
public class Client
{
[Key]
public int id { get; set; }
public string name { get; set; }
public string status { get; set; }
public DateTime created_at { get; set; }
public DateTime? modified_at { get; set; }
public Client()
{
status = "Active";
created_at = DateTime.UtcNow;
}
}
Then here is how I am creating a belong to relation using other models.
[Table("BaseClientsToUsers")]
public class ClientToUser : ModelDefault
{
[ForeignKey("User")]
public int UserID { get; set; }
[ForeignKey("Client")]
public int ClientId { get; set; }
[ForeignKey("Team")]
public int DefaultTeamId { get; set; }
public DateTime? JoinedAt { get; set; }
public bool IsActive { get; set; }
public virtual User User { get; set; }
public virtual Client Client { get; set; }
public virtual Team Team { get; set; }
public ClientToUser()
{
DateTime UtcNow = DateTime.UtcNow;
IsActive = true;
CreatedAt = UtcNow;
LastUpdatedAt = UtcNow;
}
[Table("BaseTeams")]
public class Team : ModelDefault
{
[MaxLength(250)]
public string Name { get; set; }
[ForeignKey("Client")]
public int ClientId { get; set; }
public bool IsActive { get; set; }
public virtual Client Client { get; set; }
public Team()
{
DateTime UtcNow = DateTime.UtcNow;
IsActive = true;
CreatedAt = UtcNow;
LastUpdatedAt = UtcNow;
}
}
But, when I try to update my databases I get the following error
Introducing FOREIGN KEY constraint
'FK_dbo.BaseTeams_dbo.BaseClients_ClientId' on table 'BaseTeams' may
cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or
ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints. Could
not create constraint or index. See previous errors.
I am not really sure what could be causing the error but it seems it is because I am creating multiple Foreign keys to the same `Clients model.
How can I fix this error?
Hello #Mike A When I started MVC I got this error too, so you need aditional tables that connects your DB items.
So try connect your database items with tables like that:
Here is my working example:
[Table("Products")]
public class Product
{
[Key]
public string Id { get; set; }
[Required]
public string Name { get; set; }
public string Description { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal InternalPrice { get; set; }
public string Url { get; set; }
}
[Table("Categories")]
public class Category
{
[Key]
public string Id { get; set; }
[Required]
public string Name { get; set; }
public string Url { get; set; }
}
[Table("ProductCategories")]
public class ProductCategory
{
[Key]
[Column(Order = 0)]
public string ProductId { get; set; }
[Key]
[Column(Order = 1)]
public string CategoryId { get; set; }
public virtual Category Category { get; set; }
}
So you can connect your items without problems hope this will help you.
I just started playing around with the CTP4 and Code-First. I have the following setup for a possible dating site:
public class User
{
[Key]
public int Id { get; set; }
[Required]
public string LoginName { get; set; }
[Required]
public string Firstname { get; set; }
[Required]
public string Lastname { get; set; }
public string Street { get; set; }
[Required]
public string Zip { get; set; }
[Required]
public string City { get; set; }
[Required]
public bool Gender { get; set; }
[Required]
public int SoughtGender { get; set; }
[Required]
public string Password { get; set; }
[Required]
public double Latitude { get; set; }
[Required]
public double Longitude { get; set; }
}
public class Vote
{
[Key]
public int ID { get; set; }
[Required]
public User Voter { get; set; }
[Required]
public User TargetUser { get; set; }
[Required]
public int Decision { get; set; }
[Required]
public DateTime Timestamp { get; set; }
}
public class MySQLContext : DbContext
{
public MySQLContext (string constring)
: base(constring)
{ }
public DbSet<User> Users { get; set; }
public DbSet<Vote> Votes { get; set; }
protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder modelBuilder)
{
modelBuilder.Entity<Vote>().HasRequired(b => b.Voter).WithMany();
modelBuilder.Entity<Vote>().HasRequired(b => b.TargetUser).WithMany();
base.OnModelCreating(modelBuilder);
}
}
Now the framework does a nice job of creating the DB with all the proper keys. Now I inserted some dummy data and fired up the following query:
public override IEnumerable<Domain.Vote> FindVotes(Domain.User user)
{
var query = from v in context.Votes where v.Voter.Id == user.Id select v;
return from v in query.AsEnumerable() select v;
}
The query does return the proper Vote entities but the two User properties of the Vote object are Null. Shouldnt the framework populate those properties with the foreign keys of the users referenced in the Vote table?
Let me give you some background on EF so you can understand how this works. EF from day 1 only supported explicit load like one below
Customer.Orders.Load();
Hmm, the feedback was not welcomed by the community and developers wanted lazy loading. To support Lazy Loading EF team said you must mark your navigation property as virtual. So at runtime, Ef creates a proxy object that derives from your entity and overrides the virtual property. Below is an example of such code.
public class Customer
{
public string Name{get;set;}
public virtual ICollection<Order> Orders{get;set;}
}
At runtime there is a proxy that implements IEntityWithChangeTracker and concrete type of the collection is an entitycollection which has been around since version 1.
public class CustomerProxy:Customer,IEntityWithChangeTracker
{
private ICollection<Order> orders;
public override ICollection<Order> Orders
{
if(orders == null)
{
orders = new EntityCollection<Order>();
orders.Load();
}
return orders;
}
}
change your class to the follow
public class Vote {
[Key]
public int ID { get; set; }
[Required]
public virtual User Voter { get; set; }
[Required]
public virtual User TargetUser { get; set; }
[Required]
public int Decision { get; set; }
[Required]
public DateTime Timestamp { get; set; }
}
Notice I've added virtual to the Voter && TargetUser properties and you should be good to go.