csharpfritz / InstantAPIs

A library that generates Minimal API endpoints for an Entity Framework context.
MIT License
448 stars 57 forks source link

Support for Startup.cs - ConfigureServices and Configure #65

Open Misiu opened 2 years ago

Misiu commented 2 years ago

I have a .NET 6 project, but I'm using Program.cs and 'Startup.cs' approach.

There is a method to add InstantAPI using services.AddInstantAPIs();, but no way to configure it inside Configure(IApplicationBuilder app)

https://github.com/csharpfritz/InstantAPIs/blob/main/Fritz.InstantAPIs/WebApplicationExtensions.cs#L18 supports IEndpointRouteBuilder but a method that supports IApplicationBuilder is missing.

Any plans to add support for it?

verbedr commented 2 years ago

Don't you use the app.UseEndpoints? If you are, you can do.

app.UseEndpoints(x =>
{
    x.MapInstantAPIs<MyContext>(config =>
    {
        config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.All, "addressBook");
    });
    x.MapControllers(); // example of what you are already mapping. 
});
Misiu commented 2 years ago

@verbedr thank you for the reply. Will try that ASAP

Misiu commented 2 years ago

@verbedr this works well. It would be useful to add this example to the readme. What do you think?

Misiu commented 2 years ago

@verbedr quick update: this works partially. When I create a new project using .NET 6 and add everything as shown in readme, I'm able to filter DBSets, using below snippet:

app.MapInstantAPIs<MyContext>(config =>
{
    config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.All, "addressBook");
});

but when I do the same in Progam.cs and Startup.cs approach (a .NET 3.1 project which is converted to .NET 6), nothing is filtered out - I get all the DBSets.

Here is my entire Startup.cs file:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Fritz.InstantAPIs;

namespace Test2
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "Test2", Version = "v1" });
            });

            services.AddInstantAPIs();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Test2 v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();

                endpoints.MapInstantAPIs<MyContext>(config =>
                {
                    config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.Get, "addressBook");
                });
            });
        }
    }
}

MyContext.cs

using Microsoft.EntityFrameworkCore;

public class MyContext : DbContext
{
    public MyContext(DbContextOptions<MyContext> options) : base(options) { }

    public DbSet<Contact> Contacts => Set<Contact>();
    public DbSet<Address> Addresses => Set<Address>();

}

public class Contact
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
}

Quick way to reproduce:

  1. Create a .NET 5 ASP.NET Core Web API
  2. Change TargetFramework to net6.0
  3. Update Swashbuckle.AspNetCore to 6.3.1
  4. Add Fritz.InstantAPIs
  5. Follow steps in readme

You will get this result: image Instead of: image when running from Program.cs approach:

using Fritz.InstantAPIs;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSqlite<MyContext>("Data Source=contacts.db");
builder.Services.AddInstantAPIs();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.MapInstantAPIs<MyContext>(config =>
{
    config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.Get, "addressBook");
});

app.Run();
csharpfritz commented 2 years ago

Yes, let’s get the documentation started and add samples like this

Jeff

On May 4, 2022, at 07:05, Tomasz @.***> wrote:

 @verbedr quick update: this works partially. When I create a new project using .NET 6 and add everything as shown in readme, I'm able to filter DBSets, using below snippet:

app.MapInstantAPIs(config => { config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.All, "addressBook"); }); but when I do the same in Progam.cs and Startup.cs approach (a .NET 3.1 project which is converted to .NET 6), nothing is filtered out - I get all the DBSets.

Here is my entire Startup.cs file:

using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using Fritz.InstantAPIs;

namespace Test2 { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "Test2", Version = "v1" });
        });

        services.AddInstantAPIs();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Test2 v1"));
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();

            endpoints.MapInstantAPIs<MyContext>(config =>
            {
                config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.Get, "addressBook");
            });
        });
    }
}

} MyContext.cs

using Microsoft.EntityFrameworkCore;

public class MyContext : DbContext { public MyContext(DbContextOptions options) : base(options) { }

public DbSet<Contact> Contacts => Set<Contact>();
public DbSet<Address> Addresses => Set<Address>();

}

public class Contact { public int Id { get; set; } public string Name { get; set; } public Address Address { get; set; } }

public class Address { public int Id { get; set; } public string Street { get; set; } public string City { get; set; } public string State { get; set; } public string Zip { get; set; } } Quick way to reproduce:

Create a .NET 5 ASP.NET Core Web API Change TargetFramework to net6.0 Update Swashbuckle.AspNetCore to 6.3.1 Add Fritz.InstantAPIs Follow steps in readme You will get this result:

Instead of:

when running from Program.cs approach:

using Fritz.InstantAPIs;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();

builder.Services.AddSqlite("Data Source=contacts.db"); builder.Services.AddInstantAPIs();

var app = builder.Build();

// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.MapInstantAPIs(config => { config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.Get, "addressBook"); });

app.Run(); — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.

Misiu commented 2 years ago

@csharpfritz I agree on that, but with Progam.cs and Startup.cs approach the filtering (IncludeTable and ExcludeTable) isn't working.

Misiu commented 2 years ago

In WebApplicationExtensions.cs I've changed

public static IEndpointRouteBuilder MapInstantAPIs<D>(this IEndpointRouteBuilder app, Action<InstantAPIsConfigBuilder<D>> options = null) where D : DbContext
{
    switch (app)
    {
        case IApplicationBuilder applicationBuilder:
            AddOpenAPIConfiguration(app, options, applicationBuilder);
            break;
        case IEndpointRouteBuilder routeBuilder:
            AddOpenAPIConfiguration(routeBuilder, options);
            break;
    }

    // Get the tables on the DbContext
    var dbTables = GetDbTablesForContext<D>();

    var requestedTables = !Configuration.Tables.Any() ?
            dbTables :
            Configuration.Tables.Where(t => dbTables.Any(db => db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray();

    MapInstantAPIsUsingReflection<D>(app, requestedTables);

    return app;
}

and added:

private static void AddOpenAPIConfiguration<D>(IEndpointRouteBuilder routeBuilder, Action<InstantAPIsConfigBuilder<D>> options) where D : DbContext
{
    // Check if AddInstantAPIs was called by getting the service options and evaluate EnableSwagger property
    var serviceOptions = routeBuilder.ServiceProvider.GetRequiredService<IOptions<InstantAPIsServiceOptions>>().Value;
    if (serviceOptions == null || serviceOptions.EnableSwagger == null)
    {
        throw new ArgumentException("Call builder.Services.AddInstantAPIs(options) before MapInstantAPIs.");
    }

    //var webApp = (WebApplication)app;
    if (serviceOptions.EnableSwagger == EnableSwagger.Always || (serviceOptions.EnableSwagger == EnableSwagger.DevelopmentOnly /*&& webApp.Environment.IsDevelopment()*/))
    {
        //routeBuilder.UseSwagger();
        //routeBuilder.UseSwaggerUI();
    }

    var ctx = routeBuilder.ServiceProvider.CreateScope().ServiceProvider.GetService(typeof(D)) as D;
    var builder = new InstantAPIsConfigBuilder<D>(ctx);
    if (options != null)
    {
        options(builder);
        Configuration = builder.Build();
    }
}

Things that need to be added: -checking if IsDevelopment() -enabling SwaggerUI.

I'm not sure if InstantAPIs should enable and configure Swagger. It's up to the developer to setup it, besides that, it's enabled by default in new projects.

Misiu commented 2 years ago

@verbedr @csharpfritz thoughts on this? I can create PR showing my changes. Not everyone (including me) can migrate Startup and Program to .NET 6 preferred approach.

As I mentioned before when creating new projects Swagger UI is enabled by default (unless we uncheck a checkbox during project creation in VS2022), so we should only detect if it is enabled and add the correct endpoints.

Ideally Swashbuckle dependency should be removed, because some may prefer and use NSwag (ref: https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?view=aspnetcore-6.0) But this would require adding two more packages - one for Swagger and one for NSwag.