First, a little bit of context
I have one desktop application which uses a Paradox database and I am developing a new ASP.NET application using SQL Server to replace the current desktop app.
The transition will happen in tho phases. The first version of the web app will use SQL Server and Paradox at the same time. Legacy entities data will come from Paradox and data from new entities will come from SQL Server. So the desktop and web versions of the app can be used interchangeably.
In the second phase all data from Paradox will be imported to SQL Server. The old desktop app (and the Paradox db) won't be used anymore.
Now, the technical question
Imagine a simple model with two entities:
public class Customer
{
public Int32 ID { get; set; }
public String Name { get; set; }
virtual public IList<User> MyUsers { get; set; }
}
public class User
{
public Int32 ID { get; set; }
public String Name { get; set; }
virtual public Customer MyCustomer { get; set; }
}
public class SqlServerContext : DbContext
{
public SqlServerContext()
{
base.Configuration.ValidateOnSaveEnabled = false;
base.Configuration.LazyLoadingEnabled = true;
base.Configuration.ProxyCreationEnabled = true;
base.Configuration.AutoDetectChangesEnabled = true;
}
public DbSet<User> Users { get; set; }
public DbSet<Customer> Customers { get; set; }
}
When I use only SQL Server for both entities everything works fine. But now I want to retrieve customer data from Paradox and user data from SQL Server.
I then create a repository for the customers, with an underlying OleDbConnection using the Paradox provider. Of course, Entity Framework still creates the Customers table in SQL Server, since User has a Customer property.
I then removed the database relationship (FK) between User and Customer. Now I am able to insert a User related to a Customer, even though they're in different databases.
The problem is: when I try to retrieve a user from db [eg: context.Users.Find(id)] I can read the user's ID and Name, but user.Customer is null. I expected to get the customer ID through the user.Customer.ID property, since this data is in the Users table in SQL Server.
Am I going to the right path? If so, how can EF bring the customer ID for the user?
And if not, what would be a good approach for this scenario? I don't want to expose foreign key properties in the model (i.e: i don't want to have a int CustomerId property in the User entity).
Thanks in advance!
When you access user.MyCustomer, EF will issue a select statement to the Customers table and then materialize a Customer object. But in your case the table is empty or non existent.
What you can do is include a scalar property CustomerID on User class
public class User
{
public Int32 ID { get; set; }
public String Name { get; set; }
public int? CustomerID { get; set; }//assuming the db column name is also CustomerID
virtual public Customer MyCustomer { get; set; }
}
Then map the relationship as follows
public class SqlServerContext : DbContext
{
public SqlServerContext()
{
base.Configuration.ValidateOnSaveEnabled = false;
base.Configuration.LazyLoadingEnabled = true;
base.Configuration.ProxyCreationEnabled = true;
base.Configuration.AutoDetectChangesEnabled = true;
}
public DbSet<User> Users { get; set; }
public DbSet<Customer> Customers { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOptional(u => u.User)
.WithMany(c => c.MyUsers)
.HasForeignKey(u => u.CustomerID);
}
}
Then you can access the customer ID property using user.CustomerID instead of user.Customer.ID.
Related
I have an "account" table that includes a string foreign-key ("parent_guid") to its "parent" account (if one exists). I would like to create an entity that knows its parent, as well as all of its children.
Here is my entity:
[Table(name:"accounts")]
public class Account
{
[Key]
public string Guid { get; set; }
public string Name { get; set; }
[Column("guid")]
public string accountGuid { get; set; }
[Column(name: "parent_guid")]
public string parentGuid { get; set; }
[ForeignKey("parentGuid")]
public Account Parent { get; set; }
[InverseProperty("Parent")]
public ICollection<Account> Children { get; set; }
}
Here's my dbContext:
public DbSet<Split> Splits { get; set; }
public DbSet<Account> Accounts { get; set; }
public ReportContext(DbContextOptions<ReportContext> options)
: base(options)
{ }
My query is through the 'splits' context as the source table, but I end up returning Accounts. Maybe there's a better way?
When I query for an Account by Guid, I get a result, but 'Parent' and 'Children' are always null, even though 'parentGuid' contains the correct value, and I have confirmed that there should be child records.
Any idea how to make this work, either through annotations or fluent API?
Yes, EF Core requires explicit inclusion of relational entities.
var accounts = await dbContext.Accounts.Include(account => account.Parent)
.Include(account => account.Children)
.ToListAsync();
##EDIT
As per the edits to the question, this is one way to Eager Load relational entities, but I cannot speak to the efficiency of this query without knowing the relations and indexes.
public IQueryable<Split>
FindAllByAccountGuidsPostedBefore(IEnumerable<string> accounts,
DateTime endDate) {
using (reportContext) {
return reportContext.Splits.Include(s => s.Account)
.ThenInclude(a => a.Parent)
.ThenInclude(a => a.Children)
.Where(s => accounts.Contains(s.Account.Guid)
&& s.Transaction.postDate < endDate);
}
}
One way to obtain that information is by looking at the console when this query is run to find the SQL statement(s) this produces, or by asking someone who is more experienced in Relational Databases :)
I have a simple model for the purposes of this post.
Two entities Role and Person.
public class Role : Entity
{
public Guid Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Person> PeopleWithThisRole { get; set; }
}
public class Person : Entity
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Guid? RoleId { get; set; }
}
If I get the Roles from the EF context, then the PeopleWithThisRole collection is empty (unless I .Include them. As expected.
However if I get the Roles and I then get the People as below
var roles = _context.Roles.ToList();
var people = _context.People.ToList();
Then roles.PeopleWithThisRole collection is fully populated with the people without having to .Include it.
Is this the expected behaviour or should I raise this as a bug?
Thanks
UPDATE
With many thanks to #hvd below, I have decided to keep the entities clean and not use [JsonIgnore] attribute and instead map to DTOS (which exclude those properties I don't need in the JSON) - which is probably the correct way!
It's expected and also how earlier versions of EF worked.
Your _context keeps track of entities loaded inside that context, to allow for saving changes. Inside that context, Person objects have been loaded (at your request) and their RoleId values are known. Inside that same context, Role objects with those same Id values have been loaded (also at your request). EF links the objects in memory based on those IDs. If you trace the SQL queries sent to the server, you should find that no queries have been sent other than those that you requested.
I'm attempting to build a 1-1 relationship - a Tenant has a Url, and vice versa:
Models
public class Tenant {
[Key]
[Required]
public int TenantId { get; set; }
public string Name { get; set; }
public virtual Url Url { get; set; }
public int UrlId { get; set; }
}
public class Url {
[Key]
[Required]
public int UrlId { get; set; }
public string Name { get; set; }
public virtual Tenant Tenant { get; set; }
public int TenantId { get; set; }
}
Configs
public class UrlConfiguration : EntityTypeConfiguration<Url> {
public UrlConfiguration() {
HasKey(s => s.UrlId);
}
}
public class TenantConfiguration : EntityTypeConfiguration<Tenant>
{
public TenantConfiguration() {
HasRequired(s => s.Url).WithRequiredPrincipal(s => s.Tenant);
}
}
Result:
I'd expect there to be a foreign key on both models... why is this not the case?
A one-to-one relationship with both ends having required foreign keys cannot exist in a relational database. If saving a new Tenant record requires a URL record, but in order to create that URL record, that URL requires a Tenant record, where will you begin?
Even though on a database level it can't practically exist, this model will still work. From my experience, Entity Framework will enforce the dependency on application level, and will throw an EntityException when it detects that one of the entities you're trying to save has no relationship to one of the other.
It creates this database model so that it can still save your entities, and enforce relationships on an application level.
No, this isn't nice on a database level as the one-to-one constraint won't be enforced there. If you need the database constraints as well, consider merging the tables or redesigning your data structures so that a one-to-one relationship isn't necessary.
I am working with EF 4.3 code first and using WCF Data services (5.0 just released) to expose the data over HTTP
I find that if I call a service operation from a browser I am able to get related entities back, but when I consume the service operation in a client app I am not getting back the related entities.
I have been researching this isuse it seems the EF enables lazy loading whn using the virtual key word when referencing a an ICollection, this some how prevents WCF data services from returing realted entities - is this true
If i browse locally and put a break point on my getUsersByName method I can see the related group entity but when it comes over the wire to a client app the gropup entity is missing.
Is there a configuration to enable this.
Thanks
eg
public partial class Group
{
public Group()
{
this.Users = new HashSet<User>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int GroupID { get; set; }
[Required(ErrorMessage = "Please enter a group name")]
[StringLength(50, ErrorMessage = "The group name is too long")]
public string GroupName { get; set; }
[Required]
public System.DateTime CreatedDate { get; set; }
[Required]
public bool Admin { get; set; }
public virtual ICollection<User> Users { get; set; }
}
public partial class User
{
public User()
{
this.Groups = new HashSet<Group>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[Required(ErrorMessage="Please enter a username")]
[StringLength(50,ErrorMessage="Username is too long")]
public string UserName { get; set; }
[Required(ErrorMessage="Please enter an email address")]
[RegularExpression(".+\\#.+\\..+",ErrorMessage="Please enter a valid email address")]
public string Email { get; set; }
[Required]
public System.DateTime CreatedDate { get; set; }
public virtual ICollection<Group> Groups { get; set; }
}
public partial class TestContext : DbContext
{
public Test()
: base("name=TestEntities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Tell Code First to ignore PluralizingTableName convention
// If you keep this convention then the generated tables will have pluralized names.
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
public DbSet<Group> Groups { get; set; }
public DbSet<User> Users { get; set; }
}
[ServiceBehavior(IncludeExceptionDetailInFaults=true)]
[JSONPSupportBehavior]
public class TestSVC : DataService<TestContext>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.SetServiceActionAccessRule("*", ServiceActionRights.Invoke);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
config.UseVerboseErrors = true;
}
[WebGet]
public User GetUserByName(string userName)
{
var user = (from u in this.CurrentDataSource.Users
where u.UserName == userName
select u).FirstOrDefault();
return user;
}
Actually regardless of what you do on the server side with the entity WCF DS will not return expanded entities by default. It's a feature. The reason is size of the message on the wire.
The client can request this behavior though (and that should work without making any modifications to the EF entities on the server side).
Let's assume you have a service operation GetProducts. You can issue a query like ~/GetProducts?$expand=Category which will include the Category entities in the result.
You also noted that the client doesn't get to see these, but you do see them in the browser. So are you already using the $expand? If that's the case the problem is solely on the client. Make sure that you request the results using the $expand on the client as well (depends on what code you're using, there's an Expand method in the LINQ on the client for this). And then you can use Fiddler to see if the client actually gets the results back the way you want. If that's the case and you still don't get the results in your client code, it might be due to MergeOptions. Try setting DataServiceContext.MergeOption to OverwriteChanges and try again (but be sure that you're aware of what this setting does).
Try removing the initialization of the navigation properties in the constructor because it will create problems with the proxy objects.
It's almost impossible to use lazy loading with serialization because when the serializer access the navigation properties the related entities will be loaded. This will lead to load whole database. So you need to disable lazy loading and use Include to load whatever you want or you can use some DTO with lazy loading enabled .
I have the situation where a User can have several addresses. Accordingly, I have an ICollection on my user class. But I also want the user to be able to choose a default address. So I've done the following:
public class User
{
public int Id { get; set; }
public int? DefaultAddressId { get; set; }
[ForeignKey("DefaultAddressId")]
public virtual Address DefaultAddress { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
//properties were removed for purpose of this post
}
I would like to remove the public virtual Address DefaultAddress { get; set; } altogether, keep the DefaultAddressId and map it using the Fluent API instead because the current setup is causing a lot of trouble (in this and other classes where I have a similar setup). So can this be done using the fluent api?
UPDATE:
The address class currently doesn't have any reference to the User class, it's a uni-directional relationship. But yes, an address belongs to only ONE user, it's not a many to many relationship. Here's the address class:
public class Address
{
public int Id { get; set; }
public string Name { get; set; }
public string Details { get; set; }
public virtual Area Area { get; set; }
}
I would personally move the Foreign Key relation from User to Address, and add an IsDefaultAddress property on the address class.
public class Address
{
public int Id { get; set; }
// This property marks the FK relation
public virtual User User { get; set; }
public string Name { get; set; }
public string Details { get; set; }
public virtual Area Area { get; set; }
// This property signals whether this is the user's default address
public bool IsDefaultAddress { get; set; }
}
EF will know that it needs a Foreign Key relation between Address and User.
This would simplify your model a great deal. That is, of course, if an address can only belong to one user (as asked by Slauma in the comments).
Your original model in the question should work. You can test it quite easily:
Create new console application (VS 2010)
Name it "EFTestApp"
Add reference to "EntityFramework.dll"
Delete content of Program.cs and copy the following code into the file
Program.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
namespace EFTestApp
{
public class User
{
public int Id { get; set; }
public int? DefaultAddressId { get; set; }
[ForeignKey("DefaultAddressId")]
public virtual Address DefaultAddress { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
}
public class Address
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Context : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Address> Addresses { get; set; }
}
class Program
{
static void Main(string[] args)
{
using (var context = new Context())
{
try
{
User user = new User() { Addresses = new List<Address>() };
Address address1 = new Address() { Name = "Address1" };
Address address2 = new Address() { Name = "Address2" };
user.Addresses.Add(address1);
user.Addresses.Add(address2);
context.Users.Add(user);
context.SaveChanges();
// user has now 2 addresses in the DB and no DefaultAddress
user.DefaultAddress = address1;
context.SaveChanges();
// user has now address1 as DefaultAddress
user.DefaultAddress = address2;
context.SaveChanges();
// user has now address2 as DefaultAddress
user.DefaultAddress = null;
context.SaveChanges();
// user has now no DefaultAddress again
}
catch (Exception e)
{
throw;
}
}
}
}
}
In SQL Server Express it creates a new DB called "EFTestApp.Context". You can set breakpoints on every SaveChanges above, step over and watch the changes in the DB.
If you look at the relationships in the database then there are two, and in table Addresses in the DB is a foreign key column User_Id.
I think you could also remove public int? DefaultAddressId { get; set; } and [ForeignKey("DefaultAddressId")]. It creates the same database tables and relationships with an optional DefaultAddress.
Perhaps you want the relationship Address -> User as required (Addresses cannot live alone in the DB without a User). Then you can add this to the Context class:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasMany(u => u.Addresses)
.WithRequired();
}
It makes User_Id in the Addresses table non nullable and sets up cascading delete by default. So, when a user gets deleted all its addresses get deleted as well.
DefaultAddressId doesn't need any specific mapping because it will be just column in User table without any relation (FK) to Address table. There will be no relation created because navigation property doesn't exist on either side. Also it should be one-to-one relation which will not work because EF doesn't support unique keys.
I like solution provided by #Sergi Papaseit
You don't need to map it if you are removing the DefaultAddress property. You can just have the property there and EF should know how to map it provided DefaultAddressId is in the User table