I want to develop a web application separated in modules. Suppose, for example, one module for authentication, one for products catalog, one for products and one for customers.
When using Visual Studio 2022, I created some Web Api projects. The problem I have is that all projects contain a Program.cs file and all projects have a diferent web port assigned by Visual Studio project template.
What should I do? should I create an "entry point" project which will have a Program.cs file and delete Program.cs from the other projects? If that is the case, how can I load the assemblies belonging to the other projects so that I can access controllers and views from the other assemblies?
EDIT:
I have used examples in this page to create my application: https://www.thinktecture.com/en/asp-net-core/modular-monolith/
I created a project called EntryPoint.Core which has this Program.cs file:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers().ConfigureApplicationPartManager(manager =>
{
// Clear all auto detected controllers.
manager.ApplicationParts.Clear();
// Add feature provider to allow "internal" controller
manager.FeatureProviders.Add(new InternalControllerFeatureProvider());
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register a convention allowing to us to prefix routes to modules.
builder.Services.AddTransient<IPostConfigureOptions<MvcOptions>, ModuleRoutingMvcOptionsPostConfigure>();
builder.Services.AddModule<Modules.Authenticate.Core.Startup>("/Api/Authenticate", builder.Configuration);
var app = builder.Build();
app.UseRouting();
var modules = app.Services.GetRequiredService<IEnumerable<Module>>();
foreach (var module in modules)
{
app.Map($"/{module.RoutePrefix}", ab =>
{
ab.UseRouting();
module.Startup.Configure(ab, builder.Configuration);
});
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
On the other hand, I have the Modules.Authenticate.Core project which has this Startup.cs file:
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<SecuWebModulesAuthenticateContext>(options =>
{
options
.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
#if DEBUG
options.LogTo(Console.WriteLine);
#endif
});
// Agrega autenticación
services.AddAuthentication()
.AddCookie("Cookies", options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.AccessDeniedPath = "/Account/AccessDenied";
options.ReturnUrlParameter = "ReturnUrl";
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = true;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = configuration["AuthJwt:Issuer"],
ValidateAudience = true,
ValidAudience = configuration["AuthJwt:Audience"],
ValidateIssuerSigningKey = true,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["AuthJwt:Key"] ?? string.Empty))
};
});
services.AddAuthorization();
}
public void Configure(IApplicationBuilder app, IConfiguration configuration)
{
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
endpoints.MapPost("/Api/Authenticate/CreateToken", [AllowAnonymous] async (string usuario, string clave) =>
{
var optionsBuilder = new DbContextOptionsBuilder<SecuWebModulesAuthenticateContext>();
optionsBuilder = optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
#if DEBUG
optionsBuilder.LogTo(Console.WriteLine);
#endif
using (SecuWebModulesAuthenticateContext db = new(optionsBuilder.Options))
{
ApiAcceso? acceso = await db.ApiAcceso.FirstOrDefaultAsync(aa => aa.ApiAccesoEliminadoEn == null && !aa.ApiAccesoBloqueado && aa.ApiAccesoUsuario == usuario && aa.ApiAccesoClave == clave);
if (acceso is not null)
{
var issuer = configuration["AuthJwt:Issuer"];
var audience = configuration["AuthJwt:Audience"];
var key = configuration["AuthJwt:Key"];
if (key is not null)
{
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("Id", acceso.ApiAccesoId.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, acceso.ApiAccesoUsuario),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
}),
Expires = DateTime.UtcNow.AddMinutes(5),
Issuer = issuer,
Audience = audience,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)), SecurityAlgorithms.HmacSha512Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
var jwtToken = tokenHandler.WriteToken(token);
var stringToken = tokenHandler.WriteToken(token);
return Results.Ok(stringToken);
}
}
return Results.Unauthorized();
}
})
);
}
Finally, I have a project called Modules.Integration which defines the following helper classes:
public class Module
{
/// <summary>
/// Gets the route prefix to all controller and endpoints in the module.
/// </summary>
public string RoutePrefix { get; }
/// <summary>
/// Gets the startup class of the module.
/// </summary>
public IModuleStartup Startup { get; }
/// <summary>
/// Gets the assembly of the module.
/// </summary>
public Assembly Assembly => Startup.GetType().Assembly;
public Module(string routePrefix, IModuleStartup startup)
{
RoutePrefix = routePrefix;
Startup = startup;
}
}
public class ModuleRoutingConvention : IActionModelConvention
{
private readonly IEnumerable<Module> _modules;
public ModuleRoutingConvention(IEnumerable<Module> modules)
{
_modules = modules;
}
public void Apply(ActionModel action)
{
var module = _modules.FirstOrDefault(m => m.Assembly == action.Controller.ControllerType.Assembly);
if (module == null)
{
return;
}
action.RouteValues.Add("module", module.RoutePrefix);
}
}
public class ModuleRoutingMvcOptionsPostConfigure : IPostConfigureOptions<MvcOptions>
{
private readonly IEnumerable<Module> _modules;
public ModuleRoutingMvcOptionsPostConfigure(IEnumerable<Module> modules)
{
_modules = modules;
}
public void PostConfigure(string? name, MvcOptions options)
{
options.Conventions.Add(new ModuleRoutingConvention(_modules));
}
}
public static class ModuleServiceCollection
{
/// <summary>
/// Adds a module.
/// </summary>
/// <param name="services"></param>
/// <param name="routePrefix">The prefix of the routes to the module.</param>
/// <typeparam name="TStartup">The type of the startup class of the module.</typeparam>
/// <returns></returns>
public static IServiceCollection AddModule<TStartup>(this IServiceCollection services, string routePrefix, IConfiguration configuration)
where TStartup : IModuleStartup, new()
{
// Register assembly in MVC so it can find controllers of the module
services.AddControllers().ConfigureApplicationPartManager(manager =>
manager.ApplicationParts.Add(new AssemblyPart(typeof(TStartup).Assembly)));
var startup = new TStartup();
startup.ConfigureServices(services, configuration);
services.AddSingleton(new Module(routePrefix, startup));
return services;
}
}
When I run the EntryPortal.Core project, swagger recognizes the route /Api/Authenticate/CreateToken, however, when I try to test it, a 404 error is thrown.
What else is missing in order the route to be recognized?
EDIT 2:
I realized that ModuleRoutingConvention.Apply method is not being called in my project but in the example project from that page, it is.
I noted that the Apply method, in the example code, is called in this statement:
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
In my case, I am using app.MapControllers() since when I used above instruction, a warning was shown telling that I need to use top level route registration. However, I used both ways to register routes and in neither case, the Apply method was called.
EDIT 3:
I realized that it works when I create a controller and an action in the module.
If I use endpoints.MapPost and I include the action code inside, a 404 error is shown, even when Swagger does show the route in its page.
Thanks Jaime
In .NET 6 and NET 7, Microsoft has Unifies Startup.cs and Program.cs into a single Program.cs file.(Web app template improvements)
Modules.Authenticate.Core project Move code to the program.cs
can you try Microsoft's Areas provides a way to partition an ASP.NET Core 7 Web app into smaller functional groups, each with its own set of Razor Pages, controllers, views, and models.
Microsoft's Areas in ASP.NET Core 7
Area folder structure
Program.cs
_Layout.cshtml