How to configure Swashbuckle to ignore property on model for a specific api version only - asp.net-core

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>();

Related

How to define links for OAS3 using Swashbuckle?

I have noticed that in the Swagger UI v3 and in OAS3 we now have support for something called "links"
But I cant really figure out if its possible to use this feature with Swashbuckle, and if it is.. then how? Been searching the net and haven't found anything regarding this..
Anyone been able to use links with Swashbuckle?
You can use an OperationFilter. Create a class that implements IOperationFilter
public class MyLinkFilter : IOperationFilter
{
into it select the response for which you want to add Links.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
var responses = operation.Responses;
var response = responses.FirstOrDefault(r => r.Key == "200").Value;
then update the Links property
response.Links = new Dictionary<string, OpenApiLink>
{
{
"YourKey"
,new OpenApiLink {
OperationId = "YourOperationId",
Description = ".............",
Parameters = new Dictionary<string, RuntimeExpressionAnyWrapper>
{
{
"yourParam", new RuntimeExpressionAnyWrapper
{
Any = new OpenApiString("$request.path.number")
}
}
}
}
}
};
Register your OperationFilter into startup.cs
services.AddSwaggerGen(options =>
{
options.OperationFilter<MyLinkFilter>();
});
OpenAPI, Response
OpenAPI, Link
Finally, you'll have to implement a mechanism to apply the links to the good Action in your controller.

'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).

Unable to create multiple OpenApi specifications with Swashbuckle

I am building a solution with Asp.Net Boilerplate / Asp.Net Zero
I have created two OpenApi specifications (HostApiv1 and TenantApiv1) as follows in Startup.cs:
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("HostApiv1", new Info { Title = "Host API v1", Version = "v1" });
options.SwaggerDoc("TenantApiv1", new Info { Title = "Tenant API v1", Version = "v1" });
options.DocInclusionPredicate((docName, description) => true);
options.IgnoreObsoleteActions();
options.IgnoreObsoleteProperties();
options.OrderActionsBy((apiDesc) => $"{apiDesc.RelativePath}");
options.DescribeAllEnumsAsStrings();
});
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint(_appConfiguration["App:HostApiSwaggerEndPoint"], "Host API v1");
options.SwaggerEndpoint(_appConfiguration["App:TenantApiSwaggerEndPoint"], "Tenant API v1");
//...
});
However, when I decorate my AppService classes with [ApiExplorerSettings(GroupName = "HostApiv1")], the grouping is ignored and the tag (AppService controller), along with all of its operations (actions / methods), still appear under both documents.
Any idea what is wrong, or how I can debug it?
Swashbuckle depends on ApiExplorer, and the use of the ApiExplorer attribute limits us to specifying only a single groupname per controller / action. ABP service proxies are generated via NSwag for angular project, and it seems that during this process a dependency is broken.
The workaround is to create a custom Attribute for delimiting one-or-more groupnames for an appservice controller or action, and subsequently use reflection in the DocInclusionPredicateFunction option to retrieve the groupnames for an action or its containing controller.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class SwaggerDocAttribute: Attribute
{
public SwaggerDocAttribute(params string[] includeInDocuments)
{
IncludeInDocuments = includeInDocuments;
}
public string[] IncludeInDocuments { get; }
}
options.DocInclusionPredicate((docName, apiDesc) =>
{
if (!apiDesc.ActionDescriptor.IsControllerAction())
{
return false;
}
apiDesc.TryGetMethodInfo(out MethodInfo methodInfo);
var actionDocs = methodInfo.GetCustomAttributes<SwaggerDocAttribute>()
.SelectMany(a => a.IncludeInDocuments);
var controllerDocs = methodInfo.DeclaringType.GetCustomAttributes<SwaggerDocAttribute>()
.SelectMany(a => a.IncludeInDocuments);
switch (docName)
{
case "HostApiv1":
return apiDesc.GroupName == null ||
actionDocs.Contains("HostApiv1") ||
controllerDocs.Contains("HostApiv1");
case "TenantApiv1":
return apiDesc.GroupName == null ||
actionDocs.Contains("TenantApiv1") ||
controllerDocs.Contains("TenantApiv1");
default:
return true;
}
});
Usage
[DisableAuditing]
[AbpAuthorize(AppPermissions.HostSpecific.Dashboard.Access)]
//[ApiExplorerSettings(GroupName = "HostApiv1")] // <== Don't use this
[SwaggerDoc("HostApiv1")] // <== Use this in stead
public class MyDemoAppService : ZenDetectAppServiceBase, IHostDashboardAppService
{
//...
}

How do I use IViewLocationExtender with Razor Pages to render device specific pages

Currently we are building a web application, desktop first, that needs device specific Razor Pages for specific pages. Those pages are really different from their Desktop version and it makes no sense to use responsiveness here.
We have tried to implement our own IViewLocationExpander and also tried to use the MvcDeviceDetector library (which is basically doing the same). Detection of the device type is no problem but for some reason the device specific page is not picked up and it is constantly falling back to the default Index.cshtml.
(edit: We're thinking about implementing something based on IPageConvention, IPageApplicationModelProvider or something ... ;-))
Index.mobile.cshtml
Index.cshtml
We have added the following code using the example of MvcDeviceDetector:
public static IMvcBuilder AddDeviceDetection(this IMvcBuilder builder)
{
builder.Services.AddDeviceSwitcher<UrlSwitcher>(
o => { },
d => {
d.Format = DeviceLocationExpanderFormat.Suffix;
d.MobileCode = "mobile";
d.TabletCode = "tablet";
}
);
return builder;
}
and are adding some route mapping
routes.MapDeviceSwitcher();
We expected to see Index.mobile.cshtml to be picked up when selecting a Phone Emulation in Chrome but that didnt happen.
edit Note:
we're using a combination of Razor Views/MVC (older sections) and Razor Pages (newer sections).
also not every page will have a mobile implementation. That's what would have a IViewLocationExpander solution so great.
edit 2
I think the solution would be the same as how you'd implement Culture specific Razor Pages (which is also unknown to us ;-)). Basic MVC supports Index.en-US.cshtml
Final Solution Below
If this is a Razor Pages application (as opposed to an MVC application) I don't think that the IViewLocationExpander interface is much use to you. As far as I know, it only works for partials, not routeable pages (i.e. those with an #page directive).
What you can do instead is to use Middleware to determine whether the request comes from a mobile device, and then change the file to be executed to one that ends with .mobile. Here's a very rough and ready implementation:
public class MobileDetectionMiddleware
{
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext context)
{
if(context.Request.IsFromAMobileDevice())
{
context.Request.Path = $"{context.Request.Path}.mobile";
}
await _next.Invoke(context);
}
}
It's up to you how you want to implement the IsFromAMobileDevice method to determine the nature of the user agent. There's nothing stopping you using a third party library that can do the check reliably for you. Also, you will probably only want to change the path under certain conditions - such as where there is a device specific version of the requested page.
Register this in your Configure method early:
app.UseMiddleware<MobileDetectionMiddleware>();
I've finally found the way to do it convention based. I have implemented a IViewLocationExpander in order to tackle the device handling for basic Razor Views (including Layouts) and I've implemented IPageRouteModelConvention + IActionConstraint to handle devices for Razor Pages.
Note: this solution only seems to be working on ASP.NET Core 2.2 and up though. For some reason 2.1.x and below is clearing the constraints (tested with a breakpoint in a destructor) after they've been added (can probably be fixed).
Now I can have /Index.mobile.cshtml /Index.desktop.cshtml etc. in both MVC and Razor Pages.
Note: This solution can also be used to implement a language/culture specific Razor Pages (eg. /Index.en-US.cshtml /Index.nl-NL.cshtml)
public class PageDeviceConvention : IPageRouteModelConvention
{
private readonly IDeviceResolver _deviceResolver;
public PageDeviceConvention(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void Apply(PageRouteModel model)
{
var path = model.ViewEnginePath; // contains /Index.mobile
var lastSeparator = path.LastIndexOf('/');
var lastDot = path.LastIndexOf('.', path.Length - 1, path.Length - lastSeparator);
if (lastDot != -1)
{
var name = path.Substring(lastDot + 1);
if (Enum.TryParse<DeviceType>(name, true, out var deviceType))
{
var constraint = new DeviceConstraint(deviceType, _deviceResolver);
for (var i = model.Selectors.Count - 1; i >= 0; --i)
{
var selector = model.Selectors[i];
selector.ActionConstraints.Add(constraint);
var template = selector.AttributeRouteModel.Template;
var tplLastSeparator = template.LastIndexOf('/');
var tplLastDot = template.LastIndexOf('.', template.Length - 1, template.Length - Math.Max(tplLastSeparator, 0));
template = template.Substring(0, tplLastDot); // eg Index.mobile -> Index
selector.AttributeRouteModel.Template = template;
var fileName = template.Substring(tplLastSeparator + 1);
if ("Index".Equals(fileName, StringComparison.OrdinalIgnoreCase))
{
selector.AttributeRouteModel.SuppressLinkGeneration = true;
template = selector.AttributeRouteModel.Template.Substring(0, Math.Max(tplLastSeparator, 0));
model.Selectors.Add(new SelectorModel(selector) { AttributeRouteModel = { Template = template } });
}
}
}
}
}
protected class DeviceConstraint : IActionConstraint
{
private readonly DeviceType _deviceType;
private readonly IDeviceResolver _deviceResolver;
public DeviceConstraint(DeviceType deviceType, IDeviceResolver deviceResolver)
{
_deviceType = deviceType;
_deviceResolver = deviceResolver;
}
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
return _deviceResolver.GetDeviceType() == _deviceType;
}
}
}
public class DeviceViewLocationExpander : IViewLocationExpander
{
private readonly IDeviceResolver _deviceResolver;
private const string ValueKey = "DeviceType";
public DeviceViewLocationExpander(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var deviceType = _deviceResolver.GetDeviceType();
if (deviceType != DeviceType.Other)
context.Values[ValueKey] = deviceType.ToString();
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var deviceType = context.Values[ValueKey];
if (!string.IsNullOrEmpty(deviceType))
{
return ExpandHierarchy();
}
return viewLocations;
IEnumerable<string> ExpandHierarchy()
{
var replacement = $"{{0}}.{deviceType}";
foreach (var location in viewLocations)
{
if (location.Contains("{0}"))
yield return location.Replace("{0}", replacement);
yield return location;
}
}
}
}
public interface IDeviceResolver
{
DeviceType GetDeviceType();
}
public class DefaultDeviceResolver : IDeviceResolver
{
public DeviceType GetDeviceType() => DeviceType.Mobile;
}
public enum DeviceType
{
Other,
Mobile,
Tablet,
Normal
}
Startup
services.AddMvc(o => { })
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorOptions(o =>
{
o.ViewLocationExpanders.Add(new DeviceViewLocationExpander(new DefaultDeviceResolver()));
})
.AddRazorPagesOptions(o =>
{
o.Conventions.Add(new PageDeviceConvention(new DefaultDeviceResolver()));
});

Ideablade's Cocktail Composition Container for WCF projects

I recently upgraded an application I am working on from Cocktail 1.4 to Cocktail 2.6 (Punch). I have adjusted my bootstrapper class for the wpf project which now loads with no issues. However, on my WCF / Web projects, I am receiving a runtime exception with the following error when attempting to call Composition.GetInstance:
"You must first set a valid CompositionProvider by using Composition.SetProvider."
After digging into the issue a bit, it appears the composition container is automatically configured when your bootstrapper inherits from CocktailMefBootstrapper. I currently do not have bootstrapper classes at all for non-wpf projects. Prior to the upgrade, all I had to do was call the configure method on the Composition class to configure the composition container, but it appears that it has been deprecated:
Composition.Configure();
I noticed that you can also call Composition.SetProvider(), however I am a little unsure on how to satisfy the method signature exactly. The DevForce Punch documentation states that the generic type for the bootstrapper class should be a viewmodel, and there are no views / view models in a service project. This leaves me in limbo on what to do as I don't want to rip cocktail out of these WCF projects. Is there still a way to use Cocktail's composition container without a bootstrapper for a project in Cocktail (Punch) 2.6?
UPDATE
I found this on the DevForce forums. So it appears that I ought to learn how to configure a multi threaded ICompositionProvider and call Composition.SetProvider() as mentioned above. Any recommended articles to achieving this?
After digging through Punch's source code and looking at Ideablade's MefCompositionContainer, which implements ICompositionProvider, I created my own thread safe implementation of ICompositionProvider. Below is the code I used. Basically, it's the same code for Ideablade's MefCompositionContainer which can be found here in their repository. The only change is that I am passing a bool flag of true into the CompositionContainer's constructor. MSDN lists the pros and cons of making the container thread safe
internal partial class ThreadSafeCompositionProvider : ICompositionProvider
{
static ThreadSafeCompositionProvider()
{
CompositionHost.IgnorePatterns.Add("Caliburn.Micro*");
CompositionHost.IgnorePatterns.Add("Windows.UI.Interactivity*");
CompositionHost.IgnorePatterns.Add("Cocktail.Utils*");
CompositionHost.IgnorePatterns.Add("Cocktail.Compat*");
CompositionHost.IgnorePatterns.Add("Cocktail.dll");
CompositionHost.IgnorePatterns.Add("Cocktail.SL.dll");
CompositionHost.IgnorePatterns.Add("Cocktail.WinRT.dll");
}
public IEnumerable<Assembly> GetProbeAssemblies()
{
IEnumerable<Assembly> probeAssemblies = CompositionHost.Instance.ProbeAssemblies;
var t = GetType();
// Add Cocktail assembly
probeAssemblies = probeAssemblies.Concat(GetType().GetAssembly());
return probeAssemblies.Distinct(x => x);
}
private List<Assembly> _probeAssemblies;
private AggregateCatalog _defaultCatalog;
private ComposablePartCatalog _catalog;
private CompositionContainer _container;
public ComposablePartCatalog Catalog
{
get { return _catalog ?? DefaultCatalog; }
}
public ComposablePartCatalog DefaultCatalog
{
get
{
if (_defaultCatalog == null)
{
_probeAssemblies = GetProbeAssemblies().ToList();
var mainCatalog = new AggregateCatalog(_probeAssemblies.Select(x => new AssemblyCatalog(x)));
_defaultCatalog = new AggregateCatalog(mainCatalog);
CompositionHost.Recomposed += new EventHandler<RecomposedEventArgs>(OnRecomposed)
.MakeWeak(x => CompositionHost.Recomposed -= x);
}
return _defaultCatalog;
}
}
internal void OnRecomposed(object sender, RecomposedEventArgs args)
{
if (args.HasError) return;
var newAssemblies = GetProbeAssemblies()
.Where(x => !_probeAssemblies.Contains(x))
.ToList();
if (newAssemblies.Any())
{
var catalog = new AggregateCatalog(newAssemblies.Select(x => new AssemblyCatalog(x)));
_defaultCatalog.Catalogs.Add(catalog);
_probeAssemblies.AddRange(newAssemblies);
}
// Notify clients of the recomposition
var handlers = Recomposed;
if (handlers != null)
handlers(sender, args);
}
public CompositionContainer Container
{
get { return _container ?? (_container = new CompositionContainer(Catalog, true)); }
}
public Lazy<T> GetInstance<T>() where T : class
{
var exports = GetExportsCore(typeof(T), null).ToList();
if (!exports.Any())
throw new Exception(string.Format("Could Not Locate Any Instances Of Contract", typeof(T).FullName));
return new Lazy<T>(() => (T)exports.First().Value);
}
public T TryGetInstance<T>() where T : class
{
if (!IsTypeRegistered<T>())
return null;
return GetInstance<T>().Value;
}
public IEnumerable<T> GetInstances<T>() where T : class
{
var exports = GetExportsCore(typeof(T), null);
return exports.Select(x => (T)x.Value);
}
public Lazy<object> GetInstance(Type serviceType, string contractName)
{
var exports = GetExportsCore(serviceType, contractName).ToList();
if (!exports.Any())
throw new Exception(string.Format("Could Not Locate Any Instances Of Contract",
serviceType != null ? serviceType.ToString() : contractName));
return new Lazy<object>(() => exports.First().Value);
}
public object TryGetInstance(Type serviceType, string contractName)
{
var exports = GetExportsCore(serviceType, contractName).ToList();
if (!exports.Any())
return null;
return exports.First().Value;
}
public IEnumerable<object> GetInstances(Type serviceType, string contractName)
{
var exports = GetExportsCore(serviceType, contractName);
return exports.Select(x => x.Value);
}
public ICompositionFactory<T> GetInstanceFactory<T>() where T : class
{
var factory = new ThreadSafeCompositionFactory<T>();
Container.SatisfyImportsOnce(factory);
if (factory.ExportFactory == null)
throw new CompositionException(string.Format("No export found.", typeof(T)));
return factory;
}
public ICompositionFactory<T> TryGetInstanceFactory<T>() where T : class
{
var factory = new ThreadSafeCompositionFactory<T>();
Container.SatisfyImportsOnce(factory);
if (factory.ExportFactory == null)
return null;
return factory;
}
public void BuildUp(object instance)
{
// Skip if in design mode.
if (DesignTime.InDesignMode())
return;
Container.SatisfyImportsOnce(instance);
}
public bool IsRecomposing { get; internal set; }
public event EventHandler<RecomposedEventArgs> Recomposed;
internal bool IsTypeRegistered<T>() where T : class
{
return Container.GetExports<T>().Any();
}
public void Configure(CompositionBatch compositionBatch = null, ComposablePartCatalog catalog = null)
{
_catalog = catalog;
var batch = compositionBatch ?? new CompositionBatch();
if (!IsTypeRegistered<IEventAggregator>())
batch.AddExportedValue<IEventAggregator>(new EventAggregator());
Compose(batch);
}
public void Compose(CompositionBatch compositionBatch)
{
if (compositionBatch == null)
throw new ArgumentNullException("compositionBatch");
Container.Compose(compositionBatch);
}
private IEnumerable<Lazy<object>> GetExportsCore(Type serviceType, string key)
{
return Container.GetExports(serviceType, null, key);
}
}
After setting up that class, I added a configuration during startup to instantiate my new thread safe composition provider and to set it as the provider for Punch's Composition class:
if (createThreadSafeCompositionContainer)
{
var threadSafeContainer = new ThreadSafeCompositionProvider();
Composition.SetProvider(threadSafeContainer);
}
Seems to be working like a charm!