DataAnnotations with EDMX EF 6.0 does not work - entity-framework

I have an .EDMX with my models, from EF 6.0, and I want to add attributes to some of my fields. I've read many examples where they use DataAnnotations with MetadataType... I've tried to implement it, but it does not override... For example if I have
[Queryable]
public string Name;
it will not work.
but if I have
[Queryable]
public string Name2;
It will work and I will see Name2 as part of the attributes!
The code I use in order to find those attributes is as follow :
var properties = typeof(TEntity).GetProperties().Where(prop => prop.IsDefined(typeof(QueryableAttribute), false));
Like I said, when I have Name2, i can find it in the attributes list. And when I have Name, I don't ...
here is my 3 files, they are both in "MMS.Entities" namespace
AreaMetadata.cs
namespace MMS.Entities
{
[MetadataType(typeof(AreaMetadata))]
public partial class Area
{}
public class AreaMetadata
{
[Queryable]
public string Name;
[Queryable]
public string Abbreviation;
[Queryable]
public string Description;
}
}
Area.cs
namespace MMS.Entities
{
using MMS.Common.Utilities;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public partial class Area : Entity, IEntity
{
public Area()
{
this.Plants = new HashSet<Plant>();
}
public int Id { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; }
public bool IsDeleted { get; set; }
public int UserCreatedId { get; set; }
public Nullable<int> UserModifiedId { get; set; }
public System.DateTime DateCreated { get; set; }
public Nullable<System.DateTime> DateModified { get; set; }
public virtual ICollection<Plant> Plants { get; set; }
}
}
Should the name of AreaMetadata.cs be different? Should I include anything somewhere in order to make them both work together?
Thanks for your advices!

I believe a similar question has been answered here.
When you try to access a MetadataType property attribute, you have to get to the MetadataType using reflection, and not simply the class that uses the MetadataType. There is an explanation in the link I provided and an example of how to get it.
Basically, use reflection to get the AreaMetadata properties, not the Area properties.
Try:
MetadataTypeAttribute metadata = typeof(TEntity)
.GetCustomAttributes(typeof(MetadataTypeAttribute), true)
.OfType<MetadataTypeAttribute>().ToArray().FirstOrDefault();
PropertyInfo[] properties = metadata.MetadataTypeClass.GetProperties()
.Where(prop => prop.IsDefined(typeof(Queryable), false));

Related

Scaffolding MVC Controller - how to indicate dataValueField and dataTextField?

One of the overload methods of SelectList (from the Microsoft.AspNetCore.Mvc.Rendering namespace) is defined as:
public SelectList(IEnumerable items, string dataValueField, string dataTextField);
When I scaffold an "MVC Controller with view, using Entity Framework" and I create my CRUD pages, I may see the following method inside of my Controller:
public IActionResult Create()
{
ViewData["Continent"] = new SelectList(_context.Continent, **"ContinentID", "ContinentID"**);
ViewData["Country"] = new SelectList(_context.Country, **"CountryID", "CountryName"**);
return View();
}
The field supplied to the dataTextField parameter is different between Continent/Country. How does MVC/EntityFramework decide which field to supply to dataTextField when scaffolding a Controller? Is there something in the individual models or in the DbContext that I am overlooking? I'd like for the dataTextField of Continent to be "ContinentName" so that I don't have to change it manually in the future when I need to delete and then re-scaffold the Controller.
Edit:
Here are the model definitions:
The Model of the Controller that I posted above:
using System;
using System.Collections.Generic;
namespace Project.Models
{
public partial class ProjectForm
{
public int ProjectFormID { get; set; }
public int ContinentID { get; set; }
public int CountryID { get; set; }
public virtual Continent ContinentNavigation { get; set; }
public virtual Country CountryNavigation { get; set; }
}
}
The one that displays the "CountryName" in the dataTextField the way that I want to see it:
namespace Project.Models
{
public partial class Country
{
public int CountryID { get; set; }
public string CountryName { get; set; }
public virtual ICollection<ProjectForm> ProjectForm { get; set; }
}
}
The one that displays the "ContinentID" in the dataTextField the way that I do NOT want to see it:
namespace Project.Models
{
public partial class Continent
{
public int ContinentID { get; set; }
public string ContinentName { get; set; }
public virtual ICollection<ProjectForm> ProjectForm { get; set; }
}
}
There is nothing obviously different to me in the model definitions unfortunately.
I stumbled across this post today (a bit late), but see it still hasn't been answered.
While I can't say why the scaffolding chose to use one field over another in your scenarios (unless you initially had your class/model written differently the last time you cleaned/built your project), I can say how to force it to use a specific column.
Add the DisplayColumn attribute to your class. You will need to rebuild before scaffolding again for the change to take.
namespace Project.Models
{
[DisplayColumn("ContinentName")]
public partial class Continent
{
public int ContinentID { get; set; }
public string ContinentName { get; set; }
public virtual ICollection<ProjectForm> ProjectForm { get; set; }
}
}

Can't get `Include` to work with EF core , without FK

I have 2 tables in SQL server without a FK :
So each Image has many Comments.
Using this command I've created scaffolded files :
dotnet ef dbcontext scaffold
"Server=sff-pc;Database=IMG;Trusted_Connection=True;"
Microsoft.EntityFrameworkCore.SqlServer -o Models -t Images -t
comments --context-dir Context -c MyContext -f
Here are the generated files :
public partial class Images
{
public int ImageId { get; set; }
public Guid ImgGuid { get; set; }
public string ImgExtension { get; set; }
public DateTime Datecreated { get; set; }
public string Origin { get; set; }
public decimal? Size { get; set; }
public string Ip { get; set; }
public int UserId { get; set; }
public string Description { get; set; }
public bool? IsActive { get; set; }
public int? VotesUp { get; set; }
public int? VotesDown { get; set; }
public int ImgType { get; set; }
}
public partial class Comments
{
public int CommentId { get; set; }
public int ImageId { get; set; }
public int UserId1 { get; set; }
public string CommentTxt { get; set; }
public DateTime DateCreated { get; set; }
public bool? IsActive { get; set; }
}
Question:
Using those generated classes and this full context file, How can I get each Image and Include its comments?
What have I tried :
I've tried adding the ForeignKey attribute :
public partial class Comments
{
public int CommentId { get; set; }
[ForeignKey("Images")] <--------------Here
public int ImageId { get; set; }
public int UserId1 { get; set; }
public string CommentTxt { get; set; }
public DateTime DateCreated { get; set; }
public bool? IsActive { get; set; }
}
And to run this :
var context = new MyContext();
var result = context.Images.Include("Comments"); //exception
foreach (var a in result)
{
Console.WriteLine(a);
}
Exception :
The property 'Comments' is not a navigation property of entity type 'Images'
When you use [ForeignKey("Images")] over your fk "Images" is expected to be a navigation property inside your Comments class not the name of your DbSet property in your DbContext. You can read more here and relationships here. In the case of your code based on the definition of your Comments class, you will need to have a property Images.
[ForeignKey("Images")]
public int ImageId { get; set; }
public Images Images { get; set; } // ForeignKey attribute points to this
While the above shows how you can use [ForeignKey("Images")], I would recommend making the property's name Image instead of Images. Not only will it be a better explanatory name, but it would also make the [ForeignKey("Image")] unnecessary. EF would automatically map Image to Imageid by naming convention.
Based on the way you are using to access comments using context.Images.Include("Comments"), you seem to be trying to access the list of comments under an image. However, if you notice your result variable would be a IQueryable of Images. To achieve this, you will need to add a navigation property into your Images class.
public IEnumerable<Comments> Comments { get; set; }
Since you have already mapped your foreign key, your mapping is implicit between ImageId and Comments. You can, however, use InverseProperty attribute to be safe (check the relationships link for more info). Once this is done you can use the following code:
var result = context.Images.Include(i => i.Comments);

Entity Framework fails to get child elements

I have SQLite db and these EF models and context.
Models and Context
public class CardHolder
{
public int CardHolderId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
public string EmailAddress { get; set; }
public string TenantName { get; set; }
public ICollection<AccessCard> AccessCards { get; set; }
}
public class AccessCard
{
public int AccessCardId { get; protected set; }
public CardHolder CardHolder { get; set; }
public DateTime ActivationDate { get; protected set; }
public bool ActivationProcessed { get; set; }
public DateTime? DeactivationDate { get; protected set; }
public string DeactivationReason { get; set; }
public bool DeactivationProcessed { get; set; }
}
public class MyContext : DbContext
{
public DbSet<CardHolder> CardHolders { get; set; }
public DbSet<AccessCard> AccessCards { get; set; }
}
And the Main program
class Program
{
static void Main(string[] args)
{
using (var db = new MyContext())
{
var cardHolders = db.CardHolders.Include("AccessCard").ToList();
}
}
}
Question1: Why do I get this exception
System.InvalidOperationException: 'A specified Include path is not
valid. The EntityType 'SQLiteDemo.Models.CardHolder' does not declare
a navigation property with the name 'AccessCard'.'
If I replace it with
var cardHolders = db.CardHolders.Include("AccessCards").ToList();
I get another error:
SQL logic error no such column: Extent2.CardHolder_CardHolderId
What is wrong with Entity Framework?
Question2: Why cant I use arrow function in Include statement, it doesnt compile at all?
var cardHolders = db.CardHolders.Include(x => x.AccessCards).ToList();
Question3: Why do I need to use Include at all if my ICollection association property AccessCards is NOT virtual - that means eager loading must work by itself!
Why the hell it is so problematic and buggy? Nothing works as it should :(
1 - You have a typo as you have already determined :)
1B - "SQL logic error no such column: Extent2.CardHolder_CardHolderId"
EF isn't finding your FK. You could add it to your AccessCard model:
public int CardHolderId { get; set; }
2 - You need to pull in the LINQ extensions. Make sure you have both of these using statements at the top:
using System.Data.Entity;
using System.Linq;
3 - You, like many others, are misunderstanding lazy loading. Eager loading still requires an Include() to fetch related data. Lazy loading only fetches the relations when you access them.

Circular Reference error when serializing objects in ASP.NET Web API

I'm writing a Web API project in C# that uses Entity Framework to pull data from a DB, serialize it and send it to a client.
My project has 2 classes, Post and Comment (foreign key from Post).
These are my classes.
Post class:
public partial class Post
{
public Post()
{
this.Attachment = new HashSet<Attachment>();
this.Comment = new HashSet<Comment>();
}
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public System.DateTime Created { get; set; }
public Nullable<System.DateTime> Modified { get; set; }
public virtual ICollection<Attachment> Attachment { get; set; }
public virtual ICollection<Comment> Comment { get; set; }
}
Comment class:
public partial class Comment
{
public int CommentId { get; set; }
public string Content { get; set; }
public System.DateTime Posted { get; set; }
public bool Approved { get; set; }
public int AnswersTo { get; set; }
public int PostId { get; set; }
public virtual Post Post { get; set; }
}
My problem is that when I try to get via Web API a Post, it spits me the following error:
Object graph for type 'APIServer.Models.Comment' contains cycles and cannot be serialized if reference tracking is disabled.
And when I try to get a Comment via Web API, the error is as follows:
Object graph for type 'System.Collections.Generic.HashSet`1[[APIServer.Models.Comment, APIServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' contains cycles and cannot be serialized if reference tracking is disabled.
If I annotate the Comment class with
[DataContract(IsReference = true)]
the errors disappear, but the serialization only returns the ID of the comment and ignores the other fields.
Any suggestions on how to solve this?
Thanks in advance,
Léster
Here are 2 solutions
Solution #1:
I had this same problem and so I decorated my class with DataContract and the members with DataMember like you mention. HOWEVER, I don't like editing auto-generated code directly because I have to redo it every time I regenerate the file. In order to get around this, I used the MetadataType attribute. In your case, it would look like this...
First, you will keep the auto generated entity as is:
public partial class Comment
{
public int CommentId { get; set; }
public string Content { get; set; }
public System.DateTime Posted { get; set; }
public bool Approved { get; set; }
public int AnswersTo { get; set; }
public int PostId { get; set; }
public virtual Post Post { get; set; }
}
Next, in another file, you will create another partial class and decorate it like this:
[MetadataType(typeof(Metadata))]
[DataContract(IsReference = true)]
public partial class Comment
{
private class Metadata
{
[DataMember]
public int CommentId { get; set; }
[DataMember]
public string Content { get; set; }
[DataMember]
public System.DateTime Posted { get; set; }
[DataMember]
public bool Approved { get; set; }
[DataMember]
public int AnswersTo { get; set; }
[DataMember]
public int PostId { get; set; }
[DataMember]
public virtual Post Post { get; set; } // you can remove "virtual" if you wish
}
}
MetadataType will essentially add the attributes from the Metadata buddy class to the ones with the same name in Comment (not directly, but for our purposes, it's close enough... that's a topic for a different post). Of course, if your Comment entity changes, you'll need to update this accordingly.
Solution #2:
Having to edit your second file every time you make a change is only a slight improvement from directly editing auto-generated files. Fortunately, there is another approach that is much easier to maintain. Details can be found here but as a summary, all you need to do is decorate your OperationContract that is consuming Comment with an additional attribute, ReferencePreservingDataContractFormat. Note that there is a slight error in the code provided on that page that would cause infinite recursion. As noted in this post, the fix is quite simple: instead of recursing at all, just create a new DataContractSerializer
The advantage to this approach is that no matter how much you change Comment, you still don't need update anything.
As an example for your code, let's say you are using Comment as follows:
[OperationContract]
Comment FindComment(string criteria);
All you need to do is add
[OperationContract]
[ReferencePreservingDataContractFormat]
Comment FindComment(string criteria);
And then somewhere else you need to define ReferencePreservingDataContractFormat which will look like this:
//From http://blogs.msdn.com/b/sowmy/archive/2006/03/26/561188.aspx and https://stackoverflow.com/questions/4266008/endless-loop-in-a-code-sample-on-serialization
public class ReferencePreservingDataContractFormatAttribute : Attribute, IOperationBehavior
{
public void AddBindingParameters(OperationDescription description, BindingParameterCollection parameters)
{
}
public void ApplyClientBehavior(OperationDescription description, System.ServiceModel.Dispatcher.ClientOperation proxy)
{
IOperationBehavior innerBehavior = new ReferencePreservingDataContractSerializerOperationBehavior(description);
innerBehavior.ApplyClientBehavior(description, proxy);
}
public void ApplyDispatchBehavior(OperationDescription description, System.ServiceModel.Dispatcher.DispatchOperation dispatch)
{
IOperationBehavior innerBehavior = new ReferencePreservingDataContractSerializerOperationBehavior(description);
innerBehavior.ApplyDispatchBehavior(description, dispatch);
}
public void Validate(OperationDescription description)
{
}
}
class ReferencePreservingDataContractSerializerOperationBehavior : DataContractSerializerOperationBehavior
{
public ReferencePreservingDataContractSerializerOperationBehavior(OperationDescription operationDescription) : base(operationDescription) { }
public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
{
return new DataContractSerializer(type, name, ns, knownTypes,
0x7FFF, //maxItemsInObjectGraph
false, //ignoreExtensionDataObject
true, //preserveObjectReferences
null //dataContractSurrogate
);
}
public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
{
return new DataContractSerializer(type, name, ns, knownTypes,
0x7FFF, //maxItemsInObjectGraph
false, //ignoreExtensionDataObject
true, //preserveObjectReferences
null //dataContractSurrogate
);
}
}
And that's it!
Either method will work just fine--pick the one that works for you.
You can disable Lazy Loading on your Comment class by removing virtual from the Post property definition...
public partial class Comment
{
public int CommentId { get; set; }
public string Content { get; set; }
public System.DateTime Posted { get; set; }
public bool Approved { get; set; }
public int AnswersTo { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
}
This should sort out the circular reference exception.

How to Define EF5 Navigation Property to Multiple Types

I have a shared type that I'd like to represent in a database. I need to know either if it's possible with code first or if it's bad design. (Please point to sources of your reasoning).
Here's the layout:
public class A
{
public Guid Id;
public Guid ParentId; // Points to either B or C
public string Foo;
}
public class B
{
public Guid Id;
public virtual ICollection<A> ManyA { get; set; }
// Other fields
}
public class C
{
public Guid Id;
public virtual ICollection<A> ManyA { get; set; }
// Other fields
}
I've managed GUID PKs by using the [DatabaseGenerated(DatabaseGeneratedOption.Identity)] annotation, which I believe would make this possible.
The goal is to keep the A-table simple; I'd like it not to have B_Id and C_Id columns, as I think they're unnecessary, and there may be a D type in the future that has many A just like B and C. Also, this scenario is used for a few other relationships.
I think the spirit of the problem is apparent, however the EF-specific class members are just there for illustration. If they need changing, so be it.
(I'm sorry for the contrived example, however exposing the true nature of this, IMO, doesn't clarify the problem).
Also, I tried searching for hours! I don't know the proper nomenclature to find a suitable answer. I'm pretty new to databases and EF in general.
Don't use Guid to make a index, why you choose the hard-way ? :S
use the DataAnnotations like the example:
A class:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Web.Mvc;
namespace app.Models
{
[Table("A")]
public class A
{
[Key]
public int AId { get; set; }
[Required(ErrorMessage = "Campo obrigatório")]
[DisplayName("Title")]
[StringLength(100)]
public string Title { get; set; }
public virtual ICollection<B> bCollection { get; set; }
}
}
B class:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Web.Mvc;
namespace app.Models
{
[Table("B")]
public class B
{
[ScaffoldColumn(false)]
[Key]
public int BId { get; set; }
[DisplayName("Description")]
[StringLength(100)]
public string Description { get; set; }
public virtual ICollection<A> AList{ get; set; }
}
}
You can see more details here: link!!!
For more details about EF5 Data Annotations: link
I've found that I can make a common base type for both B and C types. For example:
class Program
{
static void Main(string[] args)
{
using (var context = new Ctx())
{
var b = new B();
var c = new C();
b.ManyA.Add(new A());
c.ManyA.Add(new A());
context.Cs.Add(c);
context.Bs.Add(b);
context.SaveChanges();
}
}
}
public class Ctx : DbContext
{
public DbSet<A> As { get; set; }
public DbSet<B> Bs { get; set; }
public DbSet<C> Cs { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<BaseClass>()
.HasMany<A>(b => b.ManyA)
.WithRequired(a => a.BaseObject)
.WillCascadeOnDelete(true);
}
}
public class BaseClass
{
public BaseClass() { ManyA = new HashSet<A>(); }
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public virtual ICollection<A> ManyA { get; set; }
}
public class A
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public BaseClass BaseObject { get; set; }
public string Foo { get; set; }
}
public class B : BaseClass
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string SomeProperty { get; set; }
}
public class C : BaseClass
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public int SomeOtherProperty { get; set; }
}
And, based on this Msdn article, would result in Table Per Hierarchy database layout. TPH performs better versus TPT, which I was thinking could be an answer.
I was really hoping for something simpler, but performance is the bottom line, afterall