Multiple instances use a co-located caching but fail to access, good named caching implementation required - asp.net

We have been transferring our services and MVC4 website to the cloud, overall this process went fine.
Except for caching, since we have moved to Azure it would also be wise to use some kind of caching which azure provides. We choose for co-located / dedicated caching role which has the advantage that the cache is used over all the instances.
Setting up the caching worked fine, I've got a named caching client which I only initialize when its required. It is set up in a inherited layer of the controllers. As soon as one of the functions is called, it checks if the connection to the data-cache is still there or its created. This all seems to work fine, but I'm building a module do retrieve prices. And multiple ajax inserts (views which get inserted into the page with use of javascript) use these functions, some of them are called at the same time, by multiple ajax views. Some of these views then return either a 404 or 500 error, and I cant explain where these are coming from except a non working caching, or something alike.
Can someone help me with a good implementation of the named caching (co-located or dedicated), since all I can find is many examples illustrating the initializing of the DataCacheFactory, but not of the data insertion and retrieval.
Below is the code as I have it now, I've tried more ways with use of locking etc but this one so far worked best.
private static object magicStick = new object();
private static DataCacheFactory dcf = null;
private static DataCache priceCache = null;
protected void CreateCacheFactory()
{
dcf = new DataCacheFactory();
}
protected void CreatePricesCache()
{
if (dcf == null)
{
CreateCacheFactory();
}
priceCache = dcf.GetCache("Prices");
}
protected PriceData GetPrices(int productID)
{
if (priceCache == null)
{
CreatePricesCache();
}
string cacheKey = "something";
lock (magicStick)
{
PriceData datas = priceCache.Get(cacheKey) as PriceData;
if (datas == null)
{
lock (magicStick)
{
Services svc = new Services();
PriceData pData = svc.PriceService.GetPrices(productID);
if (pData != null && pData.Offers != null && pData.Offers.Count() > 0)
{
datas = pData;
datas.Offers = datas.Offers.OrderBy(pr => (pr.BasePrice + pr.ShippingCosts)).ToArray();
priceCache.Add(cacheKey, datas, new TimeSpan(0, cachingTimePricesKK, 0));
}
}
}
return datas;
}
}
As soon as I get to a page where there are pricelists and the function above is called multiple times with the same arguments, there is a 5-10% chance that it returns an error rather then returning the results. Can anybody help me, im totally stuck with this for a week now and its eating me up inside.

First I'd move your cache and cacheFactory instantiation out of your getPrices method. Also, evaluate your need for the lock - this may be causing timeouts. Another VERY important observation - you are using a constant cache key and saving/retrieving data for every productId with the same cache key. You should be using a cache key like: var cacheKey = string.format("priceDatabyProductId-{0}", productId);. You need to set some breakpoints and examine exactly what you are caching and retrieving from the cache. The code as written will save the first productId to the cache and then keep returning that data regardless of the productId.
Here is a full working example we use in production using the "default" named cache in dedicated cache roles:
public static class MyCache
{
private static DataCacheFactory _cacheFactory = null;
private static DataCache ACache
{
get
{
if (_cacheFactory == null)
{
try
{
_retryPolicy.ExecuteAction(() => { _cacheFactory = new DataCacheFactory(); });
return _cacheFactory == null ? null : _cacheFactory.GetDefaultCache();
}
catch (Exception ex)
{
ErrorSignal.FromCurrentContext().Raise(ex);
return null;
}
}
return _cacheFactory.GetDefaultCache();
}
}
public static void FlushCache()
{
ACache.Clear();
}
// Define your retry strategy: retry 3 times, 1 second apart.
private static readonly FixedInterval _retryStrategy = new FixedInterval(3, TimeSpan.FromSeconds(1));
// Define your retry policy using the retry strategy and the Windows Azure storage
// transient fault detection strategy.
private static RetryPolicy _retryPolicy = new RetryPolicy<StorageTransientErrorDetectionStrategy>(_retryStrategy);
// Private constructor to prevent instantiation
// and force consumers to use the Instance property
static MyCache()
{ }
/// <summary>
/// Add an item to the cache with a key and set a absolute expiration on it
/// </summary>
public static void Add(string key, object value, int minutes = 90)
{
try
{
_retryPolicy.ExecuteAction(() => { ACache.Put(key, value, TimeSpan.FromMinutes(minutes)); });
}
catch (Exception ex)
{
ErrorSignal.FromCurrentContext().Raise(ex);
}
}
/// <summary>
/// Add the object with the specified key to the cache if it does not exist, or replace the object if it does exist and set a absolute expiration on it
/// only valid for Azure caching
/// </summary>
public static void Put(string key, object value, int minutes = 90)
{
try
{
_retryPolicy.ExecuteAction(() => { ACache.Put(key, value, TimeSpan.FromMinutes(minutes)); });
}
catch (Exception ex)
{
ErrorSignal.FromCurrentContext().Raise(ex);
}
}
/// <summary>
/// Get a strongly typed item out of cache
/// </summary>
public static T Get<T>(string key) where T : class
{
try
{
object value = null;
_retryPolicy.ExecuteAction(() => { value = ACache.Get(key); });
if (value != null) return (T) value;
return null;
}
catch (DataCacheException ex)
{
ErrorSignal.FromCurrentContext().Raise(ex);
return null;
}
catch (Exception ex)
{
ErrorSignal.FromCurrentContext().Raise(ex);
return null;
}
}
/// <summary>
/// Microsoft's suggested method for cleaning up resources such as this in a static class
/// to ensure connections and other consumed resources are returned to the resource pool
/// as quickly as possible.
/// </summary>
public static void Uninitialize()
{
if (_cacheFactory == null) return;
_cacheFactory.Dispose();
_cacheFactory = null;
}
}
Note: this is also using the Transient Fault Handling block from the Enterprise Library for transient exception fault handling.

Related

Is there a way for a UWP app to know if Recurring Billing is turned on for a subscription in the Microsoft store

I have a Xamarin Forms UWP app. I am trying to provide more specific information to users of my app regarding their subscription expiration / renewal. I would like to be able to tell if they have turned off recurring billing for the subscription. Below are the details I get back from the store regarding the current subscription. I assume the isActive will turn to false after they turn off recurring billing AND cancel the subscription after Expiration date. However, in my testing I have found that if they turn off recurring billing and are still within the the subscription period of their last recurring purchase (i.e. the expiration date is still in the future), everything seems to look exactly as if they have recurring billing turned on and the subscription will auto-renew. I am trying find a way to let them know that their current subscription will expire on a specific date versus will auto-renew on a specific date. Am I missing something?
Here is the code -- Everything works fine I just don't see anything in the object returned from the Microsoft Store API that indicates if recurring billing is on for the subscription.
using FCISharedAll;
using FCISuite.UWP;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.Services.Store;
using static FCISharedAll.FCIEnums.FlexEnums;
[assembly: Xamarin.Forms.Dependency(typeof(UWPSubscriptionManager))]
namespace FCISuite.UWP
{
public class UWPSubscriptionManager : IFCISubcriptionManager, IDisposable
{
private StoreContext context = null;
private static string[] ia_productKinds = { "Durable", "Consumable", "UnmanagedConsumable" };
private List<String> iobj_filterList = null;
//We need a constructor to load the list
public UWPSubscriptionManager()
{
iobj_filterList = new List<string>(ia_productKinds);
}
/// <summary>
/// This will run at the start of the app to get the active subscription info for the user.
/// </summary>
/// <returns></returns>
public async Task<ActiveSubscriptionDetails> GetActiveSubscriptionInformation(ActiveSubscriptionDetails pobj_SubscriptionData)
{
string ls_Result = string.Empty;
try
{
if (context == null)
{
context = StoreContext.GetDefault();
// If your app is a desktop app that uses the Desktop Bridge, you
// may need additional code to configure the StoreContext object.
// For more info, see https://aka.ms/storecontext-for-desktop.
}
StoreAppLicense appLicense = await context.GetAppLicenseAsync();
// Check if the customer has the rights to the subscription.
foreach (var addOnLicense in appLicense.AddOnLicenses)
{
StoreLicense license = addOnLicense.Value;
if (license.IsActive)
{
pobj_SubscriptionData.ActiveSubscriptionID = license.SkuStoreId;
pobj_SubscriptionData.SubscriptionEndDate = license.ExpirationDate.DateTime;
}
}
}
catch (Exception ex)
{
SharedErrorHandler.ProcessException(ex);
}
//Debug.WriteLine("End of Subscription Info");
return pobj_SubscriptionData;
}
#region get available subscriptions
public async Task<List<SubscriptionDetail>> GetAvailableSubscriptions(string ps_ProductNotFoundMessage,
string ps_StoreAddOnsNotFound, string ps_EnvironmentPrefix)
{
string ls_Result = "";
List<SubscriptionDetail> lobj_AvailableSubscriptions = new List<SubscriptionDetail>();
try
{
if (context == null)
{
context = StoreContext.GetDefault();
// If your app is a desktop app that uses the Desktop Bridge, you
// may need additional code to configure the StoreContext object.
// For more info, see https://aka.ms/storecontext-for-desktop.
}
// Get app store product details. Because this might take several moments,
// display a ProgressRing during the operation.
StoreProductResult product_queryResult = await context.GetStoreProductForCurrentAppAsync();
if (product_queryResult.Product == null)
{
// Show additional error info if it is available.
if (product_queryResult.ExtendedError != null)
{
ls_Result += $"\nExtendedError: {product_queryResult.ExtendedError.Message}";
}
// The Store catalog returned an unexpected result.
throw new Exception(ps_ProductNotFoundMessage + " " + ls_Result);
}
else
{
StoreProductQueryResult addon_queryResult = await context.GetAssociatedStoreProductsAsync(iobj_filterList);
if (addon_queryResult.ExtendedError != null)
{
// The user may be offline or there might be some other server failure.
ls_Result = $"ExtendedError: {addon_queryResult.ExtendedError.Message}";
// The Store catalog returned an unexpected result for the Add-ons.
throw new Exception(ps_StoreAddOnsNotFound + " " + ls_Result);
}
List<KeyValuePair<string, StoreProduct>> lobj_ProductsforEnvironment = new List<KeyValuePair<string, StoreProduct>>();
//We are in test - do not show the production subscriptions
lobj_ProductsforEnvironment = (from tobj_Product in addon_queryResult.Products
where tobj_Product.Value.Title.ToUpper().StartsWith(ps_EnvironmentPrefix.ToUpper())
select tobj_Product).ToList();
//foreach (KeyValuePair<string, StoreProduct> item in addon_queryResult.Products)
foreach (KeyValuePair<string, StoreProduct> item in lobj_ProductsforEnvironment)
{
// Add the store product's skus to the list
foreach (StoreSku lobj_Sku in item.Value.Skus)
{
if (lobj_Sku.IsSubscription)
{
lobj_AvailableSubscriptions.Add(new SubscriptionDetail()
{
SubscriptionID = item.Value.StoreId,
SubscriptionName = lobj_Sku.Title,
SubscriptionFormattedPrice = lobj_Sku.Price.FormattedPrice,
// Use the sku.SubscriptionInfo property to get info about the subscription.
// For example, the following code gets the units and duration of the
// subscription billing period.
SubscriptionPeriodUnit = lobj_Sku.SubscriptionInfo.BillingPeriodUnit.ToString(),
SubscriptionPeriod = lobj_Sku.SubscriptionInfo.BillingPeriod,
});
}
}
}
}
}
catch (Exception ex)
{
SharedErrorHandler.ProcessException(ex);
}
return lobj_AvailableSubscriptions;
}
#endregion
#region Subscription Purchase area
/// <summary>
/// This will get the product for the ID from the store, then request to purchase it for the user.
/// </summary>
/// <param name="ps_SubscriptionStoreId">The ID of the store subscription to be purchased</param>
/// <returns>boolean that indicates if the purchase succeeded or failed.</returns>
public async Task<SubscriptionPurchaseDetail> PurchaseSubscription(string ps_SubscriptionStoreId)
{
SubscriptionPurchaseDetail lobj_SubscriptionPurchaseDetail = new SubscriptionPurchaseDetail();
try
{
lobj_SubscriptionPurchaseDetail.PurchaseStatus = FCIStorePurchaseStatus.Succeeded;
lobj_SubscriptionPurchaseDetail.ExtendedErrorMessage = string.Empty;
lobj_SubscriptionPurchaseDetail.ProductToPurchaseWasFound = true;
//This gets the product to purchase
var tobj_StoreProduct = await GetSubscriptionProductAsync(ps_SubscriptionStoreId);
if (tobj_StoreProduct == null)
{
lobj_SubscriptionPurchaseDetail.ExtendedErrorMessage = "The add-on to purchase was not found. Product ID: " + ps_SubscriptionStoreId;
lobj_SubscriptionPurchaseDetail.ProductToPurchaseWasFound = false;
}
else
{
//Get the store product to purchase
StorePurchaseResult result = await tobj_StoreProduct.RequestPurchaseAsync();
//Convert the store namespace string to our string.
lobj_SubscriptionPurchaseDetail.PurchaseStatus = (FCIStorePurchaseStatus)Enum.Parse(typeof(FCIStorePurchaseStatus), result.Status.ToString());
// Capture the error message for the operation, if any.
if (result.ExtendedError != null)
{
lobj_SubscriptionPurchaseDetail.ExtendedErrorMessage += " " + result.ExtendedError.Message;
}
}
}
catch (Exception ex)
{
SharedErrorHandler.ProcessException(ex);
}
return lobj_SubscriptionPurchaseDetail;
}
/// <summary>
/// This returns the product we will use to call the RequestPurchaseAsync function on
/// </summary>
/// <param name="ps_SubscriptionStoreId">The ID if the subscription to purchase</param>
/// <returns>StoreProduct which represents subscritpion to purchase.</returns>
private async Task<StoreProduct> GetSubscriptionProductAsync(string ps_SubscriptionStoreId)
{
try
{
if (context == null)
{
context = StoreContext.GetDefault();
// If your app is a desktop app that uses the Desktop Bridge, you
// may need additional code to configure the StoreContext object.
// For more info, see https://aka.ms/storecontext-for-desktop.
}
// Load the sellable add-ons for this app and check if the trial is still
// available for this customer. If they previously acquired a trial they won't
// be able to get a trial again, and the StoreProduct.Skus property will
// only contain one SKU.
StoreProductQueryResult result = await context.GetAssociatedStoreProductsAsync(iobj_filterList);
if (result.ExtendedError != null)
{
throw new Exception("Something went wrong while getting the add-ons from the store. ExtendedError:" + result.ExtendedError);
}
// Look for the product that represents the subscription and return it if found - this is what we
//will purchase
foreach (var item in result.Products)
{
StoreProduct product = item.Value;
if (product.StoreId == ps_SubscriptionStoreId)
{
return product;
}
}
//If we get here the subscription was not found. We will return null and the calling function will know the
//subscription was not found.
}
catch (Exception ex)
{
SharedErrorHandler.ProcessException(ex);
}
return null;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
context = null;
}
}
#endregion
}
}
Currently, there is no such API from StoreContext that could get the information about whether the user disables auto-renew for the subscription.
When users have purchased a subscription add-on, it will renew automatically by default. Detailed messages are listed here: Subscription renewals and grace periods. If users need to check subscription mode, he still need to check it from the account service site.

Kentor Auth Services choose programmaticaly entityId based on page url

I'm integrating our asp.net MVC application with SAML2 Authentication. And using Kentor.AuthServices as module as described at kentor.AuthServices Configuration
Everithing works fine. But next step is to add usage of second service provider (which configured to use another auth mechanisms on server side) only for specified range of pages.
First, how to configure it via web.config to add second SP (not the second IdP in scope of first SP) with different entityId.
And Second, how to switch programmatically to second SP? I assume that it should happend in global.asax file in method Application_BeginRequest, but how?
Using two different SP instances in the same application is a quite rare scenario. But if you are really sure you need it, it can be achieved.
You will have to use the Kentor.AuthServices.Owin package and do the configuration in code - web.config won't do. Register two instances of the middleware. Each one will have their own configuration, including their own SP EntityID. Also make sure to change the ModulePath of at least one of them so that they get different endpoint addresses.
To challenge an authentication from either one, set the right authentication scheme in the challenge (typically in a ChallengeResult returned from a controller)
Self-answering.
Here is a workaround for multiple SP for MVC or HttpModule package, switching is based on specified range of URLs. In my case different SP realize different amount of security factors.
First, implementing custom IOptions and CookieHandler, with ability to switch to correct instance. In the web.config file, two kentor.authServices sections must be defined. In my case only "entityId" attribute differs.
public class CustomOptions : IOptions
{
private IOptions options1Factor;
private IOptions options2Factor;
private Func<bool> _checkIsSecure;
public CustomOptions(Func<bool> checkIsSecure)
{
_checkIsSecure = checkIsSecure;
AddOption(out options2Factor, "kentor.authServices1");
AddOption(out options1Factor, "kentor.authServices");
}
private void AddOption(out IOptions options, string sectionName)
{
var sp = new SPOptions((KentorAuthServicesSection)ConfigurationManager.GetSection(sectionName));
options = new Options(sp);
KentorAuthServicesSection.Current.IdentityProviders.RegisterIdentityProviders(options);
KentorAuthServicesSection.Current.Federations.RegisterFederations(options);
}
public SPOptions SPOptions
{
get
{
if (_checkIsSecure())
return options2Factor.SPOptions;
return options1Factor.SPOptions;
}
}
public IdentityProviderDictionary IdentityProviders
{
get
{
if (_checkIsSecure())
return options2Factor.IdentityProviders;
return options1Factor.IdentityProviders;
}
}
public KentorAuthServicesNotifications Notifications
{
get
{
if (_checkIsSecure())
return options2Factor.Notifications;
return options1Factor.Notifications;
}
}
}
public class CustomCookieHandler : CookieHandler
{
private Func<bool> _checkIsSecure;
private CookieHandler _originalCookieHandler1Factor;
private CookieHandler _originalCookieHandler2Factor;
public CustomCookieHandler(Func<bool> checkIsSecure)
{
_checkIsSecure = checkIsSecure;
_originalCookieHandler1Factor = new ChunkedCookieHandler()
{
Name = "commonAuth",
RequireSsl = false
};
_originalCookieHandler2Factor = new ChunkedCookieHandler()
{
Name = "securedAuth",
RequireSsl = false
};
}
public override string MatchCookiePath(Uri baseUri, Uri targetUri)
{
if (_checkIsSecure())
return _originalCookieHandler2Factor.MatchCookiePath(baseUri, targetUri);
return _originalCookieHandler1Factor.MatchCookiePath(baseUri, targetUri);
}
protected override void DeleteCore(string name, string path, string domain, HttpContext context)
{
if (_checkIsSecure())
_originalCookieHandler2Factor.Delete();
else
_originalCookieHandler1Factor.Delete();
}
protected override byte[] ReadCore(string name, HttpContext context)
{
if (_checkIsSecure())
return _originalCookieHandler2Factor.Read();
return _originalCookieHandler1Factor.Read();
}
protected override void WriteCore(byte[] value, string name, string path, string domain, DateTime expirationTime, bool secure, bool httpOnly, HttpContext context)
{
if (_checkIsSecure())
_originalCookieHandler2Factor.Write(value, true, expirationTime);
else
_originalCookieHandler1Factor.Write(value, true, expirationTime);
}
}
In Global.asax file setting static properties to custom implementations. No more modifications needed.
protected void Application_Start()
{
FederatedAuthentication.FederationConfiguration.CookieHandler = new CustomCookieHandler(CheckIsSecure);
Kentor.AuthServices.Mvc.AuthServicesController.Options = new CustomOptions(CheckIsSecure);
}
private bool CheckIsSecure()
{
if (HttpContext.Current == null)
return false;
var mainHost = "http://host.local"; // host url
var sp = new [] { "/Home/Secure" }; // array of URLs which must be secured with other SP
var request = HttpContext.Current.Request;
var isSecured = sp.Any(x => x.Equals(request.Path, StringComparison.InvariantCultureIgnoreCase));
if (!isSecured && request.Path.Equals("/AuthServices/SignIn", StringComparison.InvariantCultureIgnoreCase))
{
var returnUrl = request.QueryString["ReturnUrl"];
isSecured = !string.IsNullOrEmpty(returnUrl) &&
sp.Any(x => x.Equals(returnUrl, StringComparison.InvariantCultureIgnoreCase));
}
if (!isSecured && request.Path.Equals("/AuthServices/Acs", StringComparison.InvariantCultureIgnoreCase))
{
var _r = new HttpRequestWrapper(request).ToHttpRequestData();
isSecured = _r != null && _r.StoredRequestState != null && _r.StoredRequestState.ReturnUrl != null
&& sp.Any(x => x.Equals(_r.StoredRequestState.ReturnUrl.ToString(),
StringComparison.InvariantCultureIgnoreCase));
}
if (!isSecured && !string.IsNullOrEmpty(request.Headers["Referer"]))
{
var referer = request.Headers["Referer"];
isSecured = sp
.Select(x => string.Format("{0}/{1}", mainHost.TrimEnd('/'), x.TrimStart('/')))
.Any(x => x.Equals(referer, StringComparison.InvariantCultureIgnoreCase));
}
return isSecured;
}

'database is locked' using MobileServiceClient and MobileServiceSQLiteStore in xamarin project

I'm relatively new to mobile and async data access and I'm trying to build a line-of-business app from Xamarin starter "cross platform" template in VS2017. It seems that when I do database operations too frequently I get 'database is locked' (most questions deal with roll-your-own sqlite implementations). I had added pretty verbose logging (I have to support non-technical end mobile users).
I changed to (as suggested in other answers) a singleton model for database access which is producing non-traceable (meaning no exceptions are caught and no xamarin log entries) exceptions when calling table.ReadAsync (see below).
As a secondary question, having spent so much time on this and running into so many different roadblocks (no doubt of my own making) I'm wondering whether I'm not following some unspoken rule for mobile development such as "only one async object read per page and design UI for 100% async". Am I trying to do too much? Here is my current "singleton" data access class:
public static class MainDataStore
{
private static ReaderWriterLockSlim ReadLock = new ReaderWriterLockSlim();
public static bool IsInitialized { get; set; }
public static MobileServiceClient MobileService { get; set; }
public static bool UseAuthentication = true;
public static IMobileServiceSyncTable<User> UserTable;
public static IMobileServiceSyncTable<Showroom> ShowroomTable;
public static IEnumerable<User> Users { get; set; } //= new ObservableRangeCollection<User>();
public static IEnumerable<Showroom> Showrooms { get; set; }
public static void InitializeAsync()
{
try
{
if (IsInitialized)
return;
Logging.D("Starting to initialize main store.");
AuthenticationHandler handler = null;
handler = new AuthenticationHandler();
MobileService = new MobileServiceClient(App.AzureMobileAppUrl, handler)
{
SerializerSettings = new MobileServiceJsonSerializerSettings
{
CamelCasePropertyNames = true
}
};
var store = new MobileServiceSQLiteStore(Settings.DatabaseName);
store.DefineTable<User>();
store.DefineTable<Showroom>();
MobileService.SyncContext.InitializeAsync(store, new MobileServiceSyncHandler());
UserTable = MobileService.GetSyncTable<User>();
ShowroomTable = MobileService.GetSyncTable<Showroom>();
Logging.D("Finished initializing main store.");
IsInitialized = true;
}
catch (Exception ex)
{
Logging.E(ex); // Debug.WriteLine("EXCEPTION: " + ex.Message + ". Stack: " + ex.StackTrace);
}
}
public static async void Load(ECarnavalObjectType type)
{
Logging.D("Reading lock entering. Read count: " + ReadLock.CurrentReadCount.ToString());
// ReadLock.EnterReadLock();
switch (type)
{
case ECarnavalObjectType.Users:
await GetUsersAsync();
Users = await UserTable.ToEnumerableAsync();
break;
case ECarnavalObjectType.Showrooms:
await GetShowroomsAsync();
Showrooms = await ShowroomTable.ToEnumerableAsync();
break;
}
// ReadLock.ExitReadLock();
}
public static async Task GetUsersAsync()
{
if (CrossConnectivity.Current.IsConnected)
{
try
{
// await UserTable.ReadAsync<User>(UserTable.CreateQuery());
await UserTable.PullAsync($"all{typeof(User).Name}", UserTable.CreateQuery());
}
catch (Exception ex)
{
}
}
}
public static async Task GetShowroomsAsync()
{
await ShowroomTable.ReadAsync<Showroom>(ShowroomTable.CreateQuery());
}
}
In your code, you are not awaiting the InitializeAsync(), which means it is likely that the database is still locked and being set up when you go to synchronize it.
Arrange your code in a singleton, then have every single method (read/list/etc.) call await InitializeAsync() to initialize the database. Do an early return on the InitializeAsync() method if the database is already created (you've got some good code there for that).
For more info, see my book: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/

How to unit-test transactions in Entity Framework Core?

I have a method that does some work in a transaction:
public async Task<int> AddAsync(Item item)
{
int result;
using (var transaction = await _context.Database.BeginTransactionAsync())
{
_context.Add(item);
// Save the item so it has an ItemId
result = await _context.SaveChangesAsync();
// perform some actions using that new item's ItemId
_otherRepository.Execute(item.ItemId);
transaction.Commit();
}
return result;
}
I'd like to add unit tests to check that if _context.SaveChangesAsync or _otherRepository.Execute fail then the transaction is rolled back, is that possible?
I can't see a way to do that using InMemory or SQLite?
#Ilya Chumakov's excellent answer allowed me to unit test for the transaction. Our discussion in the comments then exposed some interesting points that I thought were worth moving into an answer so they'd be more permanent and easier to see:
The primary point is that the events logged by Entity Framework change dependent on the database provider, which surprised me. If using the InMemory provider you get just one event:
Id:1; ExecutedCommand
Whereas if you use Sqlite for the in-memory database you get four events:
Id:1; ExecutedCommand
Id:5; BeginningTransaction
Id:1; ExecutedCommand
Id:6; CommittingTransaction
I hadn't expected the events logged to change depending on the DB provider.
To anyone wanting to look into this more, I captured the event details by changing Ilya's logging code as follows:
public class FakeLogger : ILogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
var record = new LogRecord
{
EventId = eventId.Id,
RelationalEventId = (RelationalEventId) eventId.Id,
Description = formatter(state, exception)
};
Events.Add(record);
}
public List<LogRecord> Events { get; set; } = new List<LogRecord>();
public bool IsEnabled(LogLevel logLevel) => true;
public IDisposable BeginScope<TState>(TState state) => null;
}
public class LogRecord
{
public EventId EventId { get; set; }
public RelationalEventId RelationalEventId { get; set; }
public string Description { get; set; }
}
And then I adjusted my code that returns an in-memory database so that I could switch in-memory DB provider as follows:
public class InMemoryDatabase
{
public FakeLogger EfLogger { get; private set; }
public MyDbContext GetContextWithData(bool useSqlite = false)
{
EfLogger = new FakeLogger();
var factoryMock = Substitute.For<ILoggerFactory>();
factoryMock.CreateLogger(Arg.Any<string>()).Returns(EfLogger);
DbContextOptions<MyDbContext> options;
if (useSqlite)
{
// In-memory database only exists while the connection is open
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
options = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlite(connection)
.UseLoggerFactory(factoryMock)
.Options;
}
else
{
options = new DbContextOptionsBuilder<MyDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
// don't raise the error warning us that the in memory db doesn't support transactions
.ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.UseLoggerFactory(factoryMock)
.Options;
}
var ctx = new MyDbContext(options);
if (useSqlite)
{
ctx.Database.EnsureCreated();
}
// code to populate the context with test data
ctx.SaveChanges();
return ctx;
}
}
Finally, in my unit test I made sure to clear the event log just before the assert part of my test to ensure I don't get a false positive due to events that were logged during the arrange part of my test:
public async Task Commits_transaction()
{
using (var context = _inMemoryDatabase.GetContextWithData(useSqlite: true))
{
// Arrange
// code to set up date for test
// make sure none of our setup added the event we are testing for
_inMemoryDatabase.EfLogger.Events.Clear();
// Act
// Call the method that has the transaction;
// Assert
var result = _inMemoryDatabase.EfLogger.Events
.Any(x => x.EventId.Id == (int) RelationalEventId.CommittingTransaction);
You could check EF Core logs for a RelationalEventId.RollingbackTransaction event type. I provided full details here:
How to trace an Entity Framework Core event for integration testing?
How it could look:
Assert.True(eventList.Contains((int)RelationalEventId.CommittingTransaction));
I think you are asking about how to rollback when a commit fails, EF core will auto rollback on if any of the statement failed
Read more here
, if you are asking for other reason or you want to do something when rollback happens, just to add try catch blocks,
using (var transaction = await
_context.Database.BeginTransactionAsync()){
try {
_context.Add(item);
// Save the item so it has an ItemId
result = await _context.SaveChangesAsync();
// perform some actions using that new item's ItemId
_otherRepository.Execute(item.ItemId);
transaction.Commit();
}
catch (Exception)
{
// failed, Do something
} }

DNX Core: Encrypt/Decrypt?

I'm porting a website to dnx core/aspnet5/mvc6. I need to store passwords to 3rd party sites in the database (it's essentially an aggregator).
In earlier versions of mvc, I did this using classes like RijndaelManaged. But those don't appear to exist in dnx core. In fact, I haven't been able to find much documentation on any general purpose encryption/decryption stuff in dnx core.
What's the recommended approach for encrypting/decrypting single field values in an mvc6 site? I don't want to encrypt the entire sql server database.
Or should I be looking at a different approach for storing the credentials necessary to access a password-protected 3rd party site?
See the DataProtection API documentation
Their guidance on using it for persistent data protection is a little hedgy but they say there is no technical reason you can't do it. Basically to store protected data persistently you need to be willing to allow unprotecting it with expired keys since the keys could expire after you protect it.
To me it seems reasonable to use it and I am using it in my own project.
Since the IPersistedDataProtector only provides methods with byte arrays I made a couple of extension methods to convert the bytes back and forth from string.
public static class DataProtectionExtensions
{
public static string PersistentUnprotect(
this IPersistedDataProtector dp,
string protectedData,
out bool requiresMigration,
out bool wasRevoked)
{
bool ignoreRevocation = true;
byte[] protectedBytes = Convert.FromBase64String(protectedData);
byte[] unprotectedBytes = dp.DangerousUnprotect(protectedBytes, ignoreRevocation, out requiresMigration, out wasRevoked);
return Encoding.UTF8.GetString(unprotectedBytes);
}
public static string PersistentProtect(
this IPersistedDataProtector dp,
string clearText)
{
byte[] clearBytes = Encoding.UTF8.GetBytes(clearText);
byte[] protectedBytes = dp.Protect(clearBytes);
string result = Convert.ToBase64String(protectedBytes);
return result;
}
}
I also created a helper class specifically for protecting certain properties on my SiteSettings object before it gets persisted to the db.
using cloudscribe.Core.Models;
using Microsoft.AspNet.DataProtection;
using Microsoft.Extensions.Logging;
using System;
namespace cloudscribe.Core.Web.Components
{
public class SiteDataProtector
{
public SiteDataProtector(
IDataProtectionProvider dataProtectionProvider,
ILogger<SiteDataProtector> logger)
{
rawProtector = dataProtectionProvider.CreateProtector("cloudscribe.Core.Models.SiteSettings");
log = logger;
}
private ILogger log;
private IDataProtector rawProtector = null;
private IPersistedDataProtector dataProtector
{
get { return rawProtector as IPersistedDataProtector; }
}
public void Protect(ISiteSettings site)
{
if (site == null) { throw new ArgumentNullException("you must pass in an implementation of ISiteSettings"); }
if (site.IsDataProtected) { return; }
if (dataProtector == null) { return; }
if (site.FacebookAppSecret.Length > 0)
{
try
{
site.FacebookAppSecret = dataProtector.PersistentProtect(site.FacebookAppSecret);
}
catch (System.Security.Cryptography.CryptographicException ex)
{
log.LogError("data protection error", ex);
}
}
// ....
site.IsDataProtected = true;
}
public void UnProtect(ISiteSettings site)
{
bool requiresMigration = false;
bool wasRevoked = false;
if (site == null) { throw new ArgumentNullException("you must pass in an implementation of ISiteSettings"); }
if (!site.IsDataProtected) { return; }
if (site.FacebookAppSecret.Length > 0)
{
try
{
site.FacebookAppSecret = dataProtector.PersistentUnprotect(site.FacebookAppSecret, out requiresMigration, out wasRevoked);
}
catch (System.Security.Cryptography.CryptographicException ex)
{
log.LogError("data protection error", ex);
}
catch (FormatException ex)
{
log.LogError("data protection error", ex);
}
}
site.IsDataProtected = false;
if (requiresMigration || wasRevoked)
{
log.LogWarning("DataProtection key wasRevoked or requires migration, save site settings for " + site.SiteName + " to protect with a new key");
}
}
}
}
If the app will need to migrate to other machines after data has been protected then you also want to take control of the key location, the default would put the keys on the OS keyring of the machine as I understand it so a lot like machinekey in the past where you would override it in web.config to be portable.
Of course protecting the keys is on you at this point. I have code like this in the startup of my project
//If you change the key persistence location, the system will no longer automatically encrypt keys
// at rest since it doesn’t know whether DPAPI is an appropriate encryption mechanism.
services.ConfigureDataProtection(configure =>
{
string pathToCryptoKeys = appBasePath + Path.DirectorySeparatorChar
+ "dp_keys" + Path.DirectorySeparatorChar;
// these keys are not encrypted at rest
// since we have specified a non default location
// that also makes the key portable so they will still work if we migrate to
// a new machine (will they work on different OS? I think so)
// this is a similar server migration issue as the old machinekey
// where we specified a machinekey in web.config so it would not change if we
// migrate to a new server
configure.PersistKeysToFileSystem(new DirectoryInfo(pathToCryptoKeys));
});
So my keys are stored in appRoot/dp_keys in this example.
If you want to do things manually;
Add a reference to System.Security.Cryptography.Algorithms
Then you can create instances of each algorithm type via the create method. For example;
var aes = System.Security.Cryptography.Aes.Create();

Resources