I have a transport application which is used in asa a pub-/sub server to relay data between clients. The pub-/sub server need only know a little about each piece of data, for example it needs the topicname to be able to relay a published topic to the correct subscribers.
To achieve this I have thought out a scheme in which a class that is decorated as a ProtoContract includes a byte[] which in turn contains protobuf-net serialized data. This way the server does only have to deserialize a very small part of the data it relays (and does not need to know types). My type looks like this;
[ProtoContract]
public class DataFrame
{
/// <summary>
/// Time the data was issued or sampled. In UTC time.
/// </summary>
[ProtoMember(1)]
public DateTime TimeStamp = DateTime.UtcNow;
/// <summary>
/// The topic associated with the data
/// </summary>
[ProtoMember(2)]
public string TopicName = string.Empty;
/// <summary>
/// Command, can be either Discover, Subscribe, Unsubscribe or Publish
/// </summary>
[ProtoMember(3)]
public string Command = string.Empty;
/// <summary>
/// The fully qualified type name of the content
/// </summary>
[ProtoMember(4)]
private string _typeName = string.Empty;
/// <summary>
/// Serialized content data (if any)
/// </summary>
[ProtoMember(5)]
private byte[] _content;
/// <summary>
/// The fully qualified type name of the content
/// </summary>
public string TypeName
{
get
{
return _typeName;
}
}
/// <summary>
/// Get the content of this DataFrame
/// </summary>
/// <typeparam name="T">Type of the content</typeparam>
/// <returns>The content</returns>
public T GetContent<T>()
{
MemoryStream ms = new MemoryStream(_content);
return Serializer.DeserializeWithLengthPrefix<T>(ms, PrefixStyle.Base128);
}
/// <summary>
/// Set the content for this DataFrame
/// </summary>
/// <param name="value">The content to set, must be serializable and decorated as a protobuf contract type</param>
public void SetContent<T>(T value)
{
MemoryStream ms = new MemoryStream();
Serializer.SerializeWithLengthPrefix(ms, value, PrefixStyle.Base128);
_content = ms.GetBuffer();
_typeName = value.GetType().AssemblyQualifiedName;
}
/// <summary>
/// Encode the frame to a serialized byte array suitable for tranmission over a network
/// </summary>
/// <returns>The encoded byte[]</returns>
public byte[] Encode()
{
DataFrame frame = (DataFrame)this;
MemoryStream ms = new MemoryStream();
Serializer.SerializeWithLengthPrefix(ms, frame, PrefixStyle.Base128);
return ms.GetBuffer();
}
/// <summary>
/// Factory function to create a frame from a byte array that has been received
/// </summary>
/// <param name="buffer">The serialized data to decode</param>
/// <returns>A new dataframe decoded from the byte[]</returns>
public static DataFrame Decode(byte[] buffer)
{
MemoryStream ms = new MemoryStream(buffer);
DataFrame frame = Serializer.DeserializeWithLengthPrefix<DataFrame>(ms, PrefixStyle.Base128);
frame._timeStamp = DateTime.SpecifyKind(frame._timeStamp, DateTimeKind.Utc);
return frame;
}
}
Problem is, that I am able to deserialized a DataFrame, but when deserializing the payload byte[] I get protobuf exceptions. That is, this works (server is a UdpClient);
data = server.Receive(ref remoteEP);
DataFrame frame = DataFrame.Decode(data);
But this will give me a protobuf exception, even if the content is a string;
string content = frame.GetContent<string>();
Does anyone have any pointers on what I am doing wrong?
Related
I'm trying to implement the following API Endpoint. Due to the fact, System.Text.Json is now preferred over Newtonsoft.Json, I decided to try it. The response clearly works, but the deserialization doesn't.
Response
https://pastebin.com/VhDw5Rsg (Pastebin because it exceeds the limits)
Issue
I pasted the response into an online converter and it used to work for a bit, but then it broke again once I put the comments.
How do I fix it? I would also like to throw an exception if it fails to deserialize it.
Snippet
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Ardalis.GuardClauses;
using RestSharp;
namespace QSGEngine.Web.Platforms.Binance;
/// <summary>
/// Binance REST API implementation.
/// </summary>
internal class BinanceRestApiClient : IDisposable
{
/// <summary>
/// The base point url.
/// </summary>
private const string BasePointUrl = "https://api.binance.com";
/// <summary>
/// The key header.
/// </summary>
private const string KeyHeader = "X-MBX-APIKEY";
/// <summary>
/// REST Client.
/// </summary>
private readonly IRestClient _restClient = new RestClient(BasePointUrl);
/// <summary>
/// Initializes a new instance of the <see cref="BinanceRestApiClient"/> class.
/// </summary>
/// <param name="apiKey">Binance API key.</param>
/// <param name="apiSecret">Binance Secret key.</param>
public BinanceRestApiClient(string apiKey, string apiSecret)
{
Guard.Against.NullOrWhiteSpace(apiKey, nameof(apiKey));
Guard.Against.NullOrWhiteSpace(apiSecret, nameof(apiSecret));
ApiKey = apiKey;
ApiSecret = apiSecret;
}
/// <summary>
/// The API key.
/// </summary>
public string ApiKey { get; }
/// <summary>
/// The secret key.
/// </summary>
public string ApiSecret { get; }
/// <summary>
/// Gets the total account cash balance for specified account type.
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public AccountInformation? GetBalances()
{
var queryString = $"timestamp={GetNonce()}";
var endpoint = $"/api/v3/account?{queryString}&signature={AuthenticationToken(queryString)}";
var request = new RestRequest(endpoint, Method.GET);
request.AddHeader(KeyHeader, ApiKey);
var response = ExecuteRestRequest(request);
if (response.StatusCode != HttpStatusCode.OK)
{
throw new Exception($"{nameof(BinanceRestApiClient)}: request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}");
}
var deserialize = JsonSerializer.Deserialize<AccountInformation>(response.Content);
return deserialize;
}
/// <summary>
/// If an IP address exceeds a certain number of requests per minute
/// HTTP 429 return code is used when breaking a request rate limit.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private IRestResponse ExecuteRestRequest(IRestRequest request)
{
const int maxAttempts = 10;
var attempts = 0;
IRestResponse response;
do
{
// TODO: RateLimiter
//if (!_restRateLimiter.WaitToProceed(TimeSpan.Zero))
//{
// Log.Trace("Brokerage.OnMessage(): " + new BrokerageMessageEvent(BrokerageMessageType.Warning, "RateLimit",
// "The API request has been rate limited. To avoid this message, please reduce the frequency of API calls."));
// _restRateLimiter.WaitToProceed();
//}
response = _restClient.Execute(request);
// 429 status code: Too Many Requests
} while (++attempts < maxAttempts && (int)response.StatusCode == 429);
return response;
}
/// <summary>
/// Timestamp in milliseconds.
/// </summary>
/// <returns>The current timestamp in milliseconds.</returns>
private long GetNonce()
{
return DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
/// <summary>
/// Creates a signature for signed endpoints.
/// </summary>
/// <param name="payload">The body of the request.</param>
/// <returns>A token representing the request params.</returns>
private string AuthenticationToken(string payload)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(ApiSecret));
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
return BitConverter.ToString(computedHash).Replace("-", "").ToLowerInvariant();
}
/// <summary>
/// The standard dispose destructor.
/// </summary>
~BinanceRestApiClient() => Dispose(false);
/// <summary>
/// Returns true if it is already disposed.
/// </summary>
public bool IsDisposed { get; private set; }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <param name="disposing">If this method is called by a user's code.</param>
private void Dispose(bool disposing)
{
if (IsDisposed) return;
if (disposing)
{
}
IsDisposed = true;
}
/// <summary>
/// Throw if disposed.
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
private void ThrowIfDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException("BinanceRestClient has been disposed.");
}
}
}
namespace QSGEngine.Web.Platforms.Binance;
/// <summary>
/// Information about the account.
/// </summary>
public class AccountInformation
{
/// <summary>
/// Commission percentage to pay when making trades.
/// </summary>
public decimal MakerCommission { get; set; }
/// <summary>
/// Commission percentage to pay when taking trades.
/// </summary>
public decimal TakerCommission { get; set; }
/// <summary>
/// Commission percentage to buy when buying.
/// </summary>
public decimal BuyerCommission { get; set; }
/// <summary>
/// Commission percentage to buy when selling.
/// </summary>
public decimal SellerCommission { get; set; }
/// <summary>
/// Boolean indicating if this account can trade.
/// </summary>
public bool CanTrade { get; set; }
/// <summary>
/// Boolean indicating if this account can withdraw.
/// </summary>
public bool CanWithdraw { get; set; }
/// <summary>
/// Boolean indicating if this account can deposit.
/// </summary>
public bool CanDeposit { get; set; }
/// <summary>
/// The time of the update.
/// </summary>
//[JsonConverter(typeof(TimestampConverter))]
public long UpdateTime { get; set; }
/// <summary>
/// The type of the account.
/// </summary>
public string AccountType { get; set; }
/// <summary>
/// List of assets with their current balances.
/// </summary>
public IEnumerable<Balance> Balances { get; set; }
/// <summary>
/// Permission types.
/// </summary>
public IEnumerable<string> Permissions { get; set; }
}
/// <summary>
/// Information about an asset balance.
/// </summary>
public class Balance
{
/// <summary>
/// The asset this balance is for.
/// </summary>
public string Asset { get; set; }
/// <summary>
/// The amount that isn't locked in a trade.
/// </summary>
public decimal Free { get; set; }
/// <summary>
/// The amount that is currently locked in a trade.
/// </summary>
public decimal Locked { get; set; }
/// <summary>
/// The total balance of this asset (Free + Locked).
/// </summary>
public decimal Total => Free + Locked;
}
System.Text.Json is implemented quite different compared to Newtonsoft.Json. It is written to be first and foremost a very fast (de)serializer, and try to be allocation-free as much as possible.
However, it also comes with its own set of limitations, and one of those limitations is that out of the box it's a bit more rigid in what it supports.
Let's look at your JSON:
{"makerCommission":10,"takerCommission":10,"buyerCommission":0,
"sellerCommission":0,"canTrade":true,"canWithdraw":true,"canDeposit":true,
"updateTime":1636983729026,"accountType":"SPOT",
"balances":[{"asset":"BTC","free":"0.00000000","locked":"0.00000000"},
{"asset":"LTC","free":"0.00000000","locked":"0.00000000"},
(reformatted and cut for example purposes)
There's two issues here that needs to be resolved:
The properties in the JSON is written with a lowercase first letter. This will simply not match the properties in your .NET Types out of the box.
The values for free and locked are strings in the JSON but typed as decimal in your .NET types.
To fix these, your deserialization code needs to tell System.Text.Json how to deal with them, and this is how:
var options = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
And then you pass this object in through the deserialization method, like this:
… = JsonSerializer.Deserialize<AccountInformation>(response.Content, options);
This should properly deserialize this content into your objects.
2 parts to this question.
I am the owner of an API and if there is no data to return (based on business rules) the response is delivered like so:
var resp = new { error = "", code = (int)HttpStatusCode.OK, data = leads};
var json = JsonConvert.SerializeObject(resp);
var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
return Ok(json);
And when I call this in Postman, this is rendered as:
"{\"error\":\"\",\"code\":200,\"data\":[]}"
What is with the slashes?
My second part, which may or may not be fixed by fixing the slashes, is when I consume the API, and deserialize the response to an object, I receive the following error
Error converting value "{"error":"","code":200,"data":[]}" to type 'DataTypes.IntegrationResponse'. Path '', line 1, position 43.
For those who need it, IntegrationResponse is:
public class IntegrationResponse
{
public string error { get; set; }
public int code { get; set; }
public List<IntegrationLead> data { get; set; }
}
I'd make this a comment if I could, need more rep. That said -
Try making List<IntegrationLead> an InegrationLead[] and all the slashes are to escape the quotes and you have an awesome name. Cheers!
Here is how i will do this
in APi
public ActionResult SomeActionMethod() {
return Json(new {foo="bar", baz="Blech"});
}
If you want to use JsonConverter and control how the data get serilized then
public class JsonNetResult : ActionResult
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonNetResult"/> class.
/// </summary>
public JsonNetResult()
{
}
/// <summary>
/// Gets or sets the content encoding.
/// </summary>
/// <value>The content encoding.</value>
public Encoding ContentEncoding { get; set; }
/// <summary>
/// Gets or sets the type of the content.
/// </summary>
/// <value>The type of the content.</value>
public string ContentType { get; set; }
/// <summary>
/// Gets or sets the data.
/// </summary>
/// <value>The data object.</value>
public object Data { get; set; }
/// <summary>
/// Enables processing of the result of an action method by a custom type that inherits from the <see cref="T:System.Web.Mvc.ActionResult"/> class.
/// </summary>
/// <param name="context">The context in which the result is executed. The context information includes the controller, HTTP content, request context, and route data.</param>
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = !String.IsNullOrWhiteSpace(this.ContentType) ? this.ContentType : "application/json";
if (this.ContentEncoding != null)
{
response.ContentEncoding = this.ContentEncoding;
}
if (this.Data != null)
{
response.Write(JsonConvert.SerializeObject(this.Data));
}
}
}
Blockquote
public class CallbackJsonResult : JsonNetResult
{
/// <summary>
/// Initializes a new instance of the <see cref="CallbackJsonResult"/> class.
/// </summary>
/// <param name="statusCode">The status code.</param>
public CallbackJsonResult(HttpStatusCode statusCode)
{
this.Initialize(statusCode, null, null);
}
/// <summary>
/// Initializes a new instance of the <see cref="CallbackJsonResult"/> class.
/// </summary>
/// <param name="statusCode">The status code.</param>
/// <param name="description">The description.</param>
public CallbackJsonResult(HttpStatusCode statusCode, string description)
{
this.Initialize(statusCode, description, null);
}
/// <summary>
/// Initializes a new instance of the <see cref="CallbackJsonResult"/> class.
/// </summary>
/// <param name="statusCode">The status code.</param>
/// <param name="data">The callback result data.</param>
public CallbackJsonResult(object data, HttpStatusCode statusCode = HttpStatusCode.OK)
{
this.ContentType = null;
this.Initialize(statusCode, null, data);
}
/// <summary>
/// Initializes a new instance of the <see cref="CallbackJsonResult"/> class.
/// </summary>
/// <param name="statusCode">The status code.</param>
/// <param name="description">The description.</param>
/// <param name="data">The callback result data.</param>
public CallbackJsonResult(HttpStatusCode statusCode, string description, object data)
{
this.Initialize(statusCode, description, data);
}
/// <summary>
/// Initializes this instance.
/// </summary>
/// <param name="statusCode">The status code.</param>
/// <param name="description">The description.</param>
/// <param name="data">The callback result data.</param>
private void Initialize(HttpStatusCode statusCode, string description, object data)
{
Data = new JsonData() { Success = statusCode == HttpStatusCode.OK, Status = (int)statusCode, Description = description, Data = data };
}
}
}
then create an extention
/// <summary>
/// return Json Action Result
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="o"></param>
/// <param name="JsonFormatting"></param>
/// <returns></returns>
public static CallbackJsonResult ViewResult<T>(this T o)
{
return new CallbackJsonResult(o);
}
No APi simple use the extention that you created
public ActionResult SomeActionMethod() {
return new { error = "", code = (int)HttpStatusCode.OK, data = leads}.ViewResult();
}
How do I use a Swagger API (JSON) to C# code generator like SwaggerHub or NSwagStudio to generate C# Client Code for a larger API such as Clio? (https://app.clio.com/api_v4.json).
These tools seem to work fine for smaller APIs, but when you put a large schema in they output code which does not compile and seems to have multiple issues.
/// <summary>Return the data for all triggers</summary>
/// <param name="x_API_VERSION">The [API minor version](#section/Minor-Versions). Default: latest version.</param>
/// <param name="x_BULK">An indicator if [bulk actions](#section/Bulk-Actions) should be performed.
/// When performing a bulk action, the id path parameter is not required.</param>
/// <param name="fields">The fields to be returned. See response samples for what fields are available. For more information see the [fields section](#section/Fields).</param>
/// <param name="is_requirements_required">Filter JurisdictionsToTrigger records to those which require addition requirements to be checked (usually specifying trigger time).</param>
/// <param name="is_served">Filter JurisdictionsToTrigger records to those which require a service type to be selected.</param>
/// <param name="jurisdiction_id">The unique identifier for the Jurisdiction.</param>
/// <param name="limit">A limit on the number of JurisdictionsToTrigger records to be returned. Limit can range between 1 and 200. Default: `200`.</param>
/// <param name="page_token">A token specifying which page to return.</param>
/// <param name="query">Wildcard search for `description` matching a given string.</param>
/// <returns>Ok</returns>
/// <exception cref="ClioAPIException">A server side error occurred.</exception>
public System.Threading.Tasks.Task<JurisdictionsToTriggerList> CourtRulesJurisdictionsId}Triggers.jsonAsync(string x_API_VERSION, bool? x_BULK, string fields, bool? is_requirements_required, bool? is_served, int jurisdiction_id, int? limit, string page_token, string query)
{
return CourtRulesJurisdictionsId}Triggers.jsonAsync(x_API_VERSION, x_BULK, fields, is_requirements_required, is_served, jurisdiction_id, limit, page_token, query, System.Threading.CancellationToken.None);
}
For instance, in the above routine, it adds a "}" to the name CourtRulesJurisdictions}Triggers
I have tried both SwaggerHub and NSwagStudio for this particular API and neither work. The NSwagStudio has the above issue and the SwaggerHub generates code which has this issue. At the end of a client API call to get data, this call to the JsonConvert.DeserializeObject fails. The data is in the Response. Content as I can see it in the debugger, and the type is set to the correct model, but no data is placed in the model.
try
{
return JsonConvert.DeserializeObject(response.Content, type, serializerSettings);
}
catch (Exception e)
{
throw new ApiException(500, e.Message);
}
I reduced the code to this, which doesn't even use anything but the generated model and it fails.
Dim x = "{""data"":{""name"":""Fred G. Jones"",""last_name"":""Jones"",""id"":345171548}}"
u = Newtonsoft.Json.JsonConvert.DeserializeObject(x, GetType(Clio_API.Model.UserShow))
This is the generated model UserShow
using System;
using System.Linq;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.ComponentModel.DataAnnotations;
using SwaggerDateConverter = Clio_API.Client.SwaggerDateConverter;
namespace Clio_API.Model
{
/// <summary>
/// UserShow
/// </summary>
[DataContract]
public partial class UserShow : IEquatable<UserShow>, IValidatableObject
{
/// <summary>
/// Initializes a new instance of the <see cref="UserShow" /> class.
/// </summary>
[JsonConstructorAttribute]
protected UserShow() { }
/// <summary>
/// Initializes a new instance of the <see cref="UserShow" /> class.
/// </summary>
/// <param name="Data">User Object Response (required).</param>
public UserShow(User Data = default(User))
{
// to ensure "Data" is required (not null)
if (Data == null)
{
throw new InvalidDataException("Data is a required property for UserShow and cannot be null");
}
else
{
this.Data = Data;
}
}
/// <summary>
/// User Object Response
/// </summary>
/// <value>User Object Response</value>
[DataMember(Name="data", EmitDefaultValue=false)]
public User Data { get; set; }
/// <summary>
/// Returns the string presentation of the object
/// </summary>
/// <returns>String presentation of the object</returns>
public override string ToString()
{
var sb = new StringBuilder();
sb.Append("class UserShow {\n");
sb.Append(" Data: ").Append(Data).Append("\n");
sb.Append("}\n");
return sb.ToString();
}
/// <summary>
/// Returns the JSON string presentation of the object
/// </summary>
/// <returns>JSON string presentation of the object</returns>
public string ToJson()
{
return JsonConvert.SerializeObject(this, Formatting.Indented);
}
/// <summary>
/// Returns true if objects are equal
/// </summary>
/// <param name="input">Object to be compared</param>
/// <returns>Boolean</returns>
public override bool Equals(object input)
{
return this.Equals(input as UserShow);
}
/// <summary>
/// Returns true if UserShow instances are equal
/// </summary>
/// <param name="input">Instance of UserShow to be compared</param>
/// <returns>Boolean</returns>
public bool Equals(UserShow input)
{
if (input == null)
return false;
return
(
this.Data == input.Data ||
(this.Data != null &&
this.Data.Equals(input.Data))
);
}
/// <summary>
/// Gets the hash code
/// </summary>
/// <returns>Hash code</returns>
public override int GetHashCode()
{
unchecked // Overflow is fine, just wrap
{
int hashCode = 41;
if (this.Data != null)
hashCode = hashCode * 59 + this.Data.GetHashCode();
return hashCode;
}
}
/// <summary>
/// To validate all properties of the instance
/// </summary>
/// <param name="validationContext">Validation context</param>
/// <returns>Validation Result</returns>
IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
{
yield break;
}
}
}
I'm writting a Wp8/C# library for querying REST Api of MongoLab.
I have an abtract object like this :
[DataContract]
public abstract class Entity
{
[DataMember(Name = "_id")]
public string _id { get; set; }
}
The field _id is autogenerate by Mongo as a ObjectId. But with WP8, I don't have the mongoDb C# driver... The serialization and deserialization doesn't work....
This is what I've tried :
var str = url;
var response = await _httpClient.GetAsync(str);
var rep = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(rep);
I've tried with Datacontractjsonserializer too.
How can I do that?
Thank you
Here is a class I wrote for dealing with JSON serialization and deserialization in .NET 3.5
Don't forget to add a reference to System.ServiceModel.Web.dll
You can use JsonTools.ObjectToJsonString(rep);
using System;
using System.Text;
using System.Runtime.Serialization.Json;
using System.IO;
namespace Utilities
{
/// <summary>
/// Group of static methods for dealing with JSON.
/// </summary>
public static class JsonTools
{
/// <summary>
/// Serializes an object to JSON string.
/// </summary>
/// <param name="obj">The object to serialize. </param>
/// <returns></returns>
/// <exception cref="System.Runtime.Serialization.InvalidDataContractException"></exception>
/// <exception cref="System.Runtime.Serialization.SerializationException"></exception>
/// <exception cref="System.ServiceModel.QuotaExceededExceptionn"></exception>
public static string ObjectToJsonString(object obj)
{
try
{
MemoryStream jsonStream = new MemoryStream();
DataContractJsonSerializer js = new DataContractJsonSerializer(obj.GetType());
js.WriteObject(jsonStream, obj);
jsonStream.Position = 0;
StreamReader sr = new StreamReader(jsonStream);
return sr.ReadToEnd();
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Serializes an object to JSON byte array.
/// </summary>
/// <param name="obj">The object to serialize. </param>
/// <returns></returns>
/// <exception cref="System.Runtime.Serialization.InvalidDataContractException"></exception>
/// <exception cref="System.Runtime.Serialization.SerializationException"></exception>
/// <exception cref="System.ServiceModel.QuotaExceededExceptionn"></exception>
public static byte[] ObjectToJsonByteArray(object obj)
{
try
{
MemoryStream jsonStream = new MemoryStream();
DataContractJsonSerializer js = new DataContractJsonSerializer(obj.GetType());
js.WriteObject(jsonStream, obj);
jsonStream.Position = 0;
return jsonStream.ToArray();
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Deserializes a JSON formatted string to an object of the defined type
/// </summary>
/// <param name="jsonString">JSON formatted string</param>
/// <param name="objType">The type of the object which the jsonString is to be Deserialized to.</param>
/// <returns>Deserialized object</returns>
/// <exception cref="System.Runtime.Serialization.SerializationException"></exception>
public static object JsonStringToObject(string jsonString, Type objType)
{
try
{
DataContractJsonSerializer js = new DataContractJsonSerializer(objType);
byte[] jsonBytes = Encoding.Default.GetBytes(jsonString);
MemoryStream jsonStream = new MemoryStream(jsonBytes);
return js.ReadObject(jsonStream);
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Deserializes a JSON formatted byte array to an object of the defined type
/// </summary>
/// <param name="jsonBytes">JSON formatted byte array</param>
/// <param name="objType">The type of the object which the jsonString is to be Deserialized to.</param>
/// <returns>Deserialized object</returns>
/// <exception cref="System.Runtime.Serialization.SerializationException"></exception>
public static object JsonByteArrayToObject(byte[] jsonBytes, Type objType)
{
try
{
DataContractJsonSerializer js = new DataContractJsonSerializer(objType);
MemoryStream jsonStream = new MemoryStream(jsonBytes);
return js.ReadObject(jsonStream);
}
catch (Exception)
{
throw;
}
}
}
}
I'm curious about a thing that I developed.
I've written a custom ActionMethod, which is a custom FileResult that will export a given DataTable to a CSV file and append it to the response.
I just want to know if this is the correct way of testing:
Here's my custom ActionResult:
/// <summary>
/// Represents an ActionResult that represents a CSV file.
/// </summary>
public class CsvActionResult : FileResult
{
#region Properties
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
public CsvActionResult(DataTable data)
: this(data, string.Format("Export_{0}.csv", DateTime.Now.ToShortTimeString()), true, Encoding.Default, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
public CsvActionResult(DataTable data, string name)
: this(data, name, true, Encoding.Default, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, string name, string usedDelimeter)
: this(data, name, true, Encoding.Default, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
public CsvActionResult(DataTable data, string name, bool addRowHeaders)
: this(data, name, addRowHeaders, Encoding.Default, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, string name, bool addRowHeaders, string usedDelimeter)
: this(data, name, addRowHeaders, Encoding.Default, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="usedEncoding">The encoding to use.</param>
public CsvActionResult(DataTable data, string name, Encoding usedEncoding)
: this(data, name, true, usedEncoding, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="usedEncoding">The encoding to use.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, string name, Encoding usedEncoding, string usedDelimeter)
: this(data, name, true, usedEncoding, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
public CsvActionResult(DataTable data, bool addRowHeaders)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), addRowHeaders, Encoding.Default, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, bool addRowHeaders, string usedDelimeter)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), addRowHeaders, Encoding.Default, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedEncoding">The encoding to use.</param>
public CsvActionResult(DataTable data, bool addRowHeaders, Encoding usedEncoding)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), addRowHeaders, usedEncoding, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedEncoding">The encoding to use.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, bool addRowHeaders, Encoding usedEncoding, string usedDelimeter)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), addRowHeaders, usedEncoding, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="usedEncoding">The encoding to use.</param>
public CsvActionResult(DataTable data, Encoding usedEncoding)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), true, usedEncoding, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="usedEncoding">The encoding to use.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, Encoding usedEncoding, string usedDelimeter)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), true, usedEncoding, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedEncoding">The encoding to use.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, string name, bool addRowHeaders, Encoding usedEncoding, string usedDelimeter)
: base("text/csv")
{
this.dataTable = data;
this.filename = name;
this.includeRowHeader = addRowHeaders;
this.encoding = usedEncoding;
this.delimeter = usedDelimeter;
}
/// <summary>
/// The datatable that needs to be exported to a Csv file.
/// </summary>
private readonly DataTable dataTable;
/// <summary>
/// The filename that the returned file should have.
/// </summary>
private readonly string filename;
/// <summary>
/// A boolean that indicates wether to include the row header in the CSV file or not.
/// </summary>
private readonly bool includeRowHeader;
/// <summary>
/// The encoding to use.
/// </summary>
private readonly Encoding encoding;
/// <summary>
/// The delimeter to use as a seperator.
/// </summary>
private readonly string delimeter;
#endregion Properties
#region Methods
/// <summary>
/// Start writing the file.
/// </summary>
/// <param name="response">The response object.</param>
protected override void WriteFile(HttpResponseBase response)
{
//// Add the header and the content type required for this view.
//response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename));
//response.ContentType = base.ContentType;
// Add the header and the content type required for this view.
string format = string.Format("attachment; filename={0}", "somefile.csv");
response.AddHeader("Content-Disposition", format);
response.ContentType = "text/csv"; //if you use base.ContentType,
//please make sure this return the "text/csv" during test execution.
// Gets the current output stream.
var outputStream = response.OutputStream;
// Create a new memorystream.
using (var memoryStream = new MemoryStream())
{
WriteDataTable(memoryStream);
outputStream.Write(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
}
}
#endregion Methods
#region Helper Methods
/// <summary>
/// Writes a datatable to a given stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
private void WriteDataTable(Stream stream)
{
var streamWriter = new StreamWriter(stream, encoding);
// Write the header only if it's indicated to write.
if (includeRowHeader)
{ WriteHeaderLine(streamWriter); }
// Move to the next line.
streamWriter.WriteLine();
WriteDataLines(streamWriter);
streamWriter.Flush();
}
/// <summary>
/// Writes the header to a given stream.
/// </summary>
/// <param name="streamWriter">The stream to write to.</param>
private void WriteHeaderLine(StreamWriter streamWriter)
{
foreach (DataColumn dataColumn in dataTable.Columns)
{
WriteValue(streamWriter, dataColumn.ColumnName);
}
}
/// <summary>
/// Writes the data lines to a given stream.
/// </summary>
/// <param name="streamWriter"><The stream to write to./param>
private void WriteDataLines(StreamWriter streamWriter)
{
// Loop over all the rows.
foreach (DataRow dataRow in dataTable.Rows)
{
// Loop over all the colums and write the value.
foreach (DataColumn dataColumn in dataTable.Columns)
{ WriteValue(streamWriter, dataRow[dataColumn.ColumnName].ToString()); }
streamWriter.WriteLine();
}
}
/// <summary>
/// Write a specific value to a given stream.
/// </summary>
/// <param name="writer">The stream to write to.</param>
/// <param name="value">The value to write.</param>
private void WriteValue(StreamWriter writer, String value)
{
writer.Write(value);
writer.Write(delimeter);
}
#endregion Helper Methods
}
The start method of this class is WriteFile, but since this is a protected method, I've created a class in my unit test project which allows me to access this:
public class CsvActionResultTestClass : CsvActionResult
{
public CsvActionResultTestClass(DataTable dt)
: base(dt)
{
}
public new void WriteFile(HttpResponseBase response)
{ base.WriteFile(response); }
}
Basiclly, I'm creating a class that inherits from the CsvActionResult and that allows me to execute the WriteFile method.
In my unit test itself, I do execute the following code:
[TestMethod]
public void CsvActionResultController_ExportToCSV_VerifyResponsePropertiesAreSetWithExpectedValues()
{
// Initialize the test.
List<Person> persons = new List<Person>();
persons.Add(new Person() { Name = "P1_Name", Firstname = "P1_Firstname", Age = 0 });
persons.Add(new Person() { Name = "P2_Name", Firstname = "P2_Firstname" });
// Execute the test.
DataTable dtPersons = persons.ConvertToDatatable<Person>();
var httpResponseBaseMock = new Mock<HttpResponseBase>();
//This would return a fake Output stream to you SUT
httpResponseBaseMock.Setup(x => x.OutputStream).Returns(new Mock<Stream>().Object);
//the rest of response setup
CsvActionResultTestClass sut = new CsvActionResultTestClass(dtPersons);
sut.WriteFile(httpResponseBaseMock.Object);
//sut
httpResponseBaseMock.VerifySet(response => response.ContentType = "text/csv");
}
This method creates a DataTable and mock the HttpResponseBase.
Then I'm calling the method WriteFile and checks the content type of the response.
Is this the correct way of testing?
If there are other, better ways of testing, please tell me.
Kind regards,
What you doing in your Unit test and how you verify the behaviour of your SUT is correct. the technique which you have decided to use the inheritance to create a testable version is also good. This is called "Extract and Override". I would make simple changes to your test and the testable sut (system under test).
a. I would change the name to TestableCsvActionResult
public class TestableCsvActionResult : CsvActionResult
{
public TestableCsvActionResult(DataTable dt)
: base(dt)
{
}
public new void WriteFile(HttpResponseBase response)
{ base.WriteFile(response); }
}
This way it is more meaningful that you have provided a testable version. And it is not a fake.
Unit Test
[TestMethod]
public void CsvActionResultController_ExportToCSV_VerifyResponseContentTypeIsTextCsv()
{
// Arrange
var httpResponseBaseMock = new Mock<HttpResponseBase>();
httpResponseBaseMock.Setup(x => x.OutputStream).Returns(new Mock<Stream>().Object);
var sut = new CsvActionResultTestClass(new DataTable());
//Act
sut.WriteFile(httpResponseBaseStub.Object);
//Verify
httpResponseBaseMock.VerifySet(response => response.ContentType = "text/csv");
}
You test method name is good, and readable. Since you are only verifying the "text/csv" I would be more explicit on the name. This way it very clear your intention. But if you have multiple verifications, the name you had was sufficient.
ConverToDataTable is not required. Keep the test simple as possible. Use the minumum amount required to make your test pass.
Apart from general commenting (which I tidy up), everything else seems fit for the purpose.