Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tech Debt - move entity updater to lbh core #59

Merged
merged 2 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions Hackney.Core/Hackney.Core.DynamoDb/EntityUpdater/EntityUpdater.cs
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;
}
}
}
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;
}
}
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);
}
}
}
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>();
}
}
45 changes: 45 additions & 0 deletions Hackney.Core/Hackney.Core.Middleware/BodyRewind.cs
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>();
}

}
}