-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tech Debt - move entity updater to lbh core (#59)
* Add EntityUpdater code * Add BodyRewind middleware
- Loading branch information
1 parent
4b8e030
commit 3b4b3fd
Showing
5 changed files
with
272 additions
and
0 deletions.
There are no files selected for viewing
129 changes: 129 additions & 0 deletions
129
Hackney.Core/Hackney.Core.DynamoDb/EntityUpdater/EntityUpdater.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
using Hackney.Core.DynamoDb.EntityUpdater.Interfaces; | ||
using Microsoft.Extensions.Logging; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
|
||
namespace Hackney.Core.DynamoDb.EntityUpdater | ||
{ | ||
public class EntityUpdater : IEntityUpdater | ||
{ | ||
private readonly ILogger<EntityUpdater> _logger; | ||
|
||
public EntityUpdater(ILogger<EntityUpdater> logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
private static JsonSerializerOptions CreateJsonOptions() | ||
{ | ||
var options = new JsonSerializerOptions | ||
{ | ||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | ||
WriteIndented = true | ||
}; | ||
options.Converters.Add(new JsonStringEnumConverter()); | ||
return options; | ||
} | ||
|
||
private static bool HasValueChanged(object existingValue, object updateValue) | ||
{ | ||
if (updateValue is null && existingValue is null) return false; | ||
if (updateValue is null && existingValue != null) return true; | ||
return !updateValue.Equals(existingValue); | ||
} | ||
|
||
/// <summary> | ||
/// Updates the supplied entity with the updated property values described in the request object / json | ||
/// Defaults the ignoreUnchangedProperties input value to true. | ||
/// </summary> | ||
/// <typeparam name="TEntity">The entity type</typeparam> | ||
/// <typeparam name="TUpdateObject">The type of the update request object</typeparam> | ||
/// <param name="entityToUpdate">The entity to update</param> | ||
/// <param name="updateJson">The raw update request json from which the request object was deserialized</param> | ||
/// <param name="updateObject">The update request object</param> | ||
/// <returns>A response object</returns> | ||
public UpdateEntityResult<TEntity> UpdateEntity<TEntity, TUpdateObject>(TEntity entityToUpdate, | ||
string updateJson, | ||
TUpdateObject updateObject) | ||
where TEntity : class | ||
where TUpdateObject : class | ||
{ | ||
return UpdateEntity(entityToUpdate, updateJson, updateObject, true); | ||
} | ||
|
||
/// <summary> | ||
/// Updates the supplied entity with the updated property values described in the request object / json. | ||
/// * This method expects both a request object and the raw request json so that the appropriate request object validation | ||
/// can be executed by the MVC pipeline. | ||
/// * The inclusion of the request object also means that each updated property value has been deserialised correctly. | ||
/// * The raw request json should contain ONLY the properties to be updated. | ||
/// * The property names in the json / request object MUST MATCH the corresponing properties on the entity type (assuming the json uses camel casing). | ||
/// * For nested objects, those classes must override the Equals() mewthod so that the algorithm will correctly determine if a suboject has changed. | ||
/// </summary> | ||
/// <typeparam name="TEntity">The entity type</typeparam> | ||
/// <typeparam name="TUpdateObject">The type of the update request object</typeparam> | ||
/// <param name="entityToUpdate">The entity to update</param> | ||
/// <param name="updateJson">The raw update request json from which the request object was deserialized</param> | ||
/// <param name="updateObject">The update request object</param> | ||
/// <param name="ignoreUnchangedProperties">Whether or not to ignore property values set in the update request | ||
/// but that are actually the same as current entity value.</param> | ||
/// <returns>A response object</returns> | ||
public UpdateEntityResult<TEntity> UpdateEntity<TEntity, TUpdateObject>(TEntity entityToUpdate, | ||
string updateJson, | ||
TUpdateObject updateObject, | ||
bool ignoreUnchangedProperties) | ||
where TEntity : class | ||
where TUpdateObject : class | ||
{ | ||
if (entityToUpdate is null) throw new ArgumentNullException(nameof(entityToUpdate)); | ||
if (updateObject is null) throw new ArgumentNullException(nameof(updateObject)); | ||
|
||
var result = new UpdateEntityResult<TEntity>() { UpdatedEntity = entityToUpdate }; | ||
if (string.IsNullOrEmpty(updateJson)) return result; | ||
|
||
var updateDic = JsonSerializer.Deserialize<Dictionary<string, object>>(updateJson, CreateJsonOptions()); | ||
var entityType = typeof(TEntity); | ||
var updateObjectType = typeof(TUpdateObject); | ||
|
||
var allEntityProperties = entityType.GetProperties(); | ||
foreach (var propName in updateDic.Keys) | ||
{ | ||
var prop = allEntityProperties.FirstOrDefault(x => x.Name.ToCamelCase() == propName); | ||
if (prop is null) | ||
{ | ||
// Received a property on the request Json that's not on the entity at all | ||
// So we log a warning, ignore it and carry on. | ||
_logger.LogWarning($"Entity object (type: {entityType.Name}) does not contain a property called {propName}. Ignoring {propName} value..."); | ||
result.IgnoredProperties.Add(propName); | ||
continue; | ||
} | ||
|
||
var requestObjectProperty = updateObjectType.GetProperty(prop.Name); | ||
if (requestObjectProperty is null) | ||
{ | ||
// Received a property on the request Json we weren't expecting (it's not on the request object) | ||
// So we log a warning, ignore it and carry on. | ||
_logger.LogWarning($"Request object (type: {updateObjectType.Name}) does not contain a property called {prop.Name} that is on the entity type ({entityType.Name}). Ignoring {prop.Name} value..."); | ||
result.IgnoredProperties.Add(propName); | ||
continue; | ||
} | ||
|
||
var updateValue = requestObjectProperty.GetValue(updateObject); | ||
var existingValue = prop.GetValue(entityToUpdate); | ||
|
||
// For sub-objects this Equals() check will only work if the Equals() method is overridden | ||
if (!ignoreUnchangedProperties || HasValueChanged(existingValue, updateValue)) | ||
{ | ||
result.OldValues.Add(propName, existingValue); | ||
result.NewValues.Add(propName, updateValue); | ||
prop.SetValue(entityToUpdate, updateValue); | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
Hackney.Core/Hackney.Core.DynamoDb/EntityUpdater/Interfaces/IEntityUpdater.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
namespace Hackney.Core.DynamoDb.EntityUpdater.Interfaces | ||
{ | ||
/// <summary> | ||
/// Interface describing generic methods for updating an instance of an entity from the suypplied request object and raw request json | ||
/// </summary> | ||
public interface IEntityUpdater | ||
{ | ||
/// <summary> | ||
/// Updates the supplied entity with the updated property values described in the request object / json | ||
/// </summary> | ||
/// <typeparam name="TEntity">The entity type</typeparam> | ||
/// <typeparam name="TUpdateObject">The type of the update request object</typeparam> | ||
/// <param name="entityToUpdate">The entity to update</param> | ||
/// <param name="updateJson">The raw update request json from which the request object was deserialized</param> | ||
/// <param name="updateObject">The update request object</param> | ||
/// <returns>A response object</returns> | ||
UpdateEntityResult<TEntity> UpdateEntity<TEntity, TUpdateObject>( | ||
TEntity entityToUpdate, | ||
string updateJson, | ||
TUpdateObject updateObject) | ||
where TEntity : class | ||
where TUpdateObject : class; | ||
|
||
/// <summary> | ||
/// Updates the supplied entity with the updated property values described in the request object / json. | ||
/// * This method expects both a request object and the raw request json so that the appropriate request object validation | ||
/// can be executed by the MVC pipeline. | ||
/// * The raw request json should contain ONLY the properties to be updated. | ||
/// * The property names in the json / request object MUST MATCH the corresponing properties on the entity type (assuming the json uses camel casing). | ||
/// </summary> | ||
/// <typeparam name="TEntity">The entity type</typeparam> | ||
/// <typeparam name="TUpdateObject">The type of the update request object</typeparam> | ||
/// <param name="entityToUpdate">The entity to update</param> | ||
/// <param name="updateJson">The raw update request json from which the request object was deserialized</param> | ||
/// <param name="updateObject">The update request object</param> | ||
/// <param name="ignoreUnchangedProperties">Whether or not to ignore property values set in the update request | ||
/// but that are actually the same as current entity value.</param> | ||
/// <returns>A response object</returns> | ||
UpdateEntityResult<TEntity> UpdateEntity<TEntity, TUpdateObject>( | ||
TEntity entityToUpdate, | ||
string updateJson, | ||
TUpdateObject updateObject, | ||
bool ignoreUnchangedProperties) | ||
where TEntity : class | ||
where TUpdateObject : class; | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
Hackney.Core/Hackney.Core.DynamoDb/EntityUpdater/StringExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
namespace Hackney.Core.DynamoDb.EntityUpdater | ||
{ | ||
public static class StringExtensions | ||
{ | ||
/// <summary> | ||
/// Converts the string to camel case (i.e. the first character is lowercase) | ||
/// </summary> | ||
/// <param name="str">The string to change</param> | ||
/// <returns>A copied of the string with the first chacater in lowercase. A null or empty string returns what was supplied. | ||
/// </returns> | ||
public static string ToCamelCase(this string str) | ||
{ | ||
if (string.IsNullOrEmpty(str)) return str; | ||
if (str.Length == 1) return str.ToLowerInvariant(); | ||
|
||
// else if (str.Length > 1) | ||
return char.ToLowerInvariant(str[0]) + str.Substring(1); | ||
} | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
Hackney.Core/Hackney.Core.DynamoDb/EntityUpdater/UpdateEntityResult.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace Hackney.Core.DynamoDb.EntityUpdater | ||
{ | ||
public class UpdateEntityResult<T> where T : class | ||
{ | ||
/// <summary> | ||
/// The updated entity | ||
/// </summary> | ||
public T UpdatedEntity { get; set; } | ||
|
||
/// <summary> | ||
/// A simple dictionary listing the previous value(s) of any entity properties actually updated by the call. | ||
/// The size and keys of this dictionary will match that of the NewValues property. | ||
/// </summary> | ||
public Dictionary<string, object> OldValues { get; set; } = new Dictionary<string, object>(); | ||
|
||
/// <summary> | ||
/// A simple dictionary listing the new value(s) of any entity properties actually updated by the call. | ||
/// The size and keys of this dictionary will match that of the OldValues property. | ||
/// </summary> | ||
public Dictionary<string, object> NewValues { get; set; } = new Dictionary<string, object>(); | ||
|
||
/// <summary> | ||
/// A collection of properties that were in the orignal request json but that were ignored because either | ||
/// * They did not exist on the request object, or | ||
/// * They did not exist on the entity at all. | ||
/// </summary> | ||
public List<string> IgnoredProperties { get; set; } = new List<string>(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.AspNetCore.Http; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace Hackney.Core.Middleware | ||
{ | ||
[ExcludeFromCodeCoverage] | ||
public sealed class BodyRewindMiddleware | ||
{ | ||
private readonly RequestDelegate _next; | ||
|
||
public BodyRewindMiddleware(RequestDelegate next) | ||
{ | ||
_next = next; | ||
} | ||
|
||
public async Task Invoke(HttpContext context) | ||
{ | ||
context.Request.EnableBuffering(); | ||
await _next(context).ConfigureAwait(false); | ||
} | ||
} | ||
|
||
[ExcludeFromCodeCoverage] | ||
public static class BodyRewindExtensions | ||
{ | ||
/// <summary> | ||
/// Enables the HttpRequset body to be access from within a controller method. | ||
/// Without this the request body is rendered unaccessible by other middleware steps. | ||
/// </summary> | ||
/// <param name="app">An App builder</param> | ||
/// <returns>An App builder</returns> | ||
public static IApplicationBuilder EnableRequestBodyRewind(this IApplicationBuilder app) | ||
{ | ||
if (app == null) throw new ArgumentNullException(nameof(app)); | ||
|
||
return app.UseMiddleware<BodyRewindMiddleware>(); | ||
} | ||
|
||
} | ||
} |