I am struggling to get Swagger to document multi-tenanted routes in WebApi.I have used this approach before but never in a self-hosted project. It seems MultipleApiVersions is never invoked - when i've added logging code.
StatupConfig.cs
public class StartupConfig
{
private static ILog _logger = LogManager.GetLogger(nameof(StartupConfig));
public void Configure(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.Filters.Add(new ApiKeyAuthorizationFilter());
config.Filters.Add(new ApiInvocationMetricsFilter());
var assembly = Assembly.GetExecutingAssembly();
var builder = new ContainerBuilder();
builder.RegisterApiControllers(assembly);
builder.RegisterWebApiFilterProvider(config);
builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces();
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
appBuilder.UseAutofacMiddleware(container);
appBuilder.UseAutofacWebApi(config);
appBuilder.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
appBuilder.UseWebApi(config);
config
.EnableSwagger(c =>
{
c.MultipleApiVersions(
ResolveVersionSupportByRouteConstraint,
(vc) =>
{
vc.Version("v2", "API v2");
vc.Version("v1", "API v1");
});
c.RootUrl((message) => ConfigurationManager.AppSettings["SwaggerRoot"]);
c.IncludeXmlComments($#"{System.AppDomain.CurrentDomain.BaseDirectory}\API.xml");
c.OperationFilter<AddRequiredApiKeyParameter>();
c.DescribeAllEnumsAsStrings(true);
})
.EnableSwaggerUi(ui =>
{
ui.EnableDiscoveryUrlSelector();
});
}
public static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion)
{
try
{
var versionConstraint = (apiDesc.Route.Constraints.ContainsKey("apiVersion"))
? apiDesc.Route.Constraints["apiVersion"] as ApiVersionConstraint
: null;
return versionConstraint?.AllowedVersion.Split('|').Select(x => x.ToLowerInvariant()).ToList().Contains(targetApiVersion.ToLowerInvariant()) ?? false;
}
catch (System.Exception excep)
{
_logger.Error("An error occurred resolving version support", excep);
throw;
}
}
}
Note: This predates WebApiVersioning so I am using a route constraint:
public class ApiVersion2RoutePrefixAttribute : RoutePrefixAttribute
{
private const string RouteBase = "api/{apiVersion:apiVersionConstraint(v2)}";
private const string PrefixRouteBase = "api/{apiVersion:apiVersionConstraint(v2)}/";
public ApiVersion2RoutePrefixAttribute(string routePrefix)
: base(string.IsNullOrWhiteSpace(routePrefix) ? "api/{apiVersion:apiVersionConstraint(v2)}" : "api/{apiVersion:apiVersionConstraint(v2)}/" + routePrefix)
{
}
}
Am I missing something here?
Thanks
KH
This problem was solved by ensuring any Name parameters in the Route attribute are unique across both controller versions. I had an operation named Add, with a Route Name parameter of Add across both controllers and this was preventing Swagger from functioning.
i.e this
[Route("", Name = nameof(AddAdvertiser))]
changed to this
[Route("", Name = nameof(V1AddAdvertiser))]
Related
I've researched this issue and found a lot of articles and also q+as on here but nothing for my scenario. I have an asp.net core 3 API with 2 versions, 1 and 2. The API has 3 consumers, ConA, ConB, and ConC, and 3 controllers. ConA accesses controllers 1 and 2, ConB accesses only controller 3, and ConC accesses one endpoint from controller 1 and one endpoint from controller 3. For v1 I show everything but I now have a requirement to filter v2 endpoints by API consumer.
What I'm trying to do is create a Swagger document for each consumer that only shows the endpoints they can access. It's easy to do for ConA and ConB as I can use [ApiExplorerSettings(GroupName = "v-xyz")] where v-xyz can be restricted by consumer and then split the Swagger documents that way. The problem is showing the endpoints for ConC - they don't have their own controller so I can't give them a GroupName. Here's a simplified version of the code:
public void ConfigureServices(IServiceCollection services)
{
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VV";
options.SubstituteApiVersionInUrl = true;
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo() { Title = "My API - Version 1", Version = "v1.0" });
c.SwaggerDoc("v2-conA", new OpenApiInfo() { Title = "My API - Version 2", Version = "v2.0" });
c.SwaggerDoc("v2-conB", new OpenApiInfo() { Title = "My API - Version 2", Version = "v2.0" });
c.SwaggerDoc("v2-conC", new OpenApiInfo() { Title = "My API - Version 2", Version = "v2.0" });
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
c.EnableAnnotations();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.EnableDeepLinking();
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.SwaggerEndpoint("/swagger/v2-conA/swagger.json", "My API V2 ConA");
c.SwaggerEndpoint("/swagger/v2-conB/swagger.json", "My API V2 ConB");
c.SwaggerEndpoint("/swagger/v2-conC/swagger.json", "My API V2 Con3");
});
}
Version 1 controllers:
[Route("api/account")]
[ApiController]
[ApiExplorerSettings(GroupName = "v1")]
public class AccountController : ControllerBase
{
[HttpGet("get-user-details")]
public ActionResult GetUserDetails([FromQuery]string userId)
{
return Ok(new { UserId = userId, Name = "John", Surname = "Smith", Version = "V1" });
}
}
[Route("api/account-admin")]
[ApiController]
[ApiExplorerSettings(GroupName = "v1")]
public class AccountAdminController : ControllerBase
{
[HttpPost("verify")]
public ActionResult Verify([FromBody]string userId)
{
return Ok($"{userId} V1");
}
}
[Route("api/notification")]
[ApiController]
[ApiExplorerSettings(GroupName = "v1")]
public class NotificationController : ControllerBase
{
[HttpPost("send-notification")]
public ActionResult SendNotification([FromBody]string userId)
{
return Ok($"{userId} V1");
}
}
Version 2 controllers (namespaced in separate folder "controllers/v2"):
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/account")]
[ApiController]
[ApiExplorerSettings(GroupName = "v2-conA")]
public class AccountController : ControllerBase
{
[HttpGet("get-user-details")]
[SwaggerOperation(Tags = new[] { "ConA - Account" })]
public ActionResult GetUserDetails([FromQuery]string userId)
{
return Ok($"{userId} V2");
}
}
[Route("api/v{version:apiVersion}/account-admin")]
[ApiController]
[ApiVersion("2.0")]
[ApiExplorerSettings(GroupName = "v2-conB")]
public class AccountAdminController : ControllerBase
{
[HttpPost("verify")]
[SwaggerOperation(Tags = new[] { "ConB - Account Admin", "ConC - Account Admin" })]
public ActionResult Verify([FromBody] string userId)
{
return Ok($"{userId} V2");
}
}
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/notification")]
[ApiController]
[ApiExplorerSettings(GroupName = "v2-conA")]
public class NotificationController : ControllerBase
{
[HttpPost("send-notification")]
[SwaggerOperation(Tags = new[] { "ConA - Notification", "ConC - Notification" })]
public ActionResult SendNotification([FromBody] string userId)
{
return Ok($"{userId} V2");
}
}
This gets me some of the way in that I can see the endpoints for ConA and ConB, although it's not perfect as it's showing duplicate endpoints, but I'm stuck on how to show the endpoints for ConC (who can see one endpoint from controller 1 and one from controller 3). My next attempt will be to go back to showing all endpoints in version 2 and then filtering using IDocumentFilter if I can't get the above working somehow. Any thoughts or tips greatly appreciated 👍
I had to do this recently, we also had multiple consumers and needed to filter the endpoints per consumer. I used a DocumentFilter and filtered the endpoints using tags.
There's a fair bit of code in it so I stuck the full solution on Github: https://github.com/cbruen1/SwaggerFilter
public class Startup
{
private static Startup Instance { get; set; }
private static string AssemblyName { get; }
private static string FullVersionNo { get; }
private static string MajorMinorVersionNo { get; }
static Startup()
{
var fmt = CultureInfo.InvariantCulture;
var assemblyName = Assembly.GetExecutingAssembly().GetName();
AssemblyName = assemblyName.Name;
FullVersionNo = string.Format(fmt, "v{0}", assemblyName.Version.ToString());
MajorMinorVersionNo = string.Format(fmt, "v{0}.{1}",
assemblyName.Version.Major, assemblyName.Version.Minor);
}
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Instance = this;
}
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.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VV";
options.SubstituteApiVersionInUrl = true;
});
// Use an IConfigureOptions for the settings
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(c =>
{
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
// Group by tag
c.EnableAnnotations();
// Include comments for current assembly - right click the project and turn on this otion in the build properties
var xmlFile = $"{AssemblyName}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.EnableDeepLinking();
// Build a swagger endpoint for each API version and consumer
c.SwaggerEndpoint($"/swagger/{Constants.ApiVersion1}/swagger.json", "MyAccount API V1");
c.SwaggerEndpoint($"/swagger/{Constants.ApiConsumerGroupNameConA}/swagger.json", $"MyAccount API V2 {Constants.ApiConsumerNameConA}");
c.SwaggerEndpoint($"/swagger/{Constants.ApiConsumerGroupNameConB}/swagger.json", $"MyAccount API V2 {Constants.ApiConsumerNameConB}");
c.SwaggerEndpoint($"/swagger/{Constants.ApiConsumerGroupNameConC}/swagger.json", $"MyAccount API V2 {Constants.ApiConsumerNameConC}");
c.DocExpansion(DocExpansion.List);
});
}
}
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
public void Configure(SwaggerGenOptions options)
{
// Filter out api-version parameters globally
options.OperationFilter<ApiVersionFilter>();
// Create Swagger documents per version and consumer
options.SwaggerDoc(Constants.ApiVersion1, CreateInfoForApiVersion("v1.0", "My Account API V1"));
options.SwaggerDoc(Constants.ApiConsumerGroupNameConA, CreateInfoForApiVersion("v2.0", $"My Account API V2 {Constants.ApiConsumerNameConA}"));
options.SwaggerDoc(Constants.ApiConsumerGroupNameConB, CreateInfoForApiVersion("v2.0", $"My Account API V2 {Constants.ApiConsumerNameConB}"));
options.SwaggerDoc(Constants.ApiConsumerGroupNameConC, CreateInfoForApiVersion("v2.0", $"My Account API V2 {Constants.ApiConsumerNameConC}"));
// Include all paths
options.DocInclusionPredicate((name, api) => true);
// Filter endpoints based on consumer
options.DocumentFilter<SwaggerDocumentFilter>();
// Take first description on any conflict
options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
}
static OpenApiInfo CreateInfoForApiVersion(string version, string title)
{
var info = new OpenApiInfo()
{
Title = title,
Version = version
};
return info;
}
}
public class SwaggerDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
// Key is read-only so make a copy of the Paths property
var pathsPerConsumer = new OpenApiPaths();
var currentConsumer = GetConsumer(swaggerDoc.Info.Title);
IDictionary<string, OpenApiSchema> allSchemas = swaggerDoc.Components.Schemas;
if (swaggerDoc.Info.Version.Contains(Constants.ApiVersion2))
{
foreach (var path in swaggerDoc.Paths)
{
// If there are any tags (all methods are decorated with "SwaggerOperation(Tags = new[]...") with the current consumer name
if (path.Value.Operations.Values.FirstOrDefault().Tags
.Where(t => t.Name.Contains(currentConsumer)).Any())
{
// Remove tags not applicable to the current consumer (for endpoints where multiple consumers have access)
var newPath = RemoveTags(currentConsumer, path);
// Add the path to the collection of paths for current consumer
pathsPerConsumer.Add(newPath.Key, newPath.Value);
}
}
//// Whatever objects are used as parameters or return objects in the API will be listed under the Schemas section in the Swagger UI
//// Use below to filter them based on the current consumer - remove schemas not belonging to the current path
//foreach (KeyValuePair<string, OpenApiSchema> schema in allSchemas)
//{
// // Get the schemas for current consumer
// if (Constants.ApiPathSchemas.TryGetValue(currentConsumer, out List<string> schemaList))
// {
// if (!schemaList.Contains(schema.Key))
// {
// swaggerDoc.Components.Schemas.Remove(schema.Key);
// }
// }
//}
}
else
{
// For version 1 list version 1 endpoints only
foreach (var path in swaggerDoc.Paths)
{
if (!path.Key.Contains(Constants.ApiVersion2))
{
pathsPerConsumer.Add(path.Key, path.Value);
}
}
}
swaggerDoc.Paths = pathsPerConsumer;
}
public KeyValuePair<string, OpenApiPathItem> RemoveTags(string currentConsumer, KeyValuePair<string, OpenApiPathItem> path)
{
foreach (var item in path.Value.Operations.Values?.FirstOrDefault().Tags?.ToList())
{
// If the tag name doesn't contain the current consumer name remove it
if (!item.Name.Contains(currentConsumer))
{
path.Value.Operations.Values?.FirstOrDefault().Tags?.Remove(item);
}
}
return path;
}
private string GetConsumer(string path)
{
if (path.Contains(Constants.ApiConsumerNameConA))
{
return Constants.ApiConsumerNameConA;
}
else if (path.Contains(Constants.ApiConsumerNameConB))
{
return Constants.ApiConsumerNameConB;
}
else if (path.Contains(Constants.ApiConsumerNameConC))
{
return Constants.ApiConsumerNameConC;
}
return string.Empty;
}
}
public class ApiVersionFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Remove version parameter field from Swagger UI
var parametersToRemove = operation.Parameters.Where(x => x.Name == "api-version").ToList();
foreach (var parameter in parametersToRemove)
{
operation.Parameters.Remove(parameter);
}
}
}
public static class Constants
{
// Swagger UI grouping and filtering
public const string ApiVersion1 = "v1";
public const string ApiVersion2 = "v2";
// The full consumer name
public const string ApiConsumerNameConA = "Consumer A";
public const string ApiConsumerNameConB = "Consumer B";
public const string ApiConsumerNameConC = "Consumer C";
// Specify the group name - this appears in the Swagger UI drop-down
public const string ApiConsumerGroupNameConA = "v2-conA";
public const string ApiConsumerGroupNameConB = "v2-conB";
public const string ApiConsumerGroupNameConC = "v2-conC";
// Decorate each controller method with the tag names below - this determines
// what consumer can access what endpoint, and also how the endpoints are
// grouped and named in the Swagger UI
// Swagger ConA tag names
public const string ApiConsumerTagNameConAAccount = ApiConsumerNameConA + " - Account";
public const string ApiConsumerTagNameConANotification = ApiConsumerNameConA + " - Notification";
// Swagger ConB tag names
public const string ApiConsumerTagNameConBAccountAdmin = ApiConsumerNameConB + " - Account Admin";
// Swagger ConC tag names
public const string ApiConsumerTagNameConCAccountAdmin = ApiConsumerNameConC + " - Account Admin";
public const string ApiConsumerTagNameConCNotification = ApiConsumerNameConC + " - Notification";
// Store the schemes belonging to each Path for Swagger so only the relevant ones are shown in the Swagger UI
public static IReadOnlyDictionary<string, List<string>> ApiPathSchemas;
static Constants()
{
ApiPathSchemas = new Dictionary<string, List<string>>()
{
//// Whatever objects are used as parameters or return objects in the API will be listed under the Schemas section in the Swagger UI
//// Use below to add the list required by each consumer
// Consumer A has access to all so only specify those for B and C
// { ApiConsumerNameConB, new List<string>() { "SearchOutcome", "AccountDetails", "ProblemDetails" }},
// { ApiConsumerNameConC, new List<string>() { "NotificationType", "SendNotificationRequest", "ProblemDetails" }}
};
}
}
// v1 controllers
[Route("api/account-admin")]
[ApiController]
[ApiExplorerSettings(GroupName = Constants.ApiVersion1)]
public class AccountAdminController : ControllerBase
{
[HttpPost("verify")]
public ActionResult Verify([FromBody]string userId)
{
return Ok($"{userId} V1");
}
}
[ApiController]
[ApiExplorerSettings(GroupName = Constants.ApiVersion1)]
public class AccountController : ControllerBase
{
[HttpGet("api/account/get-user-details")]
public ActionResult GetUserDetails([FromQuery]string userId)
{
return Ok(new { UserId = userId, Name = "John", Surname = "Smith", Version = "V1" });
}
}
[Route("api/notification")]
[ApiController]
[ApiExplorerSettings(GroupName = Constants.ApiVersion1)]
public class NotificationController : ControllerBase
{
[HttpPost("send-notification")]
public ActionResult SendNotification([FromBody]string userId)
{
return Ok($"{userId} V1");
}
}
// v2 controllers
[Route("api/v{version:apiVersion}/account-admin")]
[ApiController]
[ApiVersion("2.0")]
public class AccountAdminController : ControllerBase
{
[HttpPost("verify")]
[SwaggerOperation(Tags = new[] { Constants.ApiConsumerTagNameConBAccountAdmin, Constants.ApiConsumerTagNameConCAccountAdmin })]
public ActionResult Verify([FromBody] string userId)
{
return Ok($"{userId} V2");
}
}
[Route("api/v{version:apiVersion}/account")]
[ApiController]
[ApiVersion("2.0")]
public class AccountController : ControllerBase
{
[HttpGet("get-user-details")]
[SwaggerOperation(Tags = new[] { Constants.ApiConsumerTagNameConAAccount })]
public ActionResult GetUserDetails([FromQuery]string userId)
{
return Ok($"{userId} V2");
}
}
[Route("api/v{version:apiVersion}/notification")]
[ApiController]
[ApiVersion("2.0")]
public class NotificationController : ControllerBase
{
[HttpPost("send-notification")]
[SwaggerOperation(Tags = new[] { Constants.ApiConsumerTagNameConANotification, Constants.ApiConsumerTagNameConCNotification })]
public ActionResult SendNotification([FromBody] string userId)
{
return Ok($"{userId} V2");
}
}
Solution structure:
API filtered for Consumer C:
I am developing asp.net core 3.1 GraphQL based APIs. I used the below reference article to setup automatic DI configuration in the API layer and have used the nuget package : NetCore.AutoRegisterDi
https://www.thereformedprogrammer.net/asp-net-core-fast-and-automatic-dependency-injection-setup/
Here goes the code details:
Code:
Startup.cs:
public virtual void ConfigureServices(IServiceCollection services) => services
.AddGraphQLResolvers()
.AddProjectRepositories();
ProjectServiceCollectionExtensions.cs
public static class ProjectServiceCollectionExtensions
{
public static IServiceCollection AddProjectRepositories(this IServiceCollection services) =>
services.RegisterAssemblyPublicNonGenericClasses(Assembly.GetAssembly(typeof(CommonService)))
.Where(c => c.Name.EndsWith("Persistence"))
.AsPublicImplementedInterfaces(ServiceLifetime.Scoped);
public static IServiceCollection AddGraphQLResolvers(this IServiceCollection services) =>
services
.AddScoped<ICountriesResolver, CountriesResolver>()
.AddScoped<ICountryGroupsResolver, CountryGroupsResolver>()
.AddScoped<IDisclaimerResolver, DisclaimerResolver>();
}
Here in the above CommonService is part of the Service Layer that ends with Persistence.
CountriesResolver.cs
public class CountriesResolver : Resolver, ICountriesResolver
{
private readonly ICountryService _countryService;
private readonly IHttpContextAccessor _accessor;
private readonly IDataLoaderContextAccessor _dataLoaderContextAccessor;
public CountriesResolver(ICountryService countryService, IHttpContextAccessor accessor, IDataLoaderContextAccessor dataLoaderContextAccessor)
{
_countryService = countryService ?? throw new ArgumentNullException(nameof(countryService));
_accessor = accessor;
_dataLoaderContextAccessor = dataLoaderContextAccessor;
}
public void Resolve(GraphQLQuery graphQLQuery)
{
var language = _accessor.HttpContext.Items["language"] as LanguageDTO;
graphQLQuery.FieldAsync<ResponseGraphType<CountryResultType>>("countriesresponse", arguments: new QueryArguments(new QueryArgument<IdGraphType>{Name = "pageNo", Description = "page number"}, new QueryArgument<IdGraphType>{Name = "pageSize", Description = "page size"}), resolve: async context =>
{
var pageNo = context.GetArgument<int>("pageNo") == 0 ? 1 : context.GetArgument<int>("pageNo");
var pageSize = context.GetArgument<int>("pageSize") == 0 ? 100 : context.GetArgument<int>("pageSize");
if (language != null)
{
var loader = _dataLoaderContextAccessor.Context.GetOrAddLoader("GetAllCountries", () => _countryService.GetAllCountriesAsync(language, pageNo, pageSize));
var list = await context.TryAsyncResolve(async c => await loader.LoadAsync());
return Response(list);
}
return null;
}
, description: "All Countries data");
}
}
ICommonService.cs
using Author.Query.Persistence.DTO;
using System.Threading.Tasks;
namespace Author.Query.Persistence.Interfaces
{
public interface ICommonService
{
LanguageDTO GetLanguageFromLocale(string locale);
Task<LanguageDTO> GetLanguageFromLocaleAsync(string locale);
}
}
CommonService.cs
namespace Author.Query.Persistence
{
public class CommonService : ICommonService
{
private readonly AppDbContext _dbContext;
private readonly IOptions<AppSettings> _appSettings;
private readonly IMapper _mapper;
private readonly ICacheService<Languages, LanguageDTO> _cacheService;
public CommonService(AppDbContext dbContext, IOptions<AppSettings> appSettings, IMapper mapper, ICacheService<Languages, LanguageDTO> cacheService)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_appSettings = appSettings;
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
}
//public Languages GetLanguageFromLocale(string locale)
public LanguageDTO GetLanguageFromLocale(string locale)
{
return GetLanguagesByFilter(GetFilterValues(locale, true).ToArray());
}
}
}
ICountryService.cs
namespace Author.Query.Persistence.Interfaces
{
public interface ICountryService
{
Task<CountryResult> GetAllCountriesAsync(LanguageDTO language, int pageNo, int pageSize);
Task<CountryDTO> GetCountryAsync(LanguageDTO language, int countryId);
}
}
CountryService.cs
namespace Author.Query.Persistence
{
public class CountryService : ICountryService
{
private readonly AppDbContext _dbContext;
private readonly IOptions<AppSettings> _appSettings;
private readonly ICacheService<Images, ImageDTO> _cacheService;
public CountryService(TaxathandDbContext dbContext, IOptions<AppSettings> appSettings, ICacheService<Images, ImageDTO> cacheService)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
_appSettings = appSettings;
}
public async Task<CountryResult> GetAllCountriesAsync(LanguageDTO language, int pageNo, int pageSize)
{
var localeLangId = language.LanguageId;
var dftLanguageId = int.Parse(_appSettings.Value.DefaultLanguageId);
// By default pick the localLanguage value
var countries = await GetAllCountriesDataAsync(localeLangId, pageNo, pageSize);
// If localLanguage data is not available then pull the data based on default language
if (countries.Countries.Count == 0)
{
countries = await GetAllCountriesDataAsync(dftLanguageId, pageNo, pageSize);
}
return countries;
}
public async Task<CountryDTO> GetCountryAsync(LanguageDTO language, int countryId)
{
var localeLangId = language.LanguageId;
var dftLanguageId = int.Parse(_appSettings.Value.DefaultLanguageId);
//var country = new CountryDTO();
// By default pick the localLanguage value
var country = await GetCountryDetailsAsync(countryId, localeLangId);
// If localLanguage data is not available then pull the data based on default language
if (country == null)
{
country = await GetCountryDetailsAsync(countryId, dftLanguageId);
}
return country;
}
private async Task<CountryDTO> GetCountryDetailsAsync(int countryId, int languageId)
{
var images = await _cacheService.GetAllAsync("imagesCacheKey");
var country = await _dbContext.Countries.AsNoTracking().FirstOrDefaultAsync(c => c.CountryId.Equals(countryId) && c.IsPublished.Equals(true) && c.LanguageId.Equals(languageId));
if (country == null)
{
return null;
}
var countryDTO = new CountryDTO{Uuid = country.CountryId, PNGImagePath = images.FirstOrDefault(im => im.ImageId.Equals(country.PNGImageId)).FilePath, SVGImagePath = images.FirstOrDefault(im => im.ImageId.Equals(country.SVGImageId)).FilePath, DisplayName = country.DisplayName, DisplayNameShort = country.DisplayName, Name = Helper.ReplaceChars(country.DisplayName), Path = Helper.ReplaceChars(country.DisplayName), CompleteResponse = true};
return countryDTO;
}
private async Task<CountryResult> GetAllCountriesDataAsync(int languageId, int pageNo, int pageSize)
{
var countryList = new CountryResult();
var images = await _cacheService.GetAllAsync("imagesCacheKey");
var countries = await _dbContext.Countries.Where(cc => cc.IsPublished.Equals(true) && cc.LanguageId.Equals(languageId)).Select(c => new
{
c.CountryId, c.DisplayName, c.PNGImageId, c.SVGImageId
}
).OrderByDescending(c => c.CountryId).Skip((pageNo - 1) * pageSize).Take(pageSize).AsNoTracking().ToListAsync();
if (countries.Count == 0)
{
return null;
}
countryList.Countries.AddRange(countries.Select(co => new CountryDTO{Uuid = co.CountryId, PNGImagePath = images.FirstOrDefault(im => im.ImageId.Equals(co.PNGImageId)).FilePath, SVGImagePath = images.FirstOrDefault(im => im.ImageId.Equals(co.SVGImageId)).FilePath, DisplayName = co.DisplayName, DisplayNameShort = co.DisplayName, Name = Helper.ReplaceChars(co.DisplayName), Path = Helper.ReplaceChars(co.DisplayName), CompleteResponse = true}));
return countryList;
}
}
}
Error:
System.AggregateException
HResult=0x80131500
Message=Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Author.Query.New.API.GraphQL.Resolvers.ICountriesResolver Lifetime: Scoped ImplementationType: Author.Query.New.API.GraphQL.Resolvers.CountriesResolver':
Unable to resolve service for type 'Author.Query.Persistence.Interfaces.ICountryService' while attempting to activate 'Author.Query.New.API.GraphQL.Resolvers.CountriesResolver'.) (Error while validating the service descriptor 'ServiceType:
Author.Query.New.API.GraphQL.Resolvers.ICountryGroupsResolver Lifetime: Scoped ImplementationType: Author.Query.New.API.GraphQL.Resolvers.CountryGroupsResolver': Unable to resolve service for type 'Author.Query.Persistence.Interfaces.ICountryGroupService' while attempting to activate 'Author.Query.New.API.GraphQL.Resolvers.CountryGroupsResolver'.) (Error while validating the service descriptor 'ServiceType: Author.Query.New.API.GraphQL.Resolvers.IDisclaimerResolver Lifetime: Scoped ImplementationType: Author.Query.New.API.GraphQL.Resolvers.DisclaimerResolver': Unable to resolve service for type 'Author.Query.Persistence.Interfaces.IDisclaimerService' while attempting to activate 'Author.Query.New.API.GraphQL.Resolvers.DisclaimerResolver'.)
Source=Microsoft.Extensions.DependencyInjection
StackTrace:
at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(IEnumerable`1 serviceDescriptors, ServiceProviderOptions options)
at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
at Microsoft.Extensions.DependencyInjection.DefaultServiceProviderFactory.CreateServiceProvider(IServiceCollection containerBuilder)
at Microsoft.Extensions.Hosting.Internal.ServiceFactoryAdapter`1.CreateServiceProvider(Object containerBuilder)
at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
at Microsoft.Extensions.Hosting.HostBuilder.Build()
at Author.Query.New.API.Program.Main(String[] args) in /src/QueryStack/Author.Query.New.API/Program.cs:line 15
Inner Exception 1:
InvalidOperationException: Error while validating the service descriptor 'ServiceType: Author.Query.New.API.GraphQL.Resolvers.ICountriesResolver Lifetime: Scoped ImplementationType:
Author.Query.New.API.GraphQL.Resolvers.CountriesResolver':
Unable to resolve service for type 'Author.Query.Persistence.Interfaces.ICountryService' while attempting to activate
'Author.Query.New.API.GraphQL.Resolvers.CountriesResolver'.
Inner Exception 2:
InvalidOperationException: Unable to resolve service for type
'Author.Query.Persistence.Interfaces.ICountryService' while attempting to activate
'Author.Query.New.API.GraphQL.Resolvers.CountriesResolver'.
Can anyone help me to know how to fix this issue?
The error says that :
Unable to resolve service for type
'Author.Query.Persistence.Interfaces.ICountryService' while attempting to activate
'Author.Query.New.API.GraphQL.Resolvers.CountriesResolver'
ICountryService is never registered. AddGraphQLResolvers registers only ICountriesResolver, ICountryGroupsResolver and IDisclaimerResolver.
The method AddProjectRepositories only registers classes whose name ends in Persistence that appear in the same namespace as CommonService. It never registers CommonService itself.
The service must be registered, eg with :
services.AddScoped<ICommonService, CommonService>();
Your method AddProjectRepositories() does not register CountryService, because the Where filter in RegisterAssemblyPublicNonGenericClasses() looks at classes (Type), not at namespaces.
So perhaps you can change your filter:
services.RegisterAssemblyPublicNonGenericClasses(Assembly.GetAssembly(typeof(CommonService)))
.Where(c => c.Name.EndsWith("Service")) // <-- Change "Persistence" to "Service"
.AsPublicImplementedInterfaces(ServiceLifetime.Scoped);
That should register all classes that end with "Service" in the Assembly that contains CommonService.
For me it was a simple mistake. I had a class with the same name in another namespace and referenced the wrong class/forgot to delete the duplicate class and add the correct one to my startup.
I had a service called UserService in myapp.Utils and another in myapp.Services. I was referencing myapp.Utils when I meant to delete that one and only use myapp.Services. I was incorrectly injecting the one in myapp.Utils when my controllers were set up to use the one in myapp.Services.
I am working on the Unit Testing in Asp.Net Mvc Web Api.
I have 2 projects
1: Catalog.Api - This contains all the controllers
2: Catalog.UnitTests - This contains the Unit Test for controllers
All Controllers are Inherit with "ApiController" and every controller has custom filter [AuthenticationFilter]. Here is my values controller.
[AuthenticationFilter]
public class ValuesController : ApiController
{
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
public string Get(int id)
{
return "value";
}
// POST api/values
public void Post([FromBody]string value)
{
}
// PUT api/values/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/values/5
public void Delete(int id)
{
}
}
And my custom is check the authorization token. Here it is
public class AuthenticationFilter: AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var request = actionContext.Request;
var authorization = request.Headers.Authorization;
if (authorization == null || authorization.Scheme != "Bearer")
{
ShowAuthenticationError(actionContext, "Authorization required");
return;
}
if (string.IsNullOrEmpty(authorization.Parameter))
{
ShowAuthenticationError(actionContext, "Missing Jwt Token");
return;
}
var token = authorization.Parameter;
var principal = AuthenticateToken(token);
if (principal == null)
{
ShowAuthenticationError(actionContext, "Invalid token");
return;
}
base.OnAuthorization(actionContext);
}
private static void ShowAuthenticationError(HttpActionContext filterContext, string message)
{
var responseDTO = new ResponseDTO() { Code = 401, Message = message };
filterContext.Response =
filterContext.Request.CreateResponse(HttpStatusCode.Unauthorized, responseDTO);
}
}
public class ResponseDTO
{
public int Code { get; set; }
public string Message { get; set; }
}
Now in the Unit Test project i have a class and unit test method.
[TestMethod]
public void CheckFilter()
{
try
{
var controller = new ValuesController();
var controllerContext = new HttpControllerContext();
var request = new HttpRequestMessage();
request.Headers.Add("Authorization", "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InVhbGkiLCJlbWFpbCI6InVhbGlAaW5yZWFjaGNlLmNvbSIsIm5iZiI6MTU2NDY0NjIyMSwiZXhwI");
controllerContext.Request = request;
controller.ControllerContext = controllerContext;
var result = controller.Get();
Assert.IsTrue(result.Any());
}
catch (Exception ex)
{
Assert.Fail();
}
}
I am calling my controller by adding reference of API project into my unit test project. So all controllers are available in the unit test project.
Issue is that when i call the values controller it always return the data. And when i remove the request and header so it is also returning the data but in that case that will be unauthorized.
I think my custom filter is not calling. How should that would be called and authenticate the user.
I check your question and configure that issue it is basically you are calling the controller directly.
Basically controller is a class and when you are calling that it is behaving like a simple class and call the method and send back the result. It is simple and clear
But in your situation you have project for your api so can do this.
[TestMethod]
public void CheckFilter()
{
try
{
var config = new HttpConfiguration();
// This is the resgister method which is written in you Api project. That code is after this method this method because i did the same thing to call my controller.
Catalog.Api.WebApiConfig.Register(config);
using (var server = new HttpServer(config))
{
var client = new HttpClient(server);
string url = "http://localhost:PortNumberOfProject/api/values";
var request = new HttpRequestMessage
{
RequestUri = new Uri(url),
Method = HttpMethod.Get
};
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "Your Token");
var response = await client.SendAsync(request);
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
}
catch (Exception ex)
{
Assert.Fail();
}
}
Here is the WebApi Register method of Api project which is used to register the Api and Routes.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Here is your controller as it is. And now debug your test and add a break point in your [AuthenticationFilter] and OnAuthorization method.
[AuthenticationFilter]
public class ValuesController : ApiController
{
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
I have added a custom authorization scheme like this...
public class AuthHandler : AuthenticationHandler<AuthOptions>
{
private readonly IUserIdentifierProvider userIdentifierProvider;
public AuthHandler(IUserIdentifierProvider userIdentifierProvider, IOptionsMonitor<AuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) :
base(options, logger, encoder, clock)
{
this.userIdentifierProvider = userIdentifierProvider;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
var ticket = ...
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
public static class AuthMiddlewareAppBuilderExtensions
{
public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<AuthOptions> configureOptions)
{
return builder.AddScheme<AuthOptions, AuthHandler>("Custom Scheme", "Custom Auth", configureOptions);
}
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddMemoryCache();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Custom Scheme";
options.DefaultChallengeScheme = "Custom Auth";
})
.AddCustomAuth(o => {});
services.AddDbContext<DomainDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
var serviceProvider = ConfigureUnity(services);
return serviceProvider;
}
When MVC creates the AuthHandler class upon a request, it doesn't use my dependency injection container returned from ConfigureServices.
I get the exception...
InvalidOperationException: Unable to resolve service for type
'Web.Auth.Abstract.IUserIdentifierProvider' while attempting to
activate 'AuthHandler'.
Why is it not using my container?
It works if I do...
services.AddTransient<IUserIdentifierProvider, UserIdentifierProvider>();
inside ConfigureServices. It doesn't appear to be looking in my container at all. So where on earth is it getting the instance from? It must be keeping a reference to the IServiceCollection passed to ConfigureServices and uses it instead of the one it's supposed to.
Looking with Reflector, the ConfigureServices method is called by the following function...
public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName)
{
ConfigureBuilder builder = FindConfigureDelegate(startupType, environmentName);
ConfigureServicesBuilder builder2 = FindConfigureServicesDelegate(startupType, environmentName);
ConfigureContainerBuilder configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName);
object instance = null;
if (!builder.MethodInfo.get_IsStatic() || ((builder2 != null) && !builder2.MethodInfo.get_IsStatic()))
{
instance = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType);
}
Func<IServiceCollection, IServiceProvider> configureServicesCallback = builder2.Build(instance);
Action<object> configureContainerCallback = configureContainerMethod.Build(instance);
return new StartupMethods(instance, builder.Build(instance), delegate (IServiceCollection services) {
IServiceProvider provider = configureServicesCallback(services);
if (provider != null)
{
return provider;
}
if (configureContainerMethod.MethodInfo != null)
{
Type[] typeArray1 = new Type[] { configureContainerMethod.GetContainerType() };
Type serviceType = typeof(IServiceProviderFactory<>).MakeGenericType(typeArray1);
object requiredService = hostingServiceProvider.GetRequiredService(serviceType);
object[] objArray1 = new object[] { services };
object obj3 = serviceType.GetMethod("CreateBuilder").Invoke(requiredService, objArray1);
configureContainerCallback(obj3);
object[] objArray2 = new object[] { obj3 };
provider = (IServiceProvider) serviceType.GetMethod("CreateServiceProvider").Invoke(requiredService, objArray2);
}
else
{
provider = hostingServiceProvider.GetRequiredService<IServiceProviderFactory<IServiceCollection>>().CreateServiceProvider(services);
}
return provider ?? services.BuildServiceProvider();
});
}
If a provider is returned, it's done.
This makes no sense.
I hard coded the dependencies in the auth handler constructor.
Simple, eh?
I have a project with WebAPI controllers. I'm now adding OData controllers to it. The problem is that my OData controller has the same name as an existing WebAPI controller, and that leads to an exception:
Multiple types were found that match the controller named 'Member'. This can happen if the route that services this request ('OData/{*odataPath}') found multiple controllers defined with the same name but differing namespaces, which is not supported. The request for 'Member' has found the following matching controllers: Foo.Bar.Web.Areas.API.Controllers.MemberController Foo.Bar.Web.Odata.Controllers.MemberController
And this happens even though the controllers are in different namespaces and should have distinguishable routes. Here is a summary of the config that I have. What can I do (besides renaming the controller) to prevent this exception? I'm trying expose these endpoints as:
mysite.com/OData/Members
mysite.com/API/Members/EndPoint
It seems to me that the URLs are distinct enough that there's gotta be some way to configure routing so there's no conflict.
namespace Foo.Bar.Web.Odata.Controllers {
public class MemberController : ODataController {
[EnableQuery]
public IHttpActionResult Get() {
// ... do stuff with EF ...
}
}
}
namespace Foo.Bar.Web.Areas.API.Controllers {
public class MemberController : ApiControllerBase {
[HttpPost]
public HttpResponseMessage EndPoint(SomeModel model) {
// ... do stuff to check email ...
}
}
}
public class FooBarApp : HttpApplication {
protected void Application_Start () {
// ... snip ...
GlobalConfiguration.Configure(ODataConfig.Register);
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
// ... snip ...
}
}
public static class ODataConfig {
public static void Register(HttpConfiguration config) {
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "OData",
model: GetModel());
}
public static Microsoft.OData.Edm.IEdmModel GetModel() {
// ... build edm models ...
}
}
namespace Foo.Bar.Web.Areas.API {
public class APIAreaRegistration : AreaRegistration {
public override string AreaName {
get { return "API"; }
}
public override void RegisterArea(AreaRegistrationContext context) {
var route = context.Routes.MapHttpRoute(
"API_default",
"API/{controller}/{action}/{id}",
new { action = RouteParameter.Optional, id = RouteParameter.Optional }
);
}
}
}
If you have two controllers with same names and different namespaces for api and OData you can use this code. First add this class:
public class ODataHttpControllerSelector : DefaultHttpControllerSelector
{
private readonly HttpConfiguration _configuration;
private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerTypes;
public ODataHttpControllerSelector(HttpConfiguration configuration)
: base(configuration)
{
_configuration = configuration;
_apiControllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes);
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
return this.GetApiController(request);
}
private static ConcurrentDictionary<string, Type> GetControllerTypes()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var types = assemblies
.SelectMany(a => a
.GetTypes().Where(t =>
!t.IsAbstract &&
t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
typeof(IHttpController).IsAssignableFrom(t)))
.ToDictionary(t => t.FullName, t => t);
return new ConcurrentDictionary<string, Type>(types);
}
private HttpControllerDescriptor GetApiController(HttpRequestMessage request)
{
var isOData = IsOData(request);
var controllerName = GetControllerName(request);
var type = GetControllerType(isOData, controllerName);
return new HttpControllerDescriptor(_configuration, controllerName, type);
}
private static bool IsOData(HttpRequestMessage request)
{
var data = request.RequestUri.ToString();
bool match = data.IndexOf("/OData/", StringComparison.OrdinalIgnoreCase) >= 0 ||
data.EndsWith("/OData", StringComparison.OrdinalIgnoreCase);
return match;
}
private Type GetControllerType(bool isOData, string controllerName)
{
var query = _apiControllerTypes.Value.AsEnumerable();
if (isOData)
{
query = query.FromOData();
}
else
{
query = query.WithoutOData();
}
return query
.ByControllerName(controllerName)
.Select(x => x.Value)
.Single();
}
}
public static class ControllerTypeSpecifications
{
public static IEnumerable<KeyValuePair<string, Type>> FromOData(this IEnumerable<KeyValuePair<string, Type>> query)
{
return query.Where(x => x.Key.IndexOf(".OData.", StringComparison.OrdinalIgnoreCase) >= 0);
}
public static IEnumerable<KeyValuePair<string, Type>> WithoutOData(this IEnumerable<KeyValuePair<string, Type>> query)
{
return query.Where(x => x.Key.IndexOf(".OData.", StringComparison.OrdinalIgnoreCase) < 0);
}
public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName)
{
var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, DefaultHttpControllerSelector.ControllerSuffix);
return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase));
}
}
It drives DefaultHttpControllerSelector and you should add this line at the end of Register method inside WebApiConfig.cs file:
config.Services.Replace(typeof(IHttpControllerSelector), new ODataHttpControllerSelector(config));
Notes:
It uses controller's namespace to determine that controller is OData or not. So you should have namespace YourProject.Controllers.OData for your OData controllers and in contrast for API controllers, it should not contains OData word in the namespace.
Thanks to Martin Devillers for his post. I used his idea and a piece of his code!
You'll want to include namespace constraint on your WebAPI:
var route = context.Routes.MapHttpRoute(
name: "API_default",
routeTemplate: "API/{controller}/{action}/{id}",
defaults:new { action = RouteParameter.Optional, id = RouteParameter.Optional },
);
route.DataTokens["Namespaces"] = new string[] {"Foo.Bar.Web.Areas.API.Controllers"];
If you are getting conflicts for view controllers, you should be able to include a similar namespace constraint as:
routes.MapRoute(
name: "ViewControllers_Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional, area = "" },
namespaces: new[]{"Foo.Bar.Web.Controllers"}
);