Swagger exposes by default any schema that is used by an exposed controller (API end point). How can a schema (class) be exposed if it is not used by a controller?
For example, Swagger is showing the following Schemas:
But, the Song Schema (below) needs to be exposed. It is not exposed because it is not used by a controller (API end point).
using System;
namespace ExampleNamespace
{
public class Song
{
[Key][Required]
public int SongID { get; set; }
[Required]
public string SongName { get; set; }
public string SongDescription { get; set; }
public int SongLength { get; set; } //seconds
[Required]
public int AlbumID { get; set; }
}
}
How can this be accomplished?
You can add a schema using a DocumentFilter
public class AddSongSchemaDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var songSchema = new OpenApiSchema {...};
songSchema.Properties.Add(new KeyValuePair<string, OpenApiSchema>("songName", new OpenApiSchema { ... }));
...
context.SchemaRepository.Schemas.Add("Song", songSchema);
}
}
The class OpenApiSchema is used for the song schema itself, and for property schemas. This type contains a number of documentation related properties you can set such as Description.
You register AddSongSchemaDocumentFilter like so
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
options.DocumentFilter<AddSongSchemaDocumentFilter>();
});
}
This could be a bit tedious if there are many properties. Using reflection, you can iterate on the properties, and even reflect on associated attributes attached to those properties.
var songSchema = new OpenApiSchema() { };
var song = new Song();
var properties = typeof(Song).GetProperties();
foreach (var p in properties)
songSchema.Properties.Add(new KeyValuePair<string, OpenApiSchema(
p.Name,
new OpenApiSchema()
{
Type = p.PropertyType.ToString(),
Description = // get [Description] attribute from p,
// ... etc. for other metadata such as an example if desired
}));
context.SchemaRepository.Schemas.Add("Song", songSchema);
Full Swashbuckle documentation.
Related
I am trying to use AutoMapper to map a DTO to an Entity class but I keep getting an error.
Here is the DTO Class:
public class Product
{
public string ID { get; set; }
public string SKU { get; set; }
public string Name { get; set; }
public PriceTiers PriceTiers { get; set; }
}
and here is the Entity:
public partial class Product
{
public Product()
{
PriceTiers = new List<PriceTiers>();
}
[Key]
public string ID { get; set; }
public string SKU { get; set; }
public string Name { get; set; }
public virtual ICollection<PriceTiers> PriceTiers { get; set; }
}
Why do I keep getting the following error?
{"Missing type map configuration or unsupported
mapping.\r\n\r\nMapping types:\r\nPriceTiers ->
ICollection1\r\nWeb.Areas.DEAR.DTOs.PriceTiers -> System.Collections.Generic.ICollection1[[Web.Areas.DEAR.Data.PriceTiers,
Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]\r\n\r\n
Destination Member:\r\nPriceTiers\r\n"}
This is what I have in the Profile class:
AllowNullCollections = true;
CreateMap<DTOs.Product, Data.Product>();
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
and this is what I use to map the classes:
var products = _mapper.Map<IEnumerable<Product>>(result.Products);
This is what is in the Program.cs:
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
The exception message is quite clear, the AutoMapper doesn't know how to map the data from DTOs.PriceTiers to ICollection<Data.PriceTiers>.
Solution 1: Map from DTOs.PriceTiers to ICollection<Data.PriceTiers>
I believe that Custom Type Converters is what you need.
Create Custom Type Converters.
public class ICollectionDataPriceTiersTypeConverter : ITypeConverter<DTOs.PriceTiers, ICollection<Data.PriceTiers>>
{
public ICollection<Data.PriceTiers> Convert(DTOs.PriceTiers src, ICollection<Data.PriceTiers> dest, ResolutionContext context)
{
if (src == null)
return default;
var singleDest = context.Mapper.Map<Data.PriceTiers>(src);
return new List<Data.PriceTiers>
{
singleDest
};
}
}
Add to mapping profile.
CreateMap<DTOs.PriceTiers, ICollection<Data.PriceTiers>>()
.ConvertUsing<ICollectionDataPriceTiersTypeConverter>();
Demo # .NET Fiddle
Solution 2: Map from ICollection<DTOs.PriceTiers> to ICollection<Data.PriceTiers>
If the PriceTiers in DTOs.Product supports multiple items and mapping with many to many (to ICollection<Data.ProductTiers>), then consider modifying the property as the ICollection<DTOs.PriceTiers> type.
namespace DTOs
{
public class Product
{
...
public ICollection<PriceTiers> PriceTiers { get; set; }
}
}
Did you added "CreateMapper()" method after your configurations?
Try something like that.
public class MappingProfile : Profile
{
public MappingProfile {
AllowNullCollections = true;
CreateMap<DTOs.Product, Data.Product>();
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
}
}
After that, on your container service, inject this dependency:
var mappingConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
After some more research I found out that my mapping profile was not in the right order. These are the changes I made.
public class AutoMapperProfiles : Profile
{
public AutoMapperProfiles()
{
AllowNullCollections = true;
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
CreateMap<DTOs.Product, Data.Product>()
.ForMember(dto => dto.PriceTiers, opt => opt.MapFrom(x => x.PriceTiers));
}
}
Now it maps perfectly
We are using AutoMapper (9.0.0) in .net core for mapping values between source and destination. Till time this is working fine. However, we need to keep some of the values in destination as it is after mapping.
We have tried to used UseDestinationValue() and Ignore() methods on member, but it is not preserving the existing values. Below is the code for the same.
RequestModel
public class RequestModel
{
public int Id { get; set; }
public int SubmittedById { get; set; }
public string Description { get; set; }
public string Location { get; set; }
}
RequestDto
public class RequestDto
{
public int Id { get; set; }
public int SubmittedById { get; set; }
public string Description { get; set; }
public string Location { get; set; }
public string SubmittedByName { get; set; }
}
We are accepting Dto in API as request parameter
API
[HttpPost]
public IActionResult Save([FromBody] RequestDto requestDto)
{
// Some logic to save records
}
So, before saving the records we are mapping RequestDto to RequestModel and passing that model to DAL layer to save the records like this
var requestModel = MapperManager.Mapper.Map<RequestDto, RequestModel>(RequestDto);
And call to data layer
var requestModel = DAL.Save(RequestModel)
So, after receiving the updated request model we are again mapping it to requestDto, in this case we are loosing the value for SubmittedByName property.
return MapperManager.Mapper.Map<RequestModel, RequestDto>(requestModel);
Mapper Class
public class RequestProfile: Profile
{
public RequestProfile()
{
CreateMap<RequestModel, RequestDto>()
CreateMap<RequestDto, RequestModel>()
}
}
This SubmittedByName column is not present in the Request table, but we want to utilize its value after saving the records.
So, how can we preserve the destination value after mapping.
Any help on this appreciated !
I think you have to use the Map overload that accepts destination.
This works for me, using same model / dto you posted, in a console application:
var config = new MapperConfiguration(cfg => cfg.CreateMap<RequestModel, RequestDto>().ReverseMap());
var mapper = config.CreateMapper();
var source = new RequestDto
{
Id = 1,
SubmittedById = 100,
SubmittedByName = "User 100",
Description = "Item 1",
Location = "Location 1"
};
Console.WriteLine($"Name (original): {source.SubmittedByName}");
var destination = mapper.Map<RequestDto, RequestModel>(source);
Console.WriteLine($"Name (intermediate): {source.SubmittedByName}");
source = mapper.Map<RequestModel, RequestDto>(destination, source);
Console.WriteLine($"Name (final): {source.SubmittedByName}");
The standard Map method creates a new object but the overloaded method uses existing object as destination.
We have tried to used UseDestinationValue() and Ignore() methods on member, but it is not preserving the existing values. Below is the code for the same.
since that didn't work for you
I would suggest creating a generic class like this (assuming you have multiple classes of RequestDto)
class RequesterInfo<T>
{
public string RequesterName { get; set; } // props you want to preserve
public T RequestDto { get; set; } // props to be mapped
}
by keeping the mapping as it is,
and modifying your code to something like this:
var requestModel = MapperManager.Mapper.Map<RequestDto, RequestModel>(RequesterInfo.RequestDto);
so what happens is that you modify the T RequestDto part of the object without modifying other properties.
Im trying to map a Class which inherits from a base class to a dto.
public class LaunchConfiguration : Document
{
public string Brand { get; set; }
public string SettingName{ get; set; }
}
public class LaunchConfigurationDto
{
public string Brand { get; set; }
public string SettingName{ get; set; }
}
The point of the dto is to hide the fields of the base document when it gets returned to the user. This is my Map configuration
public class DtoProfile : Profile
{
public DtoProfile()
{
CreateMap<LaunchConfiguration,LaunchConfigurationDto>();
}
};
The problem im having is that auto mapper complains about the base class properties which are not mapped . "Unmapped members were found." The properties are the ones on the base class. I have tried specifying this to be ignored in the profile to no avail . Can anyone specify the correct way to do this ?
My ConfigureServices Method incase anyone is wondering :
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = Configuration["ApiInformation:Name"], Version = Configuration["ApiInformation:Version"] });
c.DescribeAllEnumsAsStrings();
});
services.AddAutoMapper(mc =>
{
mc.AddProfile(new DtoProfile());
});
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
}
My Base Class :
public class Document : IDocument, IDocument<Guid>
{
public Document()
{
this.Id = Guid.NewGuid();
this.AddedAtUtc = DateTime.UtcNow;
}
/// <summary>The Id of the document</summary>
[BsonId]
public Guid Id { get; set; }
/// <summary>The datetime in UTC at which the document was added.</summary>
public DateTime AddedAtUtc { get; set; }
/// <summary>The version of the schema of the document</summary>
public int Version { get; set; }
}
My implementation where _mapper is my Injected mapper and _repo My Injected Repo. Exception Occurs on Map Method call
Task ILaunchConfigurationService<LaunchConfigurationDto >.InsertLaunchConfiguration(LaunchConfigurationDto model)
{
var mapped = _mapper.Map<LaunchConfiguration >(model);
return _repo.AddOneAsync(mapped);
}
Your problem should be solved by simply adding ReverseMap() to CreateMap call:
public class DtoProfile : Profile
{
public DtoProfile()
{
CreateMap<LaunchConfiguration, LaunchConfigurationDto>().ReverseMap();
}
};
Automapper creates one way map by default. ReverseMap is just a sugar for creating reverse map in case there are no peculiar mappings in one way. You could also do it like this:
public class DtoProfile : Profile
{
public DtoProfile()
{
CreateMap<LaunchConfiguration, LaunchConfigurationDto>();
CreateMap<LaunchConfigurationDto, LaunchConfiguration>();
}
};
You can read more about this in documentation
However I cannot guarantee you that you will not experience exceptions from database with your current implementation on commiting changes.
[Table("LegalEntity")]
[ModelMetadataType(typeof(LegalEntityMeta))]
public class LegalEntity : Entity<long>
{
}
public class LegalEntityMeta
{
[JsonProperty(PropertyName = "LegalEntityId")]
public long Id { get; set; }
[JsonProperty(PropertyName = "LegalEntityName")]
public string Name { get; set; }
}
In the Startup.cs ....
services
.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
})
.AddAutoMapper(typeof(Startup))
.AddMvcCore()
.AddJsonFormatters()
.AddApiExplorer();
My expectation is to see json with attributes legalEntityId and legalEntityName yet the json produced has id and name as attributes.
Can someone pleas help me with how to change the json attributes?
Thanks
Anand
Json.NET currently has no support for Microsoft.AspNetCore.Mvc.ModelMetadataTypeAttribute. In Issue #1349: Add support for ModelMetadataType for dotnetcore like supported MetadataTypeAttribute in previous versions a request to implement support for it was declined.
Json.NET does support System.ComponentModel.DataAnnotations.MetadataTypeAttribute, albeit with some limitations described in this answer, however even if this attribute were present in .Net core (not sure it is) it would not help you, because you are trying to use the metadata type of a derived class to rename the properties in a base type, which is not an intended usage for metadata type information. I.e. the following works out of the box (in full .Net):
[System.ComponentModel.DataAnnotations.MetadataType(typeof(EntityMeta))]
public class Entity<T>
{
public T Id { get; set; }
public string Name { get; set; }
}
public class EntityMeta
{
[JsonProperty(PropertyName = "LegalEntityId")]
public long Id { get; set; }
[JsonProperty(PropertyName = "LegalEntityName")]
public string Name { get; set; }
}
But the following does not:
[System.ComponentModel.DataAnnotations.MetadataType(typeof(LegalEntityMeta))]
public class LegalEntity : Entity<long>
{
}
public class LegalEntityMeta
{
[JsonProperty(PropertyName = "LegalEntityId")]
public long Id { get; set; }
[JsonProperty(PropertyName = "LegalEntityName")]
public string Name { get; set; }
}
Why doesn't Json.NET allow derived type metadata information to modify base type contracts? You would have to ask Newtonsoft, but guesses include:
Json.NET is a contract-based serializer where each type specifies its contract through attributes. It's not intended that one type could rewrite the contract of a second type.
DataContractJsonSerializer and DataContractSerializer work the same way.
Doing so would violate the Liskov substitution principle.
So, what are your options?
You could serialize a DTO in place of your LegalEntity, and use something like automapper to map between then:
public class LegalEntityDTO
{
[JsonProperty(PropertyName = "LegalEntityId")]
public long Id { get; set; }
[JsonProperty(PropertyName = "LegalEntityName")]
public string Name { get; set; }
}
You could create a custom JsonConverter for LegalEntity with the necessary logic.
You could create a custom contract resolver with the necessary logic, similar to the one here, for instance the following:
using System.Reflection;
public class ModelMetadataTypeAttributeContractResolver : DefaultContractResolver
{
public ModelMetadataTypeAttributeContractResolver()
{
// Default from https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonSerializerSettingsProvider.cs
this.NamingStrategy = new CamelCaseNamingStrategy();
}
const string ModelMetadataTypeAttributeName = "Microsoft.AspNetCore.Mvc.ModelMetadataTypeAttribute";
const string ModelMetadataTypeAttributeProperty = "MetadataType";
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var properties = base.CreateProperties(type, memberSerialization);
var propertyOverrides = GetModelMetadataTypes(type)
.SelectMany(t => t.GetProperties())
.ToLookup(p => p.Name, p => p);
foreach (var property in properties)
{
var metaProperty = propertyOverrides[property.UnderlyingName].FirstOrDefault();
if (metaProperty != null)
{
var jsonPropertyAttribute = metaProperty.GetCustomAttributes<JsonPropertyAttribute>().FirstOrDefault();
if (jsonPropertyAttribute != null)
{
property.PropertyName = jsonPropertyAttribute.PropertyName;
// Copy other attributes over if desired.
}
}
}
return properties;
}
static Type GetModelMetadataType(Attribute attribute)
{
var type = attribute.GetType();
if (type.FullName == ModelMetadataTypeAttributeName)
{
var property = type.GetProperty(ModelMetadataTypeAttributeProperty);
if (property != null && property.CanRead)
{
return property.GetValue(attribute, null) as Type;
}
}
return null;
}
static Type[] GetModelMetadataTypes(Type type)
{
var query = from t in type.BaseTypesAndSelf()
from a in t.GetCustomAttributes(false).Cast<System.Attribute>()
let metaType = GetModelMetadataType(a)
where metaType != null
select metaType;
return query.ToArray();
}
}
public static partial class TypeExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
}
Sample .Net fiddle.
To serialize directly, do:
var settings = new JsonSerializerSettings
{
ContractResolver = new ModelMetadataTypeAttributeContractResolver(),
};
var json = JsonConvert.SerializeObject(entity, Formatting.Indented, settings);
To install the contract resolver into Asp.Net Core see here.
Note I wrote this using full .Net 4.5.1 so it is just a prototype. .Net Core uses a different reflection API, however if you install System.Reflection.TypeExtensions as described here I believe it should work.
Switch to Newtonsoft.Json will help:
Add nuget package Microsoft.AspNetCore.Mvc.Newtonsoft.Json
In Startup.cs -> ConfigureServices (Read https://www.ryadel.com/en/use-json-net-instead-of-system-text-json-in-asp-net-core-3-mvc-projects/ for more information)
services.AddControllers().AddNewtonsoftJson();
Replace using System.Text.Json.Serialization and use MetadataType instead of ModelMetadataType:
using Newtonsoft.Json;
namespace YourDbDataNamespace
{
[MetadataType(typeof(UserMetadata))]
public partial class User {}
public class UserMetadata
{
[JsonProperty(PropertyName = "LegalEntityId")]
int Id { get; set; }
[JsonIgnore]
public string PasswordHash { get; set; }
}
}
I have the following models:
public class Employee
{
public int EmployeeId { get; set; }
public string Name { get; set; }
[...]
public int OfficeId { get; set; }
public string OfficeInfo
{
get { return Office.Info; }
}
public Office Office { get; set; }
}
public class Office
{
public int OfficeId { get; set; }
public string Info { get; set; }
}
I have a grid in the client side which rows I want to feed with instances of Employee, including the OfficeInfo in one of the columns, so I'm consuming it through the following query:
"/odata/Employees?$expand=Office&$select=EmployeeId,Name,OfficeInfo"
I have both entities registered in the IEdmModel:
private static IEdmModel GetEDMModel()
{
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employees");
builder.EntitySet<Office>("Offices");
[...]
}
and my Get action looks like this:
[EnableQuery]
public IQueryable<Employees> Get()
{
[...]
}
but I keep getting this Exception:
"Could not find a property named 'OfficeInfo' on type 'Xds.Entities.Employee'"
What am I missing here?
You could mark the property OfficeInfo as required or add this property explicitly:
Noting as required:
builder
.EntitySet<Employee>("Employees")
.EntityType
.Property(_ => _.OfficeInfo)
.IsRequired();
Adding explicitly:
builder
.EntitySet<Employee>("Employees")
.EntityType
.Property(_ => _.OfficeInfo)
.AddedExplicitly = true;
You can check your model metadata and see whether following appears under 'Xds.Entities.Employee' type.
<Property Name="OfficeInfo" Type="Edm.String" />
Since it is a readonly property, you should turn on isQueryCompositionMode to get it shown in the model, like (can pass real HttpConfiguration there):
ODataModelBuilder builder = new ODataConventionModelBuilder(new System.Web.Http.HttpConfiguration(), true);
After that, the query should work.
Note that flag is marked as for testing purpose, but it should be fine if you manually verify your metadata.