I use the query parameters from the "message", and these parameters can be more than 100 and optional. The siganture should stay in this form.
So my question is, how can I document some of the query parameters, to show in swagger UI, and stay try-able?
/// <summary>
/// Callback Endpoint
/// </summary>
/// <returns>HTTP 200 <see cref="HttpStatusCode.OK"/>.</returns>
/// <param name="message">The message</param>
[HttpGet]
[SwaggerParameter("Something", "It is something")]
[Route("endpoint", Name = nameof(Endpoint))]
public virtual async Task<HttpResponseMessage> Endpoint(HttpRequestMessage message)
To ignore the "HttpRequestMessage" I use an OperationFilter:
ASP.Net Web API Swashbuckle how to ignore HttpRequestMessage
config.EnableSwagger(swagger =>
{
swagger.OperationFilter<IgnoreHttpRequestMessage>();
swagger.OperationFilter<SwaggerParameterAttributeHandler>();
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public class SwaggerParameterAttribute : Attribute
{
public SwaggerParameterAttribute(string name, string description)
{
Name = name;
Description = description;
}
public string Name { get; private set; }
public string Description { get; private set; }
public bool Required { get; set; } = false;
}
public class SwaggerParameterAttributeHandler : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
// Get all SwaggerParameterAttributes on the method
var attributes = apiDescription.ActionDescriptor.GetCustomAttributes<SwaggerParameterAttribute>(true);
if (operation.parameters == null)
{
operation.parameters = new List<Parameter>();
}
foreach (var attribute in attributes)
{
var parameter = operation.parameters.FirstOrDefault(p => p.name == attribute.Name);
if (parameter != null)
{
parameter.required = attribute.Required;
}
else
{
operation.parameters.Add(new Parameter()
{
name = attribute.Name,
description = attribute.Description,
type = "string",
required = attribute.Required
});
}
}
}
}
I got a pointment, and get the solution for this question.
I leave this article here, maybe somebody need it.
The problem with the new Parameter, the "in" need to filled out. After it on the swaggerUI the tryOut button works perfectly.
operation.parameters.Add(new Parameter()
{
name = attribute.Name,
description = attribute.Description,
type = "string",
required = attribute.Required,
#in = "query"
});
Related
Is there a way to exclude/remove properties from your example value?
I'm using XML comments on my models to provide information on the swagger page with c.IncludeXmlComments
I use the ///<example>Example Value</example> XML tag to set the example values. My request model does not require all the fields to be set by default, but if I don't set an example XML tag the example value is translated to it's type. Looking like this
{
"ID": "string",
"ExampleSetValue": "Example Value"
}
And I want my example value to only contain the ExampleSetValue property so my example value looks like this
{
"ExampleSetValue": "Example Value"
}
This is the swagger setup in my startup
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", openApiInfo);
c.AddSecurityDefinition("Bearer", openApiSecurityScheme);
c.AddSecurityRequirement(openApiSecurityRequirement);
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
My Request Model
public class CreateRequest
{
/// <summary>
/// ID of the user if available
/// </summary>
public string ID { get; set; }
/// <summary>
/// ExampleSetValue
/// </summary>
/// <example>Example Value</example>
public string ExampleSetValue { get; set; }
}
Xml comment seems cannot be used to exclude the property.
If you want to ignore the property, I suggest you use the [System.Text.Json.Serialization.JsonIgnore] attribute. But in this way it will also make your property hidden when you serialize or deserialize the data.
public class CreateRequest
{
/// <summary>
/// ID of the user if available
/// </summary>
[JsonIgnore] //add this..........
public string ID { get; set; }
/// <summary>
/// ExampleSetValue
/// </summary>
/// <example>Example Value</example>
public string ExampleSetValue { get; set; }
}
If you only want to exclude the property from example value, you need custom ISchemaFilter:
Model:
public class CreateRequest
{
/// <summary>
/// ID of the user if available
/// </summary>
[IgnoreDataMember] //add this..........
public string ID { get; set; }
/// <summary>
/// ExampleSetValue
/// </summary>
/// <example>Example Value</example>
public string ExampleSetValue { get; set; }
}
Custom ISchemaFilter:
public class MySwaggerSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var ignoreDataMemberProperties = context.Type.GetProperties()
.Where(t => t.GetCustomAttribute<IgnoreDataMemberAttribute>() != null);
foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties)
{
var propertyToHide = schema.Properties.Keys
.SingleOrDefault(x => x.ToLower() == ignoreDataMemberProperty.Name.ToLower());
if (propertyToHide != null)
{
schema.Properties.Remove(propertyToHide);
}
}
}
}
Register the filter:
services.AddSwaggerGen(c =>
{
//......
c.SchemaFilter<MySwaggerSchemaFilter>();
});
I found a different approach which suited my case exactly how I wanted.
You can set an example object on your schema which is of type OpenApiObject.
I Created a Document filter to which loops over the schemas in my swagger.
public class AddExamplesFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var schema in context.SchemaRepository.Schemas)
{
schema.Value.Example = ExampleManager.GetExample(schema.Key);
}
}
}
Created the ExampleManager class which returns the example object I want according to the schema name.
public static OpenApiObject GetExample(string requestModelName)
{
return requestModelName switch
{
"ObjectExample" => ObjectExample.GetExample(),
_ => null
};
}
And as the final step created an example class which corresponds exactly on how I want my example to look like. Where I used the OpenApiObject class
public static class ObjectExample
{
public static OpenApiObject GetExample()
{
return new OpenApiObject
{
["ObjectInsideObject"] = new OpenApiObject
{
["Name"] = new OpenApiString("name"),
},
["ArrayWithObjects"] = new OpenApiArray
{
new OpenApiObject
{
["Object"] = new OpenApiObject
{
["integer"] = new OpenApiInteger(15),
},
["Description"] = new OpenApiString("description")
}
},
["date"] = new OpenApiDate(new DateTime().AddMonths(1)),
["Description"] = new OpenApiString("description"),
};
}
}
Final look on Swagger page
I have a ASP.NET Core Web API 6.0 project containing an endpoint which expects XML in its POST body (application/xml). I have created an attribute as follows:
using System;
/// <summary>
/// Used to mark an API for Swagger as having a raw XML body (Swagger creates an inout field).
/// </summary>
public class XmlPayloadAttribute : Attribute
{
/// <summary>
///
/// </summary>
public XmlPayloadAttribute()
{
ParameterName = "payload";
Required = true;
MediaType = "application/xml";
Format = "xml";
}
/// <summary>
///
/// </summary>
public string Format { get; set; }
/// <summary>
///
/// </summary>
public string MediaType { get; set; }
/// <summary>
///
/// </summary>
public bool Required { get; set; }
/// <summary>
///
/// </summary>
public string ParameterName { get; set; }
}
...and a filter as follows:
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen; // Swashbuckle.AspNetCore version 6.2.3
public class XmlPayloadFilter : IOperationFilter
{
/// <summary>
///
/// </summary>
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var attribute = context.MethodInfo.GetCustomAttributes(typeof(XmlPayloadAttribute), false).FirstOrDefault();
if (attribute == null)
{
return;
}
operation.RequestBody = new OpenApiRequestBody() { Required = true };
var xml = #"<MyObject><Something/></MyObject>"; // How do I get this XML to appear as my example, verbatim?
operation.RequestBody.Content.Add("application/xml", new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "string",
Example = new OpenApiString(xml) // This doesn't work!
},
Example = new OpenApiString(xml) // This doesn't work either!
});
}
}
I add the [XmlPayload] annotation to my endpoint as shown here:
using System;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
/// <summary>
/// My controller.
/// </summary>
[ApiController]
public class MyController : ControllerBase
{
/// <summary>
/// My Method.
/// </summary>
[Route("MyMethod")]
[HttpPost]
[XmlPayload]
public IActionResult MyMethod()
{
using var reader = new StreamReader(Request.Body);
var content = reader.ReadToEndAsync().Result;
// I deserialize and process the xml here
return new ContentResult
{
Content = /* My XML response goes here */,
ContentType = "application/xml",
StatusCode = StatusCodes.Status200OK
};
}
}
Swagger correctly creates an input field for the post body, and correctly understands that the content type should be "application/xml", but populates the field with:
<?xml version="1.0" encoding="UTF-8"?>
<!-- XML example cannot be generated; root element name is undefined -->
If I use Name as follows:
operation.RequestBody = new OpenApiRequestBody() { Required = true };
var xml =
#"<s:MyRoot xmlns:s=""http://myschema"">
<s:MyTag>
</s:MyTag>
</s:MyRoot>";
operation.RequestBody.Content.Add("application/xml", new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Xml = new OpenApiXml
{
Name = "MyRoot",
Namespace = new Uri("http://myschema")
},
Example = new OpenApiString(xml, true)
},
});
I get:
<?xml version="1.0" encoding="UTF-8"?>
<MyRoot xmlns="http://myschema/"><s:MyRoot
xmlns:s="http://myschema">
<s:MyTag>
</s:MyTag>
</s:MyRoot></MyRoot>
I get the same result with or without Namespace and with or without setting the isExplicit parameter of OpenApiString to either true or false.
How do I get Swagger to display my XML string verbatim (how do I fix my C#)?
operation.RequestBody.Content.Add("application/xml", new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "string",
Xml = new OpenApiXml()
{
Name = "<RootElementNameHere>"
},
Example = new OpenApiString(xml) // This doesn't work!
},
Example = new OpenApiString(xml) // This doesn't work either!
});
In order for the example to be displayed verbatim (without escaping), you need to add a DTO and generate its schema.
In the attribute class, add the properties Type and Example
[AttributeUsage(validOn: AttributeTargets.Method, AllowMultiple = true)]
public class XmlPayloadAttribute : Attribute
{
public XmlPayloadAttribute(Type type, string? example = null)
{
ParameterName = "payload";
Required = true;
MediaType = "application/xml";
Format = "xml";
ParameterExample = example;
ParameterType = type;
}
public string Format { get; set; }
public string MediaType { get; set; }
public bool Required { get; set; }
public string ParameterName { get; set; }
public Type ParameterType { get; set; }
public string? ParameterExample { get; set; }
}
In the Filter, add all the examples specified through the attributes to the method
public class XmlPayloadFilter : IOperationFilter
{
private string FormatXml(string xml)
{
try
{
XDocument doc = XDocument.Parse(xml);
return doc.ToString();
}
catch (Exception)
{
return xml;
}
}
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var attributes = context.MethodInfo.GetCustomAttributes(typeof(XmlPayloadAttribute), false);
if (attributes == null || attributes.Length == 0)
{
return;
}
operation.RequestBody = new OpenApiRequestBody() { Required = true };
foreach (XmlPayloadAttribute attribute in attributes)
{
if (operation.RequestBody.Content.TryGetValue("application/xml", out var type))
{
type.Examples.Add($"{attribute.ParameterType.Name}", new OpenApiExample() { Value = new OpenApiString(FormatXml(attribute.ParameterExample)) });
}
else
{
operation.RequestBody.Content.Add(
"application/xml",
new OpenApiMediaType
{
Schema = context.SchemaGenerator.GenerateSchema(attribute.ParameterType, context.SchemaRepository),
Examples = new Dictionary<string, OpenApiExample>
{
{
$"{attribute.ParameterType.Name}", new OpenApiExample() { Value = new OpenApiString(FormatXml(attribute.ParameterExample)) }
}
}
}
);
}
}
}
}
Use attribute like this
[HttpPost("post")]
[XmlPayload(typeof(DTOClass1), "<ExampleXml1>")]
[XmlPayload(typeof(DTOClass2), "<ExampleXml2>")]
[XmlPayload(typeof(DTOClass3), "<ExampleXml3>")]
public async Task<IActionResult> post()
{
...
}
Is it better to use reflection in my case? I develop library for working with vk api. My code for base class that build URI for requests:
public abstract class VkApiMethod : IVkApiMethod
{
private string _apiUri = "https://api.vk.com/method/",
_apiVersion = "5.92";
public VkApiMethod(string AccessToken)
{
this.AccessToken = AccessToken;
Fields = new string[] { };
}
public string VkApiMethodName { get; protected set; }
public string AccessToken { get; set; }
public string[] Fields { get; set; }
protected abstract string GetMethodApiParams();
public string GetRequestString()
{
return string.Format("{0}{1}?access_token={2}&fields={3}{4}&v={5}", _apiUri,
VkApiMethodName,
AccessToken,
ArrayToString(Fields),
GetMethodApiParams(),
_apiVersion);
}
}
VkApiMethod is a base class. Derived class must override GetMethodApiParams() method. GetRequestString() call GetMethodApiParams() to get params of derived class.
For example
class GetUsers : VkApiMethod
{
public GetUsers(string AccessToken)
: base(AccessToken)
{
VkApiMethodName = "users.get";
}
/// <summary>
/// Идентификаторы пользователей или их короткие имена.
/// </summary>
public string[] UserIDs { get; set; }
protected override string GetMethodApiParams()
{
return string.Format("&user_ids={0}", ArrayToString(UserIDs));
}
}
And another way without GetMethodApiParams() method using reflection:
public string GetRequestString()
{
var #params = from p in this.GetType().GetProperties()
let attr = p.GetCustomAttributes(typeof(RequestParamAttr), true)
where attr.Length == 1
select new
{
PropValue = p.GetValue(this),
AttrName = (attr.First() as RequestParamAttr).ParamName
};
var _reqUriParams = "";
foreach (var param in #params)
_reqUriParams += string.Format("&{0}={1}", param.AttrName, param.PropValue);
return string.Format("{0}{1}?access_token={2}{3}&v={4}", _apiUri,
VkApiMethodName,
AccessToken,
_reqUriParams,
_apiVersion);
}
Derived class example:
class GetUsers : VkApiMethod
{
public GetUsers(string AccessToken)
: base(AccessToken)
{
VkApiMethodName = "users.get";
}
/// <summary>
/// Идентификаторы пользователей или их короткие имена.
/// </summary>
[RequestParamAttr("user_ids")]
public string[] UserIDs { get; set; }
}
What way is better to use?
I have an ASP.net core web API with swagger (using swashbuckle).
One of the actions in the Web API is a file upload action:
[Produces("application/json")]
[Route("[controller]")]
public class FilesController : Controller
{
[HttpPost]
public void Post(IFormFile file)
{
...
}
}
When I look up that action in the swagger UI it let's me fill in all the fields of IFormFile, which is not what I want to do to test my API.
So how can I add an upload button to the Swagger UI?
For anyone looking for an open api implementation
/// <summary>
/// Add extra parameters for uploading files in swagger.
/// </summary>
public class FileUploadOperation : IOperationFilter
{
/// <summary>
/// Applies the specified operation.
/// </summary>
/// <param name="operation">The operation.</param>
/// <param name="context">The context.</param>
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var isFileUploadOperation =
context.MethodInfo.CustomAttributes.Any(a => a.AttributeType == typeof(FileContentType));
if (!isFileUploadOperation) return;
operation.Parameters.Clear();
var uploadFileMediaType = new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "object",
Properties =
{
["uploadedFile"] = new OpenApiSchema()
{
Description = "Upload File",
Type = "file",
Format = "formData"
}
},
Required = new HashSet<string>(){ "uploadedFile" }
}
};
operation.RequestBody = new OpenApiRequestBody
{
Content = { ["multipart/form-data"] = uploadFileMediaType }
};
}
/// <summary>
/// Indicates swashbuckle should consider the parameter as a file upload
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class FileContentType : Attribute
{
}
}
Decorate controller using FileContentType attribute
[HttpPost]
[Route("PostFile")]
[FileUploadOperation.FileContentType]
public IActionResult PostFile(IFormFile uploadedFile)
File upload should be generated in the Request body like below..
First add a operation filter that consumes the multipart formdata.
public class FileUploadOperation : IOperationFilter
{
private readonly IEnumerable<string> _actionsWithUpload = new []
{
//add your upload actions here!
NamingHelpers.GetOperationId<FilesController>(nameof(FilesController.Post))
};
public void Apply(Operation operation, OperationFilterContext context)
{
if (_actionsWithUpload.Contains(operation.OperationId) )
{
operation.Parameters.Clear();
operation.Parameters.Add(new NonBodyParameter
{
Name = "file",
In = "formData",
Description = "Upload File",
Required = true,
Type = "file"
});
operation.Consumes.Add("multipart/form-data");
}
}
}
/// <summary>
/// Refatoring friendly helper to get names of controllers and operation ids
/// </summary>
public class NamingHelpers
{
public static string GetOperationId<T>(string actionName) where T : Controller => $"{GetControllerName<T>()}{actionName}";
public static string GetControllerName<T>() where T : Controller => typeof(T).Name.Replace(nameof(Controller), string.Empty);
}
Now you should add your actions to the _actionWithUpload array!
Note that I added the extesnions only to have a refactoring friendly filter.
Last but not least, make sure the operation filter is added to the options of swagger. So add options.OperationFilter<FileUploadOperation>(); to your swagger options and done.
Full example:
services.AddSwaggerGen(options =>
{
options.SwaggerDoc(Version, new Info
{
Title = Title,
Version = Version
}
);
var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, $"{_webApiAssemblyName}.xml");
options.IncludeXmlComments(filePath);
options.DescribeAllEnumsAsStrings();
//this is the step where we add the operation filter
options.OperationFilter<FileUploadOperation>();
});
In addition to #Nick's answer, I have to make 2 changes for AspNet core 2.
1] Updated GetOperationId()
Now all operationIds contain API prefix as well as Method in the suffix. So I have statically added API & Post in ActionName.
public static string GetOperationId<T>(string actionName) where T : ControllerBase => $"Api{GetControllerName<T>()}{actionName}Post";
2] Remove only file parameter
Instead of removing all parameters for that action, I want to remove only the file parameter.
var fileParameter = operation.Parameters.FirstOrDefault(x => x.Name == "file" && x.In == "body");
if (fileParameter != null)
{
operation.Parameters.Remove(fileParameter);
...
}
For anyone who has more than one endpoint that needs to upload files and want to use more generic / descriptive / individual names for their file upload parameters:
public class SwaggerFileOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var fileParams = context.MethodInfo.GetParameters().Where(p => p.ParameterType.FullName?.Equals(typeof(Microsoft.AspNetCore.Http.IFormFile).FullName) == true);
if (fileParams.Any() && fileParams.Count() == 1)
{
var title = "The file to be uploaded";
var description = "The file to be uploaded";
int? maxLength = 5_242_880;
bool required = true;
var descriptionAttribute = fileParams.First().CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(FormFileDescriptorAttribute));
if (descriptionAttribute?.ConstructorArguments.Count > 3)
{
title = descriptionAttribute.ConstructorArguments[0].Value.ToString();
description = descriptionAttribute.ConstructorArguments[1].Value.ToString();
required = (bool)descriptionAttribute.ConstructorArguments[2].Value;
maxLength = (int)descriptionAttribute.ConstructorArguments[3].Value;
}
var uploadFileMediaType = new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "object",
Properties =
{
[fileParams.First().Name] = new OpenApiSchema()
{
Description = description,
Type = "file",
Format = "binary",
Title = title,
MaxLength = maxLength
}
}
}
};
if (required)
{
uploadFileMediaType.Schema.Required = new HashSet<string>() { fileParams.First().Name };
}
operation.RequestBody = new OpenApiRequestBody
{
Content = { ["multipart/form-data"] = uploadFileMediaType }
};
}
}
}
I created an attribute to add more description for my file upload:
public class FormFileDescriptorAttribute : Attribute
{
public FormFileDescriptorAttribute(string title, string description, bool required, int maxLength)
{
Title = title;
Description = description;
Required = required;
MaxLength = maxLength;
}
public string Title { get; set; }
public string Description { get; set; }
public int MaxLength { get; set; }
public bool Required { get; set; }
}
And then I use it as such:
public async Task<IActionResult> CreateFromFileAsync(
[FromForm, FormFileDescriptor("Project JSON file", "The project as a JSON file", true, 52_428_800)] IFormFile projectJsonFileFile,
CancellationToken cancellationToken)
{
controller:
[HttpPost]
public async Task<string> Post([FromForm] ImageUploadVW imageUpload)
{
if(imageUpload.Image.Length > 0)
{
string path = _webHostEnvironment.WebRootPath + "\\uploads\\";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
using (FileStream fileStream = System.IO.File.Create(path +
imageUpload.Image.FileName))
{
imageUpload.Image.CopyTo(fileStream);
fileStream.Flush();
return "Upload Done";
}
}
else
{
return "failed to Upload Image";
}
public class ImageUploadVW
{
public IFormFile Image { get; set; }
}
I replace only file type parameters and not use clear all parameters.
/// <summary>
/// Add extra parameters for uploading files in swagger.
/// </summary>
public class FileUploadSawggerOperationFilter : IOperationFilter
{
/// <summary>
/// Applies the specified operation.
/// </summary>
/// <param name="operation">The operation.</param>
/// <param name="context">The context.</param>
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var fileTypeParameters = context.MethodInfo.GetParameters().Where(c => c.ParameterType == typeof(IFormFile));
if (!fileTypeParameters.Any()) return;
var uploadFileMediaType = new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "object",
}
};
foreach (var fileTypeParameter in fileTypeParameters)
{
var operationParameter = operation.Parameters.SingleOrDefault(c => c.Name == fileTypeParameter.Name);
if (operationParameter is not null) operation.Parameters.Remove(operationParameter);
uploadFileMediaType.Schema.Properties.Add(fileTypeParameter.Name, new OpenApiSchema()
{
Description = "Upload File",
Type = "file",
Format = "formData"
});
}
operation.RequestBody = new OpenApiRequestBody
{
Content = { ["multipart/form-data"] = uploadFileMediaType }
};
}
}
...apart from the obvious looping through the list and a dirty great case statement!
I've turned over a few Linq queries in my head but nothing seems to get anywhere close.
Here's the an example DTO if it helps:
class ClientCompany
{
public string Title { get; private set; }
public string Forenames { get; private set; }
public string Surname { get; private set; }
public string EmailAddress { get; private set; }
public string TelephoneNumber { get; private set; }
public string AlternativeTelephoneNumber { get; private set; }
public string Address1 { get; private set; }
public string Address2 { get; private set; }
public string TownOrDistrict { get; private set; }
public string CountyOrState { get; private set; }
public string PostCode { get; private set; }
}
We have no control over the fact that we're getting the data in as KV pairs, I'm afraid.
and whilst there is an effective mapping of each KV pair to each property and I do know the keys in advance they're not named the same as the DTO.
Here is an elegant, extensible, maintainable and blazingly fast solution for loading DTOs from Dictionaries.
Create a console app and add these two files. The rest is self documenting.
The salient points:
simple, brief and maintainable mapping classes.
efficient object rehydration using dynamic methods.
NOTE: If you copied the previous DynamicProperties.cs, you will want to get this one. I added a flag to allow generation of private setters that was not in the previous version.
Cheers.
program.cs
using System.Collections.Generic;
using System.Diagnostics;
using Salient.Reflection;
namespace KVDTO
{
/// <summary>
/// This is our DTO
/// </summary>
public class ClientCompany
{
public string Address1 { get; private set; }
public string Address2 { get; private set; }
public string AlternativeTelephoneNumber { get; private set; }
public string CountyOrState { get; private set; }
public string EmailAddress { get; private set; }
public string Forenames { get; private set; }
public string PostCode { get; private set; }
public string Surname { get; private set; }
public string TelephoneNumber { get; private set; }
public string Title { get; private set; }
public string TownOrDistrict { get; private set; }
}
/// <summary>
/// This is our DTO Map
/// </summary>
public sealed class ClientCompanyMapping : KeyValueDtoMap<ClientCompany>
{
static ClientCompanyMapping()
{
AddMapping("Title", "Greeting");
AddMapping("Forenames", "First");
AddMapping("Surname", "Last");
AddMapping("EmailAddress", "eMail");
AddMapping("TelephoneNumber", "Phone");
AddMapping("AlternativeTelephoneNumber", "Phone2");
AddMapping("Address1", "Address1");
AddMapping("Address2", "Address2");
AddMapping("TownOrDistrict", "City");
AddMapping("CountyOrState", "State");
AddMapping("PostCode", "Zip");
}
}
internal class Program
{
private const string Address1 = "1243 Easy Street";
private const string CountyOrState = "Az";
private const string EmailAddress = "nunya#bidnis.com";
private const string Forenames = "Sky";
private const string PostCode = "85282";
private const string Surname = "Sanders";
private const string TelephoneNumber = "800-555-1212";
private const string Title = "Mr.";
private const string TownOrDistrict = "Tempe";
private static void Main(string[] args)
{
// this represents our input data, some discrepancies
// introduced to demonstrate functionality of the map
// the keys differ from the dto property names
// there are missing properties
// there are unrecognized properties
var input = new Dictionary<string, string>
{
{"Greeting", Title},
{"First", Forenames},
{"Last", Surname},
{"eMail", EmailAddress},
{"Phone", TelephoneNumber},
// missing from this input {"Phone2", ""},
{"Address1", Address1},
// missing from this input {"Address2", ""},
{"City", TownOrDistrict},
{"State", CountyOrState},
{"Zip", PostCode},
{"SomeOtherFieldWeDontCareAbout", "qwerty"}
};
// rehydration is simple and FAST
// instantiate a map. You could store instances in a dictionary
// but it is not really necessary for performance as all of the
// work is done in the static constructors, so no matter how many
// times you 'new' a map, it is only ever built once.
var map = new ClientCompanyMapping();
// do the work.
ClientCompany myDto = map.Load(input);
// test
Debug.Assert(myDto.Address1 == Address1, "Address1");
Debug.Assert(myDto.Address2 == null, "Address2");
Debug.Assert(myDto.AlternativeTelephoneNumber == null, "AlternativeTelephoneNumber");
Debug.Assert(myDto.CountyOrState == CountyOrState, "CountyOrState");
Debug.Assert(myDto.EmailAddress == EmailAddress, "EmailAddress");
Debug.Assert(myDto.Forenames == Forenames, "Forenames");
Debug.Assert(myDto.PostCode == PostCode, "PostCode");
Debug.Assert(myDto.Surname == Surname, "Surname");
Debug.Assert(myDto.TelephoneNumber == TelephoneNumber, "TelephoneNumber");
Debug.Assert(myDto.Title == Title, "Title");
Debug.Assert(myDto.TownOrDistrict == TownOrDistrict, "TownOrDistrict");
}
}
/// <summary>
/// Base mapper class.
/// </summary>
/// <typeparam name="T"></typeparam>
public class KeyValueDtoMap<T> where T : class, new()
{
private static readonly List<DynamicProperties.Property> Props;
private static readonly Dictionary<string, string> KvMap;
static KeyValueDtoMap()
{
// this property collection is built only once
Props = new List<DynamicProperties.Property>(DynamicProperties.CreatePropertyMethods(typeof(T)));
KvMap=new Dictionary<string, string>();
}
/// <summary>
/// Adds a mapping between a DTO property and a KeyValue pair
/// </summary>
/// <param name="dtoPropertyName">The name of the DTO property</param>
/// <param name="inputKey">The expected input key</param>
protected static void AddMapping(string dtoPropertyName,string inputKey)
{
KvMap.Add(dtoPropertyName,inputKey);
}
/// <summary>
/// Creates and loads a DTO from a Dictionary
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public T Load(Dictionary<string, string> input)
{
var result = new T();
Props.ForEach(p =>
{
string inputKey = KvMap[p.Info.Name];
if (input.ContainsKey(inputKey))
{
p.Setter.Invoke(result, input[inputKey]);
}
});
return result;
}
}
}
DynamicProperties.cs
/*!
* Project: Salient.Reflection
* File : DynamicProperties.cs
* http://spikes.codeplex.com
*
* Copyright 2010, Sky Sanders
* Dual licensed under the MIT or GPL Version 2 licenses.
* See LICENSE.TXT
* Date: Sat Mar 28 2010
*/
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace Salient.Reflection
{
/// <summary>
/// Gets IL setters and getters for a property.
/// </summary>
public static class DynamicProperties
{
#region Delegates
public delegate object GenericGetter(object target);
public delegate void GenericSetter(object target, object value);
#endregion
public static IList<Property> CreatePropertyMethods(Type T)
{
var returnValue = new List<Property>();
foreach (PropertyInfo prop in T.GetProperties())
{
returnValue.Add(new Property(prop));
}
return returnValue;
}
public static IList<Property> CreatePropertyMethods<T>()
{
var returnValue = new List<Property>();
foreach (PropertyInfo prop in typeof (T).GetProperties())
{
returnValue.Add(new Property(prop));
}
return returnValue;
}
/// <summary>
/// Creates a dynamic setter for the property
/// </summary>
/// <param name="propertyInfo"></param>
/// <returns></returns>
/// <source>
/// http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/
/// </source>
public static GenericSetter CreateSetMethod(PropertyInfo propertyInfo)
{
/*
* If there's no setter return null
*/
MethodInfo setMethod = propertyInfo.GetSetMethod(true);
if (setMethod == null)
return null;
/*
* Create the dynamic method
*/
var arguments = new Type[2];
arguments[0] = arguments[1] = typeof (object);
var setter = new DynamicMethod(
String.Concat("_Set", propertyInfo.Name, "_"),
typeof (void), arguments, propertyInfo.DeclaringType);
ILGenerator generator = setter.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
generator.Emit(OpCodes.Ldarg_1);
if (propertyInfo.PropertyType.IsClass)
generator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
else
generator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);
generator.EmitCall(OpCodes.Callvirt, setMethod, null);
generator.Emit(OpCodes.Ret);
/*
* Create the delegate and return it
*/
return (GenericSetter) setter.CreateDelegate(typeof (GenericSetter));
}
/// <summary>
/// Creates a dynamic getter for the property
/// </summary>
/// <param name="propertyInfo"></param>
/// <returns></returns>
/// <source>
/// http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/
/// </source>
public static GenericGetter CreateGetMethod(PropertyInfo propertyInfo)
{
/*
* If there's no getter return null
*/
MethodInfo getMethod = propertyInfo.GetGetMethod(true);
if (getMethod == null)
return null;
/*
* Create the dynamic method
*/
var arguments = new Type[1];
arguments[0] = typeof (object);
var getter = new DynamicMethod(
String.Concat("_Get", propertyInfo.Name, "_"),
typeof (object), arguments, propertyInfo.DeclaringType);
ILGenerator generator = getter.GetILGenerator();
generator.DeclareLocal(typeof (object));
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
generator.EmitCall(OpCodes.Callvirt, getMethod, null);
if (!propertyInfo.PropertyType.IsClass)
generator.Emit(OpCodes.Box, propertyInfo.PropertyType);
generator.Emit(OpCodes.Ret);
/*
* Create the delegate and return it
*/
return (GenericGetter) getter.CreateDelegate(typeof (GenericGetter));
}
#region Nested type: Property
public class Property
{
public GenericGetter Getter;
public PropertyInfo Info;
public GenericSetter Setter;
public Property(PropertyInfo info)
{
Info = info;
Setter = CreateSetMethod(info);
Getter = CreateGetMethod(info);
}
}
#endregion
}
}
If you can get the data to look like ['Title':'Mr', 'Forenames':'John', 'Surname':'Doe',...], then you should be able to JSON deserialize the kvp into your source object.
In such a case, I would use reflection to map the Key-Value pairs to the object properties. For an example, check the accepted answer on this SO question
Alternatively if you don't want to go with reflection you could use this (this wont' work blazingly fast):
var ccd = new List<KeyValuePair<string, string>>();
ccd.Add(new KeyValuePair<string, string>("Title", ""));
ccd.Add(new KeyValuePair<string, string>("Forenames", ""));
ccd.Add(new KeyValuePair<string, string>("Surname", ""));
ccd.Add(new KeyValuePair<string, string>("EmailAddress", ""));
ccd.Add(new KeyValuePair<string, string>("TelephoneNumber", ""));
ccd.Add(new KeyValuePair<string, string>("AlternativeTelephoneNumber", ""));
ccd.Add(new KeyValuePair<string, string>("Address1", ""));
ccd.Add(new KeyValuePair<string, string>("Address2", ""));
ccd.Add(new KeyValuePair<string, string>("TownOrDistrict", ""));
ccd.Add(new KeyValuePair<string, string>("CountyOrState", ""));
ccd.Add(new KeyValuePair<string, string>("PostCode", ""));
var data = new List<List<KeyValuePair<string, string>>> { ccd, ccd, ccd };
var companies = from d in data
select new ClientCompany {
Title = d.FirstOrDefault(k => k.Key == "Title").Value,
Forenames = d.FirstOrDefault(k => k.Key == "Forenames").Value,
Surname = d.FirstOrDefault(k => k.Key == "Surname").Value,
EmailAddress = d.FirstOrDefault(k => k.Key == "EmailAddress").Value,
TelephoneNumber = d.FirstOrDefault(k => k.Key == "TelephoneNumber").Value,
AlternativeTelephoneNumber = d.FirstOrDefault(k => k.Key == "AlternativeTelephoneNumber").Value,
Address1 = d.FirstOrDefault(k => k.Key == "Address1").Value,
Address2 = d.FirstOrDefault(k => k.Key == "Address2").Value,
TownOrDistrict = d.FirstOrDefault(k => k.Key == "TownOrDistrict").Value,
CountyOrState = d.FirstOrDefault(k => k.Key == "CountyOrState").Value,
PostCode = d.FirstOrDefault(k => k.Key == "PostCode").Value,
};