I have created a new MailTo extension method for the Process class which just fills the Process with a new ProcessStartinfo which contains the required mailto arguments. I have created a method called FormatMailToArgument (Right at the end) which converts control characters to their Url Encoded equivelants and have tested this and it works but is there a better way of doing this?
/// <summary>
/// <see cref="Process"/> extension methods.
/// </summary>
public static class Processes
{
#region MailTo
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="mailMessage">The mail message.</param>
public static void MailTo(this Process process, MailMessage mailMessage)
{
MailTo(
process,
mailMessage.To.ToDelimetedString(),
mailMessage.CC.ToDelimetedString(),
mailMessage.Bcc.ToDelimetedString(),
mailMessage.Subject,
mailMessage.Body);
}
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="to">To email addresses.</param>
public static void MailTo(this Process process, IEnumerable<string> to)
{
MailTo(
process,
to.ToDelimetedString(Character.SemiColon),
null,
null,
null,
null);
}
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="to">To email addresses.</param>
/// <param name="subject">The email subject.</param>
public static void MailTo(this Process process, IEnumerable<string> to, string subject)
{
MailTo(
process,
to.ToDelimetedString(Character.SemiColon),
null,
null,
subject,
null);
}
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="to">To email addresses.</param>
/// <param name="subject">The email subject.</param>
/// <param name="body">The email body.</param>
public static void MailTo(
this Process process,
IEnumerable<string> to,
string subject,
string body)
{
MailTo(
process,
to.ToDelimetedString(Character.SemiColon),
null,
null,
subject,
body);
}
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="to">To email addresses.</param>
/// <param name="cc">The Cc email addresses.</param>
/// <param name="subject">The email subject.</param>
/// <param name="body">The email body.</param>
public static void MailTo(
this Process process,
IEnumerable<string> to,
IEnumerable<string> cc,
string subject,
string body)
{
MailTo(
process,
to.ToDelimetedString(Character.SemiColon),
cc.ToDelimetedString(Character.SemiColon),
null,
subject,
body);
}
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="to">To email addresses.</param>
/// <param name="cc">The Cc email addresses.</param>
/// <param name="bcc">The Bcc email addresses.</param>
/// <param name="subject">The email subject.</param>
/// <param name="body">The email body.</param>
public static void MailTo(
this Process process,
IEnumerable<string> to,
IEnumerable<string> cc,
IEnumerable<string> bcc,
string subject,
string body)
{
MailTo(
process,
(to == null) ? null : to.ToDelimetedString(Character.SemiColon),
(cc == null) ? null : cc.ToDelimetedString(Character.SemiColon),
(bcc == null) ? null : bcc.ToDelimetedString(Character.SemiColon),
subject,
body);
}
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="to">To email addresses.</param>
/// <param name="cc">The Cc email addresses.</param>
/// <param name="bcc">The Bcc email addresses.</param>
/// <param name="subject">The email subject.</param>
/// <param name="body">The email body.</param>
/// <param name="attachmentPath">The attachment file path.</param>
public static void MailTo(
this Process process,
IEnumerable<string> to,
IEnumerable<string> cc,
IEnumerable<string> bcc,
string subject,
string body,
string attachmentPath)
{
MailTo(
process,
(to == null) ? null : to.ToDelimetedString(Character.SemiColon),
(cc == null) ? null : cc.ToDelimetedString(Character.SemiColon),
(bcc == null) ? null : bcc.ToDelimetedString(Character.SemiColon),
subject,
body,
attachmentPath);
}
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="to">To email addresses delimeted by a semi-colon.</param>
/// <param name="cc">The Cc email addresses delimeted by a semi-colon.</param>
/// <param name="bcc">The Bcc email addresses delimeted by a semi-colon.</param>
/// <param name="subject">The email subject.</param>
/// <param name="body">The email body.</param>
public static void MailTo(this Process process, string to, string cc, string bcc, string subject, string body)
{
MailTo(process, to, cc, bcc, subject, body, null);
}
/// <summary>
/// Populates the process with mailto <see cref="ProcessStartInfo"/>. You may leave any
/// argument as <c>null</c> if not needed.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="to">To email addresses delimeted by a semi-colon.</param>
/// <param name="cc">The Cc email addresses delimeted by a semi-colon.</param>
/// <param name="bcc">The Bcc email addresses delimeted by a semi-colon.</param>
/// <param name="subject">The email subject.</param>
/// <param name="body">The email body.</param>
/// <param name="attachmentPath">The attachment file path. Note: this will not work in some
/// email applications.</param>
public static void MailTo(
this Process process,
string to,
string cc,
string bcc,
string subject,
string body,
string attachmentPath)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append(Uri.UriSchemeMailto + Character.Colon);
stringBuilder.Append(FormatMailToArgument(to));
if (!string.IsNullOrEmpty(cc) || !string.IsNullOrEmpty(bcc) ||
!string.IsNullOrEmpty(subject) || !string.IsNullOrEmpty(body) ||
!string.IsNullOrEmpty(attachmentPath))
{
stringBuilder.Append(Character.Question);
List<string> arguments = new List<string>();
if (!string.IsNullOrEmpty(subject))
{
arguments.Add("subject=" + FormatMailToArgument(subject));
}
if (!string.IsNullOrEmpty(body))
{
arguments.Add("body=" + FormatMailToArgument(body));
}
if (!string.IsNullOrEmpty(cc))
{
arguments.Add("CC=" + FormatMailToArgument(cc));
}
if (!string.IsNullOrEmpty(bcc))
{
arguments.Add("BCC=" + FormatMailToArgument(bcc));
}
if (!string.IsNullOrEmpty(attachmentPath))
{
arguments.Add("attachment=" + FormatMailToArgument(attachmentPath));
}
stringBuilder.Append(arguments.ToDelimetedString(Character.Ampersand));
}
process.StartInfo = new ProcessStartInfo(stringBuilder.ToString());
}
#endregion
#region Methods
/// <summary>
/// Formats the mailto argument. Converts <![CDATA['%', '&', ' ', '?', '\t', '\n']]> to their
/// hexadecimal representation.
/// </summary>
/// <param name="argument">The argument.</param>
/// <returns>The formatted argument.</returns>
private static string FormatMailToArgument(string argument)
{
return argument.
Replace(Character.Percent.ToString(), "%25").
Replace(Character.Ampersand.ToString(), "%26").
Replace(Character.Colon.ToString(), "%3A").
Replace(Character.HorizontalTab.ToString(), "%0D").
Replace(Character.NewLine.ToString(), "%0A").
Replace(Character.Question.ToString(), "%3F").
Replace(Character.Quote.ToString(), "%22").
Replace(Character.Space.ToString(), "%20");
}
#endregion
}
If you mean is there are more efficient way to escape your email addresses then yes you could use the System.Uri class.
string escapedAddress = Uri.EscapeUriString("mailto:joe blogg's\r\n#mail.com");
Console.WriteLine(escapedAddress);
Output:
mailto:joe%20blogg's%0D%0A#mail.com
You'll notice in my example the single quote character doesn't get escaped but that's because it's not required to be in email addresses.
As far as the general approach you have used I don't see why you would add an extension method to the Process class. To send an email and have all the address escaping. attachments, server authentication etc. etc. taken care of why not just use the System.Net.Mail classes?
As documented in is this official link, the EscapeUriString method assumes that stringToEscape parameter has no escape sequences in it.
This method is perfect for escaping Uris, but be careful because the target string is a mailto: line that can contain many parameters (not only subject, body, ...), obviously separated each one with some escape characters...like & and ?.
So, in my tests with Windows Store Apps with text taken from #anon comment, that string will not be fully escaped using just Uri.EscapeUriString() method, because it contains the escape sequence &.
You will need an extra manual escape to get the whole string be passed as parameter in a mailto: Uri:
Uri.EscapeUriString(stringToEscape).Replace("&", "%26");
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.
I'm looking for a way to automatically match the signature of a method between an interface, an abstract class and a derived class. It's really painful when I change one of the comments, to manually update it 3 times, because I have to update it in the interface, the abstract class and the derived class.. Is there a VS extension or something that could help me do that automatically? Btw, I'm using ReSharper Ultimate. Not sure if it has a such extension.
public interface IBotClient
{
/// <summary>
/// Gets account balance for an asset.
/// </summary>
/// <param name="asset">The asset.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The account balance.</returns>
/// <exception cref="T:ElonMuskBot.Core.Exceptions.CallFailedException">If the call fails.</exception>
Task<Balance> GetBalanceAsync(string asset, CancellationToken ct = default);
/// <summary>
/// Gets account balances.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>The account balances.</returns>
/// <exception cref="T:ElonMuskBot.Core.Exceptions.CallFailedException">If the call fails.</exception>
Task<IList<Balance>> GetBalancesAsync(CancellationToken ct = default);
...
}
public abstract class BotClientBase : IBotClient
{
private readonly IBinanceClient _client;
private readonly IBinanceSocketClient _socketClient;
protected BotClientBase(IBinanceClient client, IBinanceSocketClient socketClient)
{
_client = client;
_socketClient = socketClient;
}
/// <summary>
/// Gets account balance for an asset.
/// </summary>
/// <param name="asset">The asset.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The account balance.</returns>
/// <exception cref="T:ElonMuskBot.Core.Exceptions.CallFailedException">If the call fails.</exception>
public abstract Task<Balance> GetBalanceAsync(string asset, CancellationToken ct = default);
/// <summary>
/// Gets account balances.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>The account balances.</returns>
/// <exception cref="T:ElonMuskBot.Core.Exceptions.CallFailedException">If the call fails.</exception>
public abstract Task<IList<Balance>> GetBalancesAsync(CancellationToken ct = default);
...
}
public class SpotBotClient : BotClientBase
{
private readonly ILogger<SpotBotClient> _logger;
private readonly IBinanceClient _client;
private readonly IBinanceSocketClient _socketClient;
public SpotBotClient(ILogger<SpotBotClient> logger, IBinanceClient client, IBinanceSocketClient socketClient) : base(client, socketClient)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_client = client;
_socketClient = socketClient;
}
/// <summary>
/// Gets account balance for an asset.
/// </summary>
/// <param name="asset">The asset.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The account balance.</returns>
/// <exception cref="T:ElonMuskBot.Core.Exceptions.CallFailedException">If the call fails.</exception>
public override async Task<Balance> GetBalanceAsync(string asset, CancellationToken ct = default)
{
...
}
/// <summary>
/// Gets account balances.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>The account balances.</returns>
/// <exception cref="T:ElonMuskBot.Core.Exceptions.CallFailedException">If the call fails.</exception>
public override async Task<IList<Balance>> GetBalancesAsync(CancellationToken ct = default)
{
...
}
...
}
Documentation comments in C# allows you to use the <inheritdoc> tag, to automatically let derived types reuse the documentation comments from higher up in the hierarchy:
public interface IBotClient
{
/// <summary>
/// Gets account balance for an asset.
/// </summary>
/// <param name="asset">The asset.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The account balance.</returns>
/// <exception cref="T:ElonMuskBot.Core.Exceptions.CallFailedException">If the call fails.</exception>
Task<Balance> GetBalanceAsync(string asset, CancellationToken ct = default);
/// <summary>
/// Gets account balances.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>The account balances.</returns>
/// <exception cref="T:ElonMuskBot.Core.Exceptions.CallFailedException">If the call fails.</exception>
Task<IList<Balance>> GetBalancesAsync(CancellationToken ct = default);
...
}
///<inheritdoc cref="IBotClient"/>
public abstract class BotClientBase : IBotClient
{
}
///<inheritdoc/>
public class SpotBotClient : BotClientBase
{
}
I found out that ReSharper had it built-in.
Alt+Enter on the overriden method and then "Copy documentation from base".
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();
}
I wanna use this library to localize my application. I really like their tool to edit files and It looks like it esy to use. I've created tsd file and It looks right but It doesn't work inside app.
My txd file to localize application
The version of library
`<package id="Unclassified.TxLib" version="1.184.37" targetFramework="net45" />`
xmlns:Tx="http://unclassified.software/source/txtranslation"
.......
<TextBlock
x:Uid="HomePage_Title"
Name="txtTitle"
Foreground="White"
FontWeight="Bold"
FontSize="32"
Padding="30"
Text="{Tx:UT Key=homepage.title}"
HorizontalAlignment="Center" />
Your code works perfectly. You just shouldn't set the text foreground colour to white on a white background. ;-)
And you should review your dictionary. The text keys look weird. See my comment above.
I don't know why there's no extension in Tx library in order to convert whole string to the upper case string but this is true. I have created this extension by myself. Here's the code may be it'll help somebody.
/// <summary>
/// Markup extension providing the Tx.UT method functionality.
/// </summary>
public class UATExtension : TExtension
{
#region Constructors
/// <summary>
/// Initialises a new instance of the UATExtension class.
/// </summary>
public UATExtension()
: base()
{
}
/// <summary>
/// Initialises a new instance of the UATExtension class.
/// </summary>
/// <param name="key">Text key to translate.</param>
public UATExtension(string key)
: base(key)
{
}
/// <summary>
/// Initialises a new instance of the UATExtension class.
/// </summary>
/// <param name="key">Text key to translate.</param>
/// <param name="count">Count value to consider when selecting the text value.</param>
public UATExtension(string key, int count)
: base(key, count)
{
}
/// <summary>
/// Initialises a new instance of the UATExtension class.
/// </summary>
/// <param name="key">Text key to translate.</param>
/// <param name="countBinding">Binding that provides the count value to consider when selecting the text value.</param>
public UATExtension(string key, Binding countBinding)
: base(key, countBinding)
{
}
#endregion Constructors
#region Converter action
/// <summary>
/// Provides the T method in specialised classes.
/// </summary>
/// <returns></returns>
protected override Func<string, int, string> GetTFunc()
{
return TTx.UAT;
}
#endregion Converter action
}
And inside of the extension I'm using my own class TxService where I have just added all the same methods which there're in the original Tx class for UT abbreviation.
public static class TxService
{
#region UAT overloads
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key)
{
return UA(Tx.T(key));
}
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key, int count)
{
return UA(Tx.T(key, count));
}
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key, decimal count)
{
return UA(Tx.T(key, count));
}
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key, params string[] data)
{
return UA(Tx.T(key, data));
}
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key, Dictionary<string, string> data)
{
return UA(Tx.T(key, data));
}
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key, int count, params string[] data)
{
return UA(Tx.T(key, count, data));
}
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key, int count, Dictionary<string, string> data)
{
return UA(Tx.T(key, count, data));
}
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key, decimal count, params string[] data)
{
return UA(Tx.T(key, count, data));
}
/// <summary>
/// Combined abbreviation for the UpperCase and Text methods.
/// </summary>
public static string UAT(string key, decimal count, Dictionary<string, string> data)
{
return UA(Tx.T(key, count, data));
}
#endregion UT overloads
/// <summary>
/// Abbreviation for the <see cref="UpperCaseAll"/> method.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string UA(string text)
{
return UpperCaseAll(text);
}
/// <summary>
/// Transforms the first character of a text to upper case.
/// </summary>
/// <param name="text">Text to transform.</param>
/// <returns></returns>
public static string UpperCaseAll(string text)
{
if (string.IsNullOrEmpty(text)) return text;
return text.ToUpper();
}
}
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.