✅ Not able to locate web api controllers in a class library

Hello, I'm having some issues with trying to load web api controllers from a class library. I did try explicitly adding the assembly with AddControllers().AddApplicationPart, but no luck there. I'm thinking it has to do with the pattern I'm using for registering services in separate assemblies.
32 Replies
El Grande Padre
This is what my main project startup looks like:
namespace Core
{
public class Startup
{
private readonly IConfiguration _config;

public Startup(IConfiguration config)
{
_config = config;
}

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddApiServices(_config);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app)
{
app.ConfigureApi(_config);
}
}
}
namespace Core
{
public class Startup
{
private readonly IConfiguration _config;

public Startup(IConfiguration config)
{
_config = config;
}

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddApiServices(_config);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app)
{
app.ConfigureApi(_config);
}
}
}
This is the extension pattern I'm using to piggy back off the startup of the main project inside of the class library
namespace Api.Extensions
{
public static class ApiStartupExt
{
public static void AddApiServices(this IServiceCollection builder, IConfiguration config)
{
builder.AddControllers().AddApplicationPart(typeof(ApiStartupExt).Assembly);
builder.AddSwaggerGen(c => c.SwaggerDoc("v0", new OpenApiInfo
{
Title = "test_service",
Version = "v0"
}));
}
public static void ConfigureApi(this IApplicationBuilder app, IConfiguration config)
{
if (!config["Environment"]!.Equals("Production"))
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "test_service v0");
c.RoutePrefix = string.Empty;
});
}

// Map rest services to port 43000
// In my actual application I also have GRPC endpoints on the same host
// so I need to use the UseWhen method to avoid some issues there
app.UseWhen(context => context.Connection.LocalPort == config.GetValue<int>("Rest:Port"),
builder =>
{
builder.UsePathBase(new PathString("/api"));
builder.UseRouting();
builder.UseEndpoints(endpoints => endpoints.MapControllers());
}
);
}
}
}
namespace Api.Extensions
{
public static class ApiStartupExt
{
public static void AddApiServices(this IServiceCollection builder, IConfiguration config)
{
builder.AddControllers().AddApplicationPart(typeof(ApiStartupExt).Assembly);
builder.AddSwaggerGen(c => c.SwaggerDoc("v0", new OpenApiInfo
{
Title = "test_service",
Version = "v0"
}));
}
public static void ConfigureApi(this IApplicationBuilder app, IConfiguration config)
{
if (!config["Environment"]!.Equals("Production"))
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "test_service v0");
c.RoutePrefix = string.Empty;
});
}

// Map rest services to port 43000
// In my actual application I also have GRPC endpoints on the same host
// so I need to use the UseWhen method to avoid some issues there
app.UseWhen(context => context.Connection.LocalPort == config.GetValue<int>("Rest:Port"),
builder =>
{
builder.UsePathBase(new PathString("/api"));
builder.UseRouting();
builder.UseEndpoints(endpoints => endpoints.MapControllers());
}
);
}
}
}
Here's the output I get when making a request:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 POST http://localhost:43000/api/WeatherForecast - 0
dbug: Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[0]
Wildcard detected, all requests with hosts will be allowed.
dbug: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[1]
POST requests are not supported
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[9]
Connection id "0HMNNSN1PNNGU" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 POST http://localhost:43000/api/WeatherForecast - 0 - 404 0 - 11.6784ms
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 POST http://localhost:43000/api/WeatherForecast - 0
dbug: Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[0]
Wildcard detected, all requests with hosts will be allowed.
dbug: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[1]
POST requests are not supported
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[9]
Connection id "0HMNNSN1PNNGU" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 POST http://localhost:43000/api/WeatherForecast - 0 - 404 0 - 11.6784ms
minimal reproduction repo if anyone takes a look: https://github.com/RyanCallahan312/multiple-projects-with-web-api-controllers
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
I understand the issues with magic string constants, but I don't think thats relevant to the question. The repo this issue is actually happening uses options configs. I just didn't want to copy over all the configuration
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
unless im completely misunderstanding and thats preventing controllers from being discovered
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
thats why i included the repo because i realized it was too much for just copy and pasting files
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
its in the same assembly as the extention
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
No i did not. what does ... do?
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
oh then yes I did specify the controller class same result and same assembly
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
i think i accidentally added that when adding <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
yep thats pretty much what I have in the private repo.
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
The motivating factor for me right now is transitioning away from a ruby world that has bit me in the ass with convoluted "god objects" or "god models" too many times
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
MODiX
MODiX2y ago
appsettings.json:
{
"My": {
"Foo": {
"Kix": 5
}
}
}
{
"My": {
"Foo": {
"Kix": 5
}
}
}
src/Foo/FooOptions.cs:
public class FooOptions
{
public const string SectionName = "My:Foo";

public string Bar {get;set;} = "default value for bar";
public int Kix {get;set;} = -1;
public DateTime? Pouet {get;set;} = default;
}
public class FooOptions
{
public const string SectionName = "My:Foo";

public string Bar {get;set;} = "default value for bar";
public int Kix {get;set;} = -1;
public DateTime? Pouet {get;set;} = default;
}
src/Foo/FooServiceCollectionExtensions.cs:
namespace Microsoft.Extensions.DependencyInjection; // <==== recommanded for service.Add so that you don't clutter Startup file

public class FooServiceCollectionExtensions
{
public static IServiceCollection AddFoo(this IServiceCollection services) =>
services
.AddOptions<FooOptions>()
.BindConfiguration(FooOptions.SectionName)
.Validate(options => options.Kix >= 0, $"The configuration key '{FooOptions.SectionName}:{nameof(Kix)}' cannot be negative")
;

public static IServiceCollection AddFoo(this IServiceCollection services, Action<FooOptions> configure) =>
services
.AddFoo()
.Configure(configure);
namespace Microsoft.Extensions.DependencyInjection; // <==== recommanded for service.Add so that you don't clutter Startup file

public class FooServiceCollectionExtensions
{
public static IServiceCollection AddFoo(this IServiceCollection services) =>
services
.AddOptions<FooOptions>()
.BindConfiguration(FooOptions.SectionName)
.Validate(options => options.Kix >= 0, $"The configuration key '{FooOptions.SectionName}:{nameof(Kix)}' cannot be negative")
;

public static IServiceCollection AddFoo(this IServiceCollection services, Action<FooOptions> configure) =>
services
.AddFoo()
.Configure(configure);
Program.cs / Startup.cs:
services.AddFoo();
// or
services.AddFoo(fooOptions => fooOptions.Kix = 12);
services.AddFoo();
// or
services.AddFoo(fooOptions => fooOptions.Kix = 12);
Bar.cs:
public class Bar
{
private readonly FooOptions _fooOptions;

// .Value in ctor is fine only if it's always ever a non-changing value (no reload and/or no scoped resolution)
public Bar(IOptions<FooOptions> fooOptions)
=> _fooOptions = fooOptions.Value;
}
public class Bar
{
private readonly FooOptions _fooOptions;

// .Value in ctor is fine only if it's always ever a non-changing value (no reload and/or no scoped resolution)
public Bar(IOptions<FooOptions> fooOptions)
=> _fooOptions = fooOptions.Value;
}
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
I updated the project but still same behavior. Still a bit new to anything outside of basic one project crud apps so how would FrameworkReference not being defined break things?
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
MODiX
MODiX2y ago
tebeco#0205
<FrameworkReference Include="Microsoft.AspNetCore.App" />
React with ❌ to remove this embed.
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
El Grande Padre
Adding the framework reference didn't fix it. I'm trying to find a way to list out all registered controllers to see for sure they're not registered
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
MODiX
MODiX2y ago
tebeco#0205
TLDR: if you open a term and dotnet new webapi -n PleaseDeleteMeLater you'll have an example to re-use and delete later
React with ❌ to remove this embed.
El Grande Padre
so that worked... I added a project reference to the LibApi and it found the controller without going into the extension method Solved. I think it was <FrameworkReference Include="Microsoft.AspNetCore.App" /> but the secondary issue of me not including the rest port in the appsettings.json didn't show it working once that was fixed. Guess thats what I get for laziness and not copying over the option configs. ty @TeBeConsulting