Generating swagger for Dictionary<SomeEnum, string> - c#

I'm using Swashbuckle.AspNetCore.Swagger to generate a swagger document and then using NSwag to generate a C# SDK.
I have a couple classes where I use a Dictionary<SomeEnum, string> to hold miscellaneous properties.
namespace Sample
{
/// <summary>Possible properties for MyClass1 objects</summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum PropEnum1
{
/// <summary>OpenAPI doesn't see this description</summary>
PropName1,
PropName2,
PropName3,
// and more property names
}
/// <summary>The first class...</summary>
public class MyClass1
{
public string Name { get; set; }
/// <summary>Properties that vary from instance to instance.</summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public IDictionary<PropEnum1, string> Props { get; set; }
}
// There is also a PropEnum2 and MyClass2, but let's stay simple
}
Using Swashbuckle 6.14 I end up getting swagger that looks like this:
"MyClass1": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"props": {
"type": "object",
"properties": {
"PropName1": {
"type": "string"
},
"PropName2": {
"type": "string"
},
"PropName3": {
"type": "string"
}
},
"additionalProperties": false,
"description": "Properties that vary from instance to instance.",
"nullable": true
}
},
"additionalProperties": false,
"description": "The first class..."
}
and then NSwag generates a C# interface that looks like this:
/// <summary>The first class...</summary>
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.3.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class MyClass1
{
[Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string Name { get; set; }
/// <summary>Properties that vary from instance to instance.</summary>
[Newtonsoft.Json.JsonProperty("props", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public Props Props { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.3.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class Props
{
[Newtonsoft.Json.JsonProperty("PropName1", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string PropName1 { get; set; }
[Newtonsoft.Json.JsonProperty("PropName2", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string PropName2 { get; set; }
[Newtonsoft.Json.JsonProperty("PropName3", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string PropName3 { get; set; }
}
I find the generation of the Props class with a member for every possible enum value a little surprising, but I could live with it.
The problem is that instances of MyClass1 may not have all the properties. I have an API that returns an instance of MyClass1. It works from the swagger page, but using the NSwag SDK gives me an error "Newtonsoft.Json.JsonSerializationException: Required property 'PropName2' expects a non-null value."
The immediate issue is the DisallowNull's in the generated Props class. If I hand-edit them from DisallowNull to Default, the SDK works as expected. But hand-editing generated code is not good.
The DisallowNull's come from the PropName properties in the swagger not having "nullable" set to true. If I pause my generation process and hand-edit them into the swagger, I get Required.Default in the generated C# code. But hand-editing generated swagger is also not good.
So one way to fix it would be to get a "nullable" into the properties in the swagger, but I don't know how to do that.
Or is there a way to get Swashbuckle to treat the Props dictionary more like a Dictionary<string, string>?
From this post it seems like a Dictionary<int, string> gets treated this way, but that may be old.

Here's what I ended up with. It turns the type into a generic object (where the properties have to be of the valueType. Note that I already have enums serialize as strings, so this may not be appropriate for other people.
/// <summary>
/// For properties that are Dictionary[SomeEnum, valueType] alter the schema
/// so the generated SDK code will be IDictionary[string, valueType].
/// </summary>
public class EnumDictionaryToStringDictionarySchemaFilter : ISchemaFilter
{
/// <summary>
/// Apply the schema changes
/// </summary>
/// <param name="schema">The schema model</param>
/// <param name="context">The schema filter context</param>
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
// Only for fields that are Dictionary<Enum, TValue>
//
if (!context.Type.IsGenericType)
return;
if (!context.Type.GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>))
&& !context.Type.GetGenericTypeDefinition().IsAssignableFrom(typeof(IDictionary<,>)))
return;
var keyType = context.Type.GetGenericArguments()[0];
if (!keyType.IsEnum)
return;
var valueType = context.Type.GetGenericArguments()[1];
var valueTypeSchema = context.SchemaGenerator.GenerateSchema(valueType, context.SchemaRepository);
schema.Type = "object";
schema.Properties.Clear();
schema.AdditionalPropertiesAllowed = true;
schema.AdditionalProperties = valueTypeSchema;
}
}

Related

How to mark certain property as required in json content

I am writing an application where I do need to handle some scenarios beforehand in my controller class like certain property must have been provided otherwise status code would be BadRequest. Here is my class lookalike.
public class MyClass
{
[Required]
public IEnumerable<NewObject> NewObjects { get; set; }
}
public class NewObject : INewObject
{
public NewObject(string typeName, IEnumerable<Property> properties)
{
TypeName = typeName;
Properties = properties;
}
[JsonProperty(Required = Required.Always)]
public string TypeName { get; }
public IEnumerable<IProperty> Properties { get; }
}
public interface IProperty
{
string Name { get; }
object Value { get; }
}
Now though I have marked TypeName as required property and if I do not pass that in json content while sending request from swagger, json deserialization doesn't fail. I tried to search but I got an answer that setting Required to Always should work.
Below is the Json Content I am passing through swagger:
{
"NewObjects": [
{
"Properties": [
{
"Name": "string",
"Value": ''
}
]
}
]
}
I wrote below piece of code too by looking at one of the solution:
var config = new HttpConfiguration();
var jsonFormatter = config.Formatters.JsonFormatter;
jsonFormatter.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
config.MapHttpAttributeRoutes();
Still it's not working:
Note: I am using Newtonsoft.Json version 11.0.1
This seems to be swagger issue because when I serialize input C# object and when again deserialize it, I am getting proper error.
For example in my controller class if I say:
var input2 = JsonConvert.DeserializeObject<MyClass>(JsonConvert.SerializeObject(input))
Then input2 throws an exception.
You can take a look at FluentValidation. If I am not mistaken it is designed to validate data in jsons forms specifically.
using FluentValidation;
public CertainActionValidator()
{
RuleFor(x => x.PropertyName).NotEmpty()
}
You can add plenty of additional conditions in there.

Swashbuckle: Make non-nullable properties required

Using Swashbuckle.AspNetCore in an ASP.NET Core webapp, we have response types like:
public class DateRange
{
[JsonConverter(typeof(IsoDateConverter))]
public DateTime StartDate {get; set;}
[JsonConverter(typeof(IsoDateConverter))]
public DateTime EndDate {get; set;}
}
When using Swashbuckle to emit the swagger API JSON, this becomes:
{ ...
"DateRange": {
"type": "object",
"properties": {
"startDate": {
"format": "date-time",
"type": "string"
},
"endDate": {
"format": "date-time",
"type": "string"
}
}
}
...
}
The problem here is that DateTime is a value type, and can never be null; but the emitted Swagger API JSON doesn't tag the 2 properties as required. This behavior is the same for all other value types: int, long, byte, etc - they're all considered optional.
To complete the picture, we're feeding our Swagger API JSON to dtsgenerator to generate typescript interfaces for the JSON response schema. e.g. the class above becomes:
export interface DateRange {
startDate?: string; // date-time
endDate?: string; // date-time
}
Which is clearly incorrect. After digging into this a little bit, I've concluded that dtsgenerator is doing the right thing in making non-required properties nullable in typescript. Perhaps the swagger spec needs explicit support for nullable vs required, but for now the 2 are conflated.
I'm aware that I can add a [Required] attribute to every value-type property, but this spans multiple projects and hundreds of classes, is redundant information, and would have to be maintained. All non-nullable value type properties cannot be null, so it seems incorrect to represent them as optional.
Web API, Entity Framework, and Json.net all understand that value type properties cannot be null; so a [Required] attribute is not necessary when using these libraries.
I'm looking for a way to automatically mark all non-nullable value types as required in my swagger JSON to match this behavior.
If you're using C# 8.0+ and have Nullable Reference Types enabled, then the answer can be even easier. Assuming it is an acceptable division that all non-nullable types are required, and all other types that are explicitly defined as nullable are not then the following schema filter will work.
public class RequireNonNullablePropertiesSchemaFilter : ISchemaFilter
{
/// <summary>
/// Add to model.Required all properties where Nullable is false.
/// </summary>
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
var additionalRequiredProps = model.Properties
.Where(x => !x.Value.Nullable && !model.Required.Contains(x.Key))
.Select(x => x.Key);
foreach (var propKey in additionalRequiredProps)
{
model.Required.Add(propKey);
}
}
}
The Apply method will loop through each model property checking to see if Nullable is false and adding them to the list of required objects. From observation it appears that Swashbuckle does a fine job of setting the Nullable property based on if it a nullable type. If you don't trust it, you could always use Reflection to produce the same affect.
As with other schema filters don't forget to add this one in your Startup class as well as the appropriate Swashbuckle extensions to handle nullable objects.
services.AddSwaggerGen(c =>
{
/*...*/
c.SchemaFilter<RequireNonNullablePropertiesSchemaFilter>();
c.SupportNonNullableReferenceTypes(); // Sets Nullable flags appropriately.
c.UseAllOfToExtendReferenceSchemas(); // Allows $ref enums to be nullable
c.UseAllOfForInheritance(); // Allows $ref objects to be nullable
}
I found a solution for this: I was able to implement a Swashbuckle ISchemaFilter that does the trick. Implementation is:
/// <summary>
/// Makes all value-type properties "Required" in the schema docs, which is appropriate since they cannot be null.
/// </summary>
/// <remarks>
/// This saves effort + maintenance from having to add <c>[Required]</c> to all value type properties; Web API, EF, and Json.net already understand
/// that value type properties cannot be null.
///
/// More background on the problem solved by this type: https://stackoverflow.com/questions/46576234/swashbuckle-make-non-nullable-properties-required </remarks>
public sealed class RequireValueTypePropertiesSchemaFilter : ISchemaFilter
{
private readonly CamelCasePropertyNamesContractResolver _camelCaseContractResolver;
/// <summary>
/// Initializes a new <see cref="RequireValueTypePropertiesSchemaFilter"/>.
/// </summary>
/// <param name="camelCasePropertyNames">If <c>true</c>, property names are expected to be camel-cased in the JSON schema.</param>
/// <remarks>
/// I couldn't figure out a way to determine if the swagger generator is using <see cref="CamelCaseNamingStrategy"/> or not;
/// so <paramref name="camelCasePropertyNames"/> needs to be passed in since it can't be determined.
/// </remarks>
public RequireValueTypePropertiesSchemaFilter(bool camelCasePropertyNames)
{
_camelCaseContractResolver = camelCasePropertyNames ? new CamelCasePropertyNamesContractResolver() : null;
}
/// <summary>
/// Returns the JSON property name for <paramref name="property"/>.
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
private string PropertyName(PropertyInfo property)
{
return _camelCaseContractResolver?.GetResolvedPropertyName(property.Name) ?? property.Name;
}
/// <summary>
/// Adds non-nullable value type properties in a <see cref="Type"/> to the set of required properties for that type.
/// </summary>
/// <param name="model"></param>
/// <param name="context"></param>
public void Apply(Schema model, SchemaFilterContext context)
{
foreach (var property in context.SystemType.GetProperties())
{
string schemaPropertyName = PropertyName(property);
// This check ensures that properties that are not in the schema are not added as required.
// This includes properties marked with [IgnoreDataMember] or [JsonIgnore] (should not be present in schema or required).
if (model.Properties?.ContainsKey(schemaPropertyName) == true)
{
// Value type properties are required,
// except: Properties of type Nullable<T> are not required.
var propertyType = property.PropertyType;
if (propertyType.IsValueType
&& ! (propertyType.IsConstructedGenericType && (propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))))
{
// Properties marked with [Required] are already required (don't require it again).
if (! property.CustomAttributes.Any(attr =>
{
var t = attr.AttributeType;
return t == typeof(RequiredAttribute);
}))
{
// Make the value type property required
if (model.Required == null)
{
model.Required = new List<string>();
}
model.Required.Add(schemaPropertyName);
}
}
}
}
}
}
To use, register it in your Startup class:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc(c_swaggerDocumentName, new Info { Title = "Upfront API", Version = "1.0" });
c.SchemaFilter<RequireValueTypePropertiesSchemaFilter>(/*camelCasePropertyNames:*/ true);
});
This results in the DateRange type above becoming:
{ ...
"DateRange": {
"required": [
"startDate",
"endDate"
],
"type": "object",
"properties": {
"startDate": {
"format": "date-time",
"type": "string"
},
"endDate": {
"format": "date-time",
"type": "string"
}
}
},
...
}
In the swagger JSON schema, and:
export interface DateRange {
startDate: string; // date-time
endDate: string; // date-time
}
in the dtsgenerator output. I hope this helps someone else.
I was able to achieve the same effect as the accepted answer using the following schema filter and Swashbuckle 5.4.1:
public class RequireValueTypePropertiesSchemaFilter : ISchemaFilter
{
private readonly HashSet<OpenApiSchema> _valueTypes = new HashSet<OpenApiSchema>();
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (context.Type.IsValueType)
{
_valueTypes.Add(model);
}
if (model.Properties != null)
{
foreach (var prop in model.Properties)
{
if (_valueTypes.Contains(prop.Value))
{
model.Required.Add(prop.Key);
}
}
}
}
}
This relies on the fact that the ISchemaFilter must be applied to the simple schemas of each property before it can be applied to the complex schema that contains those properties - so all we have to do is keep track of the simple schemas that relate to a ValueType, and if we later encounter a schema that has one of those ValueType schemas as a property, we can mark that property name as required.
I struggled with a similar problem for several days before I realized two important things.
A property's nullability and its requiredness are completeley orthogonal concepts and should not be conflated.
As great as C#'s new nullable feature is at helping you avoid null reference exceptions, it's still a compile-time feature. As far as the CLR is concerned, and therefore as far as the reflection API is concerned, all strings (indeed all reference types) are always nullable. Period.
The second point really caused problems for any schema filter I wrote because, regardless of whether I typed something as string or string?, the context parameter of the Apply function always had it's MemberInfo.Nullable property set to true.
So I came up with the following solution.
First, create Nullable attribute.
using System;
[AttributeUsage(AttributeTargets.Property)]
public class NullableAttribute : Attribute {
public NullableAttribute(bool Property = true, bool Items = false) {
this.Property = Property;
this.Items = Items;
}
public bool Property { get; init; }
public bool Items { get; init; }
}
Next, create NullableSchemaFilter.
using MicroSearch.G4Data.Models;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
public class NullableSchemaFilter : ISchemaFilter {
public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
var attrs = context.MemberInfo?.GetInlineAndMetadataAttributes();
if (attrs != null) {
foreach (var attr in attrs) {
var nullableAttr = attr as NullableAttribute;
if (nullableAttr != null) {
schema.Nullable = nullableAttr.Property;
if (schema.Items != null)
schema.Items.Nullable = nullableAttr.Items;
}
}
}
}
}
And, of course, you have to add the schema filter in your startup code.
services.AddSwaggerGen(config => {
config.SchemaFilter<NullableSchemaFilter>();
});
The Nullable attribute takes two optional boolean parameters:
Property controls if the property itself is nullable.
Items controls if items in an array are nullable. Obviously, this only applies to properties that are arrays.
Examples:
// these all express a nullable string
string? Name { get; set; }
[Nullable] string? Name { get; set; }
[Nullable(true)] string? Name { get; set; }
[Nullable(Property: true)] string? Name { get; set; }
// non-nullable string
[Nullable(false)] string Name { get; set; }
[Nullable(Property: false)] string Name { get; set; }
// non-nullable array of non-nullable strings
[Nullable(false)] string[] Names { get; set; }
[Nullable(Property: false, Items: false) Names { get; set; }
// nullable array of non-nullable strings
[Nullable(Property: true, Items: false)] string[]? Names { get; set; }
// non-nullable array of nullable strings
[Nullable(Property: false, Items: true)] string?[] Names { get; set; }
// nullable array of nullable strings
[Nullable(Property: true, Items: true)] string?[]? Names { get; set; }
The [Required] attribute can be freely used together with the [Nullable] attribute when necessary. i.e. this does what you would expect.
[Nullable][Required] string? Name { get; set; }
I am using .NET 5 and Swashbuckle.AspNetCore 6.2.3.
Let me suggest solution based on json schema.
This scheme was described in RFC, so it should works like common solution https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1
public class AssignPropertyRequiredFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema.Properties == null || schema.Properties.Count == 0)
{
return;
}
var typeProperties = context.SystemType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in schema.Properties)
{
if (IsSourceTypePropertyNullable(typeProperties, property.Key))
{
continue;
}
// "null", "boolean", "object", "array", "number", or "string"), or "integer" which matches any number with a zero fractional part.
// see also: https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1
switch (property.Value.Type)
{
case "boolean":
case "integer":
case "number":
AddPropertyToRequired(schema, property.Key);
break;
case "string":
switch (property.Value.Format)
{
case "date-time":
case "uuid":
AddPropertyToRequired(schema, property.Key);
break;
}
break;
}
}
}
private bool IsNullable(Type type)
{
return Nullable.GetUnderlyingType(type) != null;
}
private bool IsSourceTypePropertyNullable(PropertyInfo[] typeProperties, string propertyName)
{
return typeProperties.Any(info => info.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)
&& IsNullable(info.PropertyType));
}
private void AddPropertyToRequired(Schema schema, string propertyName)
{
if (schema.Required == null)
{
schema.Required = new List<string>();
}
if (!schema.Required.Contains(propertyName))
{
schema.Required.Add(propertyName);
}
}
}
Or you can try this one
public class AssignPropertyRequiredFilter : ISchemaFilter {
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type) {
var requiredProperties = type.GetProperties()
.Where(x => x.PropertyType.IsValueType)
.Select(t => char.ToLowerInvariant(t.Name[0]) + t.Name.Substring(1));
if (schema.required == null) {
schema.required = new List<string>();
}
schema.required = schema.required.Union(requiredProperties).ToList();
}
}
and use
services.AddSwaggerGen(c =>
{
...
c.SchemaFilter<AssignPropertyRequiredFilter>();
});

How to Deserialize json that has properties containing different objects?

I'm currently working on a .NET client for the Zoho Books API in order to integrate some data within my organization. I have been struggling with deserializing the json returned by the API. The problem has been because of inheritance and varying property names. I think I'm looking at creating a custom converter in order to avoid creating response types for every model for the sake of a few varying fields.
I was able to create a custom converter that works for parent responses which look like this:
{
"code" : 0,
"message" : "success",
"invoice" : { // This field varies by model
"invoice_id" : "..."
}
}
I have created a gist for this custom converter.
One of the issues with the custom converter is that when I pass the generic return type down from the web client, I need it in either in the base response format or the paginated response format, and generics are failing me here. My converter doesn't work for paginated responses.
I defined my base type of ZohoBooks Response as so:
namespace ZohoBooks4Net.Responses
{
public class ZohoBooksResponse<T> : IZohoBooksResponse<T>
{
/// <summary>
/// Zoho Books error code. This will be zero for a success response and non-zero in case of an error.
/// </summary>
[JsonProperty("code")]
public int Code { get; set; }
/// <summary>
/// Message for the invoked API.
/// </summary>
[JsonProperty("message")]
public string Message { get; set; }
/// <summary>
/// Comprises the invoked API’s Data.
/// </summary>
public T Resource { get; set; }
}
}
Resource is what I'm calling the third field in the response.
However, when the response returns as paginated, it adds another field.
{
"code": 0,
"message": "success",
"contacts": [
{ "..." }
],
"page_context": {
"page": 1,
"per_page": 200,
"has_more_page": false,
"applied_filter": "Status.All",
"sort_column": "contact_name",
"sort_order": "D"
}
}
So I created this object that describes it:
namespace ZohoBooks4Net.Responses.PaginatedResponses
{
public class PaginatedResponse<T> : ZohoBooksResponse<T>, IPaginatedResponse<T>
{
[JsonProperty("page_context")]
public PageContext Context { get; set; }
}
public class PageContext
{
[JsonProperty("page")]
public int Page { get; set; }
[JsonProperty("per_page")]
public int PerPage { get; set; }
[JsonProperty("has_more_page")]
public bool HasMorePage { get; set; }
}
}
If anyone has any solutions I would really appreciate it.
Books already hosted .Net library in github. In Subscription java client, page context and resources handled separately.
Have you tried using the json2csharp web application tool? It will create the classes needed from your JSON response and a root object which can be used with the Newtonsoft JsonCovnert's DeserializeObject method.
Taking a referenced post from a commenter, I implemented the ReadJson method created by poster of the article. This solved my issue. Here's a link to DynamicPropertyNameConverter on Github Gist.

How to use interfaces in DTO with ASP.NET Web API and Unity?

I'm currently implementing dependency injection in an existing ASP.NET Web API project using the Unity container.
I already manage to inject my service classes into my API controller by configuring a dependency resolver.
But for a controller function, I have to use a Data Transfer Object (DTO).
In that object, I can't find how to use my model contracts.
Here is the Web API controller method:
[HttpPost]
[Route("api/application/save")]
public IHttpActionResult SaveApplication(ApplicationUpdateDTO applicationUpdate)
{
// Inner code calling service methods expecting IApplication and
// collections of ITag as parameters.
}
And here is the DTO definition:
public class ApplicationUpdateDTO
{
public IApplication Application { get; set; }
public IEnumerable<int> DeletedTagIds { get; set; }
public IEnumerable<ITag> AddedTags { get; set; }
public IEnumerable<int> DeletedPlatformIds { get; set; }
public IEnumerable<ITag> AddedPlatforms { get; set; }
}
As a result, the DTO itself is initialized, but not the properties that are all null.
I understand why the properties cannot be set : the interfaces cannot be instanciated and it doesn't have any clue of which classes to use for that. But my Unity container does, thanks to the registration.
Is it possible to use this "link" somehow to initialize the DTO properties?
Is there a better way do this?
Notes:
If I use implementations of my interfaces in the DTO, it obviously works fine.
The controller method receives a JSON object that is identical to my DTO.
edit
I also tried the implementation of a ModelBinder by referring to this post.
But for the line about the ValueProviderResult, I got a null value.
For convenience, here is the response from Todd in the other question:
public class CreateSomethingModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
string key = bindingContext.ModelName;
ValueProviderResult val = bindingContext.ValueProvider.GetValue(key);
if (val != null)
{
string s = val.AttemptedValue as string;
if (s != null)
{
return new CreateSomething(){Title = s; UserId = new Guid(ControllerContext.HttpContext.Request.Headers["userId"]);}
}
}
return null;
}
}
The small difference I got from the response of the question, is the usage of the System.Web.Http.ModelBinding.IModelBinder instead of the MVC one.
As requested, here are exerpts of my interfaces.
The IApplication interface:
public interface IApplication
{
/// <summary>
/// Identifier of the application.
/// </summary>
int Id { get; set; }
/// <summary>
/// Name of the application.
/// </summary>
string Name { get; set; }
/// <summary>
/// Version of the application.
/// </summary>
string Version { get; set; }
/// <summary>
/// Tags associated to the application.
/// </summary>
ICollection<ITag> Tags { get; }
}
The ITag interface:
public interface ITag
{
/// <summary>
/// Identifier of the tag.
/// </summary>
int Id { get; set; }
/// <summary>
/// Identifier of the application to which the tag is linked.
/// </summary>
int ApplicationId { get; set; }
/// <summary>
/// Value of the tag.
/// </summary>
string Value { get; set; }
}
An example of JSON:
{
"marketApplication": {
"Id": 20,
"Name": "MyApplication",
"Version": "2.0"
},
"deletedTagIds": [],
"addedTags": [
{
"Id": 0,
"Value": "NewTag"
}
],
"deletedProgramIds": [],
"addedPrograms": [
{
"Id": 0,
"Name": "x86"
}
]
}
Dependency Injection is the practice of composing graphs of loosly coupled components. Components are the classes in your system that contain behaviour.
Dependency Injection is not meant to build up objects that merely contain data. Using Dependency Injection we build an graph of components. After that graph has been build (using constructor injection), we pass runtime data through this graph using method calls.
Every time you try to use Dependency Injection or an DI container (like Unity) for anything else, you will get into trouble. So although your question indicates that you want to do this with Unity, Unity should be left out of the equation (for this particular case).
As others already stated, the building of Data Transfer Objects (DTOs) that come in through the request is the job of Web API's Model Binder. The default Model Binders can't deserialize interfaces for you, which is quite obvious; to what implementation should they deserialize?
Although you can replace the default model binder, you should take a step back and look closely at what it is you are trying to achieve. You are abstracting away data. Hiding a DTO behind an abstraction makes usually little sense, since interfaces are meant to abstract behavior.
So instead of using interfaces, it is usually much better to use concrete classes instead.
it would save me the copy from a "sub-DTO" to a concrete one manually
Instead of doing that, a simpler approach would be to use composition. You can compose DTOs out of smaller DTOs. That would save you from having to do the copying completely.
by using the matching type registered in my Unity container.
This assumes that those DTOs should be registered in the container, but again, an DI container should not hold any runtime data. This should be kept out. Or as stated here:
Don't inject runtime data into application components during construction; it causes ambiguity, complicates the composition root with an extra responsibility and makes it extraordinarily hard to verify the correctness of your DI configuration. My advice is to let runtime data flow through the method calls of constructed object graphs.
Update
The idea of composition is simple, you build classes from smaller classes; rather than using inheritance or duplicating object structures. How this would look like in your case obviously depends on your needs, but I imagine that you wish to copy that ITag data to another class that has more properties:
public class SomeObject
{
// Members:
public string Name { get; set; }
public string Description { get; set; }
// Members to copy from ITag
public int Id { get; set; }
public int ApplicationId { get; set; }
public string Value { get; set; }
// more members
}
Instead, you can compose SomeObject from a concrete Tag DTO:
public class SomeObject
{
// Members:
public string Name { get; set; }
public string Description { get; set; }
public Tag Tag { get; set; }
// more members
}
This way you don't have to copy Tag's members; you only have to set the Tag property with a reference to the deserialized Tag DTO.

How to set ExportMetaData with multiple values as well as single w/ custom attribute?

I have the following ExportMetaData attributes set on my class:
[Export(typeof(IDocumentViewer))]
[ExportMetadata("Name", "MyViewer")]
[ExportMetadata("SupportsEditing", true)]
[ExportMetadata("Formats", DocFormat.DOC, IsMultiple = true)]
[ExportMetadata("Formats", DocFormat.DOCX, IsMultiple = true)]
[ExportMetadata("Formats", DocFormat.RTF, IsMultiple = true)]
I also have a supporting interface:
public interface IDocumentViewerMetaData {
/// <summary>
/// Gets the format.
/// </summary>
/// <value>The format.</value>
IEnumerable<DocFormat> Formats { get; }
/// <summary>
/// Gets the name of the viewer
/// </summary>
/// <value>The name.</value>
string Name { get; }
/// <summary>
/// Gets a value indicating whether this viewer supports editing
/// </summary>
/// <value><c>true</c> if [supports editing]; otherwise, <c>false</c>.</value>
bool SupportsEditing { get; }
}
And of course my ImportMany:
[ImportMany(typeof(IDocumentViewer))]
public IEnumerable<Lazy<IDocumentViewer, IDocumentViewerMetaData>> _viewers { get; set; }
What I would like to do is use a strongly-typed attribute class instead of using the ExportMetaData attribute. I have not figured out a way to do this while also supporting single values (Name, SupportsEditing, in the example above).
I envision doing something similiar the following (or whatever is suggested as best):
[Export(typeof(IDocumentViewer))]
[DocumentViewerMetadata(Name = "MyViewer")]
[DocumentViewerMetadata(SupportsEditing = true)]
[DocumentViewerMetadata(Format = DocFormat.DOC)]
[DocumentViewerMetadata(Format = DocFormat.DOCX)]
I am fairly certain that there IS a way to do this, I just haven't found the right way to connect the dots. :)
You can subclass the ExportAttribute with your own implementation, and decorate it with a MetadataAttribute to allow MEF to use its properties to project the metadata proxy it uses during composition:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property),
MetadataAttribute]
public class ExportDocumentViewerAttribute : ExportAttribute, IDocumentViewerMetadata
{
public ExportDocumentViewer(string name, bool supportsEditing, params DocFormat[] formats)
: base(typeof(IDocumentViewer))
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Export requires a name", "name");
Name = name;
SupportsEditing = supportsEditing;
Formats = formats ?? Enumerable.Empty<DocFormat>();
}
public string Name { get; private set; }
public bool SupportsEditing { get; private set; }
public IEnumerable<DocFormat> Formats { get; private set; }
}
[ExportDocumentViewer("Word", true, DocFormat.DOC, DocFormat.DOCX)]
public WordDocumentViewer : IDocumentViewer
{
// Stuff
}
Note you don't actually need to decorate it with your IDocumentViewerMetadata contract, as MEF will project it regardless, I just prefer to so that I know if I make changes to the metadata contract, that my custom export attribute conforms.

Categories

Resources