Does anyone have any tips for dealing with ConstraintExceptions thrown by XSD datasets?
This is the exception with the cryptic message:
System.Data.ConstraintException : Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.
A couple of tips that I've found lately.
It's much better to use the TableAdapter FillByDataXXXX() methods instead of GetDataByXXXX() methods because the DataTable passed into the fill method can be interrogated for clues:
DataTable.GetErrors() returns an
array of DataRow instances in error
DataRow.RowError contains a
description of the row error
DataRow.GetColumnsInError() returns
an array of DataColumn instances in
error
Recently, I wrapped up some interrogation code into a subclass of ConstraintException that's turned out to be a useful starting point for debugging.
C# Example usage:
Example.DataSet.fooDataTable table = new DataSet.fooDataTable();
try
{
tableAdapter.Fill(table);
}
catch (ConstraintException ex)
{
// pass the DataTable to DetailedConstraintException to get a more detailed Message property
throw new DetailedConstraintException("error filling table", table, ex);
}
Output:
DetailedConstraintException : table fill failed
Errors reported for ConstraintExceptionHelper.DataSet+fooDataTable [foo]
Columns in error: [1]
[PRODUCT_ID] - total rows affected: 1085
Row errors: [4]
[Column 'PRODUCT_ID' is constrained to be unique. Value '1' is already present.] - total rows affected: 1009
[Column 'PRODUCT_ID' is constrained to be unique. Value '2' is already present.] - total rows affected: 20
[Column 'PRODUCT_ID' is constrained to be unique. Value '4' is already present.] - total rows affected: 34
[Column 'PRODUCT_ID' is constrained to be unique. Value '6' is already present.] - total rows affected: 22
----> System.Data.ConstraintException : Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.
I don't know if this is too much code to include in a Stack Overflow answer but here's the C# class in full.
Disclaimer: this works for me, please feel free to use/modify as appropriate.
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
namespace ConstraintExceptionHelper
{
/// <summary>
/// Subclass of ConstraintException that explains row and column errors in the Message property
/// </summary>
public class DetailedConstraintException : ConstraintException
{
private const int InitialCountValue = 1;
/// <summary>
/// Initialises a new instance of DetailedConstraintException with the specified string and DataTable
/// </summary>
/// <param name="message">exception message</param>
/// <param name="ErroredTable">DataTable in error</param>
public DetailedConstraintException(string message, DataTable erroredTable)
: base(message)
{
ErroredTable = erroredTable;
}
/// <summary>
/// Initialises a new instance of DetailedConstraintException with the specified string, DataTable and inner Exception
/// </summary>
/// <param name="message">exception message</param>
/// <param name="ErroredTable">DataTable in error</param>
/// <param name="inner">the original exception</param>
public DetailedConstraintException(string message, DataTable erroredTable, Exception inner)
: base(message, inner)
{
ErroredTable = erroredTable;
}
private string buildErrorSummaryMessage()
{
if (null == ErroredTable) { return "No errored DataTable specified"; }
if (!ErroredTable.HasErrors) { return "No Row Errors reported in DataTable=[" + ErroredTable.TableName + "]"; }
foreach (DataRow row in ErroredTable.GetErrors())
{
recordColumnsInError(row);
recordRowsInError(row);
}
StringBuilder sb = new StringBuilder();
appendSummaryIntro(sb);
appendErroredColumns(sb);
appendRowErrors(sb);
return sb.ToString();
}
private void recordColumnsInError(DataRow row)
{
foreach (DataColumn column in row.GetColumnsInError())
{
if (_erroredColumns.ContainsKey(column.ColumnName))
{
_erroredColumns[column.ColumnName]++;
continue;
}
_erroredColumns.Add(column.ColumnName, InitialCountValue);
}
}
private void recordRowsInError(DataRow row)
{
if (_rowErrors.ContainsKey(row.RowError))
{
_rowErrors[row.RowError]++;
return;
}
_rowErrors.Add(row.RowError, InitialCountValue);
}
private void appendSummaryIntro(StringBuilder sb)
{
sb.AppendFormat("Errors reported for {1} [{2}]{0}", Environment.NewLine, ErroredTable.GetType().FullName, ErroredTable.TableName);
}
private void appendErroredColumns(StringBuilder sb)
{
sb.AppendFormat("Columns in error: [{1}]{0}", Environment.NewLine, _erroredColumns.Count);
foreach (string columnName in _erroredColumns.Keys)
{
sb.AppendFormat("\t[{1}] - rows affected: {2}{0}",
Environment.NewLine,
columnName,
_erroredColumns[columnName]);
}
}
private void appendRowErrors(StringBuilder sb)
{
sb.AppendFormat("Row errors: [{1}]{0}", Environment.NewLine, _rowErrors.Count);
foreach (string rowError in _rowErrors.Keys)
{
sb.AppendFormat("\t[{1}] - rows affected: {2}{0}",
Environment.NewLine,
rowError,
_rowErrors[rowError]);
}
}
/// <summary>
/// Get the DataTable in error
/// </summary>
public DataTable ErroredTable
{
get { return _erroredTable; }
private set { _erroredTable = value; }
}
/// <summary>
/// Get the original ConstraintException message with extra error information
/// </summary>
public override string Message
{
get { return base.Message + Environment.NewLine + buildErrorSummaryMessage(); }
}
private readonly SortedDictionary<string, int> _rowErrors = new SortedDictionary<string, int>();
private readonly SortedDictionary<string, int> _erroredColumns = new SortedDictionary<string, int>();
private DataTable _erroredTable;
}
}
Related
I have a custom process screen which works pretty much as expected, except that the status / monitor screen that pops up during processing doesn't show the progress, or the records processed as it does on other, stock processing screens.
Here's what a stock processing popup looks like:
And here's what my custom popup looks like. It doesn't show 'Remaining' and pretty much just looks like this until it's finished:
And then shows this when finished:
Is there something I need to add to my processing screen to get this functionality?
Thanks much...
In your processing method are you using PXProcessing.SetCurrentItem(object) to set the current processing item and then the PXProcessing.SetProcessed() or PXProcessing.SetError(ExceptionObject) to set the status of the current processing item.
Example:
public PXProcessing<SOLine> linesToBeProcessed;
protected virtual void ProcessOrderLines(List<SOLines> lines)
{
foreach (SOLine line in lines)
{
PXProcessing<SOLine>.SetCurrentItem(line);
try
{
//logic
PXProcessing<SOLine>.SetProcessed();
}
catch (Exception ex)
{
PXProcessing<SOLine>.SetError(ex);
}
}
}
Note: The DACNAME to use in these methods is the main DAC used in the PXProcessing view that was declared.
This is a chunk of code from our Acumatica Surveys project that depicts how we control the messaging for various types.
Notice the SetInfo, SetWarning, and SetError
Let me know if this helps?
/// <summary>
/// This method will create a new collector record and invoke a notification on it.
/// </summary>
/// <param name="surveyUser"></param>
/// <param name="graph"></param>
/// <param name="surveyCurrent"></param>
/// <param name="surveyUserList"></param>
/// <param name="filter"></param>
/// <remarks>
/// </remarks>
/// <returns>
/// Whether or not an error has occured within the process which is used by the main calling process to throw a final exception at the end of the process
/// </returns>
private static bool SendNew(SurveyUser surveyUser, SurveyCollectorMaint graph, Survey surveyCurrent, List<SurveyUser> surveyUserList, SurveyFilter filter) {
bool errorOccurred = false;
try {
string sCollectorStatus = (surveyUser.UsingMobileApp.GetValueOrDefault(false)) ?
SurveyResponseStatus.CollectorSent : SurveyResponseStatus.CollectorNew;
graph.Clear();
SurveyCollector surveyCollector = new SurveyCollector {
CollectorName =
$"{surveyCurrent.SurveyName} {PXTimeZoneInfo.Now:yyyy-MM-dd hh:mm:ss}",
SurveyID = surveyUser.SurveyID,
UserID = surveyUser.UserID,
CollectedDate = null,
ExpirationDate = CalculateExpirationDate(filter.DurationTimeSpan),
CollectorStatus = sCollectorStatus
};
surveyCollector = graph.Collector.Insert(surveyCollector);
graph.Persist();
SendNotification(surveyUser, surveyCollector);
if (sCollectorStatus == SurveyResponseStatus.CollectorSent) {
PXProcessing<SurveyUser>.SetInfo(surveyUserList.IndexOf(surveyUser), Messages.SurveySent);
} else {
PXProcessing<SurveyUser>.SetWarning(surveyUserList.IndexOf(surveyUser), Messages.NoDeviceError);
}
} catch (AggregateException ex) {
errorOccurred = true;
var message = string.Join(";", ex.InnerExceptions.Select(e => e.Message));
PXProcessing<SurveyUser>.SetError(surveyUserList.IndexOf(surveyUser), message);
} catch (Exception e) {
errorOccurred = true;
PXProcessing<SurveyUser>.SetError(surveyUserList.IndexOf(surveyUser), e);
}
return errorOccurred;
}
I am creating a UIActivityIndicatorView in my Controller.ViewDidLoad
UIActivityIndicatorView spinner = new UIActivityIndicatorView();
spinner.StartAnimating();
spinner.Hidden = true;
this.Add(spinner);
Then I am binding it with MVVMCross
var set = this.CreateBindingSet<TipView, TipViewModel>();
set.Bind(spinner).For(v => v.Hidden).To(vm => vm.IsBusy).WithConversion("Inverse");
When the View initially loads the UIActivityIndicatorView is spinning and visible. This is incorrect as the IsBusy property is being explicitly set to False in the ViewModel's Init(). I can see this happening and I can see the Converter invert the value.
I know the binding is properly connected because if I fire a command that updates the IsBusy property the Indicator is shown and hidden as I would expect. It is just the initial state that is incorrect.
The StartAnimating method seems to cause the Hidden flag to be overridden. If I do not call StartAnimating the Indicator hides and shows as expected. Of course that means I have a non animating
Indicator.
I could get a WeakReference to the VM, listen to PropertyChanged and call StartAnimating but that seems a bit rubbish.
Does anyone have any better ideas?
Some options you can do:
Subscribe to PropertyChanged changes and write custom code in the event handler (as you suggest in your question)
Inherit from UIActivityIndicatorView and write a public get;set; property which provides the composite functionality (calling Start and Hidden) in the set handler
public class MyIndicatorView : UIActivityIndicatorView {
// ctors
private bool _superHidden;
public bool SuperHidden {
get { return _supperHidden; }
set { _superHidden = value; if (!value) StartAnimating() else StopAnimating(); Hidden = value; }
}
}
Provide a View public get;set; property and put the composite functionality in that (e.g. set.Bind(this).For(v => v.MyAdvanced)...
private bool _myAdvanced;
public bool MyAdvanced {
get { return myAdvanced; }
set { myAdvanced = value; if (!value) _spinner.StartAnimating() else _spinner.StopAnimating(); _spinner.Hidden = value; }
}
Write a custom binding for Hidden which replaces the default functionality and contains the combined Start and Hidden calls (for more on custom bindings, there's a couple of N+1 tutorials)
After reading #slodge's reply I went down the road of Weak Event Listener and ran the code to Hide and StartAnimating in the View. Having copy and pasted that approach 3 times I realised something had to change so I implemented his 4th suggestion and wrote a Custom Binding. FWIW here is that custom binding
/// <summary>
/// Custom Binding for UIActivityIndicator Hidden.
/// This binding will ensure the indicator animates when shown and stops when hidden
/// </summary>
public class ActivityIndicatorViewHiddenTargetBinding : MvxConvertingTargetBinding
{
/// <summary>
/// Initializes a new instance of the <see cref="ActivityIndicatorViewHiddenTargetBinding"/> class.
/// </summary>
/// <param name="target">The target.</param>
public ActivityIndicatorViewHiddenTargetBinding(UIActivityIndicatorView target)
: base(target)
{
if (target == null)
{
MvxBindingTrace.Trace(
MvxTraceLevel.Error,
"Error - UIActivityIndicatorView is null in ActivityIndicatorViewHiddenTargetBinding");
}
}
/// <summary>
/// Gets the default binding mode.
/// </summary>
/// <value>
/// The default mode.
/// </value>
public override MvxBindingMode DefaultMode
{
get { return MvxBindingMode.OneWay; }
}
/// <summary>
/// Gets the type of the target.
/// </summary>
/// <value>
/// The type of the target.
/// </value>
public override System.Type TargetType
{
get { return typeof(bool); }
}
/// <summary>
/// Gets the view.
/// </summary>
/// <value>
/// The view.
/// </value>
protected UIActivityIndicatorView View
{
get { return Target as UIActivityIndicatorView; }
}
/// <summary>
/// Sets the value.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="value">The value.</param>
protected override void SetValueImpl(object target, object value)
{
var view = (UIActivityIndicatorView)target;
if (view == null)
{
return;
}
view.Hidden = (bool)value;
if (view.Hidden)
{
view.StopAnimating();
}
else
{
view.StartAnimating();
}
}
}
I have created the following singleton class to handle a Redis connection, and expose BookSleeve functionality:
public class RedisConnection
{
private static RedisConnection _instance = null;
private BookSleeve.RedisSubscriberConnection _channel;
private readonly int _db;
private readonly string[] _keys; // represent channel name
public BookSleeve.RedisConnection _connection;
/// <summary>
/// Initialize all class parameters
/// </summary>
private RedisConnection(string serverList, int db, IEnumerable<string> keys)
{
_connection = ConnectionUtils.Connect(serverList);
_db = db;
_keys = keys.ToArray();
_connection.Closed += OnConnectionClosed;
_connection.Error += OnConnectionError;
// Create a subscription channel in redis
_channel = _connection.GetOpenSubscriberChannel();
// Subscribe to the registered connections
_channel.Subscribe(_keys, OnMessage);
// Dirty hack but it seems like subscribe returns before the actual
// subscription is properly setup in some cases
while (_channel.SubscriptionCount == 0)
{
Thread.Sleep(500);
}
}
/// <summary>
/// Do something when a message is received
/// </summary>
/// <param name="key"></param>
/// <param name="data"></param>
private void OnMessage(string key, byte[] data)
{
// since we are just interested in pub/sub, no data persistence is active
// however, if the persistence flag is enabled, here is where we can save the data
// The key is the stream id (channel)
//var message = RedisMessage.Deserialize(data);
var message = Helpers.BytesToString(data);
if (true) ;
//_publishQueue.Enqueue(() => OnReceived(key, (ulong)message.Id, message.Messages));
}
public static RedisConnection GetInstance(string serverList, int db, IEnumerable<string> keys)
{
if (_instance == null)
{
// could include some sort of lock for thread safety
_instance = new RedisConnection(serverList, db, keys);
}
return _instance;
}
private static void OnConnectionClosed(object sender, EventArgs e)
{
// Should we auto reconnect?
if (true)
{
;
}
}
private static void OnConnectionError(object sender, BookSleeve.ErrorEventArgs e)
{
// How do we bubble errors?
if (true)
{
;
}
}
}
In OnMessage(), var message = RedisMessage.Deserialize(data); is commented out due to the following error:
RedisMessage is inaccessible due to its protection level.
RedisMessage is an abstract class in BookSleeve, and I'm a little stuck on why I cannot use this.
I ran into this issue because as I send messages to a channel (pub/sub) I may want to do something with them in OnMessage() - for example, if a persistence flag is set, I may choose to begin recording data. The issue is that the data is serialized at this point, and I wish to deserialize it (to string) and and persist it in Redis.
Here is my test method:
[TestMethod]
public void TestRedisConnection()
{
// setup parameters
string serverList = "dbcache1.local:6379";
int db = 0;
List<string> eventKeys = new List<string>();
eventKeys.Add("Testing.FaucetChannel");
BookSleeve.RedisConnection redisConnection = Faucet.Services.RedisConnection.GetInstance(serverList, db, eventKeys)._connection;
// broadcast to a channel
redisConnection.Publish("Testing.FaucetChannel", "a published value!!!");
}
Since I wasn't able to make use of the Deserialize() method, I created a static helper class:
public static class Helpers
{
/// <summary>
/// Serializes a string to bytes
/// </summary>
/// <param name="val"></param>
/// <returns></returns>
public static byte[] StringToBytes(string str)
{
try
{
byte[] bytes = new byte[str.Length * sizeof(char)];
System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
return bytes;
}
catch (Exception ex)
{
/* handle exception omitted */
return null;
}
}
/// <summary>
/// Deserializes bytes to string
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string BytesToString(byte[] bytes)
{
string set;
try
{
char[] chars = new char[bytes.Length / sizeof(char)];
System.Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
return new string(chars);
}
catch (Exception ex)
{
// removed error handling logic!
return null;
}
}
}
Unfortunately, this is not properly deserializing the string back to its original form, and what I'm getting is something like this: 異汢獩敨慶畬㩥ㄠ, rather than the actual original text.
Suggestions?
RedisMessage represents a pending request that is about to be sent to the server; there are a few concrete implementations of this, typically relating to the nature and quantity of the parameters to be sent. It makes no sense to "deserialize" (or even "serialize") a RedisMessage - that is not their purpose. The only thing it is sensible to do is to Write(...) them to a Stream.
If you want information about a RedisMessage, then .ToString() has an overview, but this is not round-trippable and is frankly intended for debugging.
RedisMessage is an internal class; an implementation detail. Unless you're working on a pull request to the core code, you should never need to interact with a RedisMessage.
At a similar level, there is RedisResult which represents a response coming back from the server. If you want a quick way of getting data from that, fortunately that is much simpler:
object val = result.Parse(true);
(the true means "speculatively test to see if the data looks like a string"). But again, this is an internal implementation detail that you should not have to work with.
Obviously this was an encoding type issue, and in the meantime, with a little glance at this link, I simply added the encoding type of UTF8, and the output looks fine:
#region EncodingType enum
/// <summary>
/// Encoding Types.
/// </summary>
public enum EncodingType
{
ASCII,
Unicode,
UTF7,
UTF8
}
#endregion
#region ByteArrayToString
/// <summary>
/// Converts a byte array to a string using Unicode encoding.
/// </summary>
/// <param name="bytes">Array of bytes to be converted.</param>
/// <returns>string</returns>
public static string ByteArrayToString(byte[] bytes)
{
return ByteArrayToString(bytes, EncodingType.Unicode);
}
/// <summary>
/// Converts a byte array to a string using specified encoding.
/// </summary>
/// <param name="bytes">Array of bytes to be converted.</param>
/// <param name="encodingType">EncodingType enum.</param>
/// <returns>string</returns>
public static string ByteArrayToString(byte[] bytes, EncodingType encodingType)
{
System.Text.Encoding encoding=null;
switch (encodingType)
{
case EncodingType.ASCII:
encoding=new System.Text.ASCIIEncoding();
break;
case EncodingType.Unicode:
encoding=new System.Text.UnicodeEncoding();
break;
case EncodingType.UTF7:
encoding=new System.Text.UTF7Encoding();
break;
case EncodingType.UTF8:
encoding=new System.Text.UTF8Encoding();
break;
}
return encoding.GetString(bytes);
}
#endregion
--UPDATE--
Even simpler: var message = Encoding.UTF8.GetString(data);
I'm looking at doing some updates into Azure Storage Tables. I want to use the optimistic concurrency mechanism properly. It seems like you'd need to do something like:
Load row to update, possibly retrying failures
Apply updates to row
Save row, possibly retrying network errors
If there is a concurrency conflict, reload the data (possibly retrying failures) and attempt to save again (possible retrying failures)
Is there some generic class or code sample that handles this? I can code it up, but I have to imagine someone has already invented this particular wheel.
If someone invented this wheel they're not talking, so I went off and (re)invented it myself. This is intentionally very generic, more of a skeleton than a finished product. It's basically just the algorithm I outlined above. The caller has to wire in delegates to do the actual loading, updating and saving of the data. There is basic retry logic built in, but I would recommend overriding those functions with something more robust.
I believe this will work with tables or BLOBs, and single entities or batches, though I've only actually tried it with single-entity table updates.
Any comments, suggestions, improvements, etc would be appreciated.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Data.Services.Client;
using Microsoft.WindowsAzure.StorageClient;
using System.Net;
namespace SepiaLabs.Azure
{
/// <summary>
/// Attempt to write an update to storage while using optimistic concurrency.
/// Implements a basic state machine. Data will be fetched (with retries), then mutated, then updated (with retries, and possibly refetching & remutating).
/// Clients may pass in a state object with relevant information. eg, a TableServiceContext object.
/// </summary>
/// <remarks>
/// This object natively implements a very basic retry strategy.
/// Clients may want to subclass it and override the ShouldRetryRetrieval() and ShouldRetryPersist() functions to implement more advanced retry strategies.
///
/// This class intentionally avoids checking if the row is present before updating it. This is so callers may throw custom exceptions, or attempt to insert the row instead ("upsert" style interaction)
/// </remarks>
/// <typeparam name="RowType">The type of data that will be read and updated. Though it is called RowType for clarity, you could manipulate a collection of rows.</typeparam>
/// <typeparam name="StateObjectType">The type of the user-supplied state object</typeparam>
public class AzureDataUpdate<RowType, StateObjectType>
where RowType : class
{
/// <summary>
/// Function to retrieve the data that will be updated.
/// This function will be called at least once. It will also be called any time a concurrency update conflict occurs.
/// </summary>
public delegate RowType DataRetriever(StateObjectType stateObj);
/// <summary>
/// Function to apply the desired changes to the data.
/// This will be called after each time the DataRetriever function is called.
/// If you are using a TableServiceContext with MergeOption.PreserveChanges set, this function can be a no-op after the first call
/// </summary>
public delegate void DataMutator(RowType data, StateObjectType stateObj);
/// <summary>
/// Function to persist the modified data. The may be called multiple times.
/// </summary>
/// <param name="data"></param>
/// <param name="stateObj"></param>
public delegate void DataPersister(RowType data, StateObjectType stateObj);
public DataRetriever RetrieverFunction { get; set; }
public DataMutator MutatorFunction { get; set; }
public DataPersister PersisterFunction { get; set; }
public AzureDataUpdate()
{
}
public AzureDataUpdate(DataRetriever retrievalFunc, DataMutator mutatorFunc, DataPersister persisterFunc)
{
this.RetrieverFunction = retrievalFunc;
this.MutatorFunction = mutatorFunc;
this.PersisterFunction = persisterFunc;
}
public RowType Execute(StateObjectType userState)
{
if (RetrieverFunction == null)
{
throw new InvalidOperationException("Must provide a data retriever function before executing");
}
else if (MutatorFunction == null)
{
throw new InvalidOperationException("Must provide a data mutator function before executing");
}
else if (PersisterFunction == null)
{
throw new InvalidOperationException("Must provide a data persister function before executing");
}
//Retrieve and modify data
RowType data = this.DoRetrieve(userState);
//Call the mutator function.
MutatorFunction(data, userState);
//persist changes
int attemptNumber = 1;
while (true)
{
bool isPreconditionFailedResponse = false;
try
{
PersisterFunction(data, userState);
return data; //return the mutated data
}
catch (DataServiceRequestException dsre)
{
DataServiceResponse resp = dsre.Response;
int statusCode = -1;
if (resp.IsBatchResponse)
{
statusCode = resp.BatchStatusCode;
}
else if (resp.Any())
{
statusCode = resp.First().StatusCode;
}
isPreconditionFailedResponse = (statusCode == (int)HttpStatusCode.PreconditionFailed);
if (!ShouldRetryPersist(attemptNumber, dsre, isPreconditionFailedResponse, userState))
{
throw;
}
}
catch (DataServiceClientException dsce)
{
isPreconditionFailedResponse = (dsce.StatusCode == (int)HttpStatusCode.PreconditionFailed);
if (!ShouldRetryPersist(attemptNumber, dsce, isPreconditionFailedResponse, userState))
{
throw;
}
}
catch (StorageClientException sce)
{
isPreconditionFailedResponse = (sce.StatusCode == HttpStatusCode.PreconditionFailed);
if (!ShouldRetryPersist(attemptNumber, sce, isPreconditionFailedResponse, userState))
{
throw;
}
}
catch (Exception ex)
{
if (!ShouldRetryPersist(attemptNumber, ex, false, userState))
{
throw;
}
}
if (isPreconditionFailedResponse)
{
//Refetch the data, re-apply the mutator
data = DoRetrieve(userState);
MutatorFunction(data, userState);
}
attemptNumber++;
}
}
/// <summary>
/// Retrieve the data to be updated, possibly with retries
/// </summary>
/// <param name="userState">The UserState for this operation</param>
private RowType DoRetrieve(StateObjectType userState)
{
int attemptNumber = 1;
while (true)
{
try
{
return RetrieverFunction(userState);
}
catch (Exception ex)
{
if (!ShouldRetryRetrieval(attemptNumber, ex, userState))
{
throw;
}
}
attemptNumber++;
}
}
/// <summary>
/// Determine whether a data retrieval should be retried.
/// Implements a simplistic, constant wait time strategy. Users may override to provide a more complex implementation.
/// </summary>
/// <param name="attemptNumber">What number attempt is this. </param>
/// <param name="ex">The exception that was caught</param>
/// <param name="userState">The user-supplied state object for this operation</param>
/// <returns>True to attempt the retrieval again, false to abort the retrieval and fail the update attempt</returns>
protected virtual bool ShouldRetryRetrieval(int attemptNumber, Exception ex, StateObjectType userState)
{
//Simple, basic retry strategy - try 3 times, sleep for 1000msec each time
if (attemptNumber < 3)
{
Thread.Sleep(1000);
return true;
}
else
{
return false;
}
}
/// <summary>
/// Determine whether a data update should be retried. If the <paramref name="isPreconditionFailed"/> param is true,
/// then the retrieval and mutation process will be repeated as well
/// Implements a simplistic, constant wait time strategy. Users may override to provide a more complex implementation.
/// </summary>
/// <param name="attemptNumber">What number attempt is this. </param>
/// <param name="ex">The exception that was caught</param>
/// <param name="userState">The user-supplied state object for this operation</param>
/// <param name="isPreconditionFailedResponse">Indicates whether the exception is a PreconditionFailed response. ie, an optimistic concurrency failure</param>
/// <returns>True to attempt the update again, false to abort the retrieval and fail the update attempt</returns>
protected virtual bool ShouldRetryPersist(int attemptNumber, Exception ex, bool isPreconditionFailedResponse, StateObjectType userState)
{
if (isPreconditionFailedResponse)
{
return true; //retry immediately
}
else
{
//For other failures, wait to retry
//Simple, basic retry strategy - try 3 times, sleep for 1000msec each time
if (attemptNumber < 3)
{
Thread.Sleep(1000);
return true;
}
else
{
return false;
}
}
}
}
}
Our site uses ADFS for auth. To reduce the cookie payload on every request we're turning IsSessionMode on (see Your fedauth cookies on a diet).
The last thing we need to do to get this working in our load balanced environment is to implement a farm ready SecurityTokenCache. The implementation seems pretty straightforward, I'm mainly interested in finding out if there are any gotchas we should consider when dealing with SecurityTokenCacheKey and the TryGetAllEntries and TryRemoveAllEntries methods (SecurityTokenCacheKey has a custom implementation of the Equals and GetHashCode methods).
Does anyone have an example of this? We're planning on using AppFabric as the backing store but an example using any persistent store would be helpful- database table, Azure table-storage, etc.
Here are some places I've searched:
In Hervey Wilson's PDC09
session he uses a
DatabaseSecurityTokenCache. I haven't been able to find the sample
code for his session.
On page 192 of Vittorio Bertocci's excellent
book, "Programming Windows Identity Foundation" he mentions uploading
a sample implementation of an Azure ready SecurityTokenCache to the
book's website. I haven't been able to find this sample either.
Thanks!
jd
3/16/2012 UPDATE
Vittorio's blog links to a sample using the new .net 4.5 stuff:
ClaimsAwareWebFarm
This sample is an answer to the feedback we got from many of you guys: you wanted a sample showing a farm ready session cache (as opposed to a tokenreplycache) so that you can use sessions by reference instead of exchanging big cookies; and you asked for an easier way of securing cookies in a farm.
To come up with a working implementation we ultimately had to use reflector to analyze the different SessionSecurityToken related classes in Microsoft.IdentityModel. Below is what we came up with. This implementation is deployed on our dev and qa environments, seems to be working fine, it's resiliant to app pool recycles etc.
In global.asax:
protected void Application_Start(object sender, EventArgs e)
{
FederatedAuthentication.ServiceConfigurationCreated += this.OnServiceConfigurationCreated;
}
private void OnServiceConfigurationCreated(object sender, ServiceConfigurationCreatedEventArgs e)
{
var sessionTransforms = new List<CookieTransform>(new CookieTransform[]
{
new DeflateCookieTransform(),
new RsaEncryptionCookieTransform(
e.ServiceConfiguration.ServiceCertificate),
new RsaSignatureCookieTransform(
e.ServiceConfiguration.ServiceCertificate)
});
// following line is pseudo code. use your own durable cache implementation.
var durableCache = new AppFabricCacheWrapper();
var tokenCache = new DurableSecurityTokenCache(durableCache, 5000);
var sessionHandler = new SessionSecurityTokenHandler(sessionTransforms.AsReadOnly(),
tokenCache,
TimeSpan.FromDays(1));
e.ServiceConfiguration.SecurityTokenHandlers.AddOrReplace(sessionHandler);
}
private void WSFederationAuthenticationModule_SecurityTokenValidated(object sender, SecurityTokenValidatedEventArgs e)
{
FederatedAuthentication.SessionAuthenticationModule.IsSessionMode = true;
}
DurableSecurityTokenCache.cs:
/// <summary>
/// Two level durable security token cache (level 1: in memory MRU, level 2: out of process cache).
/// </summary>
public class DurableSecurityTokenCache : SecurityTokenCache
{
private ICache<string, byte[]> durableCache;
private readonly MruCache<SecurityTokenCacheKey, SecurityToken> mruCache;
/// <summary>
/// The constructor.
/// </summary>
/// <param name="durableCache">The durable second level cache (should be out of process ie sql server, azure table, app fabric, etc).</param>
/// <param name="mruCapacity">Capacity of the internal first level cache (in-memory MRU cache).</param>
public DurableSecurityTokenCache(ICache<string, byte[]> durableCache, int mruCapacity)
{
this.durableCache = durableCache;
this.mruCache = new MruCache<SecurityTokenCacheKey, SecurityToken>(mruCapacity, mruCapacity / 4);
}
public override bool TryAddEntry(object key, SecurityToken value)
{
var cacheKey = (SecurityTokenCacheKey)key;
// add the entry to the mru cache.
this.mruCache.Add(cacheKey, value);
// add the entry to the durable cache.
var keyString = GetKeyString(cacheKey);
var buffer = this.GetSerializer().Serialize((SessionSecurityToken)value);
this.durableCache.Add(keyString, buffer);
return true;
}
public override bool TryGetEntry(object key, out SecurityToken value)
{
var cacheKey = (SecurityTokenCacheKey)key;
// attempt to retrieve the entry from the mru cache.
value = this.mruCache.Get(cacheKey);
if (value != null)
return true;
// entry wasn't in the mru cache, retrieve it from the app fabric cache.
var keyString = GetKeyString(cacheKey);
var buffer = this.durableCache.Get(keyString);
var result = buffer != null;
if (result)
{
// we had a cache miss in the mru cache but found the item in the durable cache...
// deserialize the value retrieved from the durable cache.
value = this.GetSerializer().Deserialize(buffer);
// push this item into the mru cache.
this.mruCache.Add(cacheKey, value);
}
return result;
}
public override bool TryRemoveEntry(object key)
{
var cacheKey = (SecurityTokenCacheKey)key;
// remove the entry from the mru cache.
this.mruCache.Remove(cacheKey);
// remove the entry from the durable cache.
var keyString = GetKeyString(cacheKey);
this.durableCache.Remove(keyString);
return true;
}
public override bool TryReplaceEntry(object key, SecurityToken newValue)
{
var cacheKey = (SecurityTokenCacheKey)key;
// remove the entry in the mru cache.
this.mruCache.Remove(cacheKey);
// remove the entry in the durable cache.
var keyString = GetKeyString(cacheKey);
// add the new value.
return this.TryAddEntry(key, newValue);
}
public override bool TryGetAllEntries(object key, out IList<SecurityToken> tokens)
{
// not implemented... haven't been able to find how/when this method is used.
tokens = new List<SecurityToken>();
return true;
//throw new NotImplementedException();
}
public override bool TryRemoveAllEntries(object key)
{
// not implemented... haven't been able to find how/when this method is used.
return true;
//throw new NotImplementedException();
}
public override void ClearEntries()
{
// not implemented... haven't been able to find how/when this method is used.
//throw new NotImplementedException();
}
/// <summary>
/// Gets the string representation of the specified SecurityTokenCacheKey.
/// </summary>
private string GetKeyString(SecurityTokenCacheKey key)
{
return string.Format("{0}; {1}; {2}", key.ContextId, key.KeyGeneration, key.EndpointId);
}
/// <summary>
/// Gets a new instance of the token serializer.
/// </summary>
private SessionSecurityTokenCookieSerializer GetSerializer()
{
return new SessionSecurityTokenCookieSerializer(); // may need to do something about handling bootstrap tokens.
}
}
MruCache.cs:
/// <summary>
/// Most recently used (MRU) cache.
/// </summary>
/// <typeparam name="TKey">The key type.</typeparam>
/// <typeparam name="TValue">The value type.</typeparam>
public class MruCache<TKey, TValue> : ICache<TKey, TValue>
{
private Dictionary<TKey, TValue> mruCache;
private LinkedList<TKey> mruList;
private object syncRoot;
private int capacity;
private int sizeAfterPurge;
/// <summary>
/// The constructor.
/// </summary>
/// <param name="capacity">The capacity.</param>
/// <param name="sizeAfterPurge">Size to make the cache after purging because it's reached capacity.</param>
public MruCache(int capacity, int sizeAfterPurge)
{
this.mruList = new LinkedList<TKey>();
this.mruCache = new Dictionary<TKey, TValue>(capacity);
this.capacity = capacity;
this.sizeAfterPurge = sizeAfterPurge;
this.syncRoot = new object();
}
/// <summary>
/// Adds an item if it doesn't already exist.
/// </summary>
public void Add(TKey key, TValue value)
{
lock (this.syncRoot)
{
if (mruCache.ContainsKey(key))
return;
if (mruCache.Count + 1 >= this.capacity)
{
while (mruCache.Count > this.sizeAfterPurge)
{
var lru = mruList.Last.Value;
mruCache.Remove(lru);
mruList.RemoveLast();
}
}
mruCache.Add(key, value);
mruList.AddFirst(key);
}
}
/// <summary>
/// Removes an item if it exists.
/// </summary>
public void Remove(TKey key)
{
lock (this.syncRoot)
{
if (!mruCache.ContainsKey(key))
return;
mruCache.Remove(key);
mruList.Remove(key);
}
}
/// <summary>
/// Gets an item. If a matching item doesn't exist null is returned.
/// </summary>
public TValue Get(TKey key)
{
lock (this.syncRoot)
{
if (!mruCache.ContainsKey(key))
return default(TValue);
mruList.Remove(key);
mruList.AddFirst(key);
return mruCache[key];
}
}
/// <summary>
/// Gets whether a key is contained in the cache.
/// </summary>
public bool ContainsKey(TKey key)
{
lock (this.syncRoot)
return mruCache.ContainsKey(key);
}
}
ICache.cs:
/// <summary>
/// A cache.
/// </summary>
/// <typeparam name="TKey">The key type.</typeparam>
/// <typeparam name="TValue">The value type.</typeparam>
public interface ICache<TKey, TValue>
{
void Add(TKey key, TValue value);
void Remove(TKey key);
TValue Get(TKey key);
}
Here is a sample that I wrote. I use Windows Azure to store the tokens forever, defeating any possible replay.
http://tokenreplaycache.codeplex.com/releases/view/76652
You will need to place this in your web.config:
<service>
<securityTokenHandlers>
<securityTokenHandlerConfiguration saveBootstrapTokens="true">
<tokenReplayDetection enabled="true" expirationPeriod="50" purgeInterval="1">
<replayCache type="LC.Security.AzureTokenReplayCache.ACSTokenReplayCache,LC.Security.AzureTokenReplayCache, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</tokenReplayDetection>
</securityTokenHandlerConfiguration>
</securityTokenHandlers>
</service>