OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
458 stars 158 forks source link

How to create EntitySet for all DBSet<> in DbContext dynamically? #169

Open omnilogix opened 3 years ago

omnilogix commented 3 years ago

var odataBuilder = new ODataConventionModelBuilder();

When using the AddEntitySet(), my endpoints fail with the error: System.InvalidOperationException: 'The entity set 'foo' is based on type 'foo' that has no keys defined.'

var t = typeof(foo);
odataBuilder.AddEntitySet(t.Name, odataBuilder.AddEntityType(t));

I am using Ef Core with fluent api to set key and navigation attributes

    [Table(nameof(foo))]
    public partial class foo:
        IEntityTypeConfiguration<foo>
    {
        void IEntityTypeConfiguration<foo>.Configure(EntityTypeBuilder<foo> builder)
        {
            _ = builder.HasKey(x => x.RecKey);
            _ = builder.HasIndex(x => x.Name).IsUnique();
        }
    }

How can I dynamically configure each EntitySet for my DbContext? Explicitly defining each of the hundreds of DbSet<> in my DbContext is both time consuming and error prone.

omnilogix commented 3 years ago

Perhaps the more correct question is: Where do I find ODataEfCoreModelBuilder

xuzhg commented 3 years ago

@omnilogix

You can call "HasKey" to set the key property as below:

Type t= typeof(foo);
PropertyInfo keyProperty = cs.GetProperty("Id");
var config= builder.AddEntityType(t);
config.HasKey(keyProperty);
odataBuilder.AddEntitySet(t.Name, config);

And by the way, What content do you think in "ODataEfCoreModelBuilder"?

omnilogix commented 3 years ago

@xuzhg - Thanks for the example, but this is exactly what I am trying to avoid. Building the model in this way is duplicating the effort that was already put into my EF Core model builder. What would be very helpful is to have:

builder.ODataEfCoreModelBuilder(myDbContext);

This would presumably create a context and examine the metadata for each DbSet<> and construct the odata model to match. For now I have removed the EF Core fluent api calls that defined the primary keys and foreign keys, and placed [Key] and [ForeignKey] attributes onto the class properties. I am currently creating the model like this to avoid manually defining the odata model:

...
var odataBuilder = new ODataConventionModelBuilder();

var dbSets = typeof(myDbContext).GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public)
                                .Where(x => x.PropertyType.IsGenericType
                                        && typeof(DbSet<>).IsAssignableFrom(x.PropertyType.GetGenericTypeDefinition()));

foreach (var dbSet in dbSets)
{
    var pocoType = dbSet.PropertyType.GenericTypeArguments[0];
    var entitySet = odataBuilder.AddEntitySet(pocoType.Name, odataBuilder.AddEntityType(pocoType));
}
return odataBuilder.GetEdmModel();

It's not bad, but I would like to see more compatibility with EF Core. If this functionality could be added to an extension method and packaged as a nuget (Microsoft.AspNetCore.OData.EfCore), it would be ideal. Since my domain model design comes first, it seems the natural place to describe the schema. After that, I would like odata to be able to scaffold from this existing schema whether it was declared using Attributes, or Fluent api.

henrikdahl8240 commented 3 years ago

@omnilogix I had exactly the same need as you, actually several years ago. So I just programmed it using reflection. You can just do the same. I made my own builder class inheriting from the convention builder. Probably you should do the same.

ericcanadas commented 2 years ago

Any news on this investigation? I am facing the same problem... @henrikdahl8240, do you have any skeleton code to share to make this builder with reflection?

henrikdahl8240 commented 2 years ago

@ericcanadas, yes, it's OK. Here you go.

Perhaps you may respond, if it works for you?

public class HDODataConventionModelBuilder<T> : ODataConventionModelBuilder
    where T : DbContext
{
    internal HDODataConventionModelBuilder(T dbContext)
    {
        MethodInfo method = this.GetType().GetMethod("EntitySet");

        typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly).Where(x => x.PropertyType.IsGenericType && x.PropertyType.ContainsGenericParameters == false && x.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)).ToList().ForEach(x =>
        {
            Type argType = x.PropertyType.GetGenericArguments()[0];

            MethodInfo generic = method.MakeGenericMethod(argType);
            var res = generic.Invoke(this, new object[] { x.Name });
        });
    }
}

Best regards,

Henrik Dahl

ericcanadas commented 2 years ago

Hi @henrikdahl8240, It works great !! Thanks for your help

henrikdahl8240 commented 2 years ago

Hello @ericcanadas! I am glad to hear, that you like it. There is a detail, I would like to address to you. As you may see, the where clause uses DeclaredOnly. In case you don't know, DeclaredOnly includes only properties from the current class and not its base classes. It means, that if you make an inheritance hierarchy like DbContextHavingDBSetsWhichShouldBeExposedViaOData : DbContextHavingDBSetsWhichShouldNOTBeExposedViaOData : DbContext, only the DbSet properties of the leaf level DbContext will be served via OData. ´DbSet properties of DbContextHavingDBSetsWhichShouldNOTBeExposedViaOData will not be exposed via OData. Personally I use DbContextHavingDBSetsWhichShouldNOTBeExposedViaOData for my IdP entity sets, because they should obviously not be exposed via OData.

Best regards,

Henrik Dahl

ericcanadas commented 2 years ago

Ok @henrikdahl8240 ! Thanks for the clarification.

atliuhui commented 1 year ago

database first will result

public partial class GlobalSetting
{
    public string Name { get; set; } = null!;
    public object? Value { get; set; }
    public DateTime Timestamp { get; set; }
    public string LastModifiedBy { get; set; } = null!;
}
public partial class ODataDbContext : DbContext
{
    public virtual DbSet<GlobalSetting> GlobalSettings { get; set; }
    ......
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<GlobalSetting>(entity =>
        {
            entity.HasKey(e => e.Name).HasName("PK__GlobalSe__737584F7B877EFE6");
            entity.ToTable("GlobalSettings", "dt");
            entity.Property(e => e.Name)
                .HasMaxLength(300)
                .IsUnicode(false);
            entity.Property(e => e.LastModifiedBy)
                .HasMaxLength(128)
                .HasDefaultValueSql("(user_name())");
            entity.Property(e => e.Timestamp).HasDefaultValueSql("(sysutcdatetime())");
            entity.Property(e => e.Value).HasColumnType("sql_variant");
        });
        ......
    }
}

so best to convert the configuration in ModelBuilder to ODataConventionModelBuilder