简体   繁体   中英

Entity Framework - Custom Field that get data from another Table

i have this scenario:

public class Source
{
    public int ID { get; set; }
    public String Name { get; set; }
    public String Description { get; set; }
    public virtual ICollection<InvoiceMembership> InvoiceMemberships { get; set;}
}

public class InvoiceMembership
{
    public int ID { get; set; }
    [Column(TypeName = "date")]
    public DateTime StartDate { get; set; }
    [Column(TypeName = "date")]
    public DateTime? EndDate { get; set; }
    public virtual Source source { get; set; }
    public virtual InvoiceTemplate InvoiceTemplate { get; set; }
}

public class InvoiceTemplate
{
    public int ID { get; set; }
    public String Name { get; set; }
    public String Description { get; set; }
    public bool Enabled { get; set; }
    public int NumberOfPayment { get; set; }
}

How can i have a field named CurrentTemplate with type of InvoiceTemplate in Source Entity That have EndDate=null in related InvoiceMembership row?
EDIT:
i use bellow code, but it's not really true way!

[NotMapped]
    public InvoiceTemplate CurrentTemplate { get {
        var context=new MedicalContext();
        var template = context.InvoiceMemberships.Where(m => m.source == this).Where(m => m.EndDate == null).Select(m => m.InvoiceTemplate);
        if (template != null)
            return (InvoiceTemplate)template;
        else
            return null;
    } }

Yes you can, but with EF computed properties are such a hassle.

Let's say you have:

public class Source
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    [NotMapped]
    public InvoiceTemplate CurrentTemplate
    { 
        get
        {
            return InvoiceMemberships
                   .Where(i = i.EndDate == null)
                   .Select(i => i.InvoiceTemplate)
                   .FirstOrDefault();
        }
    }

    public virtual ICollection<InvoiceMembership> InvoiceMemberships { get; set;}
}

There just too many conditions (in my opinion) that should be met to make this work:

  • CurrentTemplate can't be used directly in a LINQ query: EF will complain that the expression can't be translated into SQL.
  • You always have to Include() InvoiceMemberships.InvoiceTemplate to be able to access CurrentTemplate in memory, after the context is disposed. This will always load all InvoiceMemberships + InvoiceTemplate for each Source object (in one query).
  • Without using Include() you can only access CurrentTemplate before the context is disposed. This will trigger lazy loading of all InvoiceMemberships + InvoiceTemplate for each Source object in separate queries.

As you see, you'll always load (much) more data than needed to get only one InvoiceMembership per Source .

The most efficient way is to query the required data into a projection, so the predicate EndDate == null can be included in the SQL query.

We'll have to wait for NHibernate-style formula properties.

Expose your Foreign Key value directly in your InvoiceMembership POCO (it must be there in the database anyway if the relation exists), and the entire query will be elegantly converted to SQL directly by L2E:

public class InvoiceMembership
{
    public int ID { get; set; }
    public int SourceId { get; set; }
    [ForeignKey("SourceId")]
    public virtual Source Source { get; set; }
    public virtual InvoiceTemplate InvoiceTemplate { get; set; }
}

And in Source :

[NotMapped]
public InvoiceTemplate CurrentTemplate
{
    get
    {
        using (var context = new MedicalContext())
            return context.InvoiceMemberships
                          .Where(m => m.SourceId == this.ID)
                          .Where(m => m.EndDate == null)
                          .Select(m => m.InvoiceTemplate)
                          .FirstOrDefault();
    }
}

However, this has the drawback that - each time the property is accessed - the database will be queried. It might be best to move this method to the InvoiceMembership class, where you know your InvoiceMembership objects are getting loaded anyway, and make it a method:

public class InvoiceMembership
{
    public int ID { get; set; }
    public int SourceId { get; set; }
    [ForeignKey("SourceId")]
    public virtual Source Source { get; set; }
    public virtual InvoiceTemplate InvoiceTemplate { get; set; }

    static public InvoiceTemplate ReadCurrentTemplate(int sourceId)
    {
        using (var context = new MedicalContext())
            return context.InvoiceMemberships
                          .Where(m => m.SourceId == sourceId)
                          .Where(m => m.EndDate == null)
                          .Select(m => m.InvoiceTemplate)
                          .FirstOrDefault();
    }
}

So, now, you have a method , not a property, to no longer hide the fact that an action is being taken each time you access it ... the name Read CurrentTemplate tells you that. And why not make it static now? Furthermore, making it static means you no longer have to worry about NotMappedAttribute .

But we'd still like to have access in Source , wouldn't we (especially if dropping the ICollection<InvoiceMembership> navigation property, as I see in your comments you wish to do)? This is now no longer an EF concern at all, but a regular, lazy-loading (if you wish it to be), concern in the Source class:

readonly Lazy<InvoiceTemplate> _currentTemplate;
public Source()
{
    _currentTemplate = new Lazy<InvoiceTemplate>(t => t = InvoiceMembership.ReadCurrentTemplate(ID));
}
[NotMapped]
public InvoiceTemplate CurrentTemplate
{
    get { return _currentTemplate.Value; }
}

So, doing this, you're still going to take the hit of running the database query to get the value of CurrentTemplate , but only once to evaluate the Lazy private backer; afterward, for the life of this Source object, this property will be the same as the first time it was read. This seemed to fit the model of how you were going to use it, considering it was only ever a read-only property in your examples. And it would only be for the life of the Source object, which should be in a context anyway.

If not, this should be method ReadCurrentTemplate (non- static , no parameter) on Source as well, and simply return InvoiceMembership.ReadCurrentTemplate(ID) directly, to indicate that each time it's called it's reading from the Database.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM