dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.63k stars 3.15k forks source link

Relational: TPT inheritance mapping pattern #2266

Closed satyajit-behera closed 4 years ago

satyajit-behera commented 9 years ago

Even if TPT is considered slow, it is a boon for many real world business scenarios. If implemented properly and well thought of, TPT is not slow for a given need in most of the cases.

If possible, optimization can be done to TPT. But its a very important feature for EF to be accepted for developing DDD applications.

The other method, Composition over Inheritance does not look viable since we cannot use interface property to create our model. Binding to a concrete type takes away the flexibility to customize the models. TPT makes the customization very easy and flexible.

ajcvickers commented 6 years ago

@jgonte @milosloub TPT support is something we plan to implement, but it is a big cross-cutting feature. I can't say that it will be in the next release following 2.1 because there are many other competing features that are also high priority. However, each release we also think about which building blocks we need to get closer to being able to support these big cross-cutting features. So we are getting closer, but it's going to take some more time.

jgonte commented 6 years ago

I understand that although, the entity framework 6 might provide a starting point:

Mapping the Table-Per-Type (TPT) Inheritance In the TPT mapping scenario, all types are mapped to individual tables. Properties that belong solely to a base type or derived type are stored in a table that maps to that type. Tables that map to derived types also store a foreign key that joins the derived table with the base table.

modelBuilder.Entity().ToTable("Course");
modelBuilder.Entity().ToTable("OnsiteCourse");

Meanwhile IMHO using EF Core for serious DDD or best practices development besides creating a Blog or a ToDo app is impossible.

I just hope you realize that and move it up if possible in the priority queue :-)

kakone commented 6 years ago

Meanwhile IMHO using EF Core for serious DDD or best practices development besides creating a Blog or a ToDo app is impossible.

Your domain model should be separated from your persistence model and should not depend on any ORM. You could have all inheritance you want in your domain models, and use EF Core only for persistance, nothing else. It works. Separating has a cost (more coding, more classes to maintain, ...), but it's not impossible.

jgonte commented 6 years ago

@kakone I am kind of lost here ...

If I use EF Core for persistence, nothing else ... but it does not support TPT mapping ... it works???

So the EF Core masters do not need to work on any issue anymore, since the persistence model should not depend on any ORM?

brianjlowry commented 6 years ago

@kakone It doesn't persist it in a manner that most would consider correct and forces those using a TPT mapping into poor DB design, by default.

kakone commented 6 years ago

If I use EF Core for persistence, nothing else ... but it does not support TPT mapping ... it works???

Yes, it works. There are one table per type in my database and I use EF Core, but my persistence models must use composition because of lack of TPT support in EF Core. My domain models uses inheritance and I do the mapping between persistence and domain models.

So the EF Core masters do not need to work on any issue anymore, since the persistence model should not depend on any ORM?

No, it would be really useful to have TPT (and TPC) inheritance in EF Core. I only say that it's not impossible to do serious DDD with EF Core because serious DDD should not depend on any ORM : http://www.mehdi-khalili.com/orm-anti-patterns-part-4-persistence-domain-model http://enterprisecraftsmanship.com/2016/04/05/having-the-domain-model-separate-from-the-persistence-model/ http://blog.sapiensworks.com/post/2012/04/07/Just-Stop-It!-The-Domain-Model-Is-Not-The-Persistence-Model.aspx

brianjlowry commented 6 years ago

@kakone I don't use a persistence model, and I still have zero persistence details within my domain model (code-first / POCO). Adding another layer to translate just seems like unneeded overhead. You are defeating your own argument by saying that you need a completely separate set of classes just for the ORM - what happens if you switch ORMs? Are you going to have to completely change the persistence models now to fit into nHibernate / insert ORM?

My POCOs can work with any ORM as-is - I share this guy's viewpoint from one of your links: http://disq.us/p/1idhyw4

kakone commented 6 years ago

@brianjlowry I'm just saying that in DDD, you can use EF Core without TPT support, no matter how the database is built because you can use composition instead of inheritance in your persistence models. I use TPT in my database and I use EF Core. OK, I would prefer to have TPT support in EF Core, it would be simpler, but, currently, I work without.

brianjlowry commented 6 years ago

@kakone I understand that it "works", but it leads to an absolutely terrible DB design which harms performance drastically (see my earlier comment about indexes) - especially in scenarios where you have many parents that inherit a model (like a generic Person class).

Using composition instead of inheritance is exactly the opposite of DDD. You are now building your models to suit the shortcomings of your ORM. I didn't have this problem before moving to Core - it worked fine.

JanEggers commented 6 years ago

cmon guys dont troll this thread

jgonte commented 6 years ago

I think two facts are drawn from this discussion . Either:

So please prioritize this feature so we can do it the best way Thanks

nero-philip-wang commented 6 years ago

we really need this feature. 0.0

lloydpowell88 commented 6 years ago

Does anyone have a suitable alternative to achieve TPT concepts, ideally I don't want to use TPH because I don't want 100s of columns as my solution grows, is there a 'tidy' solution in which this can be achieved.

Seeing as this has been in the priority list for nearly 2 years I guess it's not coming any time soon which is a big shame because if it's not achievable :-(

simon-tamman commented 6 years ago

@lloydpowell88 Do some sort of CQRS? I.E. Make the read and write models are different so you can denormalise for the reads.
It doesn't really solve the problem as you can't use TPT properly for the writes but it mitigates the issue slightly by introducing another. :D

jgonte commented 6 years ago

The issue is not to map all the inheritance tree to a single table. That is an absolute lack of normalization and the best practices in database design is to normalize the data ... as simple like that

simon-tamman commented 6 years ago

best practices in database design is to normalize the data

For standard-traffic source databases sure but I'm asking the question if the user needs to see the actual direct source. You can project denormalised projections of your data and map to those.

jgonte commented 6 years ago

I don't think the user needs to see that. For that we have the view model mapping, but still Entity Framework, at the entity level, accesses a database that needs to be designed with normalized data and at this stage, for inheritance, EF does not allow that

SPEMoorthy commented 6 years ago

I am waiting see TPT And TPC in EFCore as soon as possible. It is really need

ignusin commented 6 years ago

Wow, I got hit with that one too. We were really considering the move to .NET Core with EF Core, but now it seems impossible without TPT. Hopefully it will be released soon.

Small addition: after two weeks of attempts, we were able to move our synchronization services to Linux/Ubuntu, and it works great! TPT inheritence in DB was patched and became TPH. Amazing, we finally got to the point where .net code can run on linux with no issue, amazing! Thank you NETCore team!

seroche commented 6 years ago

Do we have an ETA for this? I can see it's not even included in the roadmap for 2.1.

Tyology commented 6 years ago

It's frustrating running into project showstoppers with EF Core. I would consider no support for TPT to be one of them for certain types of projects. The column bloat and lack of enforcement by using TPH outweigh the performance gain. Not to mention we would lose the flexibility of working with separate types.

+1 for TPT please, I understand there are drawbacks (maybe some unforeseen) to TPT vs TPH, but I would not take that to mean TPT is an anti-pattern. There are definitely drawbacks to TPH that TPT alleviates. It would be great to be able to have a choice between tradeoffs if it means the project will use EFCore or not at all.

Edit: I think a fellow developer summed it up best for me. Until EF Core decides to put it's big boy pants on and support enterprise level business models, it's only appropriate for small personal projects.

bugproof commented 6 years ago

What about having a couple of DbSet like:


public abstract class Person
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
}

public class SomeOtherPerson1 : Person
{
    public int MySpecialProperty1 { get; set }
}

public class SomeOtherPerson2 : Person
{
public int MySpecialProperty2 { get; set }
}

public DbSet<SomeOtherPerson1> OtherPerson1 { get; set; }
public DbSet<SomeOtherPerson2> OtherPerson2 { get; set; }

I've tested it and it seems to create separate tables with columns of properties inherited from Person.

What is wrong with this approach? Is this "TPT" ?

Using

public DbSet<Person> People { get; set; }

however, results in the TPH and the problem described in this issue?

weitzhandler commented 6 years ago

What is wrong with this approach? Is this "TPT" ?

It isn't TPT. It's maybe TPC.

TPT is where 3 tables are created: People, Person1, and Person2, where the latter two are joined with the first.

So with TPT, you have a DbSet<Person> which you can then filter out appropriately (or not).

bugproof commented 6 years ago

@weitzhandler yeah I just tested it and with TPT it was relating derived tables with base table using foreign key. While the approach above was just creating tables for derived types (no table for base type). And tables of derived types contain columns of base type.

blecalex commented 6 years ago

@rowanmiller for when is TPT planned? We really need this feature. And apparently we are not the only ones. Thanks for your news!

ajcvickers commented 6 years ago

@blecalex This issue is in the Backlog milestone. This means that it is not going to happen for the 2.1 release. We will re-assess the backlog following the 2.1 release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.

JanEggers commented 6 years ago

@ajcvickers if you sort issues by upvotes this issue is the second on the backlog list. so from my point of view it should be the second item that is done for the next release. if you have different measures on priority please share. this issue needs to be solved asap.

peppinho89 commented 6 years ago

@rowanmiller We need this features. Please prioritize.

ajcvickers commented 6 years ago

@JanEggers The number of upvotes is not the only factor, as is explained in our docs on the release planning process. We recognize that TPT is a very important feature for many people. Unfortunately, it's also one of the more cross-cutting features that requires work in many areas, which makes it difficult to fit into a schedule. We do our best to schedule work appropriately, and let me restate that we understand and take into consideration that TPT is an important feature whenever we prioritize work.

simon-tamman commented 6 years ago

@ajcvickers as much as that makes sense there is often a danger in an objective approach that one misses the woods for the trees. If the release planning process fails to imbue its thought process with subjective strategy as well as objective rationality then this feature will never get done.
Sorry to comment on what is none of my business its just that type of thinking is something that I feel some of the places I work struggle with and ultimately just spin into a decline of only making "sensible decisions" that in context make sense but from a much broader perspective (strategic) do not.

rainer-helbing commented 6 years ago

For years I'm stuck to the "old" .NET framework, because of missing features like TPT and spatial data types in EF core. So there's no way to use Linux (which Microsoft loves) and use effectively new technologies like Docker.

Please add TPT

bmonsterman commented 6 years ago

I think labeling TPT as an anti-pattern is very broad and a bit dogmatic. There are situations where TPT is not a good fit for sure, but there are also situations where it fits perfectly. It's likely that someone might design something in lieu of TPT that carries the same cost (or worse), but is not as clean. For the EF team to discard the support of this pattern because of the potential of performance/scalability problems in it's implementation seems a bit overbearing.

jimmymain commented 6 years ago

Its quite simply not an anti-pattern: https://en.wikipedia.org/wiki/Third_normal_form

Kind Regards Craig Palantir (Pty) Ltd http://www.palantir.co.za/ https://github.com/jimmymain https://www.facebook.com/craigma https://www.twitter.com/@jimmymain https://stackoverflow.com/users/231821/jim https://www.linkedin.com/in/craig-main-b768051/

On Thu, May 10, 2018 at 10:33 PM, Bryan Martin notifications@github.com wrote:

I think labeling TPT as an anti-pattern is very broad and a bit dogmatic. There are situations where TPT is not a good fit for sure, but there are also situations where it fits perfectly. It's likely that someone might design something in lieu of TPT that carries the same cost (or worse), but is not as clean. For the EF team to discard the support of this pattern because of the potential of performance/scalability problems in it's implementation seems a bit overbearing.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/aspnet/EntityFrameworkCore/issues/2266#issuecomment-388176863, or mute the thread https://github.com/notifications/unsubscribe-auth/ACb8qva8OEcDbFlKO-nwu5lwCI6gbxBDks5txKQzgaJpZM4EnOQ5 .

taspeotis commented 6 years ago

I quite like how EF Core is lighter and faster, but the reality is I have existing databases that are NOT managed by Entity Framework and the schema is ... well I get what I'm given. With EF Core I have to compromise.

At least there is some nebulous information about EF 6 coming to .NET Core over here so some day soon we can eschew EF Core's lightness-but-with-shortcomings for EF 6's maturity.

mglgmz commented 6 years ago

We are moving our application from .net to core and our database uses TPT a lot. What we are doing now to make EF Core work with that is map the inheritance as navigation properties of the base class, so we can load the entities from the DB and then map it to our business model that does use inheritance. Not what we would like to be doing, but what we can do until this is in place.

so instead of: BaseEntity DerivedEntity:BaseEntity we are doing: BaseEntity DerivedEntity Derived and we map the FK using EntityConfiguration

milosloub commented 6 years ago

@mglgmz That sounds interesting as temp solution. Can you provide some example with EntityConfiguration? Personally we map Base and "Derived" table to our business model structrure using AutoMapper.

mglgmz commented 6 years ago

@milosloub Sure

Having the following scenario where ElementA and ElementB Inherit from Element

public class Element {
    public int Id { get; set; }

    //other base props

    //map derived types as properties
    public virtual ElementA ElementA { get; set; }
    public virtual ElementB ElementB { get; set; }
}

public class ElementA {
    public int Id {get; set; }
}

public class ElementB {
    public int Id { get; set; }
}

//Configuration:
public class ElementEntityConfiguration : IEntityTypeConfiguration<Element>{
    public void Configure(EntityTypeBuilder<PackageElement> builder)
    {
        builder.HasKey(b => b.Id);

         builder.HasOne(b => b.ElementA)
            .WithOne()
            .HasForeignKey<ElementA>(ea => ea.Id);
        builder.HasOne(b => b.ElementB)
            .WithOne()
            .HasForeignKey<ElementB>(eb => eb.Id);

        builder.ToTable("Element");
    }
}
//Element A and B only need to map the Id and the table
public class ElementAEntityConfiguration : IEntityTypeConfiguration<ElementA>
{
    public void Configure(EntityTypeBuilder<ElementA> builder)
    {
        builder.HasKey(b => b.Id);
        builder.ToTable("ElementA");
    }
}
public class ElementAEntityConfiguration : IEntityTypeConfiguration<ElementB>
{
    public void Configure(EntityTypeBuilder<ElementB> builder)
    {
        builder.HasKey(b => b.Id);
        builder.ToTable("ElementB");
    }
}

I then created the following Model

public abstract class ElementModel {
    public int Id {get;set;}
    //other needed props
}
public class ElementAModel : ElementModel{
    //elementA props
}
public class ElementBModel : ElementModel{
    //elementB props
}

and when I load the entities from the database I have code to check the navigation properties like

if (elementEntity.ElementA != null) GetElementAModelFrom(elementEntity);
else if (elementEntity.ElementB != null) GetElementBModelFrom(elementEntity);

It's a little awkward, but get's the job done

tuespetre commented 6 years ago

@AndriySvyryd @divega is there anything I can do to help here? Perhaps the relational Update piece could be expanded to support such a model and be tested with ad-hoc models so that later the Metadata, Query, and Migrations could grow into it?

AndriySvyryd commented 6 years ago

@tuespetre I will investigate and prototype the model changes to give us a better idea what needs to be updated. If there is any work that we can split out we'll create separate up-for-grab issues.

markusschaber commented 6 years ago

PostgreSQL as an object-relational database supports inheritance on the table level (https://www.postgresql.org/docs/current/static/tutorial-inheritance.html). I don't know whether other databases also support this kind of inheritance, but it could map very well to C# as it's also single-inheritance, and thus could be exposed in EF Core. It should deliver both optimal speed and space usage in most cases, thus performing better on average than TPC, TPH, TPT or the hackish "Use a single table for the base class, and stash all additional fields from subclasses in a JSON column" way.

Edit: I just learned that this is already tracked: #10739.

roji commented 6 years ago

@markusschaber PostgreSQL table inheritance is already tracked by #10739.

insylogo commented 6 years ago

This is incredibly important and blocking about half of the work I'd like to do on a replacement of an existing project with a .NET Core version. Is there anything that us mortal github contributors can do to help streamline this feature?

AndriySvyryd commented 6 years ago

@insylogo Once #12846 is done we'll consider what is the best way to break down the remaining work in manageable pieces.

RowlandShaw commented 5 years ago

In our case we were looking to move our application over to .Net Core/EF Core, but as the existing database used TPT, this is a blocking issue, without having to migrate all the existing data, and ancillary tooling/reporting at the same time. Brings in to question the financial viability of migrating to netcore and EF.Core :(

SergeyLimonov commented 5 years ago

@RowlandShaw While waiting when TPT will be implemented you can try to use composition on data layer instead of inheritance but with some restrictions (for example you can't querying concrete entities from "base" table and inheritance is available only by interface types). Please see my example:

` using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace CompositionExample {

region IUser

public interface IUser
{
    Guid Id { get; }
    string UserName { get; }
}

public class User: IUser
{
    public Guid Id { get; set; }
    public string UserName { get; set; }

    public List<Content> ContentObjects { get; set; }
}

#endregion

#region BaseClasses

public enum ContentType
{
    Undefined = 0,
    ContentAlias = 1,
    UserProfile = 2,
}

public interface IContent
{
    Guid Id { get; }

    ContentType Type { get; }
    string Name { get; }

    Guid OwnerId { get; }
    IUser Owner { get; }
}

public class Content: IContent
{
    public Guid Id { get; set; }

    public ContentType Type { get; set; }
    public string Name { get; set; }

    public Guid OwnerId { get; set; }
    public User Owner { get; set; }
    IUser IContent.Owner => Owner;
}

public abstract class ContentBase: IContent
{
    protected ContentBase()
    {
    }

    protected ContentBase(Content content, ContentType type)
    {
        if (content.Type == ContentType.Undefined)
            content.Type = type;
        else if (content.Type != type)
            throw new ArgumentException(
                $"Content object should be created with content type = '{type}' or '{nameof(ContentType.Undefined)}' but was not.",
                nameof(content));

        Id = content.Id;
        Content = content;
    }

    public Guid Id { get; set; }

    public Content Content { get; set; }

    public ContentType Type { get; set; }
    ContentType IContent.Type => Type;

    public string Name { get; set; }
    string IContent.Name => Name;

    public Guid OwnerId { get; set; }
    Guid IContent.OwnerId => OwnerId;
    IUser IContent.Owner => Content?.Owner;
}

#endregion

#region IContentAlias

public interface IContentAlias: IContent
{
    Guid TargetContentId { get; }
}

public class ContentAlias: ContentBase, IContentAlias
{
    public ContentAlias()
    {
    }

    public ContentAlias(Content content)
        : base(content, ContentType.ContentAlias)
    {
    }

    public Guid TargetContentId { get; set; }
}

#endregion

#region IUserProfile

public interface IUserProfile: IContent
{
    string Nickname { get; }
}

public class UserProfile: ContentBase, IUserProfile
{
    public UserProfile()
    {
    }

    public UserProfile(Content content)
        : base(content, ContentType.UserProfile)
    {
    }

    public string Nickname { get; set; }
}

#endregion

#region Database Context

public class ThisDbContext: DbContext
{
    public DbSet<Content> ContentObjects { get; set; }

    public DbSet<ContentAlias> ContentAliases { get; set; }
    public DbSet<UserProfile> UserProfiles { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Content>(builder =>
            {
                builder.ToTable("ContentObjects")
                    //.HasDiscriminator<int>(nameof(Content.Type))
                    //.HasValue<ContentAlias>((int) ContentType.ContentAlias)
                    //.HasValue<UserProfile>((int) ContentType.UserProfile)
                    ;
                builder.Property(e => e.Name).HasMaxLength(128).IsRequired();
                builder.HasOne(e => e.Owner).WithMany(e => e.ContentObjects).HasForeignKey(e => e.OwnerId).HasPrincipalKey(e => e.Id);
            });

        modelBuilder.Entity<ContentAlias>(builder =>
            {
                builder.ToTable("ContentAliases");
                builder.HasOne<Content>().WithMany().HasForeignKey(e => e.TargetContentId).HasPrincipalKey(e => e.Id)
                    .OnDelete(DeleteBehavior.Restrict);
                builder.HasOne(e => e.Content).WithOne().HasForeignKey<ContentAlias>(e => e.Id).HasPrincipalKey<Content>(p => p.Id)
                    .OnDelete(DeleteBehavior.Cascade);
            });

        modelBuilder.Entity<UserProfile>(builder =>
            {
                builder.ToTable("UserProfiles");
                builder.Property(e => e.Nickname).HasMaxLength(64).IsRequired();
                builder.HasOne(e => e.Content).WithOne().HasForeignKey<UserProfile>(e => e.Id).HasPrincipalKey<Content>(p => p.Id)
                    .OnDelete(DeleteBehavior.Cascade);
            });
    }
}

#endregion

#region User Profile Store

public interface IUserProfileStore
{
    IQueryable<UserProfile> GetAll();
    UserProfile TryGet(Guid id);
    UserProfile Get(Guid id);

    UserProfile Save(UserProfile entry);

    UserProfile Delete(UserProfile entry);
    UserProfile Delete(Guid id);
}

public class UserProfileStore: IUserProfileStore
{
    public UserProfileStore(ThisDbContext dbContext)
    {
        DbContext = dbContext;
    }

    public ThisDbContext DbContext { get; }

    public IQueryable<UserProfile> GetAll()
    {
        return DbContext.UserProfiles.Include(e => e.Content);
    }

    public UserProfile TryGet(Guid id)
    {
        return GetAll().FirstOrDefault(e => e.Id == id);
    }

    public UserProfile Get(Guid id)
    {
        UserProfile result = TryGet(id);
        if (result == null)
            throw new InvalidOperationException($"Can't find user profile with id = '{id}'.");
        return result;
    }

    protected T Modify<T>(Func<T> action)
    {
        T result = action();
        DbContext.SaveChanges();
        return result;
    }

    public UserProfile Save(UserProfile entry)
    {
        return Modify(() =>
            {
                EntityEntry<UserProfile> entityEntry;
                if (Equals(entry.Id, Guid.Empty))
                {
                    entry.Id = Guid.NewGuid();
                    entityEntry = DbContext.UserProfiles.Add(entry);
                }
                else
                {
                    entityEntry = DbContext.Entry(entry);
                    entityEntry.State = EntityState.Modified;
                }
                return entityEntry.Entity;
            });
    }

    public UserProfile Delete(UserProfile entry)
    {
        return Modify(() => DbContext.UserProfiles.Remove(entry).Entity);
    }

    public UserProfile Delete(Guid id)
    {
        UserProfile entry = TryGet(id);
        return entry == null ? null : Delete(entry);
    }
}

#endregion

} `

RowlandShaw commented 5 years ago

One of our use cases is loosely:

class Place {...}
class Depot : Place {...}
class CustomerAddress : Place {...}

And then, we rely on the polymorphism to be able to do stuff like

Place toPlace = ...
CustomerSite cs = toPlace as cs;
if (cs != null)
{
   DoStuff(cs);
}

Appreciate this could be rewritten something like

Place toPlace = ...
if (toPlace.PlaceType == PlaceType.CustomerSite)
{
   CustomerSite cs = context.CustomerSite.SingleOrDefault( c=>c.Id == toPlace.Id)
   DoStuff(cs, p); // Or change DoStuff() to follow the relationship
}

Or

Place toPlace = ...
if (toPlace.PlaceType == PlaceType.CustomerSite)
{
    CustomerSite cs = toPlace.CustomerSite;
   DoStuff(cs, p); // Or change DoStuff() to follow the relationship
}

But that's a lot of code to change just because we've chosen TPT historically

SergeyLimonov commented 5 years ago

@RowlandShaw in any case you can use inheritance in interfaces so you can define following interfaces public interface IPlace {} public interface IDepot: IPlace {} public interface ICustomerAddress : IPlace {}

You can't use your old sources without specific to .NET Core modifications because it differs from .NET Framework in some cases.

IngbertPalm commented 5 years ago

Any news about this feature?

Since the planning of version 3.0 o .NET Core has already started, I'm asking me if this feature will be included in version 3.0 of EF Core or if this feature will be a sort of Vaporware :-)

It would be nice if anyone from MS could give a statement when this feature will be integrated.

ajcvickers commented 5 years ago

@IngbertPalm TPT will not be included in EF Core 3.0, since we don't have the resources to fit it into the schedule for 3.0. We will reconsider the backlog after 3.0, but I can't give any kind of concrete answer as to when we will get to TPT given the number of issues on the backlog compared to the number of resources working on EF.

weitzhandler commented 5 years ago

😢

But at least thanks for the update.