Swagger 2.0 with API 2.o and Odata 3.0 - asp.net-web-api2

I been trying to implement swagger, through [Swashbuckle][1] on my application, but i get no endpoints at all on my swagger ui, and my doc just returns this
{
"swagger": "2.0",
"info": {
"version": "v1",
"title": "NB.EAM.WebAPI.V4"
},
"host": "localhost:24320",
"schemes": [
"http"
],
"paths": {},
"definitions": {}
}
In my webApiConfig i set the following configuration from following the dummys
var swagConfig = new HttpSelfHostConfiguration("http://localhost:24320");
SwaggerConfig.Register();
WebApiConfig.Register(swagConfig);
using (var server = new HttpSelfHostServer(swagConfig))
{
server.OpenAsync().Wait();
}
My swagger configuration is the standart one created by Swashbuckle:
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "NB.EAM.WebAPI.Odata");
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var commentsFileName = Assembly.GetExecutingAssembly().GetName().Name + ".XML";
var commentsFile = Path.Combine(baseDirectory, "bin", commentsFileName);
c.IncludeXmlComments(commentsFile);
c.DocumentFilter<ApplyResourceDocumentation>();
c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c, GlobalConfiguration.Configuration).Configure(odataConfig =>
{
odataConfig.IncludeNavigationProperties();
}));
})
.EnableSwaggerUi(c =>
{
});
Any idea what i might be missing?
Edit:
here is more information about my setting
full code of my WebApiConfig:
public static void Register(HttpConfiguration config)
GlobalConfiguration.Configuration.MessageHandlers.Insert(0, new ServerCompressionHandler(new GZipCompressor(), new DeflateCompressor()));
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
var conventions = ODataRoutingConventions.CreateDefault();
conventions.Insert(0, new CompositeKeyRoutingConvention());
conventions.Insert(1, new CompositeKeyNavigationRoutingConvention());
conventions.Insert(2, new CountODataRoutingConvention());
ODataBatchHandler batchHandler = new UnitOfWorkBatchHandler(GlobalConfiguration.DefaultServer);
config.Routes.MapODataServiceRoute("odata", "odata", GenerateEdmModel(), new CountODataPathHandler(), conventions, batchHandler);
config.Filters.Add(new SqlExceptionFilterAttribute());
config.Filters.Add(new FilterInterceptor());
InitContentRepository();
log4net.Config.XmlConfigurator.Configure();
var swagConfig = new HttpSelfHostConfiguration("http://localhost:24320");
SwaggerConfig.Register();
WebApiConfig.Register(swagConfig);
using (var server = new HttpSelfHostServer(swagConfig))
{
server.OpenAsync().Wait();
}
}
public static Microsoft.Data.Edm.IEdmModel GenerateEdmModel()
{
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.ContainerName = "NBContext";
builder.EntitySet<as_portfolio>("as_portfolio");
builder.EntitySet<cf_usersportfolio>("cf_usersportfolio");
builder.EntitySet<as_locatportfolio>("as_locatportfolio");
builder.EntitySet<ac_bdgaset>("ac_bdgaset");
builder.EntitySet<ac_bdgcc>("ac_bdgcc");
builder.EntitySet<ac_bdgdtl>("ac_bdgdtl");
builder.EntitySet<ac_bdgloca>("ac_bdgloca");
builder.EntitySet<ac_bdgress>("ac_bdgress");
builder.EntitySet<ac_bdgsect>("ac_bdgsect");
builder.EntitySet<ac_bdgwoa>("ac_bdgwoa");
builder.EntitySet<ac_bdgwob>("ac_bdgwob");
builder.EntitySet<ac_bdgwolb>("ac_bdgwolb");
builder.EntitySet<ac_bdgwost>("ac_bdgwost");
builder.EntitySet<ac_bgdcc>("ac_bgdcc");
builder.EntitySet<ac_custome>("ac_custome");
----Very long list of enetitySets
return builder.GetEdmModel();
}
An example of my API
using NB.EAM.DataV2;
using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using System.Web.Http.OData;
namespace NB.EAM.WebAPI.Controllers
{
public class wo_hrtypeController : BaseODataController
{
// GET: odata/wo_hrtype
[Queryable]
public IQueryable<wo_hrtype> Getwo_hrtype()
{
return this.GetWo_HrtypeBll.GetAll();
}
// GET: odata/wo_hrtype(5)
[Queryable]
public SingleResult<wo_hrtype> Getwo_hrtype([FromODataUri] string key)
{
return SingleResult.Create(this.GetWo_HrtypeBll.Find(wo_hrtype =>
wo_hrtype.lb_tyhr == key));
}
}

There is no much information to work there.
We don't know what kind of filters are being aplied on ApplyResourceDocumentation (actually, this class is on the swashbuckle.odata sample proyect and may not fit your necesities:
https://github.com/rbeauchamp/Swashbuckle.OData/blob/master/Swashbuckle.OData.Sample/DocumentFilters/ApplyResourceDocumentation.cs).
We can't also check your entities and function definitions. Check this as an example: https://github.com/rbeauchamp/Swashbuckle.OData/blob/master/Swashbuckle.OData.Sample/App_Start/ODataConfig.cs
And we can't also check if your controllers are defined in a proper way (Methods as verbs. I think custom named methods are only taken into account if they are defined as functions)

I think I just had a similar issue. Try replacing the following line
GlobalConfiguration.Configuration.EnableSwagger(...
with this one:
swagConfig.EnableSwagger(...
The thing is that you should use here the same configuration instance that you pass to the HttpSelfHostServer constructor.
SwaggerConfig.Register(swagConfig); // pass the swagConfig instance to the auto-generated method
WebApiConfig.Register(swagConfig);
using (var server = new HttpSelfHostServer(swagConfig))
{
server.OpenAsync().Wait();
}

Related

How to configure Swashbuckle to ignore property on model for a specific api version only

I needed to add a property to a model and I have implemented what is suggested in the selected answer here and it's working in that it removes the property I have tagged with SwaggerIgnorePropertyAttribute attribute of the model.
My question is , if I have several API versions of my application,how to remove it from the swagger doc/schema for certain versions? I only care not to see the added property in swagger. I only really have 1 version of the model across the application even though I have several version of the app.
I added the version like this:
services.AddSwaggerGen(
swaggerOptions =>
{
swaggerOptions.SwaggerDoc(
"v1",
new Info
{
Title = "Titlebla1",
Description = "bla1",
Version = "v1"
});
swaggerOptions.SwaggerDoc(
"v2",
new Info
{
Title = "Titlebla2",
Description = "bla2",
Version = "v2"
});
etc
I know that I can find the version by using SwaggerDocument and doing something like this: swaggerDoc.Info.Version but how can I get access to SwaggerDocument from the Apply in my SwaggerExcludePropertySchemaFilter class below ?
private class SwaggerExcludePropertySchemaFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var excludedProperties = context.SystemType.GetProperties().Where(t => t.GetCustomAttribute<SwaggerIgnorePropertyAttribute>() != null);
foreach (var excludedProperty in excludedProperties)
{
var propertyToRemove = schema.Properties.Keys.SingleOrDefault(x => string.Equals(x, excludedProperty.Name, StringComparison.OrdinalIgnoreCase));
if (propertyToRemove != null)
{
schema.Properties.Remove(propertyToRemove);
}
}
}
}
Try use IDocumentFilter , see example:
public class CustomSwaggerFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var nonRequiredMYPropertyAPIs = swaggerDoc.Paths
.Where(x => !x.Key.ToLower().Contains("v1") /*the version you want to remove the property */)
.ToList();
nonRequiredMYPropertyAPIs.ForEach(x => {
swaggerDoc.Components.Schemas["YOUR_CLASS_MODEL"]
.Properties.Remove("PROPERTY_NAME_YOU_WANT_TO_IGNORE");
});
}
}
Don't forget register the filter:
c.DocumentFilter<CustomSwaggerFilter>();

RequireAuthorization and Swashbuckle IOperationFilter

I am looking for a way to determine if endpoint requires authorization (.Net Core 3.1) using IOperationFilter.
If Authorization is setup via filter or explicitly as attribute, it can be found in OperationFilterContext context.ApiDescription.ActionDescriptor.FilterDescriptors.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter) and context.ApiDescription.CustomAttributes().OfType<AuthorizeAttribute>().
But if authorization is set as
endpoints.MapControllers().RequireAuthorization();, which should add AuthorizationAttribute to all endpoints, it is not appeared neither in filters nor in attributes. Any thoughts on how to catch if auth is applied to endpoints in this case?
I was able to beat this today like so (swashbuckle 5.63):
Make a new class like this
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace YourNameSpace
{
public class SwaggerGlobalAuthFilter : IOperationFilter
{
public void Apply( OpenApiOperation operation, OperationFilterContext context )
{
context.ApiDescription.TryGetMethodInfo( out MethodInfo methodInfo );
if ( methodInfo == null )
{
return;
}
var hasAllowAnonymousAttribute = false;
if ( methodInfo.MemberType == MemberTypes.Method )
{
// NOTE: Check the controller or the method itself has AllowAnonymousAttribute attribute
hasAllowAnonymousAttribute =
methodInfo.DeclaringType.GetCustomAttributes( true ).OfType<AllowAnonymousAttribute>().Any() ||
methodInfo.GetCustomAttributes( true ).OfType<AllowAnonymousAttribute>().Any();
}
if ( hasAllowAnonymousAttribute )
{
return;
}
// NOTE: This adds the "Padlock" icon to the endpoint in swagger,
// we can also pass through the names of the policies in the List<string>()
// which will indicate which permission you require.
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2" // note this 'Id' matches the name 'oauth2' defined in the swagger extensions config section below
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
}
};
}
}
}
In swagger config extensions
options.AddSecurityDefinition( "oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
//_swaggerSettings is a custom settings object of our own
AuthorizationUrl = new Uri( _swaggerSettings.AuthorizationUrl ),
Scopes = _swaggerSettings.Scopes
}
}
} );
options.OperationFilter<SwaggerGlobalAuthFilter>();
Put together from docs, other SO and decompiled code of built-in SecurityRequirementsOperationFilter
AFAIK, it is defining a global auth setup for all your routed endpoints except those that explicitly have AllowAnonymousAttribute on controller or endpoint. since, as your original question hints at, using the extension RequireAuthorization() when setting up routing implicitly puts that attribute on all endpoints and the built-in SecurityRequirementsOperationFilter which detect the Authorize attribute fails to pick it up. Since your routing setup effectively is putting Authorize on every controller/route it seems setting up a default global filter like this that excludes AllowAnonymous would be in line with what you are configuring in the pipeline.
I suspect there may be a more 'built-in' way of doing this, but I could not find it.
Apparently, this is an open issue on the NSwag repo as well (for people like me that drive by with the same issue, but with NSwag instead of Swashbuckle):
https://github.com/RicoSuter/NSwag/issues/2817
Where there's also another example of solving the issue (not only securityrequirement, but also its scopes).
I know it's been a long time since this question was asked.
But I was facing a similar issue, and following the advice from an issue in GitHub here, managed to resolve it using this implementation of IOperationFilter (and now works like a charm):
public class AuthorizeCheckOperationFilter : IOperationFilter
{
private readonly EndpointDataSource _endpointDataSource;
public AuthorizeCheckOperationFilter(EndpointDataSource endpointDataSource)
{
_endpointDataSource = endpointDataSource;
}
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var descriptor = _endpointDataSource.Endpoints.FirstOrDefault(x =>
x.Metadata.GetMetadata<ControllerActionDescriptor>() == context.ApiDescription.ActionDescriptor);
var hasAuthorize = descriptor.Metadata.GetMetadata<AuthorizeAttribute>()!=null;
var allowAnon = descriptor.Metadata.GetMetadata<AllowAnonymousAttribute>() != null;
if (!hasAuthorize || allowAnon) return;
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new()
{
[
new OpenApiSecurityScheme {Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2"}
}
] = new[] {"api1"}
}
};
}
}
The issue stated this:
ControllerActionDescriptor.EndpointMetadata only reflects the metadata
discovered on the controller action. Any metadata configured via the
endpoint APIs do not show up here. It was primarily the reason we
documented it as being infrastructure-only since it's a bit confusing
to use.
There's a couple of options you could use
a) You could decorate your controllers using [Authorize]. That should allow the metadata to show up in the property.
b) You could look up the metadata by reading from EndpointDataSource.

'Missing type map configuration or unsupported mapping.' Automapperexception in ASP.NET Core WebApi

I'm trying to add AutoMapper to a API (built using ASP.NET Core 3) but it gives me the 'Missing type map configuration or unsupported mapping.'-exception and my google-searches doesn't help me at all... :).
The exception is thrown (as described below) in "GetAllObject1"-method
This is my current setup:
[HttpGet]
public IActionResult GetAllObject1()
{
var object1Items = _myService.GetAllObject1();
Object1ViewModel ouViewModel = _mapper.Map<Object1ViewModel>(Object1Items); // <= This line gives the exception above!!
return Ok(ouViewModel);
}
"AutoMapping.cs":
namespace DataAccess.AutoMapper
{
public class AutoMapping : Profile
{
public AutoMapping()
{
CreateMap < KollOrganizationalUnit, KollOrganizationalUnitViewModel>();
}
}
}
"Startup.cs":
public virtual void ConfigureServices(IServiceCollection services)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.IgnoreNullValues = true;
});
services.AddDbContext<RepositoryContext>(opts => opts.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.RegisterDAL();
services.RegisterBizServices();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "API",
Description = "Integration API for XXX",
TermsOfService = new Uri("https://www.xxx.se/terms-of-service"),
Contact = new OpenApiContact()
{
Name = "Integrationcontact",
Email = "integration#xxx.se",
Url = new Uri("https://www.xxx.se")
},
});
});
services.AddAutoMapper(typeof(Startup).Assembly);
}
Am I missing something obious here?
It's hard to say without knowing the return type of _myService.GetAllObject1(), but it seems like it's returning a collection of Object1Unit. If that's the case, you can try something like:
IEnumerable<Object1ViewModel> ouViewModels = Object1Items.Select(x => _mapper.Map<Object1ViewModel>(x));
return Ok(ouViewModels);
If you're trying to map a collection to a single item, you have to tell AutoMapper how to do that or you will get the exception you're seeing (your AutoMapping class only creates a map from a single item to a single item).

HttpParameterBinding without code coverage

OpenCover On Cake script does not detect coverage on my Owin.Testing usage applying HttpPArameterBiding to some ApiController action parameter.
I have Created a new type of my ApiController that as an action with my ParameterBindingAttribute that I called FromHeaderAttribute. After that I created my Owin Test Server and respective HttpClient and did the requests and the proper asserts to validate that the Binding is working properly. The tests pass with sucess.
This is my unit tests Cake Task
Task("UnitTests")
.IsDependentOn("Build")
.IsDependentOn("RestoreNugets")
.DoesForEach(GetFiles($"{testsPath}/**/*.csproj"), (file) =>
{
var openCoverSettings = new OpenCoverSettings
{
OldStyle = true,
MergeOutput = true,
Register = "user",
LogLevel = OpenCoverLogLevel.Verbose,
ArgumentCustomization = args => args.Append("-coverbytest:*.Tests.dll").Append("-mergebyhash")
}
.WithFilter("+[AppRootName.*]*");
var projectName = file.GetFilename().ToString().Replace(".csproj",string.Empty);
var dotNetTestSettings = new DotNetCoreTestSettings
{
Configuration = "Release",
DiagnosticOutput = true,
Verbosity = DotNetCoreVerbosity.Normal,
ArgumentCustomization = (args)=>
{
args.Append($"--logger \"trx;LogFileName={projectName}-TestsResults.trx\"");
args.Append("--filter \"TestCategory=Unit|Category=Unit\"");
return args;
}
};
OpenCover(context => context.DotNetCoreTest(file.FullPath, dotNetTestSettings), new FilePath($"CoverageResults.xml"), openCoverSettings);
})
.Finally(()=>
{
Information($"Copying test reports to ${outputDir}/TestsResults .... ");
CopyFiles($"{testsPath}/**/TestResults/*.trx",$"{outputDir}/TestsResults");
ReportGenerator($"*-CoverageResults.xml", $"{outputDir}/Reports");
});
this is my XUnit test:
[Fact]
[Trait("Category", "Unit")]
public async Task WhenHeadersArePresent_SettingsShouldBeSetted()
{
HttpConfiguration configuration = new HttpConfiguration();
var container = new SimpleInjector.Container();
Mock<IApiControllerValidation> mockValidationInterface = new Mock<IApiControllerValidation>();
ManualResetEvent resetEvent = new ManualResetEvent(false);
Settings settingsReceived = null;
mockValidationInterface.Setup((validator) => validator.Assert(It.IsAny<object>(), It.IsAny<object>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<IHttpActionResult>()))
.Callback<object, object, IDictionary<string, string>, IHttpActionResult>((header, body, parameters, result) =>
{
settingsReceived = header as Settings;
resetEvent.Set();
});
container.RegisterInstance(mockValidationInterface.Object);
using (var server = TestServer.Create(app =>
{
configuration.DependencyResolver = new SimpleInjectorWebApiDependencyResolver(container);
configuration.MapHttpAttributeRoutes();
app.Use((owinContext, nextHandler)=> nextHandler());
app.UseWebApi(configuration);
}))
{
var client = server.HttpClient;
client.DefaultRequestHeaders.Add("header1", new List<string>() { "headervalue1" } );
client.DefaultRequestHeaders.Add("header2", new List<string>() { "headervalue2" });
var result = await client.PostAsync<Payload>("optionalHeader", new Payload("value1"), new JsonMediaTypeFormatter());
Assert.Equal(HttpStatusCode.OK,result.StatusCode);
};
resetEvent.WaitOne();
Assert.NotNull(settingsReceived);
Assert.Equal("headervalue1", settingsReceived.Header1);
Assert.Equal("headervalue2", settingsReceived.Header2);
}
And this is my Api Action were I want to test the FromHEader attribute that I have implement.
[HttpPost]
[Route("optionalHeader",Name = "PostValidation")]
public IHttpActionResult OptionalHeaders([FromHeader]Settings settings, [FromBody]Payload payload)
{
var result = Ok();
validation.Assert(settings,payload, null, result);
return result;
}
I expect that the code coverage of the test detects the usage of This type but its not because the report is showing 0 code coverage on my type.
I figured out what was the problem, and it was not related to anything related to asp.net framework HttpParameterBinding component.
instead of execute the code cover like this:
OpenCover(context => context.DotNetCoreTest(file.FullPath, dotNetTestSettings), new FilePath($"CoverageResults.xml"), openCoverSettings);
I changed that to be like this:
OpenCover(tool => {
tool.XUnit2($"{testsPath}/**/**/**/**/{projectName}.dll",xUnit2Settings);
}, new FilePath("./OpenCoverCoverageResults.xml"),openCoverSettings);
Also the Build must be done with configuration in debug mode so the OpenCover can use the pdbs.
The only thing that I still dont like is the path to the dlls to be explicit by the number of levels that are explicit and I also did not want to copy the dlls because that will take more time.

Customize binding in ASP.NET Web Api

I am stuck with following problem in ASP.NET Web Api. Let say I have following code in my ApiController:
public void Post(Person person)
{
// Handle the argument
}
What I would like to do is to accept following JSON request:
{
"person": {
"name": "John Doe",
"age": 27
}
}
I would like to go around creating some holding object for each model just to properly bind incoming data. In previous version of MVC, it was possible to define something like Prefix to solve this.
Let me report that I have been able to solve this implementing CustomJsonMediaTypeFormatter:
public class EmberJsonMediaTypeFormatter : JsonMediaTypeFormatter
{
public override System.Threading.Tasks.Task<object> ReadFromStreamAsync(
Type type,
System.IO.Stream readStream,
System.Net.Http.HttpContent content,
IFormatterLogger formatterLogger)
{
return base.ReadFromStreamAsync(
typeof(JObject),
readStream,
content,
formatterLogger).ContinueWith<object>((task) =>
{
var data = task.Result as JObject;
var prefix= type.Name.ToLower();
if (data[prefix] == null)
{
return GetDefaultValueForType(type);
}
var serializer = JsonSerializer.Create(SerializerSettings);
return data[prefix].ToObject(type, serializer);
});
}
}
and replacing default JsonMediaTypeFormatter in GlobalConfiguration.