base.attributed.entity - grecosoft/NetFusion GitHub Wiki
Attributed entities provide a set of key/value pairs associated with an application's domain entity that can be accessed dynamically using .Net's dynamic runtime support.
nuget | NetFusion.Base |
---|---|
types | IAttributedEntity, IEntityAttributes |
The base Message type from which both the Command and DomainEvent types derive is an attributed entity. This allows message enrichers to add dynamic properties to messages without having to declare static properties.
An entity supports attributed features by implementing the IAttributedEntity interface. The definition of this interface is as follows:
using System.Collections.Generic;
namespace NetFusion.Base.Entity
{
/// <summary>
/// Identifies an entity having a set of key/value attributes that
/// can be accessed dynamically at runtime.
/// </summary>
public interface IAttributedEntity
{
/// <summary>
/// Reference to object used to manage an entity's dynamic attributes.
/// </summary>
IEntityAttributes Attributes { get; }
/// <summary>
/// A dictionary of the key value pairs associated with the entity.
/// </summary>
IDictionary<string, object> AttributeValues { get; set; }
}
}
This interface defines the following two properties:
- Attributes: Returns a reference to an implementation of the IEntityAttributes interface. The returned class instance implements the storage of the key/value pairs and provides support for access using .Net's dynamic runtime typing. The IEntityAttributes implementation also provides methods for setting and retrieving key-values that can be used in conjunction with or separately from .net's dynamic typing.
- AttributeValues: Returns and sets the underlying collection of key/value pairs associated with the entity.
Currently, attributed entities are used in the following locations within NetFusion:
-
NetFusion.Messaging: As noted above, both the Command and DomainEvent derived message types support the IAttributedEntity contact. When a message is published, a correlation Id value is set on the message if not already present. Also, the date and time the message was published is set. These values and others, can be specified without having the derived message types expectedly defining corresponding typed properties.
The correlation Id and published date are implemented as IMessageEnrichers and are invoked during the publishing of a message. These enrichers check for the existence of values and if not set, adds them as message attributes. Application specific enrichers can also be declared and registered to set cross-cutting message properties. The following is an example of the CorrelationEnricher:
using NetFusion.Messaging.Types;
using System;
using System.Threading.Tasks;
namespace NetFusion.Messaging.Enrichers
{
/// <summary>
/// Adds a GUID value as the message's correlation identifier if not present.
/// </summary>
public class CorrelationEnricher : MessageEnricher
{
//The MessageEnricherModule registers all enrichers with a scoped lifetime,
// So the below guid value is unique per request.
private readonly Guid _scopedRequestId = Guid.NewGuid();
public override Task Enrich(IMessage message)
{
message.SetCorrelationId(Guid.NewGuid().ToString());
AddMessageProperty(message, "ScopedRequestId", _scopedRequestId);
return base.Enrich(message);
}
}
}
- NetFusion.AMQP: When a serialized message is received and deserialized into its associated .Net message type, any AMQP message properties, set by the sender, are transferred to the .Net message type as key/values associated with the message. This allows the receiver to access these message specific properties without having to define explicitly typed properties. Following is the code that transfers the AMQP message properties to the message that is an attributed-entity:
private static void SetMessageApplicationProperties(Message amqpMessage, IMessage message)
{
if (amqpMessage.ApplicationProperties?.Map == null) return;
foreach (var item in amqpMessage.ApplicationProperties.Map)
{
message.Attributes.SetValue(item.Key.ToString(), item.Value);
}
}
- NetFusion.Roslyn: This plugin provides implementations for the scripting interfaces defined within the NetFusion.Base.Scripting namespace. The implementation allows for the definition of scripts, associated with an attributed-entity, to evaluate externally defined expressions against the entity. The expressions can add newly calculated properties without having to change the associated type.
The following defines an attributed entity and provides examples for setting and retrieving key/values associated with an entity supporting the IAttributedEntity interface.
The following attributed entity will be used for the following example:
touch ./src/Components/Demo.Domain/Entities/Account.cs
nano ./src/Components/Demo.Domain/Entities/Account.cs
using System.Collections.Generic;
using System.Runtime.Serialization;
using NetFusion.Base.Entity;
namespace Demo.Domain.Entities
{
public class Account : IAttributedEntity
{
public Account()
{
Attributes = new EntityAttributes();
}
// Typical static defined properties:
public string FirstName { get; }
public string LastName { get; }
public Account(string firstName, string lastName): this()
{
FirstName = firstName;
LastName = lastName;
}
/// <summary>
/// Dynamic message attributes that can be associated with the command.
/// </summary>
[IgnoreDataMember]
public IEntityAttributes Attributes { get; }
/// <summary>
/// Dynamic message attribute values.
/// </summary>
public IDictionary<string, object> AttributeValues
{
get => Attributes.GetValues();
set => Attributes.SetValues(value);
}
}
}
The above entity contains static properties and also supports the setting of dynamic properties.
Note: The Attributes property is decorated with the IgnoreDataMember attribute. This prevents the underlying dynamic type from being serialized and deserialized. Instead, the AttributeValues property is called to retrieve and set the underlying values.
Create the following controller to instantiate the Account class and dynamically add properties submitted with the request body.
touch ./src/Demo.WebApi/Controllers/AttributedEntityController.cs
nano ./src/Demo.WebApi/Controllers/AttributedEntityController.cs
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Demo.Domain.Entities;
namespace Demo.WebApi.Controllers
{
[ApiController, Route("api/attributed-entity")]
public class AttributedEntityController : ControllerBase
{
[HttpPatch]
public IActionResult UpdateEntity([FromBody]IDictionary<string, object> values)
{
var account = new Account("Justin", "Greco");
// Example of adding set of values to entity:
foreach (var (key, value) in values)
{
account.Attributes.SetValue(key, value);
}
// Example setting properties using .net dynamic runtime:
account.Attributes.Values.AccountId = Guid.NewGuid().ToString();
account.Attributes.Values.DateCreated = DateTime.UtcNow;
// A namespace providing the context of a property can also be specified:
account.Attributes.SetValue("RequestId",
Guid.NewGuid().ToString(),
typeof(Account));
return Ok(new
{
account,
account.Attributes.Values.AccountId,
dateCreated = account.Attributes.GetValue<DateTime>("DateCreated"),
requestId = account.Attributes.GetValueOrDefault("RequestId",
"Not-Set",
typeof(Account)),
});
}
}
}
The controller shows an example of adding properties using the SetValue method provided by the IEntityAttributes interface. Also shown are setting values using the Values property defined on the IEntityAttributes contract. The Values property type is of the .net Dynamic type. Using the Values property provides for cleaner code. However, using the GetValueOrDefault method allows for returning a default value if the attribute is not defined.
The SetValue and GetValue methods also allow a context to be specified with an attribute. This helps prevent name clashes. The RequestId attribute has an associated context provided when the attribute's value is set and retrieved.
Note: The provided type for the context will use the namespace of the provided type. The types name will not be part of the context name.
cd ./src/Demo.WebApi
dotnet run
Issue a Patch request to the following URL: http://localhost:5000/api/attributed-entity
Within the body of the request, specify a simple JSON object of name/value pairs to dynamically set on the Account entity. The following shows an example request and the corresponding response.