want to update a C# Winforms application to use await.
The application calls the MYOB Accountright API via an SDK.
I am using Dot Net Framework 4.5.1
The old code is like this
public void GetItems( CompanyFile companyFile )
{
var itemSvc = new ItemService(MyConfiguration, null, MyOAuthKeyService);
string pageFilter = string.Format("$top={0}&$skip={1}&$orderby=Date desc", PageSize,
PageSize * (_currentPage - 1));
itemSvc.GetRange(MyCompanyFile, pageFilter, MyCredentials, OnComplete, OnError);
}
/// <summary>
/// Method called on Async complete
/// </summary>
/// <param name="statusCode"></param>
/// <param name="items"></param>
/// <remarks></remarks>
private void OnComplete(System.Net.HttpStatusCode statusCode,
PagedCollection<Item> items)
{
myItems = items;
}
/// <summary>
/// Callback if there is an error
/// </summary>
/// <param name="uri"></param>
/// <param name="ex"></param>
/// <remarks></remarks>
private void OnError(Uri uri, Exception ex)
{
Trace.WriteLine("In OnError");
MessageBox.Show(ex.Message);
}
I want code something like this
private async Task FetchItemsAsync()
{
var itemSvc = new ItemService(MyConfiguration, null, MyOAuthKeyService);
string pageFilter = string.Format("$top={0}&$skip={1}&$orderby=Date desc", PageSize,
PageSize * (_currentPage - 1));
itemSvc.GetRange(MyCompanyFile, pageFilter, MyCredentials, OnComplete, OnError);
var totalPages = (int)Math.Ceiling((double)(myItems.Count / PageSize));
while (_currentPage < totalPages)
{
await LoadMore(); // how do I write this?
}
}
How do I do that?
[Update5]
I tried
private const double PageSize = 400;
protected CancellationTokenSource MyCancellationTokenSource;
protected CompanyFile MyCompanyFile;
protected IApiConfiguration MyConfiguration;
protected ICompanyFileCredentials MyCredentials;
protected ItemService MyItemService;
protected IOAuthKeyService MyOAuthKeyService;
private int _currentPage = 1;
private int _totalPages;
public void FetchItems(CompanyFile companyFile, IApiConfiguration configuration, ICompanyFileCredentials credentials)
{
MyCompanyFile = companyFile;
MyConfiguration = configuration;
MyCredentials = credentials;
MyCancellationTokenSource = new CancellationTokenSource();
MyItemService = new ItemService(MyConfiguration, null, MyOAuthKeyService);
FetchAllItemsAsync();
}
private async void FetchAllItemsAsync()
{
try
{
var items = new List<Item>();
int totalPages = 0;
do
{
string pageFilter = string.Format("$top={0}&$skip={1}&$orderby=Date desc", PageSize, PageSize * (_currentPage - 1));
CancellationToken ct = MyCancellationTokenSource.Token;
Log("About to Await GetRange");
Task<PagedCollection<Item>> tpc = MyItemService.GetRangeAsync(MyCompanyFile, pageFilter, MyCredentials, ct, null);
Log("About to Await GetRange B");
PagedCollection<Item> newItems = await tpc; // fails here
Log("Page {0} retrieved {1} items", _currentPage, newItems.Count);
if (totalPages == 0)
{
totalPages = (int)Math.Ceiling((items.Count / PageSize));
}
items.AddRange(newItems.Items.ToArray());
_currentPage++;
}
while (_currentPage < totalPages);
MessageBox.Show(string.Format("Fetched {0} items", items.Count));
}
catch (ApiCommunicationException ex)
{
Log(ex.ToString());
throw;
}
catch (Exception exception)
{
Log(exception.ToString());
throw;
}
}
However I get a ValidationException
{"Encountered a validation error (http://localhost:8080/AccountRight/ab5c1f96-7663-4052-8360-81004cfe8598/Inventory/Item/?$top=400&$skip=0&$orderby=Date desc)"}
[MYOB.AccountRight.SDK.ApiValidationException]: {"Encountered a validation error (http://localhost:8080/AccountRight/ab5c1f96-7663-4052-8360-81004cfe8598/Inventory/Item/?$top=400&$skip=0&$orderby=Date desc)"}
base: {"Encountered a validation error (http://localhost:8080/AccountRight/ab5c1f96-7663-4052-8360-81004cfe8598/Inventory/Item/?$top=400&$skip=0&$orderby=Date desc)"}
ErrorInformation: "Warning, error messages have not been finalised in this release and may change"
Errors: Count = 1
RequestId: "e573dfed-ec68-4aff-ac5e-3ffde1c2f943"
StatusCode: BadRequest
URI: {http://localhost:8080/AccountRight/ab5c1f96-7663-4052-8360-81004cfe8598/Inventory/Item/?$top=400&$skip=0&$orderby=Date desc}
I have cross posted this problem to
MYOB Support
The latest version of the MYOB.AccountRight.API.SDK you are referencing already has overloads for supporting async/await on .NET4, .NET45 and PCL.
The Sample code was created as an example for someone using .NET 3.5 (hence no async/await). Another sample (windows phone) shows async/await in action using the SDK
[Update]
You are probably getting an OData related exception as the Item entity does not have a Date field for which you can filter by (see docs).
When you catch an ApiCommunicationException (of which ApiValidationException is a subclass) there is an Errors property that provides more detail.
There is also a RequestId (and some other properties) which are very useful should you need to talk to the support guys if you have issues talking to the cloud hosted API.
You can use a TaskCompletionSource object that you can resolve with the result or error callback. I'm not sure what the signature of the error callback is so that part probably wont work.
private Task<PagedCollection<Item>> FetchItemsAsync()
{
var taskSource = new TaskCompletionSource<PagedCollection<Item>>();
var itemSvc = new ItemService(MyConfiguration, null, MyOAuthKeyService);
string pageFilter = string.Format("$top={0}&$skip={1}&$orderby=Date desc", PageSize,
PageSize * (_currentPage - 1));
itemSvc.GetRange(
MyCompanyFile,
pageFilter,
MyCredentials,
(statusCode, items) => taskSource.TrySetResult(items),
(error) => taskSource => taskSource.TrySetException(error) // Not sure if this is correct signature
);
return taskSource.Task;
}
You can then return the Task object that it creates which you can use for async things. I'm not really sure about what logic you are trying to implement because your question is not very detailed but you can use the method with the await command because it returns a Task object like the following.
private async void FetchAllItemsAsync()
{
int totalPages;
do
{
items = await FetchItemsAsync()
totalPages = (int)Math.Ceiling((double)(items.Count / PageSize));
_currentPage++;
} while (_currentPage < totalPages)
}
Related
I have recently completed a course about Microservices and RabbitMQ using ASP.NET Core and noticed that event processing uses dynamic invocation. The code is the following (only relevant parts retained ):
public sealed class RabbitMqBus : IEventBus
{
private readonly IMediator _mediator;
private readonly Dictionary<string, List<Type>> _handlers;
private readonly List<Type> _eventTypes;
}
public void Subscribe<TEvent, THandler>() where TEvent : Event where THandler : IEventHandler<TEvent>
{
string eventName = typeof(TEvent).Name;
var handlerType = typeof(THandler);
if (!_eventTypes.Contains(typeof(TEvent)))
_eventTypes.Add(typeof(TEvent));
if (!_handlers.ContainsKey(eventName))
_handlers.Add(eventName, new List<Type>());
if (_handlers[eventName].Any(s => s == handlerType))
throw new ArgumentException($"Handler type {handlerType.Name} is already registered for {eventName}");
_handlers[eventName].Add(handlerType);
StartBasicConsume<TEvent>();
}
private async Task ConsumerReceived(object sender, BasicDeliverEventArgs e)
{
string eventName = e.RoutingKey;
string message = Encoding.UTF8.GetString(e.Body);
try
{
await ProcessEvent(eventName, message);
}
catch (Exception exception)
{
Console.WriteLine(exception);
throw;
}
}
private async Task ProcessEvent(string eventName, string message)
{
if (_handlers.ContainsKey(eventName))
{
using var scope = _serviceScopeFactory.CreateScope();
var subscriptions = _handlers[eventName];
foreach (var subscription in subscriptions)
{
var handler = scope.ServiceProvider.GetService(subscription);
if (handler == null)
continue;
//TODO: check if this can be made typed and avoid messy dynamic invocation at the end
Type eventType = _eventTypes.SingleOrDefault(t => t.Name == eventName);
object #event = JsonConvert.DeserializeObject(message, eventType);
Type concreteType = typeof(IEventHandler<>).MakeGenericType(eventType);
await (Task) concreteType.GetMethod("Handle").Invoke(handler, new[] {#event});
}
}
}
public interface IEventHandler
{
}
public interface IEventHandler<in TEvent>: IEventHandler
where TEvent: Event
{
Task Handle(TEvent #event);
}
My issue is with the code following the TODO because it relies on reflection and dynamic invocation of a method with a clear name ("Handle").
My first improvement is eliminating the magic string through nameof:
Type eventType = _eventTypes.SingleOrDefault(t => t.Name == eventName);
Event #event = (Event) JsonConvert.DeserializeObject(message, eventType);
Type concreteType = typeof(IEventHandler<>).MakeGenericType(eventType);
await (Task)concreteType.GetMethod(nameof(IEventHandler<Event>.Handle)).Invoke(handler, new object[] { #event });
However, reflection is still used. I understand that this is required because Handle is defined as a generic method (part of a generic interface) and the received event is dynamically constructed.
Is there a way to refactor this code to avoid reflection?
With web.config going away, what is the preferred way to store sensitive info (passwords, tokens) in the configurations of a web app built using ASP.NET Core?
Is there a way to automatically get encrypted configuration sections in appsettings.json?
User secrets looks like a good solution for storing passwords, and, generally, application secrets, at least during development.
Check the official Microsoft documentation. You can also review this other SO question.
This is just a way to "hide" your secrets during development process and to avoid disclosing them into the source tree; the Secret Manager tool does not encrypt the stored secrets and should not be treated as a trusted store.
If you want to bring an encrypted appsettings.json to production, you can do so by building a custom configuration provider.
For example:
public class CustomConfigProvider : ConfigurationProvider, IConfigurationSource
{
public CustomConfigProvider()
{
}
public override void Load()
{
Data = UnencryptMyConfiguration();
}
private IDictionary<string, string> UnencryptMyConfiguration()
{
// do whatever you need to do here, for example load the file and unencrypt key by key
//Like:
var configValues = new Dictionary<string, string>
{
{"key1", "unencryptedValue1"},
{"key2", "unencryptedValue2"}
};
return configValues;
}
private IDictionary<string, string> CreateAndSaveDefaultValues(IDictionary<string, string> defaultDictionary)
{
var configValues = new Dictionary<string, string>
{
{"key1", "encryptedValue1"},
{"key2", "encryptedValue2"}
};
return configValues;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new CustomConfigProvider();
}
}
Define a static class for your extension method:
public static class CustomConfigProviderExtensions
{
public static IConfigurationBuilder AddEncryptedProvider(this IConfigurationBuilder builder)
{
return builder.Add(new CustomConfigProvider());
}
}
And then you can activate it:
// Set up configuration sources.
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEncryptedProvider()
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
I agree with #CoderSteve that writing a whole new provider is too much work. It also doesn't build on the existing standard JSON architecture. Here is a solution that I come up with the builds on top of the standard JSON architecture, uses the preferred .Net Core encryption libraries, and is very DI friendly.
public static class IServiceCollectionExtensions
{
public static IServiceCollection AddProtectedConfiguration(this IServiceCollection services)
{
services
.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(#"c:\keys"))
.ProtectKeysWithDpapi();
return services;
}
public static IServiceCollection ConfigureProtected<TOptions>(this IServiceCollection services, IConfigurationSection section) where TOptions: class, new()
{
return services.AddSingleton(provider =>
{
var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
section = new ProtectedConfigurationSection(dataProtectionProvider, section);
var options = section.Get<TOptions>();
return Options.Create(options);
});
}
private class ProtectedConfigurationSection : IConfigurationSection
{
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly IConfigurationSection _section;
private readonly Lazy<IDataProtector> _protector;
public ProtectedConfigurationSection(
IDataProtectionProvider dataProtectionProvider,
IConfigurationSection section)
{
_dataProtectionProvider = dataProtectionProvider;
_section = section;
_protector = new Lazy<IDataProtector>(() => dataProtectionProvider.CreateProtector(section.Path));
}
public IConfigurationSection GetSection(string key)
{
return new ProtectedConfigurationSection(_dataProtectionProvider, _section.GetSection(key));
}
public IEnumerable<IConfigurationSection> GetChildren()
{
return _section.GetChildren()
.Select(x => new ProtectedConfigurationSection(_dataProtectionProvider, x));
}
public IChangeToken GetReloadToken()
{
return _section.GetReloadToken();
}
public string this[string key]
{
get => GetProtectedValue(_section[key]);
set => _section[key] = _protector.Value.Protect(value);
}
public string Key => _section.Key;
public string Path => _section.Path;
public string Value
{
get => GetProtectedValue(_section.Value);
set => _section.Value = _protector.Value.Protect(value);
}
private string GetProtectedValue(string value)
{
if (value == null)
return null;
return _protector.Value.Unprotect(value);
}
}
}
Wire up your protected config sections like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Configure normal config settings
services.Configure<MySettings>(Configuration.GetSection("MySettings"));
// Configure protected config settings
services.AddProtectedConfiguration();
services.ConfigureProtected<MyProtectedSettings>(Configuration.GetSection("MyProtectedSettings"));
}
You can easily create encrypted values for your config files using a controller like this:
[Route("encrypt"), HttpGet, HttpPost]
public string Encrypt(string section, string value)
{
var protector = _dataProtectionProvider.CreateProtector(section);
return protector.Protect(value);
}
Usage:
http://localhost/cryptography/encrypt?section=SectionName:KeyName&value=PlainTextValue
I didn't want to write a custom provider – way too much work. I just wanted to tap into JsonConfigurationProvider, so I figured out a way that works for me, hope it helps someone.
public class JsonConfigurationProvider2 : JsonConfigurationProvider
{
public JsonConfigurationProvider2(JsonConfigurationSource2 source) : base(source)
{
}
public override void Load(Stream stream)
{
// Let the base class do the heavy lifting.
base.Load(stream);
// Do decryption here, you can tap into the Data property like so:
Data["abc:password"] = MyEncryptionLibrary.Decrypt(Data["abc:password"]);
// But you have to make your own MyEncryptionLibrary, not included here
}
}
public class JsonConfigurationSource2 : JsonConfigurationSource
{
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new JsonConfigurationProvider2(this);
}
}
public static class JsonConfigurationExtensions2
{
public static IConfigurationBuilder AddJsonFile2(this IConfigurationBuilder builder, string path, bool optional,
bool reloadOnChange)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("File path must be a non-empty string.");
}
var source = new JsonConfigurationSource2
{
FileProvider = null,
Path = path,
Optional = optional,
ReloadOnChange = reloadOnChange
};
source.ResolveFileProvider();
builder.Add(source);
return builder;
}
}
I managed to create a custom JSON configuration provider which uses DPAPI to encrypt and decrypt secrets. It basically uses simple regular expressions that you can define to specify what parts of the JSON needs to be encrypted.
The following steps are performed:
Json file is loaded
Determine whether the JSON parts that match the given regular expressions are already encrypted (or not). This is done by base-64 decoding of the JSON part and verify whether it starts with the expected prefix !ENC!)
If not encrypted, then encrypt the JSON part by first using DPAPI and secondly add the prefix !ENC! and encode to base-64
Overwrite the unencrypted JSON parts with the encrypted (base-64) values in the Json file
Note that the base-64 does not bring better security, but only hides the prefix !ENC! for cosmetic reasons. This is just a matter of taste of course ;)
This solution consists of the following classes:
ProtectedJsonConfigurationProvider class (= custom JsonConfigurationProvider)
ProtectedJsonConfigurationSource class (= custom JsonConfigurationSource)
AddProtectedJsonFile() extension method on the IConfigurationBuilder in order to simple add the protected configuration
Assuming the following initial authentication.json file:
{
"authentication": {
"credentials": [
{
user: "john",
password: "just a password"
},
{
user: "jane",
password: "just a password"
}
]
}
}
Which becomes (sort of) the following after loading
{
"authentication": {
"credentials": [
{
"user": "john",
"password": "IUVOQyEBAAAA0Iyd3wEV0R=="
},
{
"user": "jane",
"password": "IUVOQyEBAAAA0Iyd3wEV0R=="
}
]
}
}
And assuming the following configuration class based on the json format
public class AuthenticationConfiguration
{
[JsonProperty("credentials")]
public Collection<CredentialConfiguration> Credentials { get; set; }
}
public class CredentialConfiguration
{
[JsonProperty("user")]
public string User { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}
Below the sample code:
//Note that the regular expression will cause the authentication.credentials.password path to be encrypted.
//Also note that the byte[] contains the entropy to increase security
var configurationBuilder = new ConfigurationBuilder()
.AddProtectedJsonFile("authentication.json", true, new byte[] { 9, 4, 5, 6, 2, 8, 1 },
new Regex("authentication:credentials:[0-9]*:password"));
var configuration = configurationBuilder.Build();
var authenticationConfiguration = configuration.GetSection("authentication").Get<AuthenticationConfiguration>();
//Get the decrypted password from the encrypted JSON file.
//Note that the ProtectedJsonConfigurationProvider.TryGet() method is called (I didn't expect that :D!)
var password = authenticationConfiguration.Credentials.First().Password
Install the Microsoft.Extensions.Configuration.Binder package in order to get the configuration.GetSection("authentication").Get<T>() implementation
And finally the classes in which the magic happens :)
/// <summary>Represents a <see cref="ProtectedJsonConfigurationProvider"/> source</summary>
public class ProtectedJsonConfigurationSource : JsonConfigurationSource
{
/// <summary>Gets the byte array to increse protection</summary>
internal byte[] Entropy { get; private set; }
/// <summary>Represents a <see cref="ProtectedJsonConfigurationProvider"/> source</summary>
/// <param name="entropy">Byte array to increase protection</param>
/// <exception cref="ArgumentNullException"/>
public ProtectedJsonConfigurationSource(byte[] entropy)
{
this.Entropy = entropy ?? throw new ArgumentNullException(Localization.EntropyNotSpecifiedError);
}
/// <summary>Builds the configuration provider</summary>
/// <param name="builder">Builder to build in</param>
/// <returns>Returns the configuration provider</returns>
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new ProtectedJsonConfigurationProvider(this);
}
/// <summary>Gets or sets the protection scope of the configuration provider. Default value is <see cref="DataProtectionScope.CurrentUser"/></summary>
public DataProtectionScope Scope { get; set; }
/// <summary>Gets or sets the regular expressions that must match the keys to encrypt</summary>
public IEnumerable<Regex> EncryptedKeyExpressions { get; set; }
}
/// <summary>Represents a provider that protects a JSON configuration file</summary>
public partial class ProtectedJsonConfigurationProvider : JsonConfigurationProvider
{
private readonly ProtectedJsonConfigurationSource protectedSource;
private readonly HashSet<string> encryptedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private static readonly byte[] encryptedPrefixBytes = Encoding.UTF8.GetBytes("!ENC!");
/// <summary>Checks whether the given text is encrypted</summary>
/// <param name="text">Text to check</param>
/// <returns>Returns true in case the text is encrypted</returns>
private bool isEncrypted(string text)
{
if (text == null) { return false; }
//Decode the data in order to verify whether the decoded data starts with the expected prefix
byte[] decodedBytes;
try { decodedBytes = Convert.FromBase64String(text); }
catch (FormatException) { return false; }
return decodedBytes.Length >= encryptedPrefixBytes.Length
&& decodedBytes.AsSpan(0, encryptedPrefixBytes.Length).SequenceEqual(encryptedPrefixBytes);
}
/// <summary>Converts the given key to the JSON token path equivalent</summary>
/// <param name="key">Key to convert</param>
/// <returns>Returns the JSON token path equivalent</returns>
private string convertToTokenPath(string key)
{
var jsonStringBuilder = new StringBuilder();
//Split the key by ':'
var keyParts = key.Split(':');
for (var keyPartIndex = 0; keyPartIndex < keyParts.Length; keyPartIndex++)
{
var keyPart = keyParts[keyPartIndex];
if (keyPart.All(char.IsDigit)) { jsonStringBuilder.Append('[').Append(keyPart).Append(']'); }
else if (keyPartIndex > 0) { jsonStringBuilder.Append('.').Append(keyPart); }
else { jsonStringBuilder.Append(keyPart); }
}
return jsonStringBuilder.ToString();
}
/// <summary>Writes the given encrypted key/values to the JSON oconfiguration file</summary>
/// <param name="encryptedKeyValues">Encrypted key/values to write</param>
private void writeValues(IDictionary<string, string> encryptedKeyValues)
{
try
{
if (encryptedKeyValues == null || encryptedKeyValues.Count == 0) { return; }
using (var stream = new FileStream(this.protectedSource.Path, FileMode.Open, FileAccess.ReadWrite))
{
JObject json;
using (var streamReader = new StreamReader(stream, Encoding.UTF8, true, 4096, true))
{
using (var jsonTextReader = new JsonTextReader(streamReader))
{
json = JObject.Load(jsonTextReader);
foreach (var encryptedKeyValue in encryptedKeyValues)
{
var tokenPath = this.convertToTokenPath(encryptedKeyValue.Key);
var value = json.SelectToken(tokenPath) as JValue;
if (value.Value != null) { value.Value = encryptedKeyValue.Value; }
}
}
}
stream.Seek(0, SeekOrigin.Begin);
using (var streamWriter = new StreamWriter(stream))
{
using (var jsonTextWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented })
{
json.WriteTo(jsonTextWriter);
}
}
}
}
catch (Exception exception)
{
throw new Exception(string.Format(Localization.ProtectedJsonConfigurationWriteEncryptedValues, this.protectedSource.Path), exception);
}
}
/// <summary>Represents a provider that protects a JSON configuration file</summary>
/// <param name="source">Settings of the source</param>
/// <see cref="ArgumentNullException"/>
public ProtectedJsonConfigurationProvider(ProtectedJsonConfigurationSource source) : base(source)
{
this.protectedSource = source as ProtectedJsonConfigurationSource;
}
/// <summary>Loads the JSON data from the given <see cref="Stream"/></summary>
/// <param name="stream"><see cref="Stream"/> to load</param>
public override void Load(Stream stream)
{
//Call the base method first to ensure the data to be available
base.Load(stream);
var expressions = protectedSource.EncryptedKeyExpressions;
if (expressions != null)
{
//Dictionary that contains the keys (and their encrypted value) that must be written to the JSON file
var encryptedKeyValuesToWrite = new Dictionary<string, string>();
//Iterate through the data in order to verify whether the keys that require to be encrypted, as indeed encrypted.
//Copy the keys to a new string array in order to avoid a collection modified exception
var keys = new string[this.Data.Keys.Count];
this.Data.Keys.CopyTo(keys, 0);
foreach (var key in keys)
{
//Iterate through each expression in order to check whether the current key must be encrypted and is encrypted.
//If not then encrypt the value and overwrite the key
var value = this.Data[key];
if (!string.IsNullOrEmpty(value) && expressions.Any(e => e.IsMatch(key)))
{
this.encryptedKeys.Add(key);
//Verify whether the value is encrypted
if (!this.isEncrypted(value))
{
var protectedValue = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), protectedSource.Entropy, protectedSource.Scope);
var protectedValueWithPrefix = new List<byte>(encryptedPrefixBytes);
protectedValueWithPrefix.AddRange(protectedValue);
//Convert the protected value to a base-64 string in order to mask the prefix (for cosmetic purposes)
//and overwrite the key with the encrypted value
var protectedBase64Value = Convert.ToBase64String(protectedValueWithPrefix.ToArray());
encryptedKeyValuesToWrite.Add(key, protectedBase64Value);
this.Data[key] = protectedBase64Value;
}
}
}
//Write the encrypted key/values to the JSON configuration file
this.writeValues(encryptedKeyValuesToWrite);
}
}
/// <summary>Attempts to get the value of the given key</summary>
/// <param name="key">Key to get</param>
/// <param name="value">Value of the key</param>
/// <returns>Returns true in case the key has been found</returns>
public override bool TryGet(string key, out string value)
{
if (!base.TryGet(key, out value)) { return false; }
else if (!this.encryptedKeys.Contains(key)) { return true; }
//Key is encrypted and must therefore be decrypted in order to return.
//Note that the decoded base-64 bytes contains the encrypted prefix which must be excluded when unprotection
var protectedValueWithPrefix = Convert.FromBase64String(value);
var protectedValue = new byte[protectedValueWithPrefix.Length - encryptedPrefixBytes.Length];
Buffer.BlockCopy(protectedValueWithPrefix, encryptedPrefixBytes.Length, protectedValue, 0, protectedValue.Length);
var unprotectedValue = ProtectedData.Unprotect(protectedValue, this.protectedSource.Entropy, this.protectedSource.Scope);
value = Encoding.UTF8.GetString(unprotectedValue);
return true;
}
/// <summary>Provides extensions concerning <see cref="ProtectedJsonConfigurationProvider"/></summary>
public static class ProtectedJsonConfigurationProviderExtensions
{
/// <summary>Adds a protected JSON file</summary>
/// <param name="configurationBuilder"><see cref="IConfigurationBuilder"/> in which to apply the JSON file</param>
/// <param name="path">Path to the JSON file</param>
/// <param name="optional">Specifies whether the JSON file is optional</param>
/// <param name="entropy">Byte array to increase protection</param>
/// <returns>Returns the <see cref="IConfigurationBuilder"/></returns>
/// <exception cref="ArgumentNullException"/>
public static IConfigurationBuilder AddProtectedJsonFile(this IConfigurationBuilder configurationBuilder, string path, bool optional, byte[] entropy, params Regex[] encryptedKeyExpressions)
{
var source = new ProtectedJsonConfigurationSource(entropy)
{
Path = path,
Optional = optional,
EncryptedKeyExpressions = encryptedKeyExpressions
};
return configurationBuilder.Add(source);
}
}
public static IServiceCollection ConfigureProtected<TOptions>(this IServiceCollection services, IConfigurationSection section) where TOptions: class, new()
{
return services.AddSingleton(provider =>
{
var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
var protectedSection = new ProtectedConfigurationSection(dataProtectionProvider, section);
var options = protectedSection.Get<TOptions>();
return Options.Create(options);
});
}
This method is correct
Just a few clarifications to help avoid problems. When you encrypt a value, it's using the section as 'Purpose' (https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/purpose-strings?view=aspnetcore-2.2) When you get a 'Payload not valid' or something similar, it's likely that the purpose you used to encrypt it, differs from the purpose use to decrypt it. So, let's say I have a first level section in my appsettings.json named 'SecureSettings' and within it a connection string:
{
"SecureSettings":
{
"ConnectionString":"MyClearTextConnectionString"
}
}
To encrypt the value, I'd call: http://localhost/cryptography/encrypt?section=SecureSettings:ConnectionString&value=MyClearTextConnectionString
You may not want to keep an Encrypt controller in the app itself btw.
I am trying to implement Stripe.net into my Xamarin.Forms PCL using an ASP.NET Core MVC Web API. The goal is to process credit card payment from users. My web API runs locally on http://localhost:port for testing purposes.
In the PaymentPage, a user enters their credit card information into Entry objects and when they click the submit Button, a method in the PaymentPageViewModel is called to start the logic:
async void OnFinishBookingClicked(object sender, System.EventArgs e)
{
// TODO: Stripe integration
var viewModel = (PaymentPageViewModel)this.BindingContext;
await viewModel.ProcessPayment();
}
This is part of the PaymentPageViewModel:
private readonly IStripeRepository _repository;
private readonly IAPIRepository _api;
public PaymentPageViewModel(IStripeRepository repository, IAPIRepository api)
{
_repository = repository;
_api = api;
}
public async Task ProcessPayment()
{
try
{
if (string.IsNullOrEmpty(ExpirationDate))
ExpirationDate = "09/18";
var exp = ExpirationDate.Split('/');
var token = _repository.CreateToken(CreditCardNumber, exp[0], exp[1], SecurityCode);
await Application.Current.MainPage.DisplayAlert("Test Message", token, "OK");
await _api.ChargeCard(token, 5.00M);
}
catch (Exception ex)
{
await Application.Current.MainPage.DisplayAlert("Error", ex.Message, "OK");
}
}
This is what the APIRepository looks like:
public class APIRepository: IAPIRepository
{
const string Url = "http://localhost:5000";
private string authorizationKey;
private async Task<HttpClient> GetClient()
{
HttpClient client = new HttpClient();
if (string.IsNullOrEmpty(authorizationKey))
{
authorizationKey = await client.GetStringAsync(Url);
authorizationKey = JsonConvert.DeserializeObject<string>(authorizationKey);
}
client.DefaultRequestHeaders.Add("Authorization", authorizationKey);
client.DefaultRequestHeaders.Add("Accept", "application/json");
return client;
}
public async Task<string> ChargeCard(string token, decimal amount)
{
HttpClient client = await GetClient();
var json = JsonConvert.SerializeObject(new { token, amount });
var response = await client.PostAsync("/api/Stripe", new StringContent(json));
return await response.Content.ReadAsStringAsync();
}
}
The issue is that I get a series of errors during await _api.ChargeCard(token, 5.00M):
The first exception happens during authorizationKey = await client.GetStringAsync(Url); the exception message is the following:
{System.Net.Http.HttpRequestException: 404 (Not Found) at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode () [0x0000a] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/System.Net.Http/System.Net.Http/HttpResponseM…}
I get another exception during response = await client.PostAsync("/api/Stripe", new StringContent(json));
{System.InvalidOperationException: The request URI must either be an absolute URI or BaseAddress must be set at System.Net.Http.HttpClient.SendAsync (System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpCompletionOption completionOption, System.Thr…}
The third exception happens at the catch block of the viewModel.ProcessPayment() method:
{System.NullReferenceException: Object reference not set to an instance of an object at Zwaby.Services.APIRepository+d__3.MoveNext () [0x00184] in /Users/carlos/Projects/Zwaby/Zwaby/Services/APIRepository.cs:57 --- End of stack trace from previou…}
In my Web API project, I have a StripeController, but my implementation may not be fully correct:
[Route("api/Stripe")]
public class StripeController : Controller
{
private readonly StripeContext _context;
public StripeController(StripeContext context)
{
_context = context;
if (_context.StripeCharges.Count() == 0)
{
_context.StripeCharges.Add(new StripeItem { });
_context.SaveChanges();
}
}
[HttpGet]
public IActionResult Get(string key)
{
// TODO: implement method that returns authorization key
}
[HttpPost]
public IActionResult Charge(string stripeToken, decimal amount)
{
var customers = new StripeCustomerService();
var charges = new StripeChargeService();
var customer = customers.Create(new StripeCustomerCreateOptions
{
SourceToken = stripeToken
});
var charge = charges.Create(new StripeChargeCreateOptions
{
Amount = (int)amount,
Description = "Sample Charge",
Currency = "usd",
CustomerId = customer.Id
});
return View();
}
}
For completeness, I am including the StripeRepository class, the other parameter of the PaymentPageViewModel:
public class StripeRepository: IStripeRepository
{
public string CreateToken(string cardNumber, string cardExpMonth, string cardExpYear, string cardCVC)
{
StripeConfiguration.SetApiKey("my_test_key");
//TODO: Wireup card information below
var tokenOptions = new StripeTokenCreateOptions()
{
Card = new StripeCreditCardOptions()
{
Number = "4242424242424242",
ExpirationYear = 2018,
ExpirationMonth = 10,
Cvc = "123"
}
};
var tokenService = new StripeTokenService();
StripeToken stripeToken = tokenService.Create(tokenOptions);
return stripeToken.Id;
}
}
Thank you so much!
Request to get analyzer list just hangs. Specifically > return Client.ListAnalyzersAsync().Result
This ultimately calls an http client request.
My code is directly from the sample app. Still no love. Appreciate the help
Here is the default url in the LinquisticClient library
private const string DefaultServiceHost = "https://api.projectoxford.ai/linguistics/v1.0";
Here is the class that I created
My call is to this method - Parse()
public static class LinguisticAnalyzer
{
private static readonly LinguisticsClient Client = new LinguisticsClient("Removed_id");
public static string Parse(string line)
{
// List analyzers
Analyzer[] supportedAnalyzers = null;
try
{
supportedAnalyzers = ListAnalyzers();
var analyzersAsJson = JsonConvert.SerializeObject(supportedAnalyzers, Formatting.Indented, jsonSerializerSettings);
Console.WriteLine("Supported analyzers: " + analyzersAsJson);
}
catch (Exception e)
{
Console.Error.WriteLine("Failed to list supported analyzers: " + e.ToString());
Environment.Exit(1);
}
// Analyze text with all available analyzers
var analyzeTextRequest = new AnalyzeTextRequest()
{
Language = "en",
AnalyzerIds = supportedAnalyzers.Select(analyzer => analyzer.Id).ToArray(),
Text = line //"Welcome to Microsoft Linguistic Analysis!"
};
try
{
var analyzeTextResults = AnalyzeText(analyzeTextRequest);
var resultsAsJson = JsonConvert.SerializeObject(analyzeTextResults, Formatting.Indented, jsonSerializerSettings);
Console.WriteLine("Analyze text results: " + resultsAsJson);
return resultsAsJson;
}
catch (Exception e)
{
Console.Error.WriteLine("Failed to list supported analyzers: " + e.ToString());
Environment.Exit(1);
}
return "";
}
/// <summary>
/// Default jsonserializer settings
/// </summary>
private static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings()
{
DateFormatHandling = DateFormatHandling.IsoDateFormat,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
/// <summary>
/// List analyzers synchronously.
/// </summary>
/// <returns>An array of supported analyzers.</returns>
private static Analyzer[] ListAnalyzers()
{
try
{
return Client.ListAnalyzersAsync().Result;
}
catch (Exception exception)
{
throw new Exception("Failed to gather list of analyzers", exception as ClientException);
}
}
/// <summary>
/// Analyze text synchronously.
/// </summary>
/// <param name="request">Analyze text request.</param>
/// <returns>An array of analyze text result.</returns>
private static AnalyzeTextResult[] AnalyzeText(AnalyzeTextRequest request)
{
try
{
return Client.AnalyzeTextAsync(request).Result;
}
catch (Exception exception)
{
throw new Exception("Failed to analyze text", exception as ClientException);
}
}
}
This turned out to be an issue with the C# client library, not the service itself. More information can be found on GitHub.
Using the default Visual Studio 2013 Web API project template with individual user accounts, and posting to the /token endpoint with an Accept header of application/xml, the server still returns the response in JSON:
{"access_token":"...","token_type":"bearer","expires_in":1209599}
Is there a way to get the token back as XML?
According to RFC6749 the response format should be JSON and Microsoft implemented it accordingly. I found out that JSON formatting is implemented in Microsoft.Owin.Security.OAuth.OAuthAuthorizationServerHandler internal class with no means of extension.
I also encountered the need to have token response in XML.
The best solution I came up with was to implement HttpModule converting JSON to XML when stated in Accept header.
public class OAuthTokenXmlResponseHttpModule : IHttpModule
{
private static readonly string FilterKey = typeof(OAuthTokenXmlResponseHttpModule).Name + typeof(MemoryStreamFilter).Name;
public void Init(HttpApplication application)
{
application.BeginRequest += ApplicationOnBeginRequest;
application.EndRequest += ApplicationOnEndRequest;
}
private static void ApplicationOnBeginRequest(object sender, EventArgs eventArgs)
{
var application = (HttpApplication)sender;
if (ShouldConvertToXml(application.Context.Request) == false) return;
var filter = new MemoryStreamFilter(application.Response.Filter);
application.Response.Filter = filter;
application.Context.Items[FilterKey] = filter;
}
private static bool ShouldConvertToXml(HttpRequest request)
{
var isTokenPath = string.Equals("/token", request.Path, StringComparison.InvariantCultureIgnoreCase);
var header = request.Headers["Accept"];
return isTokenPath && (header == "text/xml" || header == "application/xml");
}
private static void ApplicationOnEndRequest(object sender, EventArgs eventArgs)
{
var context = ((HttpApplication) sender).Context;
var filter = context.Items[FilterKey] as MemoryStreamFilter;
if (filter == null) return;
var jsonResponse = filter.ToString();
var xDocument = JsonConvert.DeserializeXNode(jsonResponse, "oauth");
var xmlResponse = xDocument.ToString(SaveOptions.DisableFormatting);
WriteResponse(context.Response, xmlResponse);
}
private static void WriteResponse(HttpResponse response, string xmlResponse)
{
response.Clear();
response.ContentType = "application/xml;charset=UTF-8";
response.Write(xmlResponse);
}
public void Dispose()
{
}
}
public class MemoryStreamFilter : Stream
{
private readonly Stream _stream;
private readonly MemoryStream _memoryStream = new MemoryStream();
public MemoryStreamFilter(Stream stream)
{
_stream = stream;
}
public override void Flush()
{
_stream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return _stream.Read(buffer, offset, count);
}
public override void Write(byte[] buffer, int offset, int count)
{
_memoryStream.Write(buffer, offset, count);
_stream.Write(buffer, offset, count);
}
public override string ToString()
{
return Encoding.UTF8.GetString(_memoryStream.ToArray());
}
#region Rest of the overrides
public override bool CanRead
{
get { throw new NotImplementedException(); }
}
public override bool CanSeek
{
get { throw new NotImplementedException(); }
}
public override bool CanWrite
{
get { throw new NotImplementedException(); }
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override long Length
{
get { throw new NotImplementedException(); }
}
public override long Position
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
#endregion
}
Ok I had such a fun time trying to figure this out using OWIN I thought I would share my solution with the community, I borrowed some insight from other posts https://stackoverflow.com/a/26216511/1148288 and https://stackoverflow.com/a/29105880/1148288 along with the concepts Alexei describs in his post. Nothing fancy doing with implementation but I had a requirement for my STS to return an XML formatted response, I wanted to keep with the paradigm of honoring the Accept header, so my end point would examine that to determine if it needed to run the XML swap or not. This is what I am current using:
private void ConfigureXMLResponseSwap(IAppBuilder app)
{
app.Use(async (context, next) =>
{
if (context.Request != null &&
context.Request.Headers != null &&
context.Request.Headers.ContainsKey("Accept") &&
context.Request.Headers.Get("Accept").Contains("xml"))
{
//Set a reference to the original body stream
using (var stream = context.Response.Body)
{
//New up and set the response body as a memory stream which implements the ability to read and set length
using (var buffer = new MemoryStream())
{
context.Response.Body = buffer;
//Allow other middlewares to process
await next.Invoke();
//On the way out, reset the buffer and read the response body into a string
buffer.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(buffer))
{
string responsebody = await reader.ReadToEndAsync();
//Using our responsebody string, parse out the XML and add a declaration
var xmlVersion = JsonConvert.DeserializeXNode(responsebody, "oauth");
xmlVersion.Declaration = new XDeclaration("1.0", "UTF-8", "yes");
//Convert the XML to a byte array
var bytes = Encoding.UTF8.GetBytes(xmlVersion.Declaration + xmlVersion.ToString());
//Clear the buffer bits and write out our new byte array
buffer.SetLength(0);
buffer.Write(bytes, 0, bytes.Length);
buffer.Seek(0, SeekOrigin.Begin);
//Set the content length to the new buffer length and the type to an xml type
context.Response.ContentLength = buffer.Length;
context.Response.ContentType = "application/xml;charset=UTF-8";
//Copy our memory stream buffer to the output stream for the client application
await buffer.CopyToAsync(stream);
}
}
}
}
else
await next.Invoke();
});
}
Of course you would then wire this up during startup config like so:
public void Configuration(IAppBuilder app)
{
HttpConfiguration httpConfig = new HttpConfiguration();
//Highly recommend this is first...
ConfigureXMLResponseSwap(app);
...more config stuff...
}
Hope that helps any other lost souls that find there way to the this post seeking to do something like this!
take a look here i hope it can help how to set a Web API REST service to always return XML not JSON
Could you retry by doing the following steps:
In the WebApiConfig.Register(), specify
config.Formatters.XmlFormatter.UseXmlSerializer = true;
var supportedMediaTypes = config.Formatters.XmlFormatter.SupportedMediaTypes;
if (supportedMediaTypes.Any(it => it.MediaType.IndexOf("application/xml", StringComparison.InvariantCultureIgnoreCase) >= 0) ==false)
{
supportedMediaTypes.Insert(0,new MediaTypeHeaderValue("application/xml"));
}
I normally just remove the XmlFormatter altogether.
// Remove the XML formatter
config.Formatters.Remove(config.Formatters.XmlFormatter);
Add the line above in your WebApiConfig class...
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
// Remove the XML formatter
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}