Related
I'm using Swashbuckle to generate swagger documentation\UI for a webapi2 project. Our models are shared with some legacy interfaces so there are a couple of properties I want to ignore on the models. I can't use JsonIgnore attribute because the legacy interfaces also need to serialize to JSON so I don't want to ignore the properties globally, just in the Swashbuckle configuration.
I found a method of doing this documented here:
https://github.com/domaindrivendev/Swashbuckle/issues/73
But this appears to be out of date with the current Swashbuckle release.
The method recommended for the old version of Swashbuckle is using an IModelFilter implementation as follows:
public class OmitIgnoredProperties : IModelFilter
{
public void Apply(DataType model, DataTypeRegistry dataTypeRegistry, Type type)
{
var ignoredProperties = … // use reflection to find any properties on
// type decorated with the ignore attributes
foreach (var prop in ignoredProperties)
model.Properties.Remove(prop.Name);
}
}
SwaggerSpecConfig.Customize(c => c.ModelFilter<OmitIgnoredProperties>());
But I'm unsure how to configure Swashbuckle to use the IModelFilter in the current version? I'm using Swashbuckle 5.5.3.
If you need to do this but without using JsonIgnore (maybe you still need to serialize/deserialize the property) then just create a custom attribute.
[AttributeUsage(AttributeTargets.Property)]
public class SwaggerExcludeAttribute : Attribute
{
}
Then a schema filter similar to Johng's
public class SwaggerExcludeFilter : ISchemaFilter
{
#region ISchemaFilter Members
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
if (schema?.properties == null || type == null)
return;
var excludedProperties = type.GetProperties()
.Where(t =>
t.GetCustomAttribute<SwaggerExcludeAttribute>()
!= null);
foreach (var excludedProperty in excludedProperties)
{
if (schema.properties.ContainsKey(excludedProperty.Name))
schema.properties.Remove(excludedProperty.Name);
}
}
#endregion
}
Don't forget to register the filter
c.SchemaFilter<SwaggerExcludeFilter>();
Solution for .NET Core 3.1 and .NET Standard 2.1:
Use JsonIgnore from System.Text.Json.Serialization namespace.
( JsonIgnore from Newtonsoft.Json will NOT work )
public class Test
{
[System.Text.Json.Serialization.JsonIgnore]
public int HiddenProperty { get; set; }
public int VisibleProperty { get; set; }
}
If you mark field/property as internal or protected or private, it will be ignored automatically by swashbuckle in swagger documentation.
Update: Obviously, those properties/fields won't be populated in request/response.
The code below is very much based on #Richard's answer, but I am including it as a new answer because it has three completely new, useful features which I have added:
Runs on .NET Core on the latest version of Swashbuckle (v5)
Allows the SwaggerIgnore attribute to be applied to fields not just to properties
Handles the fact that property and field names may have been overridden using the JsonProperty attribute
EDIT: Now correctly handles camelCasing of originally TitleCased fields or properties (prompted by #mattruma's answer)
So the revised code is:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class SwaggerIgnoreAttribute : Attribute
{
}
internal static class StringExtensions
{
internal static string ToCamelCase(this string value)
{
if (string.IsNullOrEmpty(value)) return value;
return char.ToLowerInvariant(value[0]) + value.Substring(1);
}
}
public class SwaggerIgnoreFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext schemaFilterContext)
{
if (schema.Properties.Count == 0)
return;
const BindingFlags bindingFlags = BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance;
var memberList = schemaFilterContext.SystemType // In v5.3.3+ use Type instead
.GetFields(bindingFlags).Cast<MemberInfo>()
.Concat(schemaFilterContext.SystemType // In v5.3.3+ use Type instead
.GetProperties(bindingFlags));
var excludedList = memberList.Where(m =>
m.GetCustomAttribute<SwaggerIgnoreAttribute>()
!= null)
.Select(m =>
(m.GetCustomAttribute<JsonPropertyAttribute>()
?.PropertyName
?? m.Name.ToCamelCase()));
foreach (var excludedName in excludedList)
{
if (schema.Properties.ContainsKey(excludedName))
schema.Properties.Remove(excludedName);
}
}
}
and in Startup.cs:
services.AddSwaggerGen(c =>
{
...
c.SchemaFilter<SwaggerIgnoreFilter>();
...
});
The AspNetCore solution looks like:
public class SwaggerExcludeSchemaFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var excludedProperties = context.SystemType.GetProperties().Where(t => t.GetCustomAttribute<SwaggerExcludeAttribute>() != null);
foreach (PropertyInfo excludedProperty in excludedProperties)
{
if (schema.Properties.ContainsKey(excludedProperty.Name))
{
schema.Properties.Remove(excludedProperty.Name);
}
}
}
}
Well, with a bit of poking I found a way to do this using ISchemaFilter:
public class ApplyCustomSchemaFilters : ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
var excludeProperties = new[] {"myProp1", "myProp2", "myProp3"};
foreach(var prop in excludeProperties)
if (schema.properties.ContainsKey(prop))
schema.properties.Remove(prop);
}
}
then when calling httpConfiguration.EnableSwagger I set the SwaggerDocsConfig to use this SchemaFilter as follows:
c.SchemaFilter<ApplyCustomSchemaFilters>();
Hope this helps someone. I'd still be curious on whether it's possible to use the IModelFilter somehow though.
For people like me who are using .Net Core and are using the build in app.UseSwaggerUi3WithApiExplorer()
Use [JsonIgnore] tag using Newtonsoft.Json;
public class Project
{
[Required]
public string ProjectName { get; set; }
[JsonIgnore]
public string SomeValueYouWantToIgnore { get; set; }
}
It will be excluded from your documentation.
I have here a working example with DotNetCore 3 and Swashbuckle 5. It took me a few hours to get it in place so I thought to come back to this thread which helped me but didn't solve my issue.
Create a dummy custom attribute:
[AttributeUsage(AttributeTargets.Property)]
public class SwaggerExcludeAttribute : Attribute { }
Create a SchemaFilter which will be used by swagger to generate the API Model Schema
public class SwaggerExcludeFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (!(context.ApiModel is ApiObject))
{
return;
}
var model = context.ApiModel as ApiObject;
if (schema?.Properties == null || model?.ApiProperties == null)
{
return;
}
var excludedProperties = model.Type
.GetProperties()
.Where(
t => t.GetCustomAttribute<SwaggerExcludeAttribute>() != null
);
var excludedSchemaProperties = model.ApiProperties
.Where(
ap => excludedProperties.Any(
pi => pi.Name == ap.MemberInfo.Name
)
);
foreach (var propertyToExclude in excludedSchemaProperties)
{
schema.Properties.Remove(propertyToExclude.ApiName);
}
}
}
Then, inside the Startup.cs file add this to the swagger configuration
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
c.SchemaFilter<SwaggerExcludeFilter>();
});
You can now use the custom attribute on a property that you want to exclude from the API Mode Shema like this
public class MyApiModel
{
[SwaggerExclude]
public Guid Token { get; set; }
public int Id { get; set; }
public string Name { get; set; }
}
Based on Stef Heyenrath's answer.
Attribute to mark properties to exclude from the Swagger documentation.
[AttributeUsage(AttributeTargets.Property)]
public class SwaggerExcludeAttribute : Attribute
{
}
The filter to exclude the properties from the Swagger documentation.
public class SwaggerExcludeSchemaFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var excludedProperties =
context.SystemType.GetProperties().Where(
t => t.GetCustomAttribute<SwaggerExcludeAttribute>() != null);
foreach (var excludedProperty in excludedProperties)
{
var propertyToRemove =
schema.Properties.Keys.SingleOrDefault(
x => x.ToLower() == excludedProperty.Name.ToLower());
if (propertyToRemove != null)
{
schema.Properties.Remove(propertyToRemove);
}
}
}
}
The schema.Properties.Keys are camelCase, while the properties themselves are PascalCase. Tweaked the method to convert both to lower case and compare to see what should be excluded.
Swashbuckle now has support for Newtonsoft.
https://github.com/domaindrivendev/Swashbuckle.AspNetCore#systemtextjson-stj-vs-newtonsoft
dotnet add package --version 5.3.1 Swashbuckle.AspNetCore.Newtonsoft
`services.AddSwaggerGenNewtonsoftSupport(); // explicit opt-in - needs tobe placed after AddSwaggerGen();`
You can use the Swashbuckle.AspNetCore.Annotations package, it allows you to mark that some properties are only displayed in the input parameters, and some are only displayed in the output.
for example, if you want to hide the AlertId in the input parameter of the post, you just need to do this by the [SwaggerSchema]:
public class Alert
{
[SwaggerSchema(ReadOnly = true)]
public string AlertId { get; set; }
public string Type { get; set; }
}
See more about it in the Documentation
Here is what I used with Newtonsoft.Json.JsonIgnoreAttribute:
internal class ApplySchemaVendorExtensions : Swashbuckle.Swagger.ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(p => p.GetCustomAttributes(typeof(Newtonsoft.Json.JsonIgnoreAttribute), true)?.Any() == true))
if (schema?.properties?.ContainsKey(prop.Name) == true)
schema?.properties?.Remove(prop.Name);
}
}
Referring to https://stackoverflow.com/a/58193046/11748401 answer, for creating a filter you can simply use the following code:
public class SwaggerExcludeFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
var excludeProperties = context.ApiModel.Type?.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(SwaggerExcludeAttribute)));
if (excludeProperties != null)
{
foreach (var property in excludeProperties)
{
// Because swagger uses camel casing
var propertyName = $"{ToLowerInvariant(property.Name[0])}{property.Name.Substring(1)}";
if (model.Properties.ContainsKey(propertyName))
{
model.Properties.Remove(propertyName);
}
}
}
}
}
This is an older question, but an low-effort, intermediate solution has since become available in Swashbuckle.
Hiding legacy properties from documentation doesn't do much to discourage usage of these properties - it just delays discovery. After all, they're still part of the model. In fact, leaving them undocumented means consumers have no way of knowing they shouldn't use them!
Rather than have them go undocumented, you should simply consider marking them [Obsolete].
Swashbuckle will then mark them as deprecated in the swagger.json. In the UI, this will hide them in the Example Value sections, and in the Schema sections, they will show as grayed out with strikethrough on the names.
If you still want them to be completely hidden from the documentation, you can then set in SwaggerGeneratorOptions.IgnoreObsoleteProperties = true.
This was not a possible solution at the time this question was originally asked. The deprecated flag is a feature of OpenAPI v3, which was not released until 2017.
(Based on mutex's answer.)
I added another line to not have problems with NullReferenceException.
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
var excludeProperties = new[] { "myProp1", "myProp2, myProp3"};
foreach (var prop in excludeProperties)
if(schema.properties != null) // This line
if (schema.properties.ContainsKey(prop))
schema.properties.Remove(prop);
}
If you want to delete all schemas
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
schema.properties = null;
}
I get inspired by the blog of Ignoring properties from controller action model in Swagger using JsonIgnore.
I'm using .net core 2.1 and Swashbuckle.AspNetCore 5.3.1.
The code below solved the problem.
Add a new filter
public class SwaggerJsonIgnoreFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var ignoredProperties = context.MethodInfo.GetParameters()
.SelectMany(p => p.ParameterType.GetProperties()
.Where(prop => prop.GetCustomAttribute<JsonIgnoreAttribute>() != null))
.ToList();
if (!ignoredProperties.Any()) return;
foreach (var property in ignoredProperties)
{
operation.Parameters = operation.Parameters
.Where(p => (!p.Name.Equals(property.Name, StringComparison.InvariantCulture)))
.ToList();
}
}
}
Use the Filter in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
......
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "CustomApi", Version = "v1" });
options.OperationFilter<SwaggerJsonIgnoreFilter>();
});
......
}
In my case I wanted to keep my Application Layer DTOs clean (without any annotation like JsonIngore) but still being able to use them in my Controllers Web APIs.
So, in my Application Layer I have a DTO like this:
public class CreateItemCommand {
public Guid ContainerId { get; set; }
public string Name { get; set; }
}
And my API design for creating an item is something like:
POST /containers/{containerId}/items
As the ContainerId is coming from the api route, I don't want the asp.net core trying to bind it into the command DTO and I don't want swashbuckle listing it neither.
So my solution is to inherit the original DTO in the API layer like this:
public class CreateItemCommandMod : CreateItemCommand {
#pragma warning disable IDE0051
private new ContainerID { get; }
#pragma warning restore IDE0051
}
...
[HttpPost("{containerId}/items}")]
public Task Create(
[FromRoute] Guid containerId,
[FromBody] CreateItemCommandMod command,
) => useCase.Create(command.Apply(r => r.ContainerId = containerId));
The useCase.Create from the ApplicationLayer expects the base class CreateItemCommand.
.Apply is just a very simple extension method that i've made to easily set the routing parameter value into the correspondent dto property.
I needed more control to remove properties which were declared elsewhere and couldn't easly use a removal attribute.
The filter created removed all items which it came accross from my excludes list:
public class SwaggerExcludeFilter : ISchemaFilter
{
private static readonly List<string> excludes = new List<string>()
{
"StoredProcedureName", "ValidationErrors", "changeTracker",
"code", "customerId", "IsDebug",
};
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null || context == null)
return;
// Find all properties by name which need to be removed
// and not shown on the swagger spec.
schema.Properties
.Where(prp => excludes.Any(exc => string.Equals(exc, prp.Key, StringComparison.OrdinalIgnoreCase)))
.Select(prExclude => prExclude.Key)
.ToList()
.ForEach(key => schema.Properties.Remove(key));
}
}
In startup or program.cs for you .Net 6 fans.
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "2.5",
Title = "My Swagger Doc G",
});
c.SchemaFilter<SwaggerExcludeFilter>();
...
Very userful solution form #Jay Shah, but if you using N'Tier architecture you can not reach protected or private DAL data from BL. to solve this, you can make this prop's acces modifier as "protected internal"
public class Test
{
protected internal int HiddenProperty { get; set; }
}
with this you can access aforementioned data from BL but not PL. or API layer.
I'm using dotnet core 3.1 and Swashbuckle 6.3.1, here is updated version with the similar logic for using ISchemaFilter to filter properties marked with customed attribute SwaggerExcludeAttribute
public class SwaggerExcludeFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var type = context.Type;
if (!schema.Properties.Any() || type == null)
{
return;
}
var excludedPropertyNames = type
.GetProperties()
.Where(
t => t.GetCustomAttribute<SwaggerExcludeAttribute>() != null
).Select(d => d.Name).ToList();
if (!excludedPropertyNames.Any())
{
return;
}
var excludedSchemaPropertyKey = schema.Properties
.Where(
ap => excludedPropertyNames.Any(
pn => pn.ToLower() == ap.Key
)
).Select(ap => ap.Key);
foreach (var propertyToExclude in excludedSchemaPropertyKey)
{
schema.Properties.Remove(propertyToExclude);
}
}
}
I am looking into adding an OpenAPI spec to my web API project, but I am running into various obstacles that I am not able to resolve.
API endpoint: /api/some_controller/some_method/id
The content body needs to come from the http body, but I do not want automatic binding using [FromBody] as I need to stream and process the data as-is (auditing, etc).
I added swagger to my project but as expected it does not show a body parameter.
The following DOES generate a proper swagger API definition:
public void some_method([FromBody]MyType mytype);
But as stated before, I need the raw data without model binding.
I am at a loss on how to solve this. Do I need to augment the API explorer somehow? Do I need to add options to swagger? Is there some way to have the [FromBody] attribute that does not actually bind? How can I do this?
I managed to get this to work using an extra custom attribute and an IOperationFilter
[AttributeUsage(AttributeTargets.Method)]
public class OpenApiRequestBodyType: Attribute
{
public Type BodyType { get; }
public string [] ContentTypes { get; }
public OpenApiRequestBodyType(Type type, string[] contentTypes = null)
{
BodyType = type;
ContentTypes = contentTypes;
}
}
public class SwaggerBodyTypeOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var bodyTypeAttribute = context.ApiDescription.CustomAttributes().OfType<OpenApiRequestBodyType>().FirstOrDefault();
if (bodyTypeAttribute != null)
{
var schema = context.SchemaGenerator.GenerateSchema(bodyTypeAttribute.BodyType, context.SchemaRepository);
operation.RequestBody = new OpenApiRequestBody();
string[] contentTypes;
if (bodyTypeAttribute.ContentTypes != null)
contentTypes = bodyTypeAttribute.ContentTypes;
else
contentTypes = operation.Responses.Where(x => x.Key =="200").SelectMany(x=>x.Value.Content).Select(x=>x.Key).ToArray();
foreach (var contentType in contentTypes)
{
operation.RequestBody.Content.Add(KeyValuePair.Create(contentType, new OpenApiMediaType { Schema = schema }));
}
}
}
}
Then I simply tag the method:
[OpenApiRequestBodyType(typeof(my_custom_type))]
and in the Startup:
services.AddSwaggerGen(c =>
{
c.OperationFilter<SwaggerBodyTypeOperationFilter>();
}
I am still not sure if there is no better way to do this.... but at least it works for me...
I have a Rest endpoint, lets call it tags
http://api/tags
which creates tags objects passing this json format:
[{
"TagName" : "IntegerTag",
"DataType" : 1,
"IsRequired" : true
}]
If I would like to maintain the same endpoint to create new tags but with different json format. Lets say I want to create a ListTag
[{
"TagName" : "ListTag",
"DataType" : 5,
"Values" : ["Value1", "Value2", "Value3"]
"IsRequired" : true
}]]
or a RangeTag
[{
"TagName" : "RangeTag",
"DataType" : 6,
"Min": 1,
"Max": 10,
"IsRequired" : true
}]
I am not having any problem with C# to create a new Dto on my controller api and pass it as a different parameter because C# admits methods overloads for that:
void CreateTags(TagForCreateDto1 dto){…}
void CreateTags(TagForCreateDto2 dto){…}
But when I need to maintain in the same controller both methods with a POST request to create the tags, mvc does not allow for the same route to have both.
[HttpPost]
void CreateTags(TagForCreateDto1 dto){…}
[HttpPost]
void CreateTags(TagForCreateDto2 dto){…}
An unhandled exception occurred while processing the request.
AmbiguousActionException: Multiple actions matched. The following
actions matched route data and had all constraints satisfied.
Please advise
You can take leverage of the Factory pattern that will return the tags you want to create based on the JSON input. Create a factory, call it TagsFactory, that implements the following interface:
public interface ITagsFactory
{
string CreateTags(int dataType, string jsonInput);
}
Create a TagsFactory like below:
public class TagsFactory : ITagsFactory
{
public string CreateTags(int dataType, string jsonInput)
{
switch(dataType)
{
case 1:
var intTagsDto = JsonConvert.DeserializeObject<TagForCreateDto1(jsonInput);
// your logic to create the tags below
...
var tagsModel = GenerateTags();
return the JsonConvert.SerializeObject(tagsModel);
case 5:
var ListTagsDto = JsonConvert.DeserializeObject<TagForCreateDto2>(jsonInput);
// your logic to create the tags below
...
var tagsModel = GenerateTags();
return the JsonConvert.SerializeObject(tagsModel);
}
}
}
For little more separation of concerns, you can move the GenerateTags logic out of the factory to its own class.
Once the above is in place, I would suggest making a slight change to the design of your
TagsController. Add the following parameters to the CreateTags action
data-type or tag-name. Whatever is easier to handle and read it using [FromHeader]
jsonInput and read it using [FromBody]
Your controller will then look like below, making use of the ITagsFactory injected via DI
[Route("api")]
public class TagsController : Controller
{
private readonly ITagsFactory _tagsFactory;
public TagsController(ITagsFactory tagsFactory)
{
_tagsFactory= tagsFactory;
}
[HttpPost]
[Route("tags")]
public IActionResult CreateTags([FromHeader(Name = "data-type")] string dataType, [FromBody] string jsonInput)
{
var tags = _tagsFactory.CreateTags(dataType, jsonInput);
return new ObjectResult(tags)
{
StatusCode = 200
};
}
}
The work is almost done. However, in order to read the raw JSON input from the body, you need to add the CustomInputFormatter and register it at the Startup
public class RawRequestBodyInputFormatter : InputFormatter
{
public RawRequestBodyInputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
}
public override bool CanRead(InputFormatterContext context)
{
return true;
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var request = context.HttpContext.Request;
using (var reader = new StreamReader(request.Body))
{
var content = await reader.ReadToEndAsync();
return await InputFormatterResult.SuccessAsync(content);
}
}
}
Register the formatter and the TagsFactory in the Startup like below:
services.AddSingleton<ITagsFactory, TagsFactory>();
services.AddMvc(options =>
{
options.InputFormatters.Insert(0, new RawRequestBodyInputFormatter());
}
That way your endpoint will remain the same. If you need to add more TagTypes, you just need to add that case to the TagsFactory. You can probably think that it's the violation of OCP. However, the Factory needs to know what kind of object it needs to create. If you like to abstract it more, you can make use of AbstractFactory, but I think that would be overkill.
One way to accomplish what you want, having a single POST endpoint while being able to post different "versions" of Tags is by creating a custom JsonConverter.
Basically, since you already have a property DataType that can be used to identify which type of Tag it is, it's easy to serialize it into the correct type. So, in code it looks like this:
BaseTag > ListTag, RangeTag
public class BaseTag
{
public string TagName { get; set; }
public int DataType { get; set; }
public bool IsRequired { get; set; }
}
public sealed class ListTag : BaseTag
{
public ICollection<string> Values { get; set; }
}
public sealed class RangeTag: BaseTag
{
public int Min { get; set; }
public int Max { get; set; }
}
Then, the custom PolymorphicTagJsonConverter
public class PolymorphicTagJsonConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
=> typeof(BaseTag).IsAssignableFrom(objectType);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> throw new NotImplementedException();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader == null) throw new ArgumentNullException("reader");
if (serializer == null) throw new ArgumentNullException("serializer");
if (reader.TokenType == JsonToken.Null)
return null;
var jObject = JObject.Load(reader);
var target = CreateTag(jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
private BaseTag CreateTag(JObject jObject)
{
if (jObject == null) throw new ArgumentNullException("jObject");
if (jObject["DataType"] == null) throw new ArgumentNullException("DataType");
switch ((int)jObject["DataType"])
{
case 5:
return new ListTag();
case 6:
return new RangeTag();
default:
return new BaseTag();
}
}
}
The heavy work is done in ReadJson and Create methods. Create receives an JObject and inside it inspects the DataType property to figure out which type of Tag it is. Then, ReadJson just proceeds calling the Populate on the JsonSerializer for the appropriate Type.
You need to tell the framework to use your custom converter then:
[JsonConverter(typeof(PolymorphicTagJsonConverter))]
public class BaseTag
{
// the same as before
}
Finally, you can just have one POST endpoint that will accept all types of tags:
[HttpPost]
public IActionResult Post(ICollection<BaseTag> tags)
{
return Ok(tags);
}
One downside is that switch on the converter. You might be okay or not with it.. you could do some smart work and try to make the tag classes implement somehow some interface so you could just call Create on the BaseTag and it would forward the call to the correct one at runtime, but I guess you can get started with this, and if complexity increases then you can think on a smarter/more automatic way of finding the correct Tag classes.
In Web API side, I have a customer class like this
public class CustomerAPI
{
public string CustomerName { get; set; }
public string CustomerCity { get; set; }
}
In MVC side I have a customer class like this
public class CustomerMVC
{
public string CustomerName { get; set; }
public string CustomerCity{ get; set; }
}
I`m consuming Web API services in ASP.Net MVC4 like below:
var task = client.GetAsync("api/values")
.ContinueWith((taskwithresponse) =>
{
var response = taskwithresponse.Result;
var readtask = response.Content.ReadAsAsync<IEnumerable<CustomerMVC>>();
readtask.Wait();
serviceList = readtask.Result.ToList();
});
task.Wait();
I'm getting aggregate exception on doing this, How can I convert CustomerWebAPI to CustomerMVC.
It might help to split your code up a bit. I also recommend using the Newtonsoft.Json nuget package for serialization.
var task = client.GetAsync("api/values").Result;
//get results as a string
var result = task.Content.ReadAsStringAsync().Result;
//serialize to an object using Newtonsoft.Json nuget package
var customer = JsonConvert.DeserializeObject<CustomerMVC>(result);
If you wanted to make it asynchronous you could use the async and await keywords in C#5:
public async Task<CustomerMVC> GetCustomer()
{
//return control to caller until GetAsync has completed
var task = await client.GetAsync("api/values");
//return control to caller until ReadAsStringAsync has completed
var result = await task.Content.ReadAsStringAsync()
return JsonConvert.DeserializeObject<CustomerMVC>(result);
}
The deserialization solution feels like a bit of a hack here. Unless there's something you left out, you were probably running into an UnsupportedMediaTypeException which was showed up as an AggregateException because this is how uncaught Task exceptions rear their ugly heads.
Deserialization can be an expensive operation and with this solution you will end up taking the full hit every time you deserialize the object. Using response.Content.ReadAsAsync<IEnumerable<CustomerWebAPI>>() would be far more efficient due to a recent performance improvement to the ReadAsAsync extensions: http://blogs.msdn.com/b/webdev/archive/2015/02/09/asp-net-mvc-5-2-3-web-pages-3-2-3-and-web-api-5-2-3-release.aspx
As for converting from CustomerWebAPI to CustomerMVC, you could easily add a static convenience method like so:
public static CustomerMVC FromCustomerWebAPI(CustomerWebAPI customer){
return new CustomerMVC(){
CustomerName = customer.CustomerName,
CustomerCity = customer.CustomerCity
}
}
It's extra code, but should end up being far more efficient. If the customer object is a fairly large object, you can always use a tool like AutoMapper or ValueInjecter or you could just roll your own solution by caching the get (type you're mapping from) and set accessors (types you're mapping to) so you only have to incur the cost of reflection once - you would do this by compiling an expression - here's an example as to how you could do that for the Set accessors:
public static Action<object, object> BuildSetAccessor( MethodInfo method )
{
var obj = Expression.Parameter(typeof(object), "o");
var value = Expression.Parameter(typeof(object));
Expression<Action<object, object>> expr =
Expression.Lambda<Action<object, object>>(
Expression.Call(
Expression.Convert( obj, method.DeclaringType )
, method
, Expression.Convert( value, method.GetParameters()[0].ParameterType )
), obj
, value );
return expr.Compile();
}
I have been using Telerik MVC Grid for quite a while now. It is a great control, however, one annoying thing keeps showing up related to using the grid with Ajax Binding to objects created and returned from the Entity Framework. Entity objects have circular references, and when you return an IEnumerable<T> from an Ajax callback, it generates an exception from the JavascriptSerializer if there are circular references. This happens because the MVC Grid uses a JsonResult, which in turn uses JavaScriptSerializer which does not support serializing circular references.
My solution to this problem has been to use LINQ to create view objects that do not have the Related Entities. This works for all cases, but requires the creation of new objects and the copying of data to / from entity objects to these view objects. Not a lot of work, but it is work.
I have finally figured out how to generically make the grid not serialize the circular references (ignore them) and I wanted to share my solution for the general public, as I think it is generic, and plugs into the environment nicely.
The solution has a couple of parts
Swap the default grid serializer with a custom serializer
Install the Json.Net plug-in available from Newtonsoft (this is a great library)
Implement the grid serializer using Json.Net
Modify the Model.tt files to insert [JsonIgnore] attributes in front of the navigation properties
Override the DefaultContractResolver of Json.Net and look for the _entityWrapper attribute name to ensure this is also ignored (injected wrapper by the POCO classes or entity framework)
All of these steps are easy in and of themselves, but without all of them you cannot take advantage of this technique.
Once implemented correctly I can now easily send any entity framework object directly to the client without creating new View objects. I don't recommend this for every object, but sometimes it is the best option. It is also important to note that any related entities are not available on the client side, so don't use them.
Here are the Steps required
Create the following class in your application somewhere. This class is a factory object that the grid uses to obtain JSON results. This will be added to the telerik library in the global.asax file shortly.
public class CustomGridActionResultFactory : IGridActionResultFactory
{
public System.Web.Mvc.ActionResult Create(object model)
{
//return a custom JSON result which will use the Json.Net library
return new CustomJsonResult
{
Data = model
};
}
}
Implement the Custom ActionResult. This code is boilerplate for the most part. The only interesting part is at the bottom where it calls JsonConvert.SerilaizeObject passing in a ContractResolver. The ContactResolver looks for properties called _entityWrapper by name and sets them to be ignored. I am not exactly sure who injects this property, but it is part of the entity wrapper objects and it has circular references.
public class CustomJsonResult : ActionResult
{
const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.";
public string ContentType { get; set; }
public System.Text.Encoding ContentEncoding { get; set; }
public object Data { get; set; }
public JsonRequestBehavior JsonRequestBehavior { get; set; }
public int MaxJsonLength { get; set; }
public CustomJsonResult()
{
JsonRequestBehavior = JsonRequestBehavior.DenyGet;
MaxJsonLength = int.MaxValue; // by default limit is set to int.maxValue
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if ((JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(JsonRequest_GetNotAllowed);
}
var response = context.HttpContext.Response;
if (!string.IsNullOrEmpty(ContentType))
{
response.ContentType = ContentType;
}
else
{
response.ContentType = "application/json";
}
if (ContentEncoding != null)
{
response.ContentEncoding = ContentEncoding;
}
if (Data != null)
{
response.Write(JsonConvert.SerializeObject(Data, Formatting.None,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new PropertyNameIgnoreContractResolver()
}));
}
}
}
Add the factory object to the telerik grid. I do this in the global.asax Application_Start() method, but realistically it can be done anywhere that makes sense.
DI.Current.Register<IGridActionResultFactory>(() => new CustomGridActionResultFactory());
Create the DefaultContractResolver class that checks for _entityWrapper and ignores that attribute. The resolver is passed into the SerializeObject() call in step 2.
public class PropertyNameIgnoreContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (member.Name == "_entityWrapper")
property.Ignored = true;
return property;
}
}
Modify the Model1.tt file to inject attributes that ignore the related entity properties of the POCO Objects. The attribute that must be injected is [JsonIgnore]. This is the hardest part to add to this post but not hard to do in the Model1.tt (or whatever filename it is in your project). Also if you are using code first then you can manually place the [JsonIgnore] attributes in front of any attribute that creates a circular reference.
Search for the region.Begin("Navigation Properties") in the .tt file. This is where all of the navigation properties are code generated. There are two cases that have to be taken care of the many to XXX and the Singular reference. There is an if statement that checks if the property is
RelationshipMultiplicity.Many
Just after that code block you need to insert the [JSonIgnore] attribute prior to the line
<#=PropertyVirtualModifier(Accessibility.ForReadOnlyProperty(navProperty))#> ICollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>> <#=code.Escape(navProperty)#>
Which injects the property name into the generated code file.
Now look for this line which handles the Relationship.One and Relationship.ZeroOrOne relationships.
<#=PropertyVirtualModifier(Accessibility.ForProperty(navProperty))#> <#=code.Escape(navProperty.ToEndMember.GetEntityType())#> <#=code.Escape(navProperty)#>
Add the [JsonIgnore] attribute just before this line.
Now the only thing left is to make sure the NewtonSoft.Json library is "Used" at the top of each generated file. Search for the call to WriteHeader() in the Model.tt file. This method takes a string array parameter that adds extra usings (extraUsings). Instead of passing null, construct an array of strings and send in the "Newtonsoft.Json" string as the first element of the array. The call should now look like:
WriteHeader(fileManager, new [] {"Newtonsoft.Json"});
That's all there is to do, and everything starts working, for every object.
Now for the disclaimers
I have never used Json.Net so my implementation of it might not be
optimal.
I have been testing for about two days now and haven't found any cases where this technique fails.
I also have not found any incompatibilities between the JavascriptSerializer and the JSon.Net serializer but that doesn't mean
there aren't any
The only other caveat is that the I am testing for a property called "_entityWrapper" by name to set its ignored property to true. This is obviously not optimal.
I would welcome any feedback on how to improve this solution. I hope it helps someone else.
The first solution works with the grid editing mode, but we have the same problem with the load of the grid that already has rows of objects with circular reference, and to resolve this we need to create a new IClientSideObjectWriterFactory and a new IClientSideObjectWriter.
This is what I do:
1- Create a new IClientSideObjectWriterFactory:
public class JsonClientSideObjectWriterFactory : IClientSideObjectWriterFactory
{
public IClientSideObjectWriter Create(string id, string type, TextWriter textWriter)
{
return new JsonClientSideObjectWriter(id, type, textWriter);
}
}
2- Create a new IClientSideObjectWriter, this time I do not implement the interface, I've inherited the ClientSideObjectWriter and overrided the AppendObject and AppendCollection methods:
public class JsonClientSideObjectWriter : ClientSideObjectWriter
{
public JsonClientSideObjectWriter(string id, string type, TextWriter textWriter)
: base(id, type, textWriter)
{
}
public override IClientSideObjectWriter AppendObject(string name, object value)
{
Guard.IsNotNullOrEmpty(name, "name");
var data = JsonConvert.SerializeObject(value,
Formatting.None,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new PropertyNameIgnoreContractResolver()
});
return Append("{0}:{1}".FormatWith(name, data));
}
public override IClientSideObjectWriter AppendCollection(string name, System.Collections.IEnumerable value)
{
public override IClientSideObjectWriter AppendCollection(string name, System.Collections.IEnumerable value)
{
Guard.IsNotNullOrEmpty(name, "name");
var data = JsonConvert.SerializeObject(value,
Formatting.Indented,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new PropertyNameIgnoreContractResolver()
});
data = data.Replace("<", #"\u003c").Replace(">", #"\u003e");
return Append("{0}:{1}".FormatWith((object)name, (object)data));
}
}
NOTE: The replace its because the grid renders html tags for the client template in edit mode and if we don't encode then the browser will render the tags. I didn't find a workarround yet if not using a Replace from string object.
3- On my Application_Start on Global.asax.cs I registered my new factory like this:
DI.Current.Register<IClientSideObjectWriterFactory>(() => new JsonClientSideObjectWriterFactory());
And it worked for all components that Telerik has. The only thing that I do not changed was the PropertyNameIgnoreContractResolver that was the same for the EntityFramework classes.
I put the new call into my Application_Start for implement the CustomGridActionResultFactory but the create method never called...
I have taken a slightly different approach which I believe might be a little easier to implement.
All I do is apply an extended [Grid] attribute to the grid json returning method instead of the normal [GridAction] attribute
public class GridAttribute : GridActionAttribute, IActionFilter
{
/// <summary>
/// Determines the depth that the serializer will traverse
/// </summary>
public int SerializationDepth { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="GridActionAttribute"/> class.
/// </summary>
public GridAttribute()
: base()
{
ActionParameterName = "command";
SerializationDepth = 1;
}
protected override ActionResult CreateActionResult(object model)
{
return new EFJsonResult
{
Data = model,
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
MaxSerializationDepth = SerializationDepth
};
}
}
and
public class EFJsonResult : JsonResult
{
const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.";
public EFJsonResult()
{
MaxJsonLength = 1024000000;
RecursionLimit = 10;
MaxSerializationDepth = 1;
}
public int MaxJsonLength { get; set; }
public int RecursionLimit { get; set; }
public int MaxSerializationDepth { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(JsonRequest_GetNotAllowed);
}
var response = context.HttpContext.Response;
if (!String.IsNullOrEmpty(ContentType))
{
response.ContentType = ContentType;
}
else
{
response.ContentType = "application/json";
}
if (ContentEncoding != null)
{
response.ContentEncoding = ContentEncoding;
}
if (Data != null)
{
var serializer = new JavaScriptSerializer
{
MaxJsonLength = MaxJsonLength,
RecursionLimit = RecursionLimit
};
serializer.RegisterConverters(new List<JavaScriptConverter> { new EFJsonConverter(MaxSerializationDepth) });
response.Write(serializer.Serialize(Data));
}
}
Combine this with my serializer Serializing Entity Framework problems and you have a simple way of avoiding circular references but also optionally serializing multiple levels (which I need)
Note: Telerik added this virtual CreateActionResult very recently for me so you may have to download the latest version (not sure but I think maybe 1.3+)
Another good pattern is to simply not avoid creating a ViewModel from the Model.
It is a good pattern to include a ViewModel. It gives you the opportunity to make last minute UI related tweaks to the model. For example, you can tweak a bool to have an associated string Y or N to help make the UI look nice, or vice versa.
Sometimes the ViewModel is exactly like the Model and the code to copy the properties seems unnecessary, but the pattern is a good one and sticking to it is the best practice.