MongoDB C#: ID Serialization best pattern - c#

I have a class User and I need to work with them in web services.
Then problem is that if I try to serialize Id that is type of BsonObjectId, I see
that have an empty property, that have an empty property, and so on ...
I have write this workaround in order, it's is a good solution?
public partial class i_User
{
[BsonId(IdGenerator = typeof(BsonObjectIdGenerator))]
[NonSerialized]
public BsonObjectId _id;
public String Id
{
get
{
return this._id.ToString();
}
}
}
In this way, I can keep _Id as BsonObjectId but I send an string representation over the web in the property Id.
Another solution is to work with StringObjectIdGenerator
public partial class i_User
{
[BsonId(IdGenerator = typeof(StringObjectIdGenerator))]
public String id;
}
But is see that MongoDB will store a string into database instead of ObjectId.
What is the best approach in order to work in a serialization environmental like web services and/or an client-server (Flash+C#)?

If I understand you correctly, you want to access the Id property as a string, but have the Id saved as an ObjectId in MongoDB. This can be accomplished using BsonRepresentation with BsonId.
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
Details can be found here.

If you want to do it with a Class Map - this is the way to do it:
BsonClassMap.RegisterClassMap<i_User>(cm =>
{
cm.AutoMap();
cm.SetIdMember(cm.GetMemberMap(x => x.Id)
.SetIdGenerator(StringObjectIdGenerator.Instance));
});

There is also a more generic approach using conventions.
This approach allows your to setup rules for all models in one place.
First. Add a convention for ID generator
public class IdGeneratorConvention : ConventionBase, IPostProcessingConvention
{
public void PostProcess(BsonClassMap classMap)
{
var idMemberMap = classMap.IdMemberMap;
if (idMemberMap == null || idMemberMap.IdGenerator != null)
{
return;
}
idMemberMap.SetIdGenerator(StringObjectIdGenerator.Instance);
}
}
Second. Register our convention. Register method must be called before the first query.
var conventionPack = new ConventionPack { new IdGeneratorConvention() };
ConventionRegistry.Register("Pack", conventionPack, x => true);

Working with the ASP minimal APIs and with record types for models, here is what works for me.
Basic custom converter:
using MongoDB.Bson;
using System.Text.Json;
using System.Text.Json.Serialization;
public class ObjectIdJsonConverter : JsonConverter<ObjectId> {
public override ObjectId Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options) => new ObjectId(reader.GetString());
public override void Write(Utf8JsonWriter writer, ObjectId value,
JsonSerializerOptions options) => writer.WriteStringValue(value.ToString());
}
The record:
using System.Text.Json.Serialization;
using MongoDB.Bson;
public record User(
[property: JsonConverter(typeof(ObjectIdJsonConverter))]
ObjectId Id)
In the controller:
app.MapGet("/user/{id}",
async (string id, UserService userService) => {
var user = await userService.Find(id);
return Results.Json(user);
});

Related

How to hide a request field in Swagger with .Net Web API on GET Request [duplicate]

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

Cannot convert string to TimeSpan

So, i am creating a service (using ASP.NETCore 3.1) that receives information about the operation schedule of a shop that can be registered in the system. For that i have an entity which communicates to the database and a model which communicates with this entity, as follows:
Entity:
public class OperationSchedule
{
public int Id { get; set; }
public TimeSpan Open { get; set; }
public TimeSpan Close { get; set; }
public DayOfWeek DatofWeek { get; set; }
}
Model:
public class OperationScheduleModel
{
public TimeSpan Open { get; set; }
public TimeSpan Close { get; set; }
public DayOfWeek DatofWeek { get; set; }
}
Of course they were already mapped using automapper. I'm using swagger to test such communications. The problem begins when i try to post a new "operation schedule", swagger shows me the following output:
Error converting value "string" to type 'System.TimeSpan'
In my first attemption to resolve this problem i'd tryied to use Microsoft.AspNetCore.Mvc.NewtonsoftJson, and configured it in startup this way: services.AddMvc().AddNewtonsoftJson();
As long it does not resolved my problem i was thinking in map the types string and TimeSpan on Automapper by doing something like this:
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
// Custom types mapping
CreateMap<string, TimeSpan>.ConvertUsing<StringToTimeSpanConverter>();
// OperationSchedule mapping
CreateMap<OperationSchedule, OperationScheduleModel>();
CreateMap<OperationScheduleModel, OperationSchedule>();
}
}
But VisualStudio isn't letting me do so, saying that a cannot use "StringToTimeSpanConverter" as a generic property, something like ConvertUsing(new StringToTimeSpanConverter()); doesn't make any difference.
I really can't even imagine another way to resolve it, but may i missing something? Help please
Error converting value "string" to type 'System.TimeSpan'
The reason for this is that System.Text.Json doesn't support TimeSpan.
One solution is to go back to JSON.NET. Here are the steps:
Add a package reference to Microsoft.AspNetCore.Mvc.NewtonsoftJson.
Update Startup.ConfigureServices to call AddNewtonsoftJson.
services.AddControllers()
.AddNewtonsoftJson();
Another option is to use a custom converter for that TimeSpan:
public class CustomConverter : JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
return TimeSpan.Parse(value);
}
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
Register it in Startup.ConfigureServices with AddJsonOptions:
services.AddControllers()
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new CustomConverter())
);
Result:

Keeping same rest endpoint for complex types in asp.net core api

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.

How can I enhance ModelBinder in ASP.NET Core (update property values for a model class)

I would like to enhance final result that ModelBinder returns.
For example:
public class MyModel
{
public int Order {get;set;}
[MyUpperCaseAttribute]
public string Title {get;set;}
}
In API method I expect that all string properties in MyModel which has MyUpperCaseAttribute is in upper case.
For example:
[HttpPost("AddRecord")]
public async Task<ActionResult<int>> AddRecord(MyModel model)
{
model.Title should be upper case, even if send from client in lower case.
}
My idea was to override default ModelBinder and enumerate through all properties and check if property is string and has MyUpperCaseAttribute and correct property value to upper case. I check documentation, but doesn't examples doesn't fill right, since they completely redesign what is returned. I would like to just modify result properties.
What would be the best option to achieve desired behaviour?
Important: (edited):
It would be nice if directive attributes could be stacked:
public class MyModel
{
public int Order {get;set;}
[MyUpperCaseAttribute]
[RemoveSpacesAttribute]
public string Title {get;set;}
}
Edited:
It looks similar to this, but if not other, this is ASP.NET Core, and on link is just ASP.NET. Method, properties, interfaces... are not the same.
I should say, that it would be nice if JSON case rule would work:
public class MyModel
{
public int Order {get;set;}
public string Title {get;set;}
}
should work if {order: 1, title: "test"} (notice lowercase) is send from JavaScript.
This might not be the 'best' option, but I would just use .ToUpper() extension method instead of a custom attribute filter.
public class MyModel
{
private string _title;
public int Order {get;set;}
public string Title { get => _title.ToUpper(); set => _title = value.ToUpper(); }
}
There's a big red herring here, and that's the fact that it appears that this is the sort of thing that could and should be accomplished via model binding. Unfortunately, that's not the case in ASP.Net Core Web API: because the incoming data is JSON, it is in fact handled by input formatters, not model binders. Therefore, in order to achieve the desired effect, you need to create your own custom input formatter that replaces the standard JsonInputFormatter.
First the attribute:
[AttributeUsage(AttributeTargets.Property)]
public class ToUppercaseAttribute : Attribute
{
}
Then we decorate our model class with it:
public class MyModel
{
public int Order { get; set; }
[ToUppercase]
public string Title { get; set; }
}
Now create our custom input formatter that checks for that attribute and transforms the output if necessary. In this case, it simply wraps and delegates to JsonInputFormatter to do the heavy lifting as normal, then modifies the result if it finds our ToUppercaseAttribute attribute on any string property:
public class ToUppercaseJsonInputFormatter : TextInputFormatter
{
private readonly JsonInputFormatter _jsonInputFormatter;
public ToUppercaseJsonInputFormatter(JsonInputFormatter jsonInputFormatter)
{
_jsonInputFormatter = jsonInputFormatter;
foreach (var supportedEncoding in _jsonInputFormatter.SupportedEncodings)
SupportedEncodings.Add(supportedEncoding);
foreach (var supportedMediaType in _jsonInputFormatter.SupportedMediaTypes)
SupportedMediaTypes.Add(supportedMediaType);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
var result = _jsonInputFormatter.ReadRequestBodyAsync(context, encoding);
foreach (var property in context.ModelType.GetProperties().Where(p => p.PropertyType.IsAssignableFrom(typeof(string))
&& p.CustomAttributes.Any(a => a.AttributeType.IsAssignableFrom(typeof(ToUppercaseAttribute)))))
{
var value = (string)property.GetValue(result.Result.Model);
property.SetValue(result.Result.Model, value.ToUpper());
}
return result;
}
}
Next we create an extension method that makes it simple to substitute the default JsonInputFormatter with our custom formatter:
public static class MvcOptionsExtensions
{
public static void UseToUppercaseJsonInputFormatter(this MvcOptions opts)
{
if (opts.InputFormatters.FirstOrDefault(f => f is JsonInputFormatter && !(f is JsonPatchInputFormatter)) is JsonInputFormatter jsonInputFormatter)
{
var jsonInputFormatterIndex = opts.InputFormatters.IndexOf(jsonInputFormatter);
opts.InputFormatters[jsonInputFormatterIndex] = new ToUppercaseJsonInputFormatter(jsonInputFormatter);
}
}
}
And finally, call that method to effect the replacement in Startup.cs:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc(options => options.UseToUppercaseJsonInputFormatter());
}
}
Et voilà!
You can do this thing inside your MyUpperCaseAttribute as follows:
public class MyUpperCaseAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if(value != null)
{
validationContext.ObjectType
.GetProperty(validationContext.MemberName)
.SetValue(validationContext.ObjectInstance, value.ToString().ToUpper(), null);
}
return null;
}
}
Property value will be converted to UpperCase during Model Binding. I have checked it in my side and it works perfectly.

JSON.NET Case Insensitive Deserialization not working

I need to deserialize some JSON into my object where the casing of the JSON is unknown/inconsistent. JSON.NET is supposed to be case insensitive but it not working for me.
My class definition:
public class MyRootNode
{
public string Action {get;set;}
public MyData Data {get;set;}
}
public class MyData
{
public string Name {get;set;}
}
The JSON I receive has Action & Data in lowercase and has the correct casing for MyRootNode.
I'm using this to deserialize:
MyRootNode ResponseObject = JsonConvert.DeserializeObject<MyRootnode>(JsonString);
It returns to be an initialised MyRootNode but the Action and Data properties are null.
Any ideas?
EDIT: Added JSON
{
"MyRootNode":{
"action":"PACT",
"myData":{
"name":"jimmy"
}
}
}
This is the .NET Core built-in JSON library.
I found another way of doing it.. just in case, somebody is still looking for a cleaner way of doing it. Assume there exists a Movie class
using System.Text.Json;
.
.
.
var movies = await JsonSerializer.DeserializeAsync
<IEnumerable<Movie>>(responseStream,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
Startup Options:
You can also configure at the time of application startup using the below extension method.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddJsonOptions(
x =>
{
x.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
}
Simply add JsonProperty attribute and set jsonProperty name
public class MyRootNode
{
[JsonProperty(PropertyName = "action")]
public string Action {get;set;}
[JsonProperty(PropertyName = "myData")]
public MyData Data {get;set;}
}
public class MyData
{
[JsonProperty(PropertyName = "name")]
public string Name {get;set;}
}
UPD: and yes, add some base type as #mjwills suggest
You need to add an additional class:
public class MyRootNodeWrapper
{
public MyRootNode MyRootNode {get;set;}
}
and then use:
MyRootNodeWrapperResponseObject = JsonConvert.DeserializeObject<MyRootNodeWrapper>(JsonString);
https://stackoverflow.com/a/45384366/34092 may be worth a read. It is basically the same scenario.
Also, change:
public MyData Data {get;set;}
to:
public MyData MyData {get;set;}
as per advice from #demo and #Guy .

Categories