Navigation Properties are not working properly - c#

I'm facing query a weird issue using the latest version of Entity Framework, regarding navigation properties.
I do have an entity of which I have a few required navigation properties which are marked as virtual.
See my entities class below:
public class Folder : UserReferencedEntityBase<int>
{
#region Constructors
public Folder()
{ }
public Folder(IUnitOfWork unitOfWork)
: base(unitOfWork)
{
ParentFolder = unitOfWork.Context.GetCurrentFolder as Folder;
}
#endregion
#region Properties
[Required]
public string Name { get; set; }
[Required]
public string Data { get; set; }
[Column(Order = 998)]
public Folder ParentFolder { get; set; }
[Required]
public bool IsPublished { get; set; }
#endregion
}
This one is inheriting from UserReferencedEntityBase{T} which looks like:
public class UserReferencedEntityBase<TKey> : EntityBase<TKey>
{
#region Constructors
public UserReferencedEntityBase() { }
public UserReferencedEntityBase(IUnitOfWork unitOfWork)
{
unitOfWork.ThrowIfNull("unitOfWork");
CreatedBy = unitOfWork.Context.GetCurrentUser;
}
#endregion
#region Properties
[Required]
[Column(Order = 996)]
public virtual IdentityUser CreatedBy { get; set; }
[Column(Order = 997)]
public virtual IdentityUser UpdatedBy { get; set; }
#endregion
}
Now, I do have my MVC website where I'm loading an entity, updating a property and saving it in the database again:
var model = new FolderManager(UnitOfWork).GetFolder(id);
model.IsPublished = true;
UnitOfWork.Commit();
I use a custom Unit Of Work here, but no rocket sience at all. Everything is happening with the same context, within the same request, no async calls, ...
When I do execute the code, I receive:
Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.
Looking at this, reveals the following error:
"The CreatedBy field is required."
Now, the weird this is, when I'm debugging my code, the 3 lines given above, the created_by property is filled in and the code does execute without any problem.
I'm using ASP.NET identity Framework, thus using an IdentityDbContext in case that matters.
Anybody has a clue?
Kind regards
UPDATE - Folder Manager
The manager is just a wrapper to get my contents out of my unit of work:
public Folder GetFolder(int id)
{
return UnitOfWork.FolderRepository.GetByFilter(x => x.Id == id);
}
The GetByFilter method is constructed like:
public virtual TEntity GetByFilter(Func<TEntity, bool> filter)
{
DbSet.ThrowIfNull("DbSet");
if (OnBeforeEntityGet != null)
{ OnBeforeEntityGet(this, new RepositoryEventArgs(typeof(TEntity))); }
if (OnEntityGet != null)
{ OnEntityGet(this, new RepositoryEventArgs(typeof(TEntity))); }
return !Entities.Any() ? null : !Entities.Where(filter).Any() ? null : Entities.First(filter);
}

Just want to let you know that I've found a solution.
It seems that when you're loading an entity that contains virtual properties but never inspecting them, they stay null and therefore the code is not working.
With the debugger is attached, it's working flaslessy after inspecting this element.
Is this normal behaviour?
Kind regards,

Related

Primary Key not defined, even though it already is

I been developing a C# ASP.NET Core Application and I am currently doing databases using SQL Server Object Explorer. I want to display certain hardcoded data as a testing, however when I try to run the page, I got this error
System.InvalidOperationException: 'The entity type 'recent_health_issue' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'
My primary key is already defined and the display page works normally without that line of error code, so I am not sure here.
My database:
Model class:
public class recent_health_issue
{
[Required, MinLength(3, ErrorMessage = "Enter at least 3 characters"),
MaxLength(5)]
public string recent_id { get; set; }
[Required, MaxLength(25)]
public string recent_name { get; set; }
[Required, EmailAddress]
public string recent_email { get; set; }
[Required]
public string recent_description { get; set; }
}
My DbContext:
public class RecentHealthIssueDBContext : DbContext
{
private readonly IConfiguration _config;
public RecentHealthIssueDBContext(IConfiguration configuration)
{
_config = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connectionString = _config.GetConnectionString("MyConn");
optionsBuilder.UseSqlServer(connectionString);
}
public DbSet<recent_health_issue> HealthIssues { get; set; }
}
My controller (where the error happens):
public class RecentHealthController
{
private RecentHealthIssueDBContext _context;
public RecentHealthController(RecentHealthIssueDBContext context)
{
_context = context;
}
public List<recent_health_issue> getAllRecentHealthIssues()
{
List<recent_health_issue> AllRecentHealth = new List<recent_health_issue>();
// error occured here
AllRecentHealth = _context.HealthIssues.ToList();
return AllRecentHealth;
}
public recent_health_issue getRecentHealthIssueById(String id)
{
recent_health_issue RecentHealthIssue = new recent_health_issue();
return RecentHealthIssue;
}
}
The error message says you need to define a primary key for the table:
[Key]
[Required, MinLength(3, ErrorMessage = "Enter at least 3 characters"),
MaxLength(5)]
public string recent_id { get; set; }
Also try to rename the DbSet property in your DbContext to match the DB table name, which is RecentHealthIssues instead of HealthIssues:
public DbSet<recent_health_issue> RecentHealthIssues { get; set; }
To clarify, from your question, you're doing database first development.
This means that your model and your database need to match exactly, so if there is any discrepancy then in most cases won't work.
Entity Framework expects Pascal Case for properties, and if it finds a single occurrence of property either named or containing "Id" then it will assume that is the key.
If you don't follow pascal case (which is the C# standard), or don't use a property containing the letters "Id", then you must use the [Key] data annotation.
As you're doing database first, you've chosen snake case for your SQL table, which means that you need to align the names with the C# without breaking coding conventions and allowing EF to do it's thing.
Also, looking at your code and what you're doing, as you're using snake_case to define the names in the database, then you should be using data annotations for tables/column names, not changing your property names to align, as defined here: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/data-annotations#table-and-column
For your information, EF expects Pascal casing in C#, so it can do it's automation (which is why you should be using it)
public long Id {get;set;} //ef will recognise this as a Key
public long HelloCheekyThisIsMyId {get;set;} //ef will recognise this as a Key
[Key] //ef will be told this is a key
public long product_identity { get; set; }
So your model should look like this:
[Table("recent_health_issue")]
public class RecentHealthIssue
{
[Key] //this shouldn't be needed but it's good for readability
[Required, MinLength(3, ErrorMessage = "Enter at least 3 characters"), MaxLength(5)]
[Column("recent_id")]
public string RecentId { get; set; }
[Required, MaxLength(25)]
[Column("recent_name")]
public string RecentName { get; set; }
[Required, EmailAddress]
[Column("recent_email")]
public string RecentEmail { get; set; }
[Required]
[Column("recent_description")]
public string RecentDescription { get; set; }
}
as defined in the documentation: "Don’t confuse Column’s TypeName attribute with the DataType DataAnnotation. DataType is an annotation used for the UI and is ignored by Code First."
Your context like this:
public class RecentHealthIssueDBContext : DbContext
{
private readonly IConfiguration _config;
public RecentHealthIssueDBContext(IConfiguration configuration)
{
_config = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connectionString = _config.GetConnectionString("MyConn");
optionsBuilder.UseSqlServer(connectionString);
}
public DbSet<RecentHealthIssue> HealthIssues { get; set; }
}
and your controller should look like this:
public class RecentHealthController
{
private RecentHealthIssueDBContext _context;
public RecentHealthController(RecentHealthIssueDBContext context)
{
_context = context;
}
//This should be PascalCase (GetAllRecentHealthIssues)
public List<recent_health_issue> getAllRecentHealthIssues()
{
List<RecentHealthIssue> allRecentHealth = new List<RecentHealthIssue>();
allRecentHealth = _context.HealthIssues.ToList(); //error occured here
return allRecentHealth;
}
//This should be PascalCase (GetRecentHealthIssuesById)
public recent_health_issue getRecentHealthIssueById(String id)
{
//return recent health issues using an Id
}
}
Also I would reccomend some defensive programming, it will save you headaches in the future. (Null checks, try/catch blocks etc.)
This is the C# coding convention:
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
You can find out more about the available annotations here:
https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/data-annotations#key

Is it possible to have extra (ignored) properties in C#?

I have a repository for a DocumentDb database. My documents all have a set of common properties so all documents implement the IDocumentEntity interface.
public interface IDocumentEntity {
[JsonProperty("id")]
Guid Id { get; set; }
[JsonProperty("documentClassification")]
DocumentClassification DocumentClassification { get; set; }
}
public class KnownDocument : IDocumentEntity {
[JsonProperty("id")]
Guid Id { get; set; }
[JsonProperty("documentClassification")]
DocumentClassification DocumentClassification { get; set; }
[JsonProperty("knownProperty")]
string KnownProperty { get; set; }
}
public class BaseDocumentRepository<T> where T : IDocumentEntity {
public Set(T entity) {
// ... stuff
}
}
This works fine with a KnownDocument where I know all of the properties. But, of course, what's great about a Document Db is that I don't need to know all of the properties (and in many cases I won't).
So my client submits something like this-
{unknownProperty1: 1, unknownProperty2: 2}
And I want to upsert this using my document repository.
public OtherDocumentService() {
_otherDocumentService = new OtherDocumentRepository();
}
public UpsertDocument(dynamic entity) {
entity.id = new Guid();
entity.documentClassification = DocumentClassification.Other;
_otherDocumentRepository.Set(entity);
}
But I get an InvalidCastException from dynamic to IDocumentEntity. I assume it's because of the extra properties that exist on the dynamic object but not on the IDocumentEntity interface?
What I'm trying to do is leave my document entities open to be dynamic, but rely on a few properties being there to maintain them.
Entity parameter passed to the UpsertDocument should explicitly implement IDocumentEntity in order do make the code works, it is not enough just have a Id property.
Some options:
1) Proxy may be applied:
public class ProxyDocumentEntity : IDocumentEntity
{
public dynamic Content { get; private set; }
public ProxyDocumentEntity(dynamic #content)
{
Content = #content;
}
public Guid Id
{
get { return Content.Id; }
set { Content.Id = value; }
}
}
... using
public void UpsertDocument(dynamic entity)
{
entity.Id = new Guid();
repo.Set(new ProxyDocumentEntity(entity));
}
The stored document will have nested Object property, which may be not acceptable
2)There is a lib https://github.com/ekonbenefits/impromptu-interface which creates a proxy dynamically
and does not make extra property like solution above.
Drawback will be in performance.
Technically it could be 2 methods:
public void UpsertDocument(IDocumentEntity entity){...}
public void UpsertDocument(dynamic entity){...}
so the first (fast) will work for the objects which implement IDocumentEntity and second(slow) for the rest of the objects.
But this is a speculation a bit , as I dunno the details of the whole code base of the project you have.
If you have some flexibility as to how to name those dynamic properties, you could stuff them into a Dictionary property on your object:
public Dictionary<string, dynamic> extra { get; set; }

The relationship could not be changed because one or more of the foreign-key properties is non-nullable

Exception :
System.InvalidOperationException: The operation failed: The relationship could not
be changed because one or more of the foreign-key properties is non-nullable. When
a change is made to a relationship, the related foreign-key property is set to a
null value. If the foreign-key does not support null values, a new relationship
must be defined, the foreign-key property must be assigned another non-null value,
or the unrelated object must be deleted.
I have seen some solutions for the above issue as shown below.But none of them worked for me :( May be due to those solutions are for the EF 4.x versions.My app's EF version is 6.x.
Solution 1 and Solution 2
[Table("IpTaxMapLots")]
public class TaxMapLot : FullAuditedEntity
{
public const int MaxLength = 50;
[Required]
[MaxLength(MaxLength)]
public virtual string District { get; set; }
[ForeignKey("PropertyId")]
public virtual Property Property { get; set; }
public virtual int PropertyId { get; set; }
}
[Table("IpProperties")]
public class Property : FullAuditedEntity
{
public const int MaxLength = 50;
[MaxLength(MaxLength)]
public virtual string Dist { get; set; }
public virtual ICollection<TaxMapLot> TaxMapLots { get; set; }
}
public async Task<int?> EditPropertyAsync(CreateOrEditPropertyInput input)
{
var property = await _propertyRepository.FirstOrDefaultAsync(p => p.Id == input.Property.Id);
input.Property.MapTo(property);
await _propertyRepository.UpdateAsync(property);//issue is here
return input.Property.Id;
}
public class CreateOrEditPropertyInput : IInputDto
{
[Required]
public PropertyEditDto Property { get; set; }
}
[AutoMap(typeof(Property))]
public class PropertyEditDto
{
public const int MaxLength = 50;
public int? Id { get; set; }
[MaxLength(MaxLength)]
public string Dist { get; set; }
public List<TaxMapLotDto> TaxMapLots { get; set; }
}
As I mentioned above Where I have used repositories on my app.So could you tell me how to sort out above issue ? Thanks.
Note : I can add records to the db.But problem occurs when I try to do the update.
Image representation :
1 : M
This comes when you click the Lot Info Button above.
Note : There are 2 tables.One is Properties and other is TaxMapLots.The relationship is 1 : M. User can add/edit or delete records on the TaxMapLot form.
Update : Actually I can Add a record.The problem occurs when I try to edit the record.
This works fine (CreatePropertyAsync): It adds record to both the table (Properties and TaxMapLots).The problem is on the Edit method as shown above.
public async Task<int> CreatePropertyAsync(CreateOrEditPropertyInput input)
{
var property = input.Property.MapTo<Property>();
var propertyId = await _propertyRepository.InsertAndGetIdAsync(property);
return propertyId;
}
This happens because previous related TaxMapLot are being removed from the TaxMapLots collection when you are mapping dto to entity, as your dto contains full graph.
EF thinks your collection items are new ones and previous are being removed so theirs PropertyIds becomes null.
You need to handle updating TaxMapLot by yourself instead of letting mapper do it for you.
Make foreign key PropertyId nullable in both, datatabase table IpTaxMapLots and TaxMapLot entity:
public virtual int? PropertyId { get; set; }
Of course you need to think if for your business make sense to have a TaxMapLot without a Property.

ASP.NET MVC How to get FilePath pass into PeopleDB?

I'm a newbie to ASP.NET using Entity Framework. I have different models for People, FileType and FilePath. I want to display the image by retrieving the file path from FilPath together with data like name, age, etc. in index view. I made it happen in Detail view, but in index view page I received error as "Value can not be null", which caused by the FilePath in PeopleDB is null.
Below is my code, please help. Thanks.
/Model/PeopleDB.cs
namespace MvcDemo.Models {
public class PeopleDB
{
public PeopleDB()
{
this.FilePaths = new HashSet<FilePath>();
}
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public string Interests { get; set; }
public ICollection<FilePath> FilePaths { get; set; }
}
public class PeopleDBContext : DbContext
{
public DbSet<FilePath> FilePaths { get; set; }
public DbSet<PeopleDB> People { get; set; }
}
}
/Model/FilePath.cs
namespace Assessment_HC.Models
{
public class FilePath
{
public int FilePathId {get;set;}
[StringLength(255)]
public string FileName {get;set;}
public FileType FileType {get;set;}
public int PersonID {get;set;}
public virtual PeopleDB Person {get;set;}
}
}
Moedel/FileType.cs
namespace Assessment_HC.Models
{
public enum FileType
{
Avatar = 1, Photo
}
}
Here is the controller for index view
//Get: /People/Index
public ActionResult Index()
{
return View(db.People.ToList());
}
In db.People.ToList(), People.FilePath view is null.
In the controller, the detail view is like this, from where I can get the image showing on detail page:
// GET: /People/Details
public ActionResult Details(int id = 0)
{
PeopleDB peopledb = db.People.Find(id);
PeopleDB people = db.People.Include(i => i.FilePaths).SingleOrDefault(i => i.ID == id);
if (peopledb == null)
{
return HttpNotFound();
}
return View(peopledb);
}
Thanks for your help. Let me know if you need more code.
Based on comments, It seems the only thing you should do is changing FilePaths property of your PeopleDB to be virtual to work with Lazy Loading (which is enabled by default):
public virtual ICollection<FilePath> FilePaths { get; set; }
Lazy Loading is enabled by default, and as stated in comments you didn't change it and there is nothing about Lazy Loading in your context constructor, So it seems the problem is in your FilePaths navigation property that is not virtual.
For index action:
return View(db.People.ToList());
For details action its better to do like:
var people = db.People.Where(x => x.ID == id).FirstOrDefault();
if (people == null)
{
return HttpNotFound();
}
return View(people );
But any way, If disable lazy Loading, you should use Include to include your navigation property in result. In this situation you can load data in your index action use:
db.People.Include(x => x.FilePaths).ToList()
or
//Remember to add using System.Data.Entity;
db.People.Include("FilePaths").ToList()
And to disable Lazy Loading you can
db.Configuration.LazyLoadingEnabled = true;
Or in the constructor of your context:
this.Configuration.LazyLoadingEnabled = false;
More information:
Loading Related Entities
Lazy loading is the process whereby an entity or collection of
entities is automatically loaded from the database the first time that
a property referring to the entity/entities is accessed. When using
POCO entity types, lazy loading is achieved by creating instances of
derived proxy types and then overriding virtual properties to add the
loading hook.
I've tested the code, the only one thing that you need is enabling Eager loading using Include method:
public ActionResult Index()
{
var _db = new ApplicationDbContext();
var model = _db.People.Include("FilePaths").ToList();
return View(model);
}
In this case all related file paths will be loaded.
You can also make FilePaths as virtual:
public virtual ICollection<FilePath> FilePaths { get; set; }
And change your query this way:
var model = _db.People.ToList();
In both cases, all related file paths will be loaded.

CTP5 EF Code First Question

You can find the source code demonstrating this issue # http://code.google.com/p/contactsctp5/
I have three model objects. Contact,ContactInfo,ContactInfoType. Where a contact has many contactinfo's and each contactinfo is a contactinfotype. Fairly simple I guess. The problem I'm running into is when I go to edit the contact object. I pulled it from my contact repository. Then I run "UpdateModel(contact);" and it updates the object with all the values from my form. (monitoring with debug) When I save the changes though, I get the following error:
The operation failed: The relationship
could not be changed because one or
more of the foreign-key properties is
non-nullable. When a change is made to
a relationship, the related
foreign-key property is set to a null
value. If the foreign-key does not
support null values, a new
relationship must be defined, the
foreign-key property must be assigned
another non-null value, or the
unrelated object must be deleted.
It seems like after I call update model it nulls out my references and this seems to break everything? Any thoughts on how to remedy would be greatly appreciated. Thanks.
Here are my models:
public partial class Contact {
public Contact() {
this.ContactInformation = new HashSet<ContactInformation>();
}
public int ContactId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual ICollection<ContactInformation> ContactInformation { get; set; }
}
public partial class ContactInformation {
public int ContactInformationId { get; set; }
public int ContactId { get; set; }
public int ContactInfoTypeId { get; set; }
public string Information { get; set; }
public virtual Contact Contact { get; set; }
public virtual ContactInfoType ContactInfoType { get; set; }
}
public partial class ContactInfoType {
public ContactInfoType() {
this.ContactInformation = new HashSet<ContactInformation>();
}
public int ContactInfoTypeId { get; set; }
public string Type { get; set; }
public virtual ICollection<ContactInformation> ContactInformation { get; set; }
}
My Controller Action:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Contact person) {
if (this.ModelState.IsValid) {
var contact = this.contactRepository.GetById(person.ContactId);
UpdateModel(contact);
this.contactRepository.Save();
TempData["message"] = "Contact Saved.";
return PartialView("Details", contact);
} else {
return PartialView(person);
}
}
Context Code:
protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder modelBuilder) {
modelBuilder.Entity<Contact>()
.HasMany(c => c.ContactInformation)
.WithRequired()
.HasForeignKey(c => c.ContactId);
modelBuilder.Entity<ContactInfoType>()
.HasMany(c => c.ContactInformation)
.WithRequired()
.HasForeignKey(c => c.ContactInfoTypeId);
}
There's a few things going on here.
1 If you are set up for lazy loading child objects are only loaded if you tell them to load. This can be done with the following in your query.
..
context.Contacts.Include(c => c.ContactInfos).Include(c => c.ContactInfos.ContactInfoType)
see this article for full details on making sure objects are loaded as you want.
2 If you don't want to save contactinfo and contactinfotype (because they are not loaded or you just don't want to), you will need to tell the context not to save child objects that shouldn't be updated. This can be done using:
..
context.StateManager.ChangeObjectState(entity.ContactInfos.ContactInfoType, EntityState.Unchanged);
I find I need to do that when changing/using a country object to user data. I definitely never want that to be updated by a user.
In the middle of writing a bit of a guide to all this, but could be weeks until it's done on my blog
3 MVC won't store/send back anything you don't put into the form. If you send an object heirarchy to the form and the values aren't represented in hidden inputs, they will come back empty on your model. For this reason, I generally make viewmodels that are editable only versions of the entities with a ToEntity and a ToModel method on them. This also covers me for security as I don't want all sorts of user ids in hidden inputs, just so my entities map straight into MVC (see this article on overposting).
I WOULD have thought that the fact you have your contactinfo properties set to virtual, the UpdateModel wouldn't mind if they didn't exist on the return, but I could well be wrong as I haven't tried it.
I figured this question out thanks to Morteza Manavi on the entity framework website. My issue was caused by my ContactInformation model properties, 'contactid' & 'contacttypeid' not being nullable. Once I fixed this everything with UpdateModel() worked correctly. Thank you very much!

Categories