Best Practices
Recommendations for using the SDK securely and efficiently.
Credential Security
Never Store in Plain Text
// BAD - Never do this
public static Guid ApiPassword = new Guid("xxxx-xxxx-xxxx");
// GOOD - Use environment variables
public static Guid ApiPassword => new Guid(Environment.GetEnvironmentVariable("SE_API_PASSWORD"));
// GOOD - Use a secrets manager (Azure Key Vault, AWS Secrets Manager)
public static Guid ApiPassword => secretsManager.GetSecret("SecureExchanges:ApiPassword");
Protect Configuration Files
<!-- Encrypt sensitive sections of web.config -->
<appSettings configProtectionProvider="DataProtectionConfigurationProvider">
<EncryptedData>...</EncryptedData>
</appSettings>
Use DataProtectionHelper to Store Credentials (Recommended)
The SDK provides DataProtectionHelper which uses the Windows DPAPI with the machine key (DataProtectionScope.LocalMachine). Data is encrypted and can only be decrypted on the same machine.
using SecureExchangesSDK.Helpers;
using SecureExchangesSDK.Models.Transport;
// 1. Create a model to store your credentials
public class ApiCredentials
{
public Guid Serial { get; set; }
public Guid ApiUser { get; set; }
public Guid ApiPassword { get; set; }
public string Endpoint { get; set; }
}
// 2. Save credentials (encrypted with machine key)
public void SaveCredentials(ApiCredentials credentials, string filePath)
{
// Serializes to JSON then encrypts with DPAPI (machine key)
string json = SerializationHelper.SerializeToJson(credentials);
DataProtectionHelper<ApiCredentials>.ProtectAndSaveToFile(json, filePath);
}
// 3. Load credentials
public ApiCredentials LoadCredentials(string filePath)
{
// Decrypts and deserializes automatically
// Uses in-memory cache for subsequent calls
return DataProtectionHelper<ApiCredentials>.GetUnprotectedAsType(filePath);
}
// 4. Usage in your application
public class SecureMessageService
{
private readonly ApiCredentials _credentials;
private const string CredentialsPath = @"C:\ProgramData\MyApp\credentials.dat";
public SecureMessageService()
{
_credentials = DataProtectionHelper<ApiCredentials>.GetUnprotectedAsType(CredentialsPath);
}
public string SendMessage(string recipientEmail, string subject, string body)
{
var recipients = new List<RecipientInfo> { new RecipientInfo { Email = recipientEmail } };
var args = new MutliRecipientArgs(
_credentials.Endpoint,
_credentials.Serial,
_credentials.ApiUser,
_credentials.ApiPassword,
recipients, body, subject,
null, null, SendMethodEnum.onlyEmail,
false, true, true, "en-CA", 5, 1440
);
var response = MessageHelper.MultiRecipientMessage(args);
return response.Status == 200 ? response.RecipientsAnswer[0].Answer.URL : null;
}
}
Note
How DataProtectionHelper works:
- Uses
ProtectedData.Protect()withDataProtectionScope.LocalMachine - Data is encrypted with the Windows machine key
- Only processes running on the same machine can decrypt
- Includes in-memory caching to avoid repeated file reads
- Supports an optional
entropyparameter for additional security
Tip
For enhanced security, you can add custom entropy:
byte[] entropy = Encoding.UTF8.GetBytes("MySecretApplication");
DataProtectionHelper<ApiCredentials>.ProtectAndSaveToFile(json, filePath, entropy);
var credentials = DataProtectionHelper<ApiCredentials>.GetUnprotectedAsType(filePath, true, entropy);
Performance
Reuse Instances
// GOOD - Singleton service
public class MessageService
{
private readonly Guid _serial;
private readonly Guid _apiUser;
private readonly Guid _apiPassword;
public MessageService(IConfiguration config)
{
_serial = new Guid(config["SecureExchanges:Serial"]);
_apiUser = new Guid(config["SecureExchanges:ApiUser"]);
_apiPassword = new Guid(config["SecureExchanges:ApiPassword"]);
}
public string Send(...) { ... }
}
// Register as singleton
services.AddSingleton<MessageService>();
Limit In-Memory Files
// For large files, use FilesPath instead of FileArgs
if (fileSize > 50 * 1024 * 1024) // > 50 MB
{
args.FilesPath = new List<string> { filePath };
}
else
{
args.BinaryFiles = new List<FileArgs> { new FileArgs(File.ReadAllBytes(filePath), ...) };
}
Error Handling
Always Check Status
var response = MessageHelper.MultiRecipientMessage(args);
// ALWAYS check before accessing data
if (response.Status != 200)
{
_logger.LogError($"Error {response.Status}: {response.Data}");
throw new Exception(response.Data);
}
// Now you can access the data
var url = response.RecipientsAnswer[0].Answer.URL;
Implement Retries
public async Task<T> ExecuteWithRetry<T>(Func<T> action, int maxRetries = 3)
{
for (int i = 0; i < maxRetries; i++)
{
try
{
return action();
}
catch (Exception ex) when (IsTransientError(ex))
{
if (i == maxRetries - 1) throw;
await Task.Delay(1000 * (i + 1));
}
}
throw new Exception("Max retries exceeded");
}
Logging
Log Important Operations
public string SendSecureMessage(string recipient, string subject)
{
_logger.LogInformation("Sending message to {Recipient}", recipient);
var response = MessageHelper.MultiRecipientMessage(args);
if (response.Status == 200)
{
_logger.LogInformation("Message sent. URL: {Url}", response.RecipientsAnswer[0].Answer.URL);
}
else
{
_logger.LogError("Send failed. Status: {Status}, Details: {Details}",
response.Status, response.Data);
}
return response.Status == 200 ? response.RecipientsAnswer[0].Answer.URL : null;
}
Never Log Sensitive Data
// BAD
_logger.LogDebug($"Sending with password: {args.Password}");
// GOOD
_logger.LogDebug("Sending with password: [MASKED]");
Data Validation
Validate Emails Before Sending
using SecureExchangesSDK.Helpers;
foreach (var recipient in recipients)
{
if (!EmailHelper.IsValidEmail(recipient.Email))
{
throw new ArgumentException($"Invalid email: {recipient.Email}");
}
}
Validate Phone Numbers
if (!string.IsNullOrEmpty(recipient.Phone))
{
if (!PhoneHelper.IsValidPhoneNumber(recipient.Phone))
{
throw new ArgumentException($"Invalid phone: {recipient.Phone}");
}
}