Skip to content

Commit a83b844

Browse files
committed
Merge branch 'attribute-value-timestamp' into develop
2 parents c7c9d33 + a21d308 commit a83b844

File tree

8 files changed

+86
-37
lines changed

8 files changed

+86
-37
lines changed

OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,18 @@ ORDER BY SortOrder
102102
--------------------------------------------------------------------------------------------------------------------------------
103103
SELECT Attributes.TopicID,
104104
Attributes.AttributeKey,
105-
Attributes.AttributeValue
105+
Attributes.AttributeValue,
106+
Attributes.Version
106107
FROM AttributeIndex Attributes
107108
JOIN #Topics AS Storage
108109
ON Storage.TopicID = Attributes.TopicID
109110

110111
--------------------------------------------------------------------------------------------------------------------------------
111-
-- SELECT AttributeXml
112+
-- SELECT EXTENDED ATTRIBUTES
112113
--------------------------------------------------------------------------------------------------------------------------------
113114
SELECT Attributes.TopicID,
114-
Attributes.AttributesXml
115+
Attributes.AttributesXml,
116+
Attributes.Version
115117
FROM ExtendedAttributeIndex AS Attributes
116118
JOIN #Topics AS Storage
117119
ON Storage.TopicID = Attributes.TopicID

OnTopic.Data.Sql.Database/Views/AttributeIndex.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ WITH Attributes AS (
1313
SELECT TopicID,
1414
AttributeKey,
1515
AttributeValue,
16+
Version,
1617
RowNumber = ROW_NUMBER() OVER (
1718
PARTITION BY TopicID,
1819
AttributeKey
@@ -27,6 +28,7 @@ WITH Attributes AS (
2728
)
2829
SELECT Attributes.TopicID,
2930
Attributes.AttributeKey,
30-
Attributes.AttributeValue
31+
Attributes.AttributeValue,
32+
Attributes.Version
3133
FROM Attributes
3234
WHERE RowNumber = 1

OnTopic.Data.Sql.Database/Views/ExtendedAttributeIndex.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ AS
1212
WITH TopicExtendedAttributes AS (
1313
SELECT TopicID,
1414
AttributesXml,
15+
Version,
1516
RowNumber = ROW_NUMBER() OVER (
1617
PARTITION BY TopicID
1718
ORDER BY Version DESC
1819
)
1920
FROM [dbo].[ExtendedAttributes]
2021
)
2122
SELECT TopicID,
22-
AttributesXml
23+
AttributesXml,
24+
Version
2325
FROM TopicExtendedAttributes
2426
WHERE RowNumber = 1

OnTopic.Data.Sql/SqlTopicRepository.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ private static void SetIndexedAttributes(SqlDataReader reader, Dictionary<int, T
121121
var id = Int32.Parse(reader["TopicID"]?.ToString(), CultureInfo.InvariantCulture);
122122
var name = reader["AttributeKey"]?.ToString();
123123
var value = reader["AttributeValue"]?.ToString();
124+
var version = DateTime.Now;
125+
126+
//Check field count to avoid breaking changes with the 4.0.0 release, which didn't include a "Version" column
127+
//### TODO JJC20200221: This condition can be removed and accepted as a breaking change in v5.0.
128+
if (reader.FieldCount > 3) {
129+
version = DateTime.Parse(reader["Version"]?.ToString(), CultureInfo.InvariantCulture);
130+
}
124131

125132
/*------------------------------------------------------------------------------------------------------------------------
126133
| Validate conditions
@@ -141,7 +148,7 @@ private static void SetIndexedAttributes(SqlDataReader reader, Dictionary<int, T
141148
/*------------------------------------------------------------------------------------------------------------------------
142149
| Set attribute value
143150
\-----------------------------------------------------------------------------------------------------------------------*/
144-
current.Attributes.SetValue(name, value, false);
151+
current.Attributes.SetValue(name, value, false, version);
145152

146153
}
147154

@@ -169,6 +176,13 @@ private static void SetExtendedAttributes(SqlDataReader reader, Dictionary<int,
169176
| Identify attributes
170177
\-----------------------------------------------------------------------------------------------------------------------*/
171178
var id = Int32.Parse(reader["TopicID"]?.ToString(), CultureInfo.InvariantCulture);
179+
var version = DateTime.Now;
180+
181+
//Check field count to avoid breaking changes with the 4.0.0 release, which didn't include a "Version" column
182+
//### TODO JJC20200221: This condition can be removed and accepted as a breaking change in v5.0.
183+
if (reader.FieldCount > 2) {
184+
version = DateTime.Parse(reader["Version"]?.ToString(), CultureInfo.InvariantCulture);
185+
}
172186

173187
/*------------------------------------------------------------------------------------------------------------------------
174188
| Validate conditions
@@ -214,7 +228,7 @@ private static void SetExtendedAttributes(SqlDataReader reader, Dictionary<int,
214228
| Set attribute value
215229
\---------------------------------------------------------------------------------------------------------------------*/
216230
if (String.IsNullOrEmpty(value)) continue;
217-
current.Attributes.SetValue(name, value, false);
231+
current.Attributes.SetValue(name, value, false, version);
218232

219233
} while (xmlReader.Name == "attribute");
220234

OnTopic/Attributes/AttributeSetterAttribute.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ namespace OnTopic.Attributes {
1313
\---------------------------------------------------------------------------------------------------------------------------*/
1414
/// <summary>
1515
/// Flags that a property should be used when setting an attribute via
16-
/// <see cref="AttributeValueCollection.SetValue(String, String, Boolean?)"/>.
16+
/// <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, DateTime?)"/>.
1717
/// </summary>
1818
/// <remarks>
1919
/// <para>
20-
/// When a call is made to <see cref="AttributeValueCollection.SetValue(String, String, Boolean?)"/>, the code will check
21-
/// to see if a property with the same name as the attribute key exists, and whether that property is decorated with the
22-
/// <see cref="AttributeSetterAttribute"/> (i.e., <code>[AttributeSetter]</code>). If it is, then the update will be
23-
/// routed through that property. This ensures that business logic is enforced by local properties, instead of allowing
20+
/// When a call is made to <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, DateTime?)"/>, the code
21+
/// will check to see if a property with the same name as the attribute key exists, and whether that property is decorated
22+
/// with the <see cref="AttributeSetterAttribute"/> (i.e., <code>[AttributeSetter]</code>). If it is, then the update will
23+
/// be routed through that property. This ensures that business logic is enforced by local properties, instead of allowing
2424
/// business logic to be potentially bypassed by writing directly to the <see cref="Topic.Attributes"/> collection.
2525
/// </para>
2626
/// <para>
@@ -33,10 +33,10 @@ namespace OnTopic.Attributes {
3333
/// </para>
3434
/// <para>
3535
/// To ensure this logic, it is critical that implementers of <see cref="AttributeSetterAttribute"/> ensure that the
36-
/// property setters call <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, Boolean)"/> overload with
37-
/// the final parameter set to false to disable the enforcement of business logic. Otherwise, an infinite loop will occur.
38-
/// Calling that overload tells <see cref="AttributeValueCollection"/> that the business logic has already been enforced
39-
/// by the caller. As this is an internal overload, implementers should use the local proxy at
36+
/// property setters call <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, Boolean, DateTime?)"/>
37+
/// overload with the final parameter set to false to disable the enforcement of business logic. Otherwise, an infinite
38+
/// loop will occur. Calling that overload tells <see cref="AttributeValueCollection"/> that the business logic has
39+
/// already been enforced by the caller. As this is an internal overload, implementers should use the local proxy at
4040
/// <see cref="Topic.SetAttributeValue(String, String, Boolean?)"/>, which ensures that final parameter is set to false.
4141
/// </para>
4242
/// </remarks>

OnTopic/Attributes/AttributeValue.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ namespace OnTopic.Attributes {
3333
/// <para>
3434
/// This class is immutable: once it is constructed, the values cannot be changed. To change a value, callers must either
3535
/// create a new instance of the <see cref="AttributeValue"/> class or, preferably, call the
36-
/// <see cref="Topic.Attributes"/>'s <see cref="AttributeValueCollection.SetValue(String, String, Boolean?)"/> method.
36+
/// <see cref="Topic.Attributes"/>'s <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, DateTime?)"/>
37+
/// method.
3738
/// </para>
3839
/// </remarks>
3940
public class AttributeValue {
@@ -92,12 +93,28 @@ public AttributeValue(string key, string? value, bool isDirty = true) {
9293
/// If disabled, <see cref="AttributeValueCollection"/> will not call local properties on <see cref="Topic"/> that
9394
/// correspond to the <paramref name="key"/> as a means of enforcing the business logic.
9495
/// </param>
96+
/// <param name="lastModified">
97+
/// The <see cref="DateTime"/> value that the attribute was last modified. This is intended exclusively for use when
98+
/// populating the topic graph from a persistent data store as a means of indicating the current version for each
99+
/// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value.
100+
/// </param>
95101
/// <requires
96102
/// description="The key must be specified for the key/value pair." exception="T:System.ArgumentNullException">
97103
/// !String.IsNullOrWhiteSpace(key)
98104
/// </requires>
99-
internal AttributeValue(string key, string? value, bool isDirty, bool enforceBusinessLogic) : this(key, value, isDirty) {
100-
EnforceBusinessLogic = enforceBusinessLogic;
105+
internal AttributeValue(
106+
string key,
107+
string? value,
108+
bool isDirty,
109+
bool enforceBusinessLogic,
110+
DateTime? lastModified = null
111+
): this(
112+
key,
113+
value,
114+
isDirty
115+
) {
116+
EnforceBusinessLogic = enforceBusinessLogic;
117+
LastModified = lastModified?? DateTime.Now;
101118
}
102119

103120
/*==========================================================================================================================
@@ -147,12 +164,12 @@ internal AttributeValue(string key, string? value, bool isDirty, bool enforceBus
147164
/// <see cref="AttributeValueCollection"/>.
148165
/// </summary>
149166
/// <remarks>
150-
/// By default, when a user attempts to update an attribute's value by calling
151-
/// <see cref="AttributeValueCollection.SetValue(String, String, Boolean?)"/>, or when an <see cref="AttributeValue"/> is
152-
/// added to the <see cref="AttributeValueCollection"/>, the <see cref="AttributeValueCollection"/> will automatically
153-
/// attempt to call any corresponding setters on <see cref="Topic"/> (or a derived instance) to ensure that the business
154-
/// logic is enforced. To avoid an infinite loop, however, this is disabled when properties on <see cref="Topic"/> call
155-
/// <see cref="Topic.SetAttributeValue(String, String, Boolean?)"/>. When that happens, the
167+
/// By default, when a user attempts to update an attribute's value by calling <see
168+
/// cref="AttributeValueCollection.SetValue(String, String, Boolean?, DateTime?)"/>, or when an <see cref="AttributeValue"
169+
/// /> is added to the <see cref="AttributeValueCollection"/>, the <see cref="AttributeValueCollection"/> will
170+
/// automatically attempt to call any corresponding setters on <see cref="Topic"/> (or a derived instance) to ensure that
171+
/// the business logic is enforced. To avoid an infinite loop, however, this is disabled when properties on <see
172+
/// cref="Topic"/> call <see cref="Topic.SetAttributeValue(String, String, Boolean?)"/>. When that happens, the
156173
/// <see cref="EnforceBusinessLogic"/> value is set to false to communicate to the <see cref="AttributeValueCollection"/>
157174
/// that it should not call the local property. This value is only intended for internal use.
158175
/// </remarks>
@@ -172,7 +189,7 @@ internal AttributeValue(string key, string? value, bool isDirty, bool enforceBus
172189
/// <summary>
173190
/// Read-only reference to the last DateTime the <see cref="AttributeValue"/> instance was updated.
174191
/// </summary>
175-
public DateTime LastModified { get; } = DateTime.Now;
192+
public DateTime LastModified { get; internal set; } = DateTime.Now;
176193

177194
} //Class
178195
} //Namespace

OnTopic/Collections/AttributeValueCollection.cs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,11 @@ public bool IsDirty(string name) {
204204
/// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being
205205
/// persisted to the data store on <see cref="Repositories.ITopicRepository.Save(Topic, Boolean, Boolean)"/>.
206206
/// </param>
207+
/// <param name="version">
208+
/// The <see cref="DateTime"/> value that the attribute was last modified. This is intended exclusively for use when
209+
/// populating the topic graph from a persistent data store as a means of indicating the current version for each
210+
/// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value.
211+
/// </param>
207212
/// <requires
208213
/// description="The key must be specified for the AttributeValue key/value pair."
209214
/// exception="T:System.ArgumentNullException">
@@ -219,7 +224,8 @@ public bool IsDirty(string name) {
219224
/// exception="T:System.ArgumentException">
220225
/// !value.Contains(" ")
221226
/// </requires>
222-
public void SetValue(string key, string? value, bool? isDirty = null) => SetValue(key, value, isDirty, true);
227+
public void SetValue(string key, string? value, bool? isDirty = null, DateTime? version = null)
228+
=> SetValue(key, value, isDirty, true, version);
223229

224230
/// <summary>
225231
/// Protected helper method that either adds a new <see cref="AttributeValue"/> object or updates the value of an existing
@@ -242,6 +248,11 @@ public bool IsDirty(string name) {
242248
/// Instructs the underlying code to call corresponding properties, if available, to ensure business logic is enforced.
243249
/// This should be set to false if setting attributes from internal properties in order to avoid an infinite loop.
244250
/// </param>
251+
/// <param name="version">
252+
/// The <see cref="DateTime"/> value that the attribute was last modified. This is intended exclusively for use when
253+
/// populating the topic graph from a persistent data store as a means of indicating the current version for each
254+
/// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value.
255+
/// </param>
245256
/// <requires
246257
/// description="The key must be specified for the AttributeValue key/value pair."
247258
/// exception="T:System.ArgumentNullException">
@@ -257,7 +268,7 @@ public bool IsDirty(string name) {
257268
/// exception="T:System.ArgumentException">
258269
/// !value.Contains(" ")
259270
/// </requires>
260-
internal void SetValue(string key, string? value, bool? isDirty, bool enforceBusinessLogic) {
271+
internal void SetValue(string key, string? value, bool? isDirty, bool enforceBusinessLogic, DateTime? version = null) {
261272

262273
/*------------------------------------------------------------------------------------------------------------------------
263274
| Validate input
@@ -292,15 +303,15 @@ internal void SetValue(string key, string? value, bool? isDirty, bool enforceBus
292303
else if (originalAttribute.Value != value) {
293304
markAsDirty = true;
294305
}
295-
var newAttribute = new AttributeValue(key, value, markAsDirty, enforceBusinessLogic);
306+
var newAttribute = new AttributeValue(key, value, markAsDirty, enforceBusinessLogic, version);
296307
this[IndexOf(originalAttribute)] = newAttribute;
297308
}
298309

299310
/*------------------------------------------------------------------------------------------------------------------------
300311
| Create new attribute value
301312
\-----------------------------------------------------------------------------------------------------------------------*/
302313
else {
303-
Add(new AttributeValue(key, value, isDirty ?? true, enforceBusinessLogic));
314+
Add(new AttributeValue(key, value, isDirty ?? true, enforceBusinessLogic, version));
304315
}
305316

306317
}
@@ -316,8 +327,8 @@ internal void SetValue(string key, string? value, bool? isDirty, bool enforceBus
316327
/// <para>
317328
/// If a settable property is available corresponding to the <see cref="AttributeValue.Key"/>, the call should be routed
318329
/// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is
319-
/// set by the <see cref="SetValue(String, String, Boolean?, Boolean)"/>'s enforceBusinessLogic parameter. To avoid an
320-
/// infinite loop, internal setters _must_ call this overload.
330+
/// set by the <see cref="SetValue(String, String, Boolean?, Boolean, DateTime?)"/>'s enforceBusinessLogic parameter. To
331+
/// avoid an infinite loop, internal setters _must_ call this overload.
321332
/// </para>
322333
/// <para>
323334
/// Compared to the base implementation, will throw a specific <see cref="ArgumentException"/> error if a duplicate key
@@ -357,8 +368,8 @@ protected override void InsertItem(int index, AttributeValue item) {
357368
/// <remarks>
358369
/// If a settable property is available corresponding to the <see cref="AttributeValue.Key"/>, the call should be routed
359370
/// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is
360-
/// set by the <see cref="SetValue(String, String, Boolean?, Boolean)"/>'s enforceBusinessLogic parameter. To avoid an
361-
/// infinite loop, internal setters _must_ call this overload.
371+
/// set by the <see cref="SetValue(String, String, Boolean?, Boolean, DateTime?)"/>'s enforceBusinessLogic parameter. To
372+
/// avoid an infinite loop, internal setters _must_ call this overload.
362373
/// </remarks>
363374
/// <param name="index">The location that the <see cref="AttributeValue"/> should be set.</param>
364375
/// <param name="item">The <see cref="AttributeValue"/> object which is being inserted.</param>
@@ -379,8 +390,8 @@ protected override void SetItem(int index, AttributeValue item) {
379390
/// <remarks>
380391
/// If a settable property is available corresponding to the <see cref="AttributeValue.Key"/>, the call should be routed
381392
/// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is
382-
/// set by the <see cref="SetValue(String, String, Boolean?, Boolean)"/>'s enforceBusinessLogic parameter. To avoid an
383-
/// infinite loop, internal setters _must_ call this overload.
393+
/// set by the <see cref="SetValue(String, String, Boolean?, Boolean, DateTime?)"/>'s enforceBusinessLogic parameter. To
394+
/// avoid an infinite loop, internal setters _must_ call this overload.
384395
/// </remarks>
385396
/// <param name="originalAttribute">The <see cref="AttributeValue"/> object which is being inserted.</param>
386397
/// <param name="settableAttribute">
@@ -403,6 +414,7 @@ private bool EnforceBusinessLogic(AttributeValue originalAttribute, out Attribut
403414
}
404415
_typeCache.SetPropertyValue(_associatedTopic, originalAttribute.Key, originalAttribute.Value);
405416
this[originalAttribute.Key].IsDirty = originalAttribute.IsDirty;
417+
this[originalAttribute.Key].LastModified = originalAttribute.LastModified;
406418
_setCounter = 0;
407419
return false;
408420
}

OnTopic/Topic.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ public Topic? DerivedTopic {
635635
/// called by the <see cref="AttributeValueCollection"/>. This is intended to enforce local business logic, and prevent
636636
/// callers from introducing invalid data.To prevent a redirect loop, however, local properties need to inform the
637637
/// <see cref="AttributeValueCollection"/> that the business logic has already been enforced. To do that, they must either
638-
/// call <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, Boolean)"/> with the
638+
/// call <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, Boolean, DateTime?)"/> with the
639639
/// <c>enforceBusinessLogic</c> flag set to <c>false</c>, or, if they're in a separate assembly, call this overload.
640640
/// </remarks>
641641
/// <param name="key">The string identifier for the AttributeValue.</param>

0 commit comments

Comments
 (0)