CH
you are a dotnet architect giving objective advice to design questions.
i have been working on a dotnet dll plugin app that can read and execute on naive clr objects. i have worked out the reflection needed to serialize and deserialize clr objects to dictionary<string,object>
recursively and interact with the clr object properties and methods. my concern is i want to use these clr objects as view models for a data system so they will need non serializable objects.
As a .NET architect, I can see you’re building an interesting plugin system with dynamic object manipulation. Let me address your concern about non-serializable objects in view models.
Your current Dictionary<string,object>
approach works well for simple serializable properties, but breaks down when encountering:
Database contexts
File streams
Network connections
UI controls
Event handlers
Complex dependency-injected services
1. Hybrid Serialization Strategy
Implement a two-tier approach:
public class SerializationContext
{
public Dictionary<string, object> SerializableData { get; set; }
public Dictionary<string, ITransientService> TransientServices { get; set; }
public Dictionary<string, Func<object>> ServiceFactories { get; set; }
}
2. Attribute-Based Exclusion
Mark non-serializable members explicitly:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class NonSerializableAttribute : Attribute { }
// In your reflection logic
var properties = type.GetProperties()
.Where(p => !p.HasAttribute<NonSerializableAttribute>());
3. Service Locator Pattern for Plugins
Provide a service resolution mechanism:
public interface IPluginServiceProvider
{
T GetService<T>() where T : class;
object GetService(Type serviceType);
void RegisterTransient(string key, Func<object> factory);
}
Option A: Proxy Pattern Create lightweight proxy objects that hold only serializable state, with methods that delegate to injected services:
public class ViewModelProxy
{
public Dictionary<string, object> State { get; set; }
[NonSerializable] public IDataService DataService { get; set; }
public void SaveData() => DataService.Save(State);
}
Option B: State/Behavior Separation Separate pure data from behavior completely:
public class ViewModelState
{
// Only serializable properties
}
public class ViewModelBehavior
{
public ViewModelState State { get; set; }
// All non-serializable dependencies and methods
}
Lifecycle Management: Non-serializable objects often need explicit disposal
Plugin Isolation: Consider AppDomain boundaries or AssemblyLoadContext for true isolation
Performance: Reflection-heavy operations should be cached
Security: Validate which types/methods plugins can access
Would you like me to elaborate on any of these approaches, or do you have specific constraints that might influence the architectural direction?
CH
i failed to mention that the current code will serialize the object to Dictionary<string, object>
for all properties and i need to transmit all of the dictionary to the consumer but only part of it to the storage consumer. you mentioned serialization context
Ah, this clarifies the architecture significantly! You have two different consumers with different serialization needs:
Full Consumer - needs everything (including non-serializable object references)
Storage Consumer - needs only persistable data
Here’s a robust approach using serialization context with consumer targeting:
public class SerializationContext
{
public Dictionary<string, object> AllData { get; private set; }
public SerializationTarget Target { get; set; }
public Dictionary<string, object> GetFilteredData()
{
return Target switch
{
SerializationTarget.FullConsumer => AllData,
SerializationTarget.Storage => GetStorableData(),
_ => throw new ArgumentException("Unknown target")
};
}
private Dictionary<string, object> GetStorableData()
{
return AllData
.Where(kvp => IsStorable(kvp.Key, kvp.Value))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
}
public enum SerializationTarget
{
FullConsumer,
Storage
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class StorageAttribute : Attribute
{
public bool Persistable { get; set; } = true;
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class TransientAttribute : Attribute { }
// Usage in your view models
public class CustomerViewModel
{
[Storage(Persistable = true)]
public string Name { get; set; }
[Storage(Persistable = true)]
public int CustomerId { get; set; }
[Transient] // Never goes to storage
public DbContext DatabaseContext { get; set; }
[Transient]
public ILogger Logger { get; set; }
// This goes to full consumer but not storage
public FileStream TempFile { get; set; }
}
public class ObjectSerializer
{
public SerializationResult Serialize(object obj, SerializationTarget target)
{
var allData = new Dictionary<string, object>();
var type = obj.GetType();
foreach (var prop in type.GetProperties())
{
var value = prop.GetValue(obj);
allData[prop.Name] = value;
}
return new SerializationResult
{
AllData = allData,
FilteredData = FilterForTarget(allData, type, target)
};
}
private Dictionary<string, object> FilterForTarget(
Dictionary<string, object> allData,
Type sourceType,
SerializationTarget target)
{
if (target == SerializationTarget.FullConsumer)
return allData;
var storableProperties = sourceType.GetProperties()
.Where(p => IsStorable(p))
.Select(p => p.Name)
.ToHashSet();
return allData
.Where(kvp => storableProperties.Contains(kvp.Key))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
private bool IsStorable(PropertyInfo property)
{
// Has Transient attribute - never storable
if (property.HasAttribute<TransientAttribute>())
return false;
// Has Storage attribute with explicit setting
var storageAttr = property.GetAttribute<StorageAttribute>();
if (storageAttr != null)
return storageAttr.Persistable;
// Default logic - check if type is serializable
return IsTypeStorable(property.PropertyType);
}
private bool IsTypeStorable(Type type)
{
// Your existing logic for determining storable types
if (type.IsSerializable) return true;
if (type.IsPrimitive) return true;
if (type == typeof(string)) return true;
// etc...
return false;
}
}
// For full consumer (gets everything)
var fullResult = serializer.Serialize(viewModel, SerializationTarget.FullConsumer);
transmitToConsumer(fullResult.FilteredData);
// For storage (gets only persistable data)
var storageResult = serializer.Serialize(viewModel, SerializationTarget.Storage);
saveToDatabase(storageResult.FilteredData);
Performance Optimization: Cache the reflection analysis per type:
private static readonly ConcurrentDictionary<Type, PropertySerializationInfo[]> _typeCache = new();
Validation: Ensure storage consumer never receives non-serializable objects:
private void ValidateStorageData(Dictionary<string, object> data)
{
foreach (var kvp in data)
{
if (!IsTypeStorable(kvp.Value?.GetType()))
throw new InvalidOperationException($"Non-storable type {kvp.Value?.GetType()} in storage data");
}
}
Does this approach align with your architecture? Would you like me to elaborate on any specific aspect, such as the caching strategy or handling of nested objects? CH i like the attribute approach, may even be able to inject the attribute type as a filter
Excellent insight! Injectable attribute filtering would make your system highly flexible and plugin-friendly. Here’s how to architect that:
public interface IAttributeFilter
{
bool ShouldInclude(PropertyInfo property, SerializationTarget target);
Type[] SupportedAttributes { get; }
}
public class SerializationEngine
{
private readonly List<IAttributeFilter> _filters = new();
public void RegisterFilter(IAttributeFilter filter)
{
_filters.Add(filter);
}
public void RegisterFilter<TAttribute>() where TAttribute : Attribute
{
_filters.Add(new GenericAttributeFilter<TAttribute>());
}
private bool ShouldIncludeProperty(PropertyInfo property, SerializationTarget target)
{
// Apply all registered filters - all must pass
return _filters.All(filter => filter.ShouldInclude(property, target));
}
}
public class GenericAttributeFilter<TAttribute> : IAttributeFilter
where TAttribute : Attribute
{
public Type[] SupportedAttributes => new[] { typeof(TAttribute) };
public virtual bool ShouldInclude(PropertyInfo property, SerializationTarget target)
{
var attribute = property.GetCustomAttribute<TAttribute>();
return attribute == null ? true : ProcessAttribute(attribute, target);
}
protected virtual bool ProcessAttribute(TAttribute attribute, SerializationTarget target)
{
// Default behavior - override in derived classes
return true;
}
}
// Storage-specific filter
public class StorageAttributeFilter : GenericAttributeFilter<StorageAttribute>
{
protected override bool ProcessAttribute(StorageAttribute attribute, SerializationTarget target)
{
return target switch
{
SerializationTarget.Storage => attribute.Persistable,
SerializationTarget.FullConsumer => true,
_ => true
};
}
}
// Transient filter
public class TransientAttributeFilter : GenericAttributeFilter<TransientAttribute>
{
protected override bool ProcessAttribute(TransientAttribute attribute, SerializationTarget target)
{
return target != SerializationTarget.Storage;
}
}
// Custom plugin filter example
public class SecurityAttributeFilter : GenericAttributeFilter<SecurityClassificationAttribute>
{
private readonly SecurityLevel _currentUserLevel;
public SecurityAttributeFilter(SecurityLevel userLevel)
{
_currentUserLevel = userLevel;
}
protected override bool ProcessAttribute(SecurityClassificationAttribute attribute, SerializationTarget target)
{
return _currentUserLevel >= attribute.RequiredLevel;
}
}
public class PluginSerializationBuilder
{
private readonly SerializationEngine _engine;
public PluginSerializationBuilder(SerializationEngine engine)
{
_engine = engine;
}
// Type-based registration
public PluginSerializationBuilder WithFilter<TAttribute>()
where TAttribute : Attribute
{
_engine.RegisterFilter<TAttribute>();
return this;
}
// Instance-based registration
public PluginSerializationBuilder WithFilter(IAttributeFilter filter)
{
_engine.RegisterFilter(filter);
return this;
}
// Lambda-based registration for simple cases
public PluginSerializationBuilder WithFilter<TAttribute>(
Func<TAttribute, SerializationTarget, bool> predicate)
where TAttribute : Attribute
{
_engine.RegisterFilter(new LambdaAttributeFilter<TAttribute>(predicate));
return this;
}
}
public class LambdaAttributeFilter<TAttribute> : GenericAttributeFilter<TAttribute>
where TAttribute : Attribute
{
private readonly Func<TAttribute, SerializationTarget, bool> _predicate;
public LambdaAttributeFilter(Func<TAttribute, SerializationTarget, bool> predicate)
{
_predicate = predicate;
}
protected override bool ProcessAttribute(TAttribute attribute, SerializationTarget target)
{
return _predicate(attribute, target);
}
}
// Plugin registration
var builder = new PluginSerializationBuilder(engine)
.WithFilter<StorageAttribute>()
.WithFilter<TransientAttribute>()
.WithFilter<SecurityClassificationAttribute>()
.WithFilter<CacheableAttribute>((attr, target) =>
target == SerializationTarget.Cache ? attr.Cacheable : true);
// Runtime filter injection
engine.RegisterFilter(new CustomPluginFilter());
// View model with multiple attribute types
public class SecureCustomerViewModel
{
[Storage(Persistable = true)]
public string Name { get; set; }
[Storage(Persistable = true)]
[SecurityClassification(RequiredLevel = SecurityLevel.Public)]
public string PublicId { get; set; }
[Storage(Persistable = true)]
[SecurityClassification(RequiredLevel = SecurityLevel.Confidential)]
public decimal CreditLimit { get; set; }
[Transient]
public ILogger Logger { get; set; }
[Cacheable(Cacheable = false)]
public DateTime LastAccessed { get; set; }
}
public class CompositeAttributeFilter : IAttributeFilter
{
private readonly IAttributeFilter[] _filters;
private readonly FilterComposition _composition;
public CompositeAttributeFilter(FilterComposition composition, params IAttributeFilter[] filters)
{
_composition = composition;
_filters = filters;
}
public Type[] SupportedAttributes => _filters.SelectMany(f => f.SupportedAttributes).ToArray();
public bool ShouldInclude(PropertyInfo property, SerializationTarget target)
{
return _composition switch
{
FilterComposition.All => _filters.All(f => f.ShouldInclude(property, target)),
FilterComposition.Any => _filters.Any(f => f.ShouldInclude(property, target)),
FilterComposition.None => !_filters.Any(f => f.ShouldInclude(property, target)),
_ => true
};
}
}
public enum FilterComposition
{
All, // AND operation
Any, // OR operation
None // NOT operation
}
This approach gives you:
Plugin extensibility - plugins can inject their own attribute filters
Composability - combine multiple filters with different logic
Performance - cache attribute analysis per type
Flexibility - lambda-based filters for simple cases, full classes for complex logic
Would you like me to elaborate on any aspect, such as performance optimization with caching or handling of nested object filtering? CH thought about the system, consider you have the need for a cascading selection. someone selects country and now you get a list of states then on selection you get a list of cities. how could a system like that implement that functionality
Excellent question! A cascading selection system needs to handle dependent data loading and state management across the plugin boundary. Here are several architectural approaches:
public interface ICascadingDataProvider
{
Task<IEnumerable<SelectionItem>> GetDependentDataAsync(string parentProperty, object parentValue);
Task<IEnumerable<SelectionItem>> GetRootDataAsync(string property);
}
[AttributeUsage(AttributeTargets.Property)]
public class CascadingSelectionAttribute : Attribute
{
public string DependsOn { get; set; }
public string DataSourceKey { get; set; }
public bool ClearOnParentChange { get; set; } = true;
}
public class LocationViewModel
{
[CascadingSelection(DataSourceKey = "countries")]
public string CountryId { get; set; }
[CascadingSelection(DependsOn = nameof(CountryId), DataSourceKey = "states")]
public string StateId { get; set; }
[CascadingSelection(DependsOn = nameof(StateId), DataSourceKey = "cities")]
public string CityId { get; set; }
}
public class CascadingManager
{
private readonly ICascadingDataProvider _dataProvider;
private readonly Dictionary<string, List<string>> _dependencyChain = new();
public async Task<CascadeResult> HandlePropertyChangeAsync(
object viewModel,
string changedProperty,
object newValue)
{
var result = new CascadeResult();
var dependentProperties = GetDependentProperties(viewModel.GetType(), changedProperty);
foreach (var dependent in dependentProperties)
{
// Clear dependent values
if (ShouldClearOnChange(viewModel.GetType(), dependent))
{
SetPropertyValue(viewModel, dependent, null);
result.ClearedProperties.Add(dependent);
}
// Load new data for dependent
if (newValue != null)
{
var data = await _dataProvider.GetDependentDataAsync(dependent, newValue);
result.NewDataSources[dependent] = data;
}
}
return result;
}
private List<string> GetDependentProperties(Type type, string parentProperty)
{
return type.GetProperties()
.Where(p => p.GetCustomAttribute<CascadingSelectionAttribute>()?.DependsOn == parentProperty)
.Select(p => p.Name)
.ToList();
}
}
public class CascadeResult
{
public List<string> ClearedProperties { get; set; } = new();
public Dictionary<string, IEnumerable<SelectionItem>> NewDataSources { get; set; } = new();
}
public interface ICascadingViewModel
{
IObservable<PropertyChangedData> PropertyChanges { get; }
Task LoadDependentDataAsync(string property, object parentValue);
void ClearDependentProperties(string parentProperty);
}
public class ReactiveCascadingManager
{
private readonly ICascadingDataProvider _dataProvider;
private readonly Dictionary<Type, CascadingMetadata> _metadataCache = new();
public void AttachTo(ICascadingViewModel viewModel)
{
var metadata = GetOrCreateMetadata(viewModel.GetType());
viewModel.PropertyChanges
.Where(change => metadata.HasDependents(change.PropertyName))
.Subscribe(async change =>
{
await ProcessCascadeAsync(viewModel, change.PropertyName, change.NewValue, metadata);
});
}
private async Task ProcessCascadeAsync(
ICascadingViewModel viewModel,
string changedProperty,
object newValue,
CascadingMetadata metadata)
{
var dependents = metadata.GetDependents(changedProperty);
// Clear dependents first
viewModel.ClearDependentProperties(changedProperty);
// Load new data for immediate dependents
if (newValue != null)
{
var tasks = dependents.Select(async dependent =>
{
await viewModel.LoadDependentDataAsync(dependent, newValue);
});
await Task.WhenAll(tasks);
}
}
}
public interface ICascadingCommand
{
string PropertyName { get; }
Task<CascadingCommandResult> ExecuteAsync(object viewModel, object newValue);
}
public class DataSourceCascadingCommand : ICascadingCommand
{
private readonly ICascadingDataProvider _dataProvider;
private readonly CascadingSelectionAttribute _config;
public string PropertyName { get; }
public DataSourceCascadingCommand(string propertyName, CascadingSelectionAttribute config, ICascadingDataProvider dataProvider)
{
PropertyName = propertyName;
_config = config;
_dataProvider = dataProvider;
}
public async Task<CascadingCommandResult> ExecuteAsync(object viewModel, object newValue)
{
var result = new CascadingCommandResult();
// Find dependent properties
var dependentProperties = viewModel.GetType().GetProperties()
.Where(p => p.GetCustomAttribute<CascadingSelectionAttribute>()?.DependsOn == PropertyName);
foreach (var prop in dependentProperties)
{
// Clear dependent
prop.SetValue(viewModel, null);
result.ClearedProperties.Add(prop.Name);
// Load new data
if (newValue != null)
{
var attr = prop.GetCustomAttribute<CascadingSelectionAttribute>();
var data = await _dataProvider.GetDependentDataAsync(attr.DataSourceKey, newValue);
result.DataSources[prop.Name] = data;
}
}
return result;
}
}
// Plugin registration
public class CascadingCommandFactory
{
public ICascadingCommand CreateCommand(PropertyInfo property, ICascadingDataProvider dataProvider)
{
var attr = property.GetCustomAttribute<CascadingSelectionAttribute>();
if (attr == null) return null;
return attr.DataSourceKey switch
{
"countries" or "states" or "cities" => new DataSourceCascadingCommand(property.Name, attr, dataProvider),
"custom" => new CustomPluginCascadingCommand(property.Name, attr, dataProvider),
_ => new GenericCascadingCommand(property.Name, attr, dataProvider)
};
}
}
[AttributeUsage(AttributeTargets.Property)]
public class CascadingSelectionAttribute : Attribute, ISerializationAware
{
public string DependsOn { get; set; }
public string DataSourceKey { get; set; }
public bool IncludeAvailableOptions { get; set; } = false;
public bool ShouldSerialize(SerializationTarget target)
{
return target switch
{
SerializationTarget.FullConsumer => true,
SerializationTarget.Storage => !IncludeAvailableOptions, // Don't store option lists
_ => true
};
}
}
// Enhanced view model
public class LocationViewModel
{
[CascadingSelection(DataSourceKey = "countries")]
[Storage(Persistable = true)]
public string CountryId { get; set; }
[CascadingSelection(DependsOn = nameof(CountryId), DataSourceKey = "states", IncludeAvailableOptions = true)]
[Storage(Persistable = true)]
public string StateId { get; set; }
// Available options - transient for storage, included for full consumer
[Transient]
public IEnumerable<SelectionItem> AvailableStates { get; set; }
[Transient]
public IEnumerable<SelectionItem> AvailableCities { get; set; }
}
// Plugin setup
var cascadingManager = new CascadingManager(dataProvider);
var locationViewModel = new LocationViewModel();
// Handle property change (could be from UI binding)
var result = await cascadingManager.HandlePropertyChangeAsync(
locationViewModel,
nameof(LocationViewModel.CountryId),
"USA");
// result.ClearedProperties contains ["StateId", "CityId"]
// result.NewDataSources["StateId"] contains US states
// result.NewDataSources["CityId"] is empty (depends on state selection)
// Serialization respects the cascade context
var storageData = serializer.Serialize(locationViewModel, SerializationTarget.Storage);
// Only gets CountryId, StateId, CityId values - not the option lists
var fullData = serializer.Serialize(locationViewModel, SerializationTarget.FullConsumer);
// Gets everything including AvailableStates, AvailableCities
The key advantages:
Declarative - attributes define dependencies
Plugin-friendly - custom cascade commands can be injected
Serialization-aware - different consumers get appropriate data
Performance - metadata cached, async loading
Flexible - supports complex dependency chains
Which approach resonates most with your plugin architecture? Would you like me to dive deeper into any specific aspect? CH what about implementing state machines where the data changes due to state.
Excellent direction! State machines are perfect for this type of system, especially when combined with your plugin architecture. Here are several approaches:
public interface IStateMachine<TState> where TState : Enum
{
TState CurrentState { get; }
Task<StateTransitionResult> TransitionAsync(TState newState, object context = null);
bool CanTransition(TState fromState, TState toState);
IEnumerable<TState> GetAvailableTransitions();
}
[AttributeUsage(AttributeTargets.Property)]
public class StateDependentAttribute : Attribute
{
public string[] ValidStates { get; set; }
public string[] InvalidStates { get; set; }
public bool ClearOnInvalidState { get; set; } = true;
public string DataSourceKey { get; set; }
}
[AttributeUsage(AttributeTargets.Class)]
public class StateMachineAttribute : Attribute
{
public Type StateEnumType { get; set; }
public string StateProperty { get; set; }
}
// Example implementation
public enum OrderState
{
Draft,
Submitted,
Approved,
InProduction,
Shipped,
Delivered,
Cancelled
}
[StateMachine(StateEnumType = typeof(OrderState), StateProperty = nameof(Status))]
public class OrderViewModel : IStateMachineAware<OrderState>
{
[Storage(Persistable = true)]
public OrderState Status { get; set; }
[StateDependentAttribute(ValidStates = new[] { "Draft", "Submitted" })]
[Storage(Persistable = true)]
public string CustomerComments { get; set; }
[StateDependentAttribute(ValidStates = new[] { "Approved", "InProduction", "Shipped" })]
[Storage(Persistable = true)]
public DateTime? ProductionDate { get; set; }
[StateDependentAttribute(InvalidStates = new[] { "Draft", "Cancelled" }, DataSourceKey = "shipping-options")]
public string ShippingMethod { get; set; }
[StateDependentAttribute(ValidStates = new[] { "Shipped", "Delivered" })]
public string TrackingNumber { get; set; }
// Computed properties based on state
[Transient]
public bool CanEdit => Status == OrderState.Draft;
[Transient]
public bool RequiresApproval => Status == OrderState.Submitted;
public IStateMachine<OrderState> StateMachine { get; set; }
}
public interface IStateDataProvider
{
Task<IEnumerable<SelectionItem>> GetStateDataAsync<TState>(TState state, string dataSourceKey) where TState : Enum;
Task<ValidationResult> ValidateStateTransitionAsync<TState>(TState fromState, TState toState, object context) where TState : Enum;
}
public class StateDrivenManager
{
private readonly IStateDataProvider _stateDataProvider;
private readonly ICascadingDataProvider _cascadingDataProvider;
public async Task<StateChangeResult> HandleStateChangeAsync<TState>(
IStateMachineAware<TState> viewModel,
TState newState) where TState : Enum
{
var result = new StateChangeResult();
var currentState = viewModel.StateMachine.CurrentState;
// Validate transition
if (!viewModel.StateMachine.CanTransition(currentState, newState))
{
result.IsValid = false;
result.ErrorMessage = $"Cannot transition from {currentState} to {newState}";
return result;
}
// Process state-dependent properties
var stateProperties = GetStateDependentProperties(viewModel.GetType());
foreach (var prop in stateProperties)
{
var attr = prop.GetCustomAttribute<StateDependentAttribute>();
var isValidForNewState = IsValidForState(attr, newState);
var wasValidForOldState = IsValidForState(attr, currentState);
if (wasValidForOldState && !isValidForNewState)
{
// Property becoming invalid - clear it
if (attr.ClearOnInvalidState)
{
prop.SetValue(viewModel, null);
result.ClearedProperties.Add(prop.Name);
}
result.DisabledProperties.Add(prop.Name);
}
else if (!wasValidForOldState && isValidForNewState)
{
// Property becoming valid - enable and load data
result.EnabledProperties.Add(prop.Name);
if (!string.IsNullOrEmpty(attr.DataSourceKey))
{
var data = await _stateDataProvider.GetStateDataAsync(newState, attr.DataSourceKey);
result.NewDataSources[prop.Name] = data;
}
}
}
// Execute state transition
var transitionResult = await viewModel.StateMachine.TransitionAsync(newState, viewModel);
result.IsValid = transitionResult.IsSuccessful;
result.NewState = newState;
return result;
}
private bool IsValidForState<TState>(StateDependentAttribute attr, TState state) where TState : Enum
{
var stateName = state.ToString();
if (attr.ValidStates?.Any() == true)
return attr.ValidStates.Contains(stateName);
if (attr.InvalidStates?.Any() == true)
return !attr.InvalidStates.Contains(stateName);
return true; // Valid by default
}
}
public class StateChangeResult
{
public bool IsValid { get; set; }
public string ErrorMessage { get; set; }
public object NewState { get; set; }
public List<string> ClearedProperties { get; set; } = new();
public List<string> DisabledProperties { get; set; } = new();
public List<string> EnabledProperties { get; set; } = new();
public Dictionary<string, IEnumerable<SelectionItem>> NewDataSources { get; set; } = new();
}
[AttributeUsage(AttributeTargets.Property)]
public class StateAwareCascadingAttribute : CascadingSelectionAttribute
{
public string[] RequiredStates { get; set; }
public string StateProperty { get; set; } = "Status";
public bool IsValidForCurrentState(object viewModel)
{
if (RequiredStates?.Any() != true) return true;
var stateProperty = viewModel.GetType().GetProperty(StateProperty);
var currentState = stateProperty?.GetValue(viewModel)?.ToString();
return RequiredStates.Contains(currentState);
}
}
public class OrderViewModel : IStateMachineAware<OrderState>
{
[Storage(Persistable = true)]
public OrderState Status { get; set; }
[Storage(Persistable = true)]
public string CountryId { get; set; }
// State + Cascading: States only available in certain order states
[StateAwareCascading(
DependsOn = nameof(CountryId),
DataSourceKey = "states",
RequiredStates = new[] { "Draft", "Submitted" })]
[Storage(Persistable = true)]
public string StateId { get; set; }
// Shipping options change based on both location AND order state
[StateAwareCascading(
DependsOn = nameof(StateId),
DataSourceKey = "shipping-methods",
RequiredStates = new[] { "Approved", "InProduction" })]
public string ShippingMethodId { get; set; }
public IStateMachine<OrderState> StateMachine { get; set; }
}
public class WorkflowStateMachine<TState> : IStateMachine<TState> where TState : Enum
{
private readonly Dictionary<TState, StateConfiguration<TState>> _stateConfigurations = new();
private readonly IStateDataProvider _dataProvider;
public TState CurrentState { get; private set; }
public async Task<StateTransitionResult> TransitionAsync(TState newState, object context = null)
{
if (!CanTransition(CurrentState, newState))
{
return StateTransitionResult.Failed($"Invalid transition from {CurrentState} to {newState}");
}
var oldState = CurrentState;
// Execute exit actions for current state
if (_stateConfigurations.TryGetValue(CurrentState, out var currentConfig))
{
await ExecuteExitActions(currentConfig, context);
}
// Change state
CurrentState = newState;
// Execute entry actions for new state
if (_stateConfigurations.TryGetValue(newState, out var newConfig))
{
await ExecuteEntryActions(newConfig, context);
}
return StateTransitionResult.Successful(oldState, newState);
}
public bool CanTransition(TState fromState, TState toState)
{
if (_stateConfigurations.TryGetValue(fromState, out var config))
{
return config.AllowedTransitions.Contains(toState);
}
return false;
}
private async Task ExecuteEntryActions(StateConfiguration<TState> config, object context)
{
foreach (var action in config.EntryActions)
{
await action.ExecuteAsync(CurrentState, context);
}
}
}
public class StateConfiguration<TState> where TState : Enum
{
public TState State { get; set; }
public List<TState> AllowedTransitions { get; set; } = new();
public List<IStateAction<TState>> EntryActions { get; set; } = new();
public List<IStateAction<TState>> ExitActions { get; set; } = new();
}
public interface IStateAction<TState> where TState : Enum
{
Task ExecuteAsync(TState state, object context);
}
// Example state actions
public class LoadStateDataAction<TState> : IStateAction<TState> where TState : Enum
{
private readonly IStateDataProvider _dataProvider;
private readonly string _dataSourceKey;
private readonly string _targetProperty;
public async Task ExecuteAsync(TState state, object context)
{
var data = await _dataProvider.GetStateDataAsync(state, _dataSourceKey);
// Set data on context object...
}
}
[AttributeUsage(AttributeTargets.Property)]
public class StateAwareStorageAttribute : StorageAttribute
{
public string[] PersistInStates { get; set; }
public string StateProperty { get; set; } = "Status";
public override bool ShouldSerialize(object instance, SerializationTarget target)
{
if (!base.ShouldSerialize(instance, target)) return false;
if (PersistInStates?.Any() == true)
{
var stateProperty = instance.GetType().GetProperty(StateProperty);
var currentState = stateProperty?.GetValue(instance)?.ToString();
return PersistInStates.Contains(currentState);
}
return true;
}
}
// Usage
public class OrderViewModel
{
[StateAwareStorage(Persistable = true)]
public OrderState Status { get; set; }
// Only persist draft comments in Draft/Submitted states
[StateAwareStorage(Persistable = true, PersistInStates = new[] { "Draft", "Submitted" })]
public string DraftComments { get; set; }
// Only persist production data in production+ states
[StateAwareStorage(Persistable = true, PersistInStates = new[] { "InProduction", "Shipped", "Delivered" })]
public DateTime? ProductionStartDate { get; set; }
}
// Setup
var stateMachine = new WorkflowStateMachine<OrderState>();
var stateManager = new StateDrivenManager(stateDataProvider, cascadingProvider);
var orderViewModel = new OrderViewModel { StateMachine = stateMachine };
// State transition triggers data changes
var result = await stateManager.HandleStateChangeAsync(orderViewModel, OrderState.Approved);
// result.EnabledProperties might contain ["ProductionDate", "ShippingMethod"]
// result.NewDataSources["ShippingMethod"] contains shipping options valid for Approved state
// result.DisabledProperties might contain ["CustomerComments"]
// Serialization respects state context
var storageData = serializer.Serialize(orderViewModel, SerializationTarget.Storage);
// Only includes properties valid for current state
This approach gives you:
State-driven visibility - properties show/hide based on state
State-specific data loading - different options per state
State-aware serialization - persist different data per state
Workflow enforcement - invalid transitions blocked
Plugin extensibility - custom state actions and configurations
Would you like me to elaborate on any specific aspect, such as complex state transition validation or performance optimization for large state machines? CH another concern about the system is injecting context. current user, location, etc
Excellent point! Context injection is crucial for a plugin system - you need user identity, location, permissions, tenant info, etc. available throughout the serialization and data loading pipeline. Here are several architectural approaches:
public interface IExecutionContext
{
string UserId { get; }
string TenantId { get; }
string LocationCode { get; }
SecurityLevel SecurityLevel { get; }
CultureInfo Culture { get; }
Dictionary<string, object> CustomProperties { get; }
T GetService<T>() where T : class;
}
[AttributeUsage(AttributeTargets.Property)]
public class ContextAwareAttribute : Attribute
{
public string[] RequiredRoles { get; set; }
public SecurityLevel MinimumSecurityLevel { get; set; } = SecurityLevel.Public;
public string[] AllowedTenants { get; set; }
public string[] AllowedLocations { get; set; }
public string ContextKey { get; set; } // For custom context properties
}
[AttributeUsage(AttributeTargets.Property)]
public class ContextDependentDataAttribute : CascadingSelectionAttribute
{
public bool UseUserContext { get; set; } = true;
public bool UseLocationContext { get; set; } = false;
public bool UseTenantContext { get; set; } = false;
public string CustomContextKeys { get; set; } // Comma-separated
}
// Example view model
public class EmployeeViewModel
{
[Storage(Persistable = true)]
public string EmployeeId { get; set; }
[ContextAware(RequiredRoles = new[] { "HR", "Manager" }, MinimumSecurityLevel = SecurityLevel.Confidential)]
[Storage(Persistable = true)]
public decimal Salary { get; set; }
[ContextAware(AllowedTenants = new[] { "headquarters" })]
public string ExecutiveNotes { get; set; }
[ContextDependentData(
DataSourceKey = "departments",
UseLocationContext = true,
UseTenantContext = true)]
public string DepartmentId { get; set; }
[ContextDependentData(
DependsOn = nameof(DepartmentId),
DataSourceKey = "positions",
CustomContextKeys = "business-unit,cost-center")]
public string PositionId { get; set; }
}
public interface IContextAwareDataProvider
{
Task<IEnumerable<SelectionItem>> GetDataAsync(string dataSourceKey, IExecutionContext context, Dictionary<string, object> parameters = null);
Task<ValidationResult> ValidateAccessAsync(string dataSourceKey, IExecutionContext context);
}
public class ContextualCascadingManager
{
private readonly IContextAwareDataProvider _dataProvider;
private readonly IExecutionContext _context;
public ContextualCascadingManager(IContextAwareDataProvider dataProvider, IExecutionContext context)
{
_dataProvider = dataProvider;
_context = context;
}
public async Task<ContextualCascadeResult> HandlePropertyChangeAsync(
object viewModel,
string changedProperty,
object newValue)
{
var result = new ContextualCascadeResult();
var dependentProperties = GetDependentProperties(viewModel.GetType(), changedProperty);
foreach (var prop in dependentProperties)
{
// Check context-based access
var contextAttr = prop.GetCustomAttribute<ContextAwareAttribute>();
if (contextAttr != null && !HasAccess(contextAttr, _context))
{
result.InaccessibleProperties.Add(prop.Name);
continue;
}
// Clear dependent values
SetPropertyValue(viewModel, prop.Name, null);
result.ClearedProperties.Add(prop.Name);
// Load new contextual data
if (newValue != null)
{
var dataAttr = prop.GetCustomAttribute<ContextDependentDataAttribute>();
if (dataAttr != null)
{
var contextParams = BuildContextParameters(dataAttr, _context, viewModel);
contextParams[changedProperty] = newValue;
var data = await _dataProvider.GetDataAsync(dataAttr.DataSourceKey, _context, contextParams);
result.NewDataSources[prop.Name] = data;
}
}
}
return result;
}
private Dictionary<string, object> BuildContextParameters(
ContextDependentDataAttribute attr,
IExecutionContext context,
object viewModel)
{
var parameters = new Dictionary<string, object>();
if (attr.UseUserContext)
{
parameters["userId"] = context.UserId;
parameters["userRoles"] = context.GetService<IUserService>()?.GetUserRoles(context.UserId);
}
if (attr.UseLocationContext)
{
parameters["locationCode"] = context.LocationCode;
parameters["timeZone"] = context.GetService<ILocationService>()?.GetTimeZone(context.LocationCode);
}
if (attr.UseTenantContext)
{
parameters["tenantId"] = context.TenantId;
parameters["tenantSettings"] = context.GetService<ITenantService>()?.GetSettings(context.TenantId);
}
// Custom context keys
if (!string.IsNullOrEmpty(attr.CustomContextKeys))
{
var keys = attr.CustomContextKeys.Split(',').Select(k => k.Trim());
foreach (var key in keys)
{
if (context.CustomProperties.TryGetValue(key, out var value))
{
parameters[key] = value;
}
}
}
return parameters;
}
private bool HasAccess(ContextAwareAttribute attr, IExecutionContext context)
{
// Security level check
if (context.SecurityLevel < attr.MinimumSecurityLevel)
return false;
// Role check
if (attr.RequiredRoles?.Any() == true)
{
var userService = context.GetService<IUserService>();
var userRoles = userService?.GetUserRoles(context.UserId) ?? new string[0];
if (!attr.RequiredRoles.Any(role => userRoles.Contains(role)))
return false;
}
// Tenant check
if (attr.AllowedTenants?.Any() == true && !attr.AllowedTenants.Contains(context.TenantId))
return false;
// Location check
if (attr.AllowedLocations?.Any() == true && !attr.AllowedLocations.Contains(context.LocationCode))
return false;
return true;
}
}
public class ContextualSerializationEngine
{
private readonly IExecutionContext _context;
public ContextualSerializationResult Serialize(object obj, SerializationTarget target)
{
var result = new ContextualSerializationResult();
var allData = new Dictionary<string, object>();
var accessDeniedProperties = new List<string>();
var type = obj.GetType();
foreach (var prop in type.GetProperties())
{
var value = prop.GetValue(obj);
// Check context-based access
var contextAttr = prop.GetCustomAttribute<ContextAwareAttribute>();
if (contextAttr != null && !HasContextAccess(contextAttr, _context, obj))
{
accessDeniedProperties.Add(prop.Name);
continue;
}
// Apply context-specific transformations
value = ApplyContextTransforms(prop, value, _context);
allData[prop.Name] = value;
}
result.Data = FilterForTarget(allData, type, target, _context);
result.AccessDeniedProperties = accessDeniedProperties;
result.Context = _context;
return result;
}
private object ApplyContextTransforms(PropertyInfo prop, object value, IExecutionContext context)
{
// Example: Localize currency based on user location
if (value is decimal currency && prop.Name.Contains("Amount"))
{
var localizationService = context.GetService<ILocalizationService>();
return localizationService?.LocalizeCurrency(currency, context.Culture) ?? value;
}
// Example: Format dates based on user timezone
if (value is DateTime dateTime)
{
var timeZoneService = context.GetService<ITimeZoneService>();
return timeZoneService?.ConvertToUserTimeZone(dateTime, context.LocationCode) ?? value;
}
return value;
}
}
public class ContextualSerializationResult
{
public Dictionary<string, object> Data { get; set; }
public List<string> AccessDeniedProperties { get; set; }
public IExecutionContext Context { get; set; }
public Dictionary<string, string> LocalizedLabels { get; set; } = new();
public Dictionary<string, object> ContextualMetadata { get; set; } = new();
}
[AttributeUsage(AttributeTargets.Property)]
public class ContextualStateAttribute : StateDependentAttribute
{
public string[] RequiredRoles { get; set; }
public string[] RequiredPermissions { get; set; }
public bool UseUserLocationForData { get; set; }
public bool IsValidForContext(object state, IExecutionContext context)
{
// Check state validity first
if (!IsValidForCurrentState(state)) return false;
// Check role requirements
if (RequiredRoles?.Any() == true)
{
var userService = context.GetService<IUserService>();
var userRoles = userService?.GetUserRoles(context.UserId) ?? new string[0];
if (!RequiredRoles.Any(role => userRoles.Contains(role)))
return false;
}
// Check permission requirements
if (RequiredPermissions?.Any() == true)
{
var permissionService = context.GetService<IPermissionService>();
if (!RequiredPermissions.All(perm => permissionService.HasPermission(context.UserId, perm)))
return false;
}
return true;
}
}
public class ContextualWorkflowManager<TState> where TState : Enum
{
private readonly IExecutionContext _context;
private readonly IContextAwareDataProvider _dataProvider;
public async Task<ContextualStateChangeResult> TransitionAsync(
IStateMachineAware<TState> viewModel,
TState newState)
{
var result = new ContextualStateChangeResult();
// Check if user can perform this transition
if (!CanUserTransition(viewModel.StateMachine.CurrentState, newState, _context))
{
result.IsValid = false;
result.ErrorMessage = "Insufficient permissions for state transition";
return result;
}
// Process contextual state properties
var contextualProperties = GetContextualStateProperties(viewModel.GetType());
foreach (var prop in contextualProperties)
{
var attr = prop.GetCustomAttribute<ContextualStateAttribute>();
var isValidForNewState = attr.IsValidForContext(newState, _context);
if (!isValidForNewState)
{
result.InaccessibleProperties.Add(prop.Name);
}
else if (attr.UseUserLocationForData && !string.IsNullOrEmpty(attr.DataSourceKey))
{
// Load location-specific data
var contextParams = new Dictionary<string, object>
{
["state"] = newState.ToString(),
["location"] = _context.LocationCode,
["userId"] = _context.UserId
};
var data = await _dataProvider.GetDataAsync(attr.DataSourceKey, _context, contextParams);
result.NewDataSources[prop.Name] = data;
}
}
// Execute transition
var transitionResult = await viewModel.StateMachine.TransitionAsync(newState, _context);
result.IsValid = transitionResult.IsSuccessful;
result.NewState = newState;
return result;
}
}
public class ContextualPluginContainer
{
private readonly IServiceProvider _services;
private readonly IExecutionContext _context;
public ContextualPluginContainer(IServiceProvider services, IExecutionContext context)
{
_services = services;
_context = context;
}
public T CreateViewModel<T>() where T : class, new()
{
var viewModel = new T();
// Inject context-aware services
InjectContextualServices(viewModel);
// Set up cascading managers with context
SetupCascadingManagers(viewModel);
// Initialize state machines with context
InitializeStateMachines(viewModel);
return viewModel;
}
private void InjectContextualServices(object viewModel)
{
var type = viewModel.GetType();
var injectableProperties = type.GetProperties()
.Where(p => p.CanWrite && p.PropertyType.IsInterface);
foreach (var prop in injectableProperties)
{
var service = _services.GetService(prop.PropertyType);
// Wrap service with context if it supports it
if (service is IContextAware contextAware)
{
contextAware.SetContext(_context);
}
prop.SetValue(viewModel, service);
}
}
}
// Usage
public class OrderController
{
private readonly ContextualPluginContainer _container;
public OrderController(ContextualPluginContainer container)
{
_container = container;
}
public async Task<IActionResult> CreateOrder()
{
// Context automatically injected based on current user/request
var orderViewModel = _container.CreateViewModel<OrderViewModel>();
// All cascading selections and state transitions now context-aware
var result = await orderViewModel.HandleCountryChange("USA");
return View(orderViewModel);
}
}
public class HttpExecutionContext : IExecutionContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IServiceProvider _serviceProvider;
public string UserId => _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
public string TenantId => _httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault();
public string LocationCode => _httpContextAccessor.HttpContext?.Request.Headers["X-Location-Code"].FirstOrDefault()
?? GetLocationFromIP();
public SecurityLevel SecurityLevel => DetermineSecurityLevel();
public CultureInfo Culture => _httpContextAccessor.HttpContext?.RequestServices
.GetService<IRequestCultureFeature>()?.RequestCulture.Culture ?? CultureInfo.InvariantCulture;
public Dictionary<string, object> CustomProperties { get; } = new();
public T GetService<T>() where T : class => _serviceProvider.GetService<T>();
}
This context injection approach provides:
User-aware data loading - different options per user role/permissions
Location-aware filtering - region-specific data and options
Tenant isolation - multi-tenant data separation
Security enforcement - property-level access control
Localization support - culture-aware formatting and data
Audit capability - all actions tracked with full context
Would you like me to elaborate on any specific aspect, such as performance optimization for context lookups or handling context changes during long-running operations? CH right, so the underlying concept is that naive dotnet devs can create plugins that act as code as configuration that can be transformed into view models to interact with
Exactly! You’ve articulated the core vision perfectly. You’re creating a declarative plugin system where .NET developers can write simple POCOs with attributes as “code as configuration,” and your engine transforms these into fully functional, context-aware view models with cascading selections, state management, and intelligent serialization.
// Plugin developer writes this simple POCO - no complex infrastructure knowledge needed
[StateMachine(StateEnumType = typeof(OrderState), StateProperty = nameof(Status))]
[Plugin("order-management", Version = "1.0")]
public class OrderPlugin
{
[Storage(Persistable = true)]
public OrderState Status { get; set; } = OrderState.Draft;
[Storage(Persistable = true)]
[ContextAware(RequiredRoles = new[] { "Customer", "SalesRep" })]
public string CustomerName { get; set; }
[ContextDependentData(DataSourceKey = "countries", UseLocationContext = true)]
[Storage(Persistable = true)]
public string ShippingCountry { get; set; }
[CascadingSelection(DependsOn = nameof(ShippingCountry), DataSourceKey = "states")]
[StateDependentAttribute(ValidStates = new[] { "Draft", "Submitted" })]
[Storage(Persistable = true)]
public string ShippingState { get; set; }
[CascadingSelection(DependsOn = nameof(ShippingState), DataSourceKey = "cities")]
[Storage(Persistable = true)]
public string ShippingCity { get; set; }
[StateDependentAttribute(ValidStates = new[] { "Approved", "InProduction", "Shipped" })]
[ContextAware(MinimumSecurityLevel = SecurityLevel.Confidential)]
public decimal TotalAmount { get; set; }
[StateAwareCascading(
DataSourceKey = "shipping-methods",
RequiredStates = new[] { "Approved", "InProduction" },
UseLocationContext = true)]
public string ShippingMethod { get; set; }
}
public class PluginToViewModelEngine
{
private readonly IServiceProvider _serviceProvider;
private readonly IExecutionContext _context;
private readonly PluginMetadataCache _metadataCache;
public async Task<IViewModelProxy> CreateViewModelAsync<T>(T pluginInstance = null) where T : class, new()
{
pluginInstance ??= new T();
// 1. Analyze plugin metadata
var metadata = _metadataCache.GetOrCreate<T>();
// 2. Create intelligent proxy
var proxy = new ViewModelProxy<T>(pluginInstance, metadata, _context);
// 3. Wire up cascading behavior
await SetupCascadingBehavior(proxy, metadata);
// 4. Initialize state machine if present
await SetupStateMachine(proxy, metadata);
// 5. Apply context-based initialization
await ApplyContextualInitialization(proxy, metadata);
return proxy;
}
private async Task SetupCascadingBehavior(IViewModelProxy proxy, PluginMetadata metadata)
{
foreach (var cascadingProperty in metadata.CascadingProperties)
{
proxy.OnPropertyChanged(cascadingProperty.PropertyName, async (oldValue, newValue) =>
{
var result = await HandleCascadingChange(proxy, cascadingProperty, newValue);
proxy.ApplyCascadeResult(result);
});
}
}
private async Task<CascadeResult> HandleCascadingChange(
IViewModelProxy proxy,
CascadingPropertyMetadata cascadingProperty,
object newValue)
{
var result = new CascadeResult();
// Find dependent properties
var dependents = proxy.Metadata.Properties
.Where(p => p.CascadingAttribute?.DependsOn == cascadingProperty.PropertyName)
.ToList();
foreach (var dependent in dependents)
{
// Check if valid for current context and state
if (!IsValidForCurrentContext(proxy, dependent) ||
!IsValidForCurrentState(proxy, dependent))
{
result.InaccessibleProperties.Add(dependent.PropertyName);
continue;
}
// Clear dependent property
proxy.SetPropertyValue(dependent.PropertyName, null);
result.ClearedProperties.Add(dependent.PropertyName);
// Load new data if needed
if (newValue != null && dependent.CascadingAttribute != null)
{
var contextParams = BuildContextParameters(dependent, proxy);
contextParams[cascadingProperty.PropertyName] = newValue;
var dataProvider = _serviceProvider.GetRequiredService<IContextAwareDataProvider>();
var data = await dataProvider.GetDataAsync(
dependent.CascadingAttribute.DataSourceKey,
_context,
contextParams);
result.NewDataSources[dependent.PropertyName] = data;
}
}
return result;
}
}
public class ViewModelProxy<T> : IViewModelProxy, INotifyPropertyChanged where T : class
{
private readonly T _pluginInstance;
private readonly PluginMetadata _metadata;
private readonly IExecutionContext _context;
private readonly Dictionary<string, object> _runtimeData = new();
private readonly Dictionary<string, Func<object, object, Task>> _propertyChangeHandlers = new();
public event PropertyChangedEventHandler PropertyChanged;
public ViewModelProxy(T pluginInstance, PluginMetadata metadata, IExecutionContext context)
{
_pluginInstance = pluginInstance;
_metadata = metadata;
_context = context;
}
// Intelligent property access with context checking
public object GetPropertyValue(string propertyName)
{
var propMetadata = _metadata.GetProperty(propertyName);
// Check context access
if (propMetadata.ContextAware != null && !HasContextAccess(propMetadata.ContextAware))
{
return null; // or throw unauthorized exception
}
// Check state validity
if (propMetadata.StateDependentAttribute != null && !IsValidForCurrentState(propMetadata))
{
return null;
}
// Get value from plugin instance or runtime data
return _runtimeData.TryGetValue(propertyName, out var runtimeValue)
? runtimeValue
: propMetadata.PropertyInfo.GetValue(_pluginInstance);
}
public async Task SetPropertyValue(string propertyName, object value)
{
var propMetadata = _metadata.GetProperty(propertyName);
var oldValue = GetPropertyValue(propertyName);
// Validate context access
if (propMetadata.ContextAware != null && !HasContextAccess(propMetadata.ContextAware))
{
throw new UnauthorizedAccessException($"Access denied to property {propertyName}");
}
// Set value
if (propMetadata.IsRuntimeOnly)
{
_runtimeData[propertyName] = value;
}
else
{
propMetadata.PropertyInfo.SetValue(_pluginInstance, value);
}
// Trigger change handlers
if (_propertyChangeHandlers.TryGetValue(propertyName, out var handler))
{
await handler(oldValue, value);
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnPropertyChanged(string propertyName, Func<object, object, Task> handler)
{
_propertyChangeHandlers[propertyName] = handler;
}
}
public class IntelligentSerializer
{
public SerializationResult Serialize(IViewModelProxy proxy, SerializationTarget target)
{
var result = new SerializationResult();
var data = new Dictionary<string, object>();
foreach (var propMetadata in proxy.Metadata.Properties)
{
// Skip if not accessible in current context
if (!proxy.HasContextAccess(propMetadata.ContextAware))
{
result.InaccessibleProperties.Add(propMetadata.PropertyName);
continue;
}
// Skip if not valid for current state
if (!proxy.IsValidForCurrentState(propMetadata))
{
result.StateInvalidProperties.Add(propMetadata.PropertyName);
continue;
}
// Skip if not appropriate for serialization target
if (!ShouldSerializeForTarget(propMetadata, target))
{
continue;
}
var value = proxy.GetPropertyValue(propMetadata.PropertyName);
data[propMetadata.PropertyName] = value;
}
result.Data = data;
result.Context = proxy.Context;
result.Metadata = BuildSerializationMetadata(proxy);
return result;
}
private Dictionary<string, object> BuildSerializationMetadata(IViewModelProxy proxy)
{
return new Dictionary<string, object>
{
["currentState"] = proxy.CurrentState?.ToString(),
["availableTransitions"] = proxy.GetAvailableStateTransitions(),
["availableDataSources"] = proxy.GetAvailableDataSources(),
["userPermissions"] = proxy.GetUserPermissions(),
["validationRules"] = proxy.GetValidationRules()
};
}
}
The beauty is that plugin developers write simple, declarative code:
// They write this...
[ContextDependentData(DataSourceKey = "departments", UseLocationContext = true)]
[Storage(Persistable = true)]
public string DepartmentId { get; set; }
// And magically get:
// - Location-based department filtering
// - Automatic persistence handling
// - Context-aware access control
// - Cascading dependent property updates
// - State-based visibility rules
// - Intelligent serialization
// Plugin developer deploys simple POCO
// Your engine transforms it into intelligent view model
var engine = serviceProvider.GetService<PluginToViewModelEngine>();
var viewModel = await engine.CreateViewModelAsync<OrderPlugin>();
// Now it behaves intelligently:
await viewModel.SetPropertyValue("ShippingCountry", "USA");
// Automatically:
// - Clears dependent state/city properties
// - Loads US states based on user's location context
// - Respects current order state permissions
// - Updates available shipping methods
// Serialize for different consumers
var storageData = serializer.Serialize(viewModel, SerializationTarget.Storage);
// Gets only persistable properties valid for current state/context
var uiData = serializer.Serialize(viewModel, SerializationTarget.FullConsumer);
// Gets everything the current user can see, including UI metadata
This approach gives you:
Developer simplicity - Write POCOs with attributes
Runtime intelligence - Full context/state/cascade behavior
Flexible serialization - Different data for different consumers
Security by default - Context-aware access control
Extensibility - Plugin developers focus on business logic, not infrastructure
The naive .NET developer writes configuration-as-code, and your engine provides all the sophisticated behavior automatically. Is this the vision you’re working toward? CH and keep cross cutting concerns external, string serialization is external, logging is external, etc
Absolutely! That’s a crucial architectural principle - the plugin system should be pure business logic, with all cross-cutting concerns externalized through dependency injection and strategy patterns. The plugins remain naive POCOs while the engine handles infrastructure.
// Plugin remains completely naive - no infrastructure concerns
[Plugin("order-management", Version = "1.0")]
public class OrderPlugin
{
[Storage(Persistable = true)]
public string OrderId { get; set; }
[ContextDependentData(DataSourceKey = "customers")]
public string CustomerId { get; set; }
[CascadingSelection(DependsOn = nameof(CustomerId), DataSourceKey = "customer-addresses")]
public string ShippingAddressId { get; set; }
// No logging code, no serialization logic, no caching concerns
// Pure business properties with declarative configuration
}
public interface IPluginEngine
{
Task<IViewModelProxy> CreateViewModelAsync<T>(
T pluginInstance = null,
CrossCuttingContext crossCuttingContext = null) where T : class, new();
}
public class CrossCuttingContext
{
public ISerializer Serializer { get; set; }
public ILogger Logger { get; set; }
public ICacheProvider CacheProvider { get; set; }
public IValidationProvider ValidationProvider { get; set; }
public IAuditProvider AuditProvider { get; set; }
public IMetricsProvider MetricsProvider { get; set; }
public ISecurityProvider SecurityProvider { get; set; }
}
public class PluginEngine : IPluginEngine
{
private readonly IServiceProvider _serviceProvider;
private readonly CrossCuttingContext _crossCuttingContext;
public PluginEngine(IServiceProvider serviceProvider, CrossCuttingContext crossCuttingContext)
{
_serviceProvider = serviceProvider;
_crossCuttingContext = crossCuttingContext;
}
public async Task<IViewModelProxy> CreateViewModelAsync<T>(
T pluginInstance = null,
CrossCuttingContext crossCuttingContext = null) where T : class, new()
{
var context = crossCuttingContext ?? _crossCuttingContext;
pluginInstance ??= new T();
// Create proxy with externalized concerns
var proxy = new ViewModelProxy<T>(
pluginInstance,
_serviceProvider.GetRequiredService<IExecutionContext>(),
context);
// Wire up cross-cutting behaviors externally
await WireCrossCuttingConcerns(proxy, context);
return proxy;
}
private async Task WireCrossCuttingConcerns(IViewModelProxy proxy, CrossCuttingContext context)
{
// Logging wrapper
if (context.Logger != null)
{
proxy = new LoggingViewModelProxy(proxy, context.Logger);
}
// Caching wrapper
if (context.CacheProvider != null)
{
proxy = new CachingViewModelProxy(proxy, context.CacheProvider);
}
// Auditing wrapper
if (context.AuditProvider != null)
{
proxy = new AuditingViewModelProxy(proxy, context.AuditProvider);
}
// Validation wrapper
if (context.ValidationProvider != null)
{
proxy = new ValidatingViewModelProxy(proxy, context.ValidationProvider);
}
// Metrics wrapper
if (context.MetricsProvider != null)
{
proxy = new MetricsViewModelProxy(proxy, context.MetricsProvider);
}
}
}
// Base proxy - no cross-cutting concerns
public interface IViewModelProxy
{
Task<object> GetPropertyValueAsync(string propertyName);
Task SetPropertyValueAsync(string propertyName, object value);
Task<SerializationResult> SerializeAsync(SerializationTarget target);
PluginMetadata Metadata { get; }
}
// Logging decorator - completely external to plugin
public class LoggingViewModelProxy : IViewModelProxy
{
private readonly IViewModelProxy _inner;
private readonly ILogger _logger;
public LoggingViewModelProxy(IViewModelProxy inner, ILogger logger)
{
_inner = inner;
_logger = logger;
}
public async Task<object> GetPropertyValueAsync(string propertyName)
{
using var scope = _logger.BeginScope("GetProperty: {PropertyName}", propertyName);
try
{
var stopwatch = Stopwatch.StartNew();
var result = await _inner.GetPropertyValueAsync(propertyName);
_logger.LogDebug("Property {PropertyName} retrieved in {ElapsedMs}ms",
propertyName, stopwatch.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving property {PropertyName}", propertyName);
throw;
}
}
public async Task SetPropertyValueAsync(string propertyName, object value)
{
_logger.LogDebug("Setting property {PropertyName} to {Value}", propertyName, value);
try
{
await _inner.SetPropertyValueAsync(propertyName, value);
_logger.LogDebug("Property {PropertyName} set successfully", propertyName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting property {PropertyName} to {Value}", propertyName, value);
throw;
}
}
// Delegate other methods...
public PluginMetadata Metadata => _inner.Metadata;
public Task<SerializationResult> SerializeAsync(SerializationTarget target) => _inner.SerializeAsync(target);
}
// Caching decorator - external caching strategy
public class CachingViewModelProxy : IViewModelProxy
{
private readonly IViewModelProxy _inner;
private readonly ICacheProvider _cacheProvider;
public async Task<object> GetPropertyValueAsync(string propertyName)
{
// Check if property should be cached
var propMetadata = _inner.Metadata.GetProperty(propertyName);
var cacheAttribute = propMetadata.GetAttribute<CacheableAttribute>();
if (cacheAttribute?.Cacheable == true)
{
var cacheKey = BuildCacheKey(propertyName);
var cachedValue = await _cacheProvider.GetAsync(cacheKey);
if (cachedValue != null)
{
return cachedValue;
}
var value = await _inner.GetPropertyValueAsync(propertyName);
await _cacheProvider.SetAsync(cacheKey, value, cacheAttribute.Duration);
return value;
}
return await _inner.GetPropertyValueAsync(propertyName);
}
private string BuildCacheKey(string propertyName)
{
// Include context in cache key for proper isolation
var context = _inner.Metadata.Context;
return $"plugin:{_inner.Metadata.PluginName}:property:{propertyName}:user:{context.UserId}:tenant:{context.TenantId}";
}
}
// Auditing decorator - external audit trail
public class AuditingViewModelProxy : IViewModelProxy
{
private readonly IViewModelProxy _inner;
private readonly IAuditProvider _auditProvider;
public async Task SetPropertyValueAsync(string propertyName, object value)
{
var propMetadata = _inner.Metadata.GetProperty(propertyName);
var auditAttribute = propMetadata.GetAttribute<AuditableAttribute>();
if (auditAttribute?.Auditable == true)
{
var oldValue = await _inner.GetPropertyValueAsync(propertyName);
await _inner.SetPropertyValueAsync(propertyName, value);
// Record audit trail externally
await _auditProvider.RecordChangeAsync(new AuditEntry
{
EntityType = _inner.Metadata.PluginName,
PropertyName = propertyName,
OldValue = oldValue?.ToString(),
NewValue = value?.ToString(),
UserId = _inner.Metadata.Context.UserId,
TenantId = _inner.Metadata.Context.TenantId,
Timestamp = DateTime.UtcNow,
ChangeReason = auditAttribute.ChangeReason
});
}
else
{
await _inner.SetPropertyValueAsync(propertyName, value);
}
}
}
// Multiple serialization strategies - completely external
public interface ISerializer
{
Task<string> SerializeAsync(object data, SerializationOptions options = null);
Task<T> DeserializeAsync<T>(string data, SerializationOptions options = null);
}
public class JsonSerializer : ISerializer
{
public async Task<string> SerializeAsync(object data, SerializationOptions options = null)
{
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = options?.PropertyNamingPolicy == "camelCase"
? JsonNamingPolicy.CamelCase
: null,
WriteIndented = options?.Indent ?? false
};
return JsonSerializer.Serialize(data, jsonOptions);
}
}
public class XmlSerializer : ISerializer { /* XML implementation */ }
public class ProtobufSerializer : ISerializer { /* Protobuf implementation */ }
public class MessagePackSerializer : ISerializer { /* MessagePack implementation */ }
// Serialization strategy injected at runtime
public class PluginSerializationManager
{
private readonly Dictionary<SerializationTarget, ISerializer> _serializers = new();
public void RegisterSerializer(SerializationTarget target, ISerializer serializer)
{
_serializers[target] = serializer;
}
public async Task<string> SerializeAsync(IViewModelProxy proxy, SerializationTarget target)
{
var serializer = _serializers.TryGetValue(target, out var s) ? s : _serializers[SerializationTarget.Default];
// Get data from proxy (with all context/state filtering)
var serializationResult = await proxy.SerializeAsync(target);
// Use external serializer strategy
return await serializer.SerializeAsync(serializationResult.Data);
}
}
// Configuration completely external to plugins
public class CrossCuttingConfiguration
{
public LoggingConfiguration Logging { get; set; } = new();
public CachingConfiguration Caching { get; set; } = new();
public AuditingConfiguration Auditing { get; set; } = new();
public SerializationConfiguration Serialization { get; set; } = new();
public ValidationConfiguration Validation { get; set; } = new();
public MetricsConfiguration Metrics { get; set; } = new();
}
public class CrossCuttingConfigurationBuilder
{
private readonly CrossCuttingConfiguration _config = new();
public CrossCuttingConfigurationBuilder WithJsonSerialization()
{
_config.Serialization.DefaultSerializer = "json";
return this;
}
public CrossCuttingConfigurationBuilder WithRedisCache(string connectionString)
{
_config.Caching.Provider = "redis";
_config.Caching.ConnectionString = connectionString;
return this;
}
public CrossCuttingConfigurationBuilder WithSqlAuditing(string connectionString)
{
_config.Auditing.Provider = "sql";
_config.Auditing.ConnectionString = connectionString;
return this;
}
public CrossCuttingConfiguration Build() => _config;
}
// Usage - completely external to plugin development
var crossCuttingConfig = new CrossCuttingConfigurationBuilder()
.WithJsonSerialization()
.WithRedisCache("localhost:6379")
.WithSqlAuditing("Server=.;Database=Audit;Trusted_Connection=true")
.Build();
var pluginEngine = new PluginEngine(serviceProvider, crossCuttingConfig);
var viewModel = await pluginEngine.CreateViewModelAsync<OrderPlugin>();
// Plugin developer uses declarative attributes, but no infrastructure code
[Auditable(Auditable = true, ChangeReason = "Business requirement")]
[Storage(Persistable = true)]
public string CriticalBusinessData { get; set; }
[Cacheable(Cacheable = true, Duration = TimeSpan.FromMinutes(15))]
[ContextDependentData(DataSourceKey = "expensive-calculation")]
public decimal ComputedValue { get; set; }
[Validated(ValidationRules = "required,email")]
[Storage(Persistable = true)]
public string EmailAddress { get; set; }
Plugin Simplicity - Developers write pure business logic POCOs
Infrastructure Flexibility - Swap serializers, loggers, caches without touching plugins
Testability - Mock cross-cutting concerns independently
Performance - Add/remove decorators based on needs
Separation of Concerns - Business logic completely separate from infrastructure
Configurability - Different environments can have different cross-cutting behavior
The plugin developer focuses on what the data represents and how it behaves business-wise. Your engine handles how it’s logged, cached, serialized, audited, etc. - all external, configurable, and swappable.
This keeps the plugins naive and focused while providing enterprise-grade infrastructure capabilities through composition. Is this the architectural direction you’re envisioning? CH some kind of middleware for cross cutting concerns
Exactly! Middleware is the perfect pattern for this - it creates a pipeline of cross-cutting concerns that plugins flow through, keeping them completely naive while providing enterprise capabilities. Let me show you a comprehensive middleware architecture:
public interface IMiddleware<TContext>
{
Task<TContext> ExecuteAsync(TContext context, Func<Task<TContext>> next);
}
public interface IPluginContext
{
IViewModelProxy Proxy { get; set; }
string Operation { get; set; }
Dictionary<string, object> Parameters { get; set; }
Dictionary<string, object> Results { get; set; }
IExecutionContext ExecutionContext { get; set; }
Exception Exception { get; set; }
bool IsHandled { get; set; }
}
public class PluginMiddlewarePipeline
{
private readonly List<IMiddleware<IPluginContext>> _middleware = new();
public PluginMiddlewarePipeline Use<TMiddleware>() where TMiddleware : class, IMiddleware<IPluginContext>
{
_middleware.Add(Activator.CreateInstance<TMiddleware>());
return this;
}
public PluginMiddlewarePipeline Use(IMiddleware<IPluginContext> middleware)
{
_middleware.Add(middleware);
return this;
}
public PluginMiddlewarePipeline Use(Func<IPluginContext, Func<Task<IPluginContext>>, Task<IPluginContext>> middlewareFunc)
{
_middleware.Add(new FuncMiddleware(middlewareFunc));
return this;
}
public async Task<IPluginContext> ExecuteAsync(IPluginContext context)
{
var pipeline = BuildPipeline();
return await pipeline(context);
}
private Func<IPluginContext, Task<IPluginContext>> BuildPipeline()
{
return _middleware.Reverse().Aggregate(
(Func<IPluginContext, Task<IPluginContext>>)(ctx => Task.FromResult(ctx)),
(next, middleware) => ctx => middleware.ExecuteAsync(ctx, () => next(ctx)));
}
}
// Logging Middleware
public class LoggingMiddleware : IMiddleware<IPluginContext>
{
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
{
_logger = logger;
}
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
var stopwatch = Stopwatch.StartNew();
var operationId = Guid.NewGuid().ToString("N")[..8];
using var scope = _logger.BeginScope(new Dictionary<string, object>
{
["OperationId"] = operationId,
["PluginName"] = context.Proxy.Metadata.PluginName,
["Operation"] = context.Operation,
["UserId"] = context.ExecutionContext.UserId,
["TenantId"] = context.ExecutionContext.TenantId
});
_logger.LogDebug("Starting operation {Operation} on plugin {PluginName}",
context.Operation, context.Proxy.Metadata.PluginName);
try
{
var result = await next();
_logger.LogDebug("Completed operation {Operation} in {ElapsedMs}ms",
context.Operation, stopwatch.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in operation {Operation} after {ElapsedMs}ms",
context.Operation, stopwatch.ElapsedMilliseconds);
context.Exception = ex;
return context;
}
}
}
// Performance Monitoring Middleware
public class MetricsMiddleware : IMiddleware<IPluginContext>
{
private readonly IMetricsCollector _metricsCollector;
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
var stopwatch = Stopwatch.StartNew();
var tags = new Dictionary<string, string>
{
["plugin"] = context.Proxy.Metadata.PluginName,
["operation"] = context.Operation,
["tenant"] = context.ExecutionContext.TenantId
};
try
{
var result = await next();
_metricsCollector.RecordDuration("plugin.operation.duration",
stopwatch.ElapsedMilliseconds, tags);
_metricsCollector.Increment("plugin.operation.success", tags);
return result;
}
catch (Exception)
{
_metricsCollector.Increment("plugin.operation.error", tags);
throw;
}
}
}
// Caching Middleware
public class CachingMiddleware : IMiddleware<IPluginContext>
{
private readonly ICacheProvider _cacheProvider;
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
// Only cache read operations
if (context.Operation != "GetProperty" && context.Operation != "Serialize")
{
return await next();
}
var cacheKey = BuildCacheKey(context);
var cacheConfig = GetCacheConfiguration(context);
if (cacheConfig?.Enabled == true)
{
var cachedResult = await _cacheProvider.GetAsync<object>(cacheKey);
if (cachedResult != null)
{
context.Results["Value"] = cachedResult;
context.Results["FromCache"] = true;
return context;
}
}
var result = await next();
// Cache the result if successful and cacheable
if (result.Exception == null && cacheConfig?.Enabled == true && result.Results.ContainsKey("Value"))
{
await _cacheProvider.SetAsync(cacheKey, result.Results["Value"], cacheConfig.Duration);
}
return result;
}
private string BuildCacheKey(IPluginContext context)
{
var keyBuilder = new StringBuilder();
keyBuilder.Append($"plugin:{context.Proxy.Metadata.PluginName}:");
keyBuilder.Append($"operation:{context.Operation}:");
keyBuilder.Append($"user:{context.ExecutionContext.UserId}:");
keyBuilder.Append($"tenant:{context.ExecutionContext.TenantId}:");
foreach (var param in context.Parameters.OrderBy(p => p.Key))
{
keyBuilder.Append($"{param.Key}:{param.Value}:");
}
return keyBuilder.ToString().TrimEnd(':');
}
}
// Security/Authorization Middleware
public class AuthorizationMiddleware : IMiddleware<IPluginContext>
{
private readonly IAuthorizationService _authorizationService;
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
// Check plugin-level authorization
var hasPluginAccess = await _authorizationService.HasPluginAccessAsync(
context.ExecutionContext.UserId,
context.Proxy.Metadata.PluginName);
if (!hasPluginAccess)
{
context.Exception = new UnauthorizedAccessException(
$"User does not have access to plugin {context.Proxy.Metadata.PluginName}");
return context;
}
// Check operation-level authorization
if (context.Parameters.TryGetValue("PropertyName", out var propertyName))
{
var hasPropertyAccess = await _authorizationService.HasPropertyAccessAsync(
context.ExecutionContext.UserId,
context.Proxy.Metadata.PluginName,
propertyName.ToString(),
context.Operation);
if (!hasPropertyAccess)
{
context.Exception = new UnauthorizedAccessException(
$"User does not have {context.Operation} access to property {propertyName}");
return context;
}
}
return await next();
}
}
// Validation Middleware
public class ValidationMiddleware : IMiddleware<IPluginContext>
{
private readonly IValidationService _validationService;
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
// Only validate write operations
if (context.Operation != "SetProperty")
{
return await next();
}
var propertyName = context.Parameters["PropertyName"].ToString();
var value = context.Parameters["Value"];
var validationResult = await _validationService.ValidatePropertyAsync(
context.Proxy.Metadata.PluginName,
propertyName,
value,
context.ExecutionContext);
if (!validationResult.IsValid)
{
context.Exception = new ValidationException(
$"Validation failed for {propertyName}: {string.Join(", ", validationResult.Errors)}");
return context;
}
return await next();
}
}
// Auditing Middleware
public class AuditingMiddleware : IMiddleware<IPluginContext>
{
private readonly IAuditService _auditService;
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
object oldValue = null;
// Capture old value for audit trail on property changes
if (context.Operation == "SetProperty")
{
var propertyName = context.Parameters["PropertyName"].ToString();
oldValue = await context.Proxy.GetPropertyValueAsync(propertyName);
}
var result = await next();
// Record audit entry if operation was successful and auditable
if (result.Exception == null && ShouldAudit(context))
{
await RecordAuditEntry(context, oldValue, result);
}
return result;
}
private async Task RecordAuditEntry(IPluginContext context, object oldValue, IPluginContext result)
{
var auditEntry = new AuditEntry
{
Timestamp = DateTime.UtcNow,
UserId = context.ExecutionContext.UserId,
TenantId = context.ExecutionContext.TenantId,
PluginName = context.Proxy.Metadata.PluginName,
Operation = context.Operation,
PropertyName = context.Parameters.GetValueOrDefault("PropertyName")?.ToString(),
OldValue = SerializeForAudit(oldValue),
NewValue = SerializeForAudit(context.Parameters.GetValueOrDefault("Value")),
Success = true
};
await _auditService.RecordAsync(auditEntry);
}
}
public class MiddlewareEnabledViewModelProxy : IViewModelProxy
{
private readonly IViewModelProxy _innerProxy;
private readonly PluginMiddlewarePipeline _pipeline;
public MiddlewareEnabledViewModelProxy(IViewModelProxy innerProxy, PluginMiddlewarePipeline pipeline)
{
_innerProxy = innerProxy;
_pipeline = pipeline;
}
public async Task<object> GetPropertyValueAsync(string propertyName)
{
var context = new PluginContext
{
Proxy = this,
Operation = "GetProperty",
Parameters = new Dictionary<string, object> { ["PropertyName"] = propertyName },
Results = new Dictionary<string, object>(),
ExecutionContext = _innerProxy.ExecutionContext
};
// Execute through middleware pipeline
var result = await _pipeline.ExecuteAsync(context);
if (result.Exception != null)
{
throw result.Exception;
}
// If middleware provided the result (e.g., from cache), return it
if (result.Results.ContainsKey("Value"))
{
return result.Results["Value"];
}
// Otherwise, call the actual implementation
var value = await _innerProxy.GetPropertyValueAsync(propertyName);
result.Results["Value"] = value;
return value;
}
public async Task SetPropertyValueAsync(string propertyName, object value)
{
var context = new PluginContext
{
Proxy = this,
Operation = "SetProperty",
Parameters = new Dictionary<string, object>
{
["PropertyName"] = propertyName,
["Value"] = value
},
Results = new Dictionary<string, object>(),
ExecutionContext = _innerProxy.ExecutionContext
};
var result = await _pipeline.ExecuteAsync(context);
if (result.Exception != null)
{
throw result.Exception;
}
// If no middleware handled it, call the actual implementation
if (!result.IsHandled)
{
await _innerProxy.SetPropertyValueAsync(propertyName, value);
}
}
// Delegate other methods to inner proxy
public PluginMetadata Metadata => _innerProxy.Metadata;
public IExecutionContext ExecutionContext => _innerProxy.ExecutionContext;
}
public class MiddlewarePipelineBuilder
{
private readonly PluginMiddlewarePipeline _pipeline = new();
private readonly IServiceProvider _serviceProvider;
public MiddlewarePipelineBuilder(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public MiddlewarePipelineBuilder UseLogging()
{
_pipeline.Use(_serviceProvider.GetRequiredService<LoggingMiddleware>());
return this;
}
public MiddlewarePipelineBuilder UseMetrics()
{
_pipeline.Use(_serviceProvider.GetRequiredService<MetricsMiddleware>());
return this;
}
public MiddlewarePipelineBuilder UseCaching()
{
_pipeline.Use(_serviceProvider.GetRequiredService<CachingMiddleware>());
return this;
}
public MiddlewarePipelineBuilder UseAuthorization()
{
_pipeline.Use(_serviceProvider.GetRequiredService<AuthorizationMiddleware>());
return this;
}
public MiddlewarePipelineBuilder UseValidation()
{
_pipeline.Use(_serviceProvider.GetRequiredService<ValidationMiddleware>());
return this;
}
public MiddlewarePipelineBuilder UseAuditing()
{
_pipeline.Use(_serviceProvider.GetRequiredService<AuditingMiddleware>());
return this;
}
public MiddlewarePipelineBuilder UseCustom<T>() where T : class, IMiddleware<IPluginContext>
{
_pipeline.Use(_serviceProvider.GetRequiredService<T>());
return this;
}
public MiddlewarePipelineBuilder UseCustom(Func<IPluginContext, Func<Task<IPluginContext>>, Task<IPluginContext>> middleware)
{
_pipeline.Use(middleware);
return this;
}
public PluginMiddlewarePipeline Build() => _pipeline;
}
// Configure middleware pipeline
var pipeline = new MiddlewarePipelineBuilder(serviceProvider)
.UseLogging() // Log all operations
.UseMetrics() // Collect performance metrics
.UseAuthorization() // Check permissions
.UseValidation() // Validate inputs
.UseCaching() // Cache results
.UseAuditing() // Record audit trail
.UseCustom(async (context, next) =>
{
// Custom inline middleware
Console.WriteLine($"Custom middleware: {context.Operation}");
return await next();
})
.Build();
// Create plugin with middleware pipeline
var baseProxy = await engine.CreateBaseViewModelAsync<OrderPlugin>();
var middlewareEnabledProxy = new MiddlewareEnabledViewModelProxy(baseProxy, pipeline);
// Now all operations flow through the middleware pipeline
await middlewareEnabledProxy.SetPropertyValueAsync("CustomerName", "John Doe");
// Flows through: Logging -> Metrics -> Authorization -> Validation -> Caching -> Auditing -> Actual Operation
var customerName = await middlewareEnabledProxy.GetPropertyValueAsync("CustomerName");
// Flows through: Logging -> Metrics -> Authorization -> Caching (might return from cache) -> Actual Operation
// Development pipeline - minimal overhead
var devPipeline = new MiddlewarePipelineBuilder(serviceProvider)
.UseLogging()
.UseValidation()
.Build();
// Production pipeline - full enterprise features
var prodPipeline = new MiddlewarePipelineBuilder(serviceProvider)
.UseLogging()
.UseMetrics()
.UseAuthorization()
.UseValidation()
.UseCaching()
.UseAuditing()
.UseCustom<RateLimitingMiddleware>()
.UseCustom<CircuitBreakerMiddleware>()
.Build();
// Testing pipeline - no external dependencies
var testPipeline = new MiddlewarePipelineBuilder(serviceProvider)
.UseValidation()
.Build();
This middleware approach gives you:
Complete Separation - Plugins remain 100% naive POCOs
Composable Cross-Cutting Concerns - Mix and match middleware as needed
Environment-Specific Pipelines - Different middleware for dev/test/prod
Extensibility - Easy to add custom middleware
Performance Control - Only pay for middleware you use
Testability - Mock or exclude middleware during testing
Order Independence - Middleware order can be configured per environment
The plugin developer writes pure business logic, and the middleware pipeline handles all infrastructure concerns transparently. Perfect separation of concerns! CH maybe also event middleware for action history
Brilliant addition! Event middleware can capture a rich action history and enable event sourcing, undo/redo functionality, and comprehensive audit trails. Here’s how to implement event-driven middleware:
public interface IPluginEvent
{
string EventId { get; }
DateTime Timestamp { get; }
string EventType { get; }
string PluginName { get; }
string UserId { get; }
string TenantId { get; }
Dictionary<string, object> Data { get; }
Dictionary<string, object> Metadata { get; }
}
public class PluginEvent : IPluginEvent
{
public string EventId { get; set; } = Guid.NewGuid().ToString();
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string EventType { get; set; }
public string PluginName { get; set; }
public string UserId { get; set; }
public string TenantId { get; set; }
public Dictionary<string, object> Data { get; set; } = new();
public Dictionary<string, object> Metadata { get; set; } = new();
}
// Specific event types
public class PropertyChangedEvent : PluginEvent
{
public string PropertyName { get; set; }
public object OldValue { get; set; }
public object NewValue { get; set; }
public PropertyChangedEvent()
{
EventType = "PropertyChanged";
}
}
public class StateTransitionEvent : PluginEvent
{
public object FromState { get; set; }
public object ToState { get; set; }
public string TransitionReason { get; set; }
public StateTransitionEvent()
{
EventType = "StateTransition";
}
}
public class CascadingDataLoadedEvent : PluginEvent
{
public string PropertyName { get; set; }
public string DataSourceKey { get; set; }
public int ItemCount { get; set; }
public Dictionary<string, object> ContextParameters { get; set; }
public CascadingDataLoadedEvent()
{
EventType = "CascadingDataLoaded";
}
}
public class ValidationFailedEvent : PluginEvent
{
public string PropertyName { get; set; }
public object AttemptedValue { get; set; }
public string[] ValidationErrors { get; set; }
public ValidationFailedEvent()
{
EventType = "ValidationFailed";
}
}
public class EventMiddleware : IMiddleware<IPluginContext>
{
private readonly IEventStore _eventStore;
private readonly IEventPublisher _eventPublisher;
public EventMiddleware(IEventStore eventStore, IEventPublisher eventPublisher)
{
_eventStore = eventStore;
_eventPublisher = eventPublisher;
}
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
// Capture pre-operation state for events
var preOperationState = await CapturePreOperationState(context);
var result = await next();
// Generate and store events based on what happened
var events = await GenerateEvents(context, result, preOperationState);
foreach (var eventObj in events)
{
// Store event for history/replay
await _eventStore.StoreAsync(eventObj);
// Publish event for real-time consumers
await _eventPublisher.PublishAsync(eventObj);
// Add to context for other middleware
if (!result.Results.ContainsKey("Events"))
{
result.Results["Events"] = new List<IPluginEvent>();
}
((List<IPluginEvent>)result.Results["Events"]).Add(eventObj);
}
return result;
}
private async Task<Dictionary<string, object>> CapturePreOperationState(IPluginContext context)
{
var state = new Dictionary<string, object>();
if (context.Operation == "SetProperty")
{
var propertyName = context.Parameters["PropertyName"].ToString();
try
{
var oldValue = await context.Proxy.GetPropertyValueAsync(propertyName);
state["OldValue"] = oldValue;
state["PropertyName"] = propertyName;
}
catch
{
// Property might not exist yet or might not be accessible
state["OldValue"] = null;
}
}
else if (context.Operation == "StateTransition")
{
state["OldState"] = context.Parameters.GetValueOrDefault("CurrentState");
}
return state;
}
private async Task<List<IPluginEvent>> GenerateEvents(
IPluginContext context,
IPluginContext result,
Dictionary<string, object> preState)
{
var events = new List<IPluginEvent>();
// Property change events
if (context.Operation == "SetProperty" && result.Exception == null)
{
events.Add(new PropertyChangedEvent
{
PluginName = context.Proxy.Metadata.PluginName,
UserId = context.ExecutionContext.UserId,
TenantId = context.ExecutionContext.TenantId,
PropertyName = context.Parameters["PropertyName"].ToString(),
OldValue = preState.GetValueOrDefault("OldValue"),
NewValue = context.Parameters["Value"],
Data = new Dictionary<string, object>
{
["operation"] = context.Operation,
["success"] = true
},
Metadata = new Dictionary<string, object>
{
["userAgent"] = context.ExecutionContext.CustomProperties.GetValueOrDefault("UserAgent"),
["ipAddress"] = context.ExecutionContext.CustomProperties.GetValueOrDefault("IpAddress"),
["correlationId"] = context.Parameters.GetValueOrDefault("CorrelationId")
}
});
}
// State transition events
if (context.Operation == "StateTransition" && result.Exception == null)
{
events.Add(new StateTransitionEvent
{
PluginName = context.Proxy.Metadata.PluginName,
UserId = context.ExecutionContext.UserId,
TenantId = context.ExecutionContext.TenantId,
FromState = preState.GetValueOrDefault("OldState"),
ToState = context.Parameters["NewState"],
TransitionReason = context.Parameters.GetValueOrDefault("Reason")?.ToString(),
Data = new Dictionary<string, object>
{
["transitionValid"] = true,
["affectedProperties"] = result.Results.GetValueOrDefault("ClearedProperties"),
["enabledProperties"] = result.Results.GetValueOrDefault("EnabledProperties")
}
});
}
// Cascading data loaded events
if (result.Results.ContainsKey("NewDataSources"))
{
var dataSources = (Dictionary<string, IEnumerable<object>>)result.Results["NewDataSources"];
foreach (var dataSource in dataSources)
{
events.Add(new CascadingDataLoadedEvent
{
PluginName = context.Proxy.Metadata.PluginName,
UserId = context.ExecutionContext.UserId,
TenantId = context.ExecutionContext.TenantId,
PropertyName = dataSource.Key,
DataSourceKey = GetDataSourceKey(context, dataSource.Key),
ItemCount = dataSource.Value?.Count() ?? 0,
ContextParameters = ExtractContextParameters(context)
});
}
}
// Validation failure events
if (result.Exception is ValidationException validationEx)
{
events.Add(new ValidationFailedEvent
{
PluginName = context.Proxy.Metadata.PluginName,
UserId = context.ExecutionContext.UserId,
TenantId = context.ExecutionContext.TenantId,
PropertyName = context.Parameters.GetValueOrDefault("PropertyName")?.ToString(),
AttemptedValue = context.Parameters.GetValueOrDefault("Value"),
ValidationErrors = ExtractValidationErrors(validationEx),
Data = new Dictionary<string, object>
{
["validationRule"] = ExtractValidationRule(context),
["attemptedOperation"] = context.Operation
}
});
}
return events;
}
}
public class ActionHistoryMiddleware : IMiddleware<IPluginContext>
{
private readonly IActionHistoryService _actionHistoryService;
public ActionHistoryMiddleware(IActionHistoryService actionHistoryService)
{
_actionHistoryService = actionHistoryService;
}
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
var actionEntry = new ActionHistoryEntry
{
ActionId = Guid.NewGuid().ToString(),
Timestamp = DateTime.UtcNow,
PluginName = context.Proxy.Metadata.PluginName,
Operation = context.Operation,
UserId = context.ExecutionContext.UserId,
TenantId = context.ExecutionContext.TenantId,
Parameters = new Dictionary<string, object>(context.Parameters),
SessionId = context.ExecutionContext.CustomProperties.GetValueOrDefault("SessionId")?.ToString()
};
try
{
var result = await next();
// Record successful action
actionEntry.Success = result.Exception == null;
actionEntry.Results = new Dictionary<string, object>(result.Results);
actionEntry.Duration = TimeSpan.FromMilliseconds(
(DateTime.UtcNow - actionEntry.Timestamp).TotalMilliseconds);
if (result.Exception != null)
{
actionEntry.ErrorMessage = result.Exception.Message;
actionEntry.ErrorType = result.Exception.GetType().Name;
}
// Store action in history
await _actionHistoryService.RecordActionAsync(actionEntry);
// Add action ID to results for potential undo operations
result.Results["ActionId"] = actionEntry.ActionId;
return result;
}
catch (Exception ex)
{
actionEntry.Success = false;
actionEntry.ErrorMessage = ex.Message;
actionEntry.ErrorType = ex.GetType().Name;
actionEntry.Duration = TimeSpan.FromMilliseconds(
(DateTime.UtcNow - actionEntry.Timestamp).TotalMilliseconds);
await _actionHistoryService.RecordActionAsync(actionEntry);
throw;
}
}
}
public class ActionHistoryEntry
{
public string ActionId { get; set; }
public DateTime Timestamp { get; set; }
public string PluginName { get; set; }
public string Operation { get; set; }
public string UserId { get; set; }
public string TenantId { get; set; }
public string SessionId { get; set; }
public Dictionary<string, object> Parameters { get; set; } = new();
public Dictionary<string, object> Results { get; set; } = new();
public bool Success { get; set; }
public string ErrorMessage { get; set; }
public string ErrorType { get; set; }
public TimeSpan Duration { get; set; }
public Dictionary<string, object> Context { get; set; } = new();
}
public class UndoRedoMiddleware : IMiddleware<IPluginContext>
{
private readonly IUndoRedoService _undoRedoService;
public UndoRedoMiddleware(IUndoRedoService undoRedoService)
{
_undoRedoService = undoRedoService;
}
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
// Handle undo/redo operations
if (context.Operation == "Undo")
{
return await HandleUndo(context);
}
if (context.Operation == "Redo")
{
return await HandleRedo(context);
}
// For regular operations, capture undo information
var undoInfo = await CaptureUndoInformation(context);
var result = await next();
// If operation was successful and undoable, store undo command
if (result.Exception == null && IsUndoableOperation(context.Operation))
{
var undoCommand = CreateUndoCommand(context, result, undoInfo);
await _undoRedoService.PushUndoCommandAsync(
context.ExecutionContext.UserId,
context.ExecutionContext.TenantId,
context.Proxy.Metadata.PluginName,
undoCommand);
result.Results["CanUndo"] = true;
result.Results["UndoCommandId"] = undoCommand.CommandId;
}
return result;
}
private async Task<IPluginContext> HandleUndo(IPluginContext context)
{
var undoCommand = await _undoRedoService.PopUndoCommandAsync(
context.ExecutionContext.UserId,
context.ExecutionContext.TenantId,
context.Proxy.Metadata.PluginName);
if (undoCommand == null)
{
context.Results["UndoResult"] = "Nothing to undo";
return context;
}
// Execute undo command
await ExecuteUndoCommand(context, undoCommand);
// Push to redo stack
await _undoRedoService.PushRedoCommandAsync(
context.ExecutionContext.UserId,
context.ExecutionContext.TenantId,
context.Proxy.Metadata.PluginName,
CreateRedoCommand(undoCommand));
context.Results["UndoResult"] = "Undo successful";
context.Results["UndoCommandType"] = undoCommand.OperationType;
return context;
}
private async Task ExecuteUndoCommand(IPluginContext context, UndoCommand undoCommand)
{
switch (undoCommand.OperationType)
{
case "SetProperty":
await context.Proxy.SetPropertyValueAsync(
undoCommand.PropertyName,
undoCommand.OldValue);
break;
case "StateTransition":
// Restore previous state
var stateMachine = context.Proxy.GetStateMachine();
await stateMachine.TransitionAsync(undoCommand.OldState, context);
break;
case "CascadingChange":
// Restore multiple property values
foreach (var property in undoCommand.PropertyValues)
{
await context.Proxy.SetPropertyValueAsync(property.Key, property.Value);
}
break;
}
}
}
public class UndoCommand
{
public string CommandId { get; set; } = Guid.NewGuid().ToString();
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string OperationType { get; set; }
public string PropertyName { get; set; }
public object OldValue { get; set; }
public object NewValue { get; set; }
public object OldState { get; set; }
public object NewState { get; set; }
public Dictionary<string, object> PropertyValues { get; set; } = new();
public Dictionary<string, object> Context { get; set; } = new();
}
public interface IActionHistoryService
{
Task<ActionHistoryEntry[]> GetActionHistoryAsync(string userId, string tenantId, string pluginName = null, int limit = 100);
Task<ActionHistoryEntry[]> GetSessionHistoryAsync(string sessionId, int limit = 100);
Task<ActionStatistics> GetActionStatisticsAsync(string userId, string tenantId, TimeSpan period);
Task<ActionHistoryEntry> GetActionAsync(string actionId);
Task RecordActionAsync(ActionHistoryEntry action);
Task ReplayActionsAsync(string userId, string tenantId, string pluginName, DateTime fromTimestamp);
}
public class ActionHistoryService : IActionHistoryService
{
private readonly IEventStore _eventStore;
private readonly IActionHistoryRepository _repository;
public async Task<ActionHistoryEntry[]> GetActionHistoryAsync(string userId, string tenantId, string pluginName = null, int limit = 100)
{
return await _repository.GetActionsAsync(new ActionHistoryQuery
{
UserId = userId,
TenantId = tenantId,
PluginName = pluginName,
Limit = limit,
OrderBy = "Timestamp DESC"
});
}
public async Task<ActionStatistics> GetActionStatisticsAsync(string userId, string tenantId, TimeSpan period)
{
var fromDate = DateTime.UtcNow.Subtract(period);
var actions = await _repository.GetActionsAsync(new ActionHistoryQuery
{
UserId = userId,
TenantId = tenantId,
FromTimestamp = fromDate
});
return new ActionStatistics
{
TotalActions = actions.Length,
SuccessfulActions = actions.Count(a => a.Success),
FailedActions = actions.Count(a => !a.Success),
AverageActionDuration = actions.Average(a => a.Duration.TotalMilliseconds),
MostUsedOperations = actions.GroupBy(a => a.Operation)
.OrderByDescending(g => g.Count())
.Take(10)
.ToDictionary(g => g.Key, g => g.Count()),
ActionsByPlugin = actions.GroupBy(a => a.PluginName)
.ToDictionary(g => g.Key, g => g.Count())
};
}
public async Task ReplayActionsAsync(string userId, string tenantId, string pluginName, DateTime fromTimestamp)
{
var events = await _eventStore.GetEventsAsync(new EventQuery
{
UserId = userId,
TenantId = tenantId,
PluginName = pluginName,
FromTimestamp = fromTimestamp,
EventTypes = new[] { "PropertyChanged", "StateTransition", "CascadingDataLoaded" }
});
// Replay events in chronological order
foreach (var eventObj in events.OrderBy(e => e.Timestamp))
{
await ReplayEvent(eventObj);
}
}
}
var pipeline = new MiddlewarePipelineBuilder(serviceProvider)
.UseLogging()
.UseMetrics()
.UseAuthorization()
.UseValidation()
.UseCustom<EventMiddleware>() // Capture all events
.UseCustom<ActionHistoryMiddleware>() // Record detailed action history
.UseCustom<UndoRedoMiddleware>() // Enable undo/redo functionality
.UseCaching()
.UseAuditing()
.Build();
// Usage
var proxy = new MiddlewareEnabledViewModelProxy(baseProxy, pipeline);
// All actions now generate events and history
await proxy.SetPropertyValueAsync("CustomerName", "John Doe");
// Get action history
var history = await actionHistoryService.GetActionHistoryAsync(userId, tenantId, "OrderPlugin");
// Undo last action
await proxy.ExecuteOperationAsync("Undo");
// Get events for analysis
var events = await eventStore.GetEventsAsync(new EventQuery
{
UserId = userId,
EventTypes = new[] { "PropertyChanged" },
FromTimestamp = DateTime.Today
});
This event middleware system provides:
Complete Action History - Every operation tracked with full context
Event Sourcing Capability - Replay system state from events
Undo/Redo Functionality - User-friendly operation reversal
Rich Analytics - Action patterns, performance metrics, usage statistics
Audit Trail - Comprehensive record of who did what when
Debugging Support - Replay issues and analyze event sequences
Real-time Notifications - Publish events to external systems
The plugin remains naive - it doesn’t know about events, history, or undo functionality. The middleware handles all event generation and management transparently! CH that’s a great observation, the need for event driven storage that can rebuild if the logic changes. the plugins again act as code as configuration and when changed could be used to rerun data at checkpoints
Absolutely brilliant insight! You’ve hit on one of the most powerful aspects of this architecture - event sourcing with code-as-configuration replay. When plugin logic changes, you can replay events through the new logic to rebuild state, making the system incredibly resilient to business rule evolution.
public interface IEventSourcingEngine
{
Task<TPlugin> ReplayToCurrentAsync<TPlugin>(string aggregateId, string userId, string tenantId)
where TPlugin : class, new();
Task<TPlugin> ReplayToCheckpointAsync<TPlugin>(string aggregateId, string checkpointId)
where TPlugin : class, new();
Task<TPlugin> ReplayToVersionAsync<TPlugin>(string aggregateId, string pluginVersion)
where TPlugin : class, new();
Task<TPlugin> ReplayWithNewLogicAsync<TPlugin>(string aggregateId, TPlugin newPluginLogic)
where TPlugin : class, new();
Task CreateCheckpointAsync(string aggregateId, string checkpointId, object currentState);
Task<IEnumerable<string>> GetAvailableCheckpointsAsync(string aggregateId);
Task InvalidateCheckpointsAsync(string pluginName, string fromVersion);
}
public class EventSourcingEngine : IEventSourcingEngine
{
private readonly IEventStore _eventStore;
private readonly ICheckpointStore _checkpointStore;
private readonly IPluginVersionManager _pluginVersionManager;
private readonly IPluginEngine _pluginEngine;
public async Task<TPlugin> ReplayWithNewLogicAsync<TPlugin>(string aggregateId, TPlugin newPluginLogic)
where TPlugin : class, new()
{
// Get all events for this aggregate
var events = await _eventStore.GetEventsAsync(aggregateId);
// Create a fresh instance with new logic
var proxy = await _pluginEngine.CreateViewModelAsync(newPluginLogic);
// Replay events through the new plugin logic
foreach (var eventObj in events.OrderBy(e => e.Timestamp))
{
await ReplayEventWithNewLogic(proxy, eventObj);
}
return proxy.GetPluginInstance<TPlugin>();
}
public async Task<TPlugin> ReplayToCheckpointAsync<TPlugin>(string aggregateId, string checkpointId)
where TPlugin : class, new()
{
// Load state from checkpoint
var checkpoint = await _checkpointStore.LoadCheckpointAsync(aggregateId, checkpointId);
if (checkpoint == null)
{
throw new CheckpointNotFoundException($"Checkpoint {checkpointId} not found for aggregate {aggregateId}");
}
// Deserialize state to plugin instance
var pluginInstance = checkpoint.DeserializeState<TPlugin>();
var proxy = await _pluginEngine.CreateViewModelAsync(pluginInstance);
// Replay events since checkpoint
var eventsSinceCheckpoint = await _eventStore.GetEventsAsync(aggregateId,
fromTimestamp: checkpoint.Timestamp);
foreach (var eventObj in eventsSinceCheckpoint.OrderBy(e => e.Timestamp))
{
await ReplayEvent(proxy, eventObj);
}
return proxy.GetPluginInstance<TPlugin>();
}
private async Task ReplayEventWithNewLogic(IViewModelProxy proxy, IPluginEvent eventObj)
{
switch (eventObj.EventType)
{
case "PropertyChanged":
var propEvent = (PropertyChangedEvent)eventObj;
// Apply through new plugin logic - might trigger different cascading rules
await proxy.SetPropertyValueAsync(propEvent.PropertyName, propEvent.NewValue);
break;
case "StateTransition":
var stateEvent = (StateTransitionEvent)eventObj;
// New state machine logic might have different transition rules
var stateMachine = proxy.GetStateMachine();
if (stateMachine.CanTransition(stateEvent.FromState, stateEvent.ToState))
{
await stateMachine.TransitionAsync(stateEvent.ToState, proxy);
}
else
{
// Log that transition is no longer valid with new logic
await LogInvalidTransition(stateEvent, proxy.Metadata.PluginVersion);
}
break;
case "CascadingDataLoaded":
var cascadeEvent = (CascadingDataLoadedEvent)eventObj;
// New cascading logic might load different data
await ReapplyCascadingLogic(proxy, cascadeEvent);
break;
}
}
}
public interface ICheckpointStore
{
Task<Checkpoint> LoadCheckpointAsync(string aggregateId, string checkpointId);
Task SaveCheckpointAsync(string aggregateId, string checkpointId, object state, Dictionary<string, object> metadata = null);
Task<IEnumerable<Checkpoint>> GetCheckpointsAsync(string aggregateId);
Task InvalidateCheckpointAsync(string aggregateId, string checkpointId);
Task InvalidateCheckpointsSinceAsync(string aggregateId, DateTime timestamp);
}
public class Checkpoint
{
public string CheckpointId { get; set; }
public string AggregateId { get; set; }
public DateTime Timestamp { get; set; }
public string PluginName { get; set; }
public string PluginVersion { get; set; }
public string SerializedState { get; set; }
public Dictionary<string, object> Metadata { get; set; } = new();
public long EventSequenceNumber { get; set; }
public T DeserializeState<T>() where T : class
{
return JsonSerializer.Deserialize<T>(SerializedState);
}
}
// Automatic checkpoint creation middleware
public class CheckpointMiddleware : IMiddleware<IPluginContext>
{
private readonly ICheckpointStore _checkpointStore;
private readonly CheckpointConfiguration _config;
public async Task<IPluginContext> ExecuteAsync(IPluginContext context, Func<Task<IPluginContext>> next)
{
var result = await next();
// Check if we should create a checkpoint
if (ShouldCreateCheckpoint(context, result))
{
var checkpointId = GenerateCheckpointId(context);
var state = await SerializeCurrentState(context.Proxy);
await _checkpointStore.SaveCheckpointAsync(
GetAggregateId(context),
checkpointId,
state,
new Dictionary<string, object>
{
["triggerOperation"] = context.Operation,
["pluginVersion"] = context.Proxy.Metadata.PluginVersion,
["eventCount"] = GetEventCountSinceLastCheckpoint(context)
});
result.Results["CheckpointCreated"] = checkpointId;
}
return result;
}
private bool ShouldCreateCheckpoint(IPluginContext context, IPluginContext result)
{
// Create checkpoint on significant operations
if (context.Operation == "StateTransition" && result.Exception == null)
return true;
// Create checkpoint every N events
if (GetEventCountSinceLastCheckpoint(context) >= _config.EventCountThreshold)
return true;
// Create checkpoint on time intervals
if (GetTimeSinceLastCheckpoint(context) >= _config.TimeThreshold)
return true;
return false;
}
}
public interface IPluginVersionManager
{
Task RegisterPluginVersionAsync<TPlugin>(TPlugin pluginInstance, string version) where TPlugin : class;
Task<TPlugin> GetPluginVersionAsync<TPlugin>(string version) where TPlugin : class, new();
Task<string[]> GetAvailableVersionsAsync<TPlugin>() where TPlugin : class;
Task<VersionMigrationPlan> CreateMigrationPlanAsync<TPlugin>(string fromVersion, string toVersion) where TPlugin : class;
Task ExecuteMigrationAsync<TPlugin>(string aggregateId, VersionMigrationPlan plan) where TPlugin : class, new();
}
public class VersionMigrationPlan
{
public string FromVersion { get; set; }
public string ToVersion { get; set; }
public List<MigrationStep> Steps { get; set; } = new();
public bool RequiresFullReplay { get; set; }
public string[] InvalidatedCheckpoints { get; set; }
public Dictionary<string, object> MigrationContext { get; set; } = new();
}
public class MigrationStep
{
public string StepType { get; set; } // "ReplayFromCheckpoint", "FullReplay", "DataTransform"
public string CheckpointId { get; set; }
public Dictionary<string, object> Parameters { get; set; } = new();
public Func<IViewModelProxy, Task> CustomMigrationLogic { get; set; }
}
// Example plugin evolution
[Plugin("order-management", Version = "1.0")]
public class OrderPluginV1
{
[Storage(Persistable = true)]
public string CustomerId { get; set; }
[CascadingSelection(DependsOn = nameof(CustomerId), DataSourceKey = "customer-addresses")]
[Storage(Persistable = true)]
public string ShippingAddress { get; set; }
}
[Plugin("order-management", Version = "2.0")]
public class OrderPluginV2
{
[Storage(Persistable = true)]
public string CustomerId { get; set; }
// NEW: Split address into components with different cascading rules
[CascadingSelection(DependsOn = nameof(CustomerId), DataSourceKey = "countries")]
[Storage(Persistable = true)]
public string ShippingCountry { get; set; }
[CascadingSelection(DependsOn = nameof(ShippingCountry), DataSourceKey = "states")]
[Storage(Persistable = true)]
public string ShippingState { get; set; }
[CascadingSelection(DependsOn = nameof(ShippingState), DataSourceKey = "cities")]
[Storage(Persistable = true)]
public string ShippingCity { get; set; }
// NEW: Enhanced state machine
[StateDependentAttribute(ValidStates = new[] { "Draft", "Submitted" })]
public OrderState Status { get; set; }
}
public class PluginMigrationService
{
private readonly IEventSourcingEngine _eventSourcingEngine;
private readonly IPluginVersionManager _versionManager;
private readonly ICheckpointStore _checkpointStore;
public async Task MigrateAggregateAsync<TOld, TNew>(string aggregateId, string reason = null)
where TOld : class, new()
where TNew : class, new()
{
var migrationId = Guid.NewGuid().ToString();
try
{
// Create pre-migration checkpoint
var currentState = await _eventSourcingEngine.ReplayToCurrentAsync<TOld>(aggregateId, null, null);
await _checkpointStore.SaveCheckpointAsync(aggregateId, $"pre-migration-{migrationId}", currentState);
// Get migration plan
var oldVersion = GetPluginVersion<TOld>();
var newVersion = GetPluginVersion<TNew>();
var migrationPlan = await _versionManager.CreateMigrationPlanAsync<TNew>(oldVersion, newVersion);
// Execute migration
var newPluginInstance = new TNew();
var migratedState = await _eventSourcingEngine.ReplayWithNewLogicAsync(aggregateId, newPluginInstance);
// Create post-migration checkpoint
await _checkpointStore.SaveCheckpointAsync(aggregateId, $"post-migration-{migrationId}", migratedState,
new Dictionary<string, object>
{
["migrationId"] = migrationId,
["fromVersion"] = oldVersion,
["toVersion"] = newVersion,
["migrationReason"] = reason,
["migratedAt"] = DateTime.UtcNow
});
// Invalidate old checkpoints if needed
if (migrationPlan.RequiresFullReplay)
{
await InvalidateOldCheckpoints(aggregateId, migrationId);
}
}
catch (Exception ex)
{
// Rollback on failure
await RollbackMigration(aggregateId, migrationId);
throw new MigrationFailedException($"Migration failed for aggregate {aggregateId}: {ex.Message}", ex);
}
}
public async Task<ValidationResult> ValidateMigrationAsync<TOld, TNew>(string aggregateId)
where TOld : class, new()
where TNew : class, new()
{
// Replay with old logic
var oldState = await _eventSourcingEngine.ReplayToCurrentAsync<TOld>(aggregateId, null, null);
// Replay with new logic
var newPluginInstance = new TNew();
var newState = await _eventSourcingEngine.ReplayWithNewLogicAsync(aggregateId, newPluginInstance);
// Compare results and identify differences
return CompareStates(oldState, newState);
}
private ValidationResult CompareStates<TOld, TNew>(TOld oldState, TNew newState)
{
var result = new ValidationResult { IsValid = true };
// Compare properties that exist in both versions
var oldProperties = typeof(TOld).GetProperties().ToDictionary(p => p.Name, p => p.GetValue(oldState));
var newProperties = typeof(TNew).GetProperties().ToDictionary(p => p.Name, p => p.GetValue(newState));
foreach (var oldProp in oldProperties)
{
if (newProperties.TryGetValue(oldProp.Key, out var newValue))
{
if (!Equals(oldProp.Value, newValue))
{
result.Warnings.Add($"Property {oldProp.Key} changed from {oldProp.Value} to {newValue}");
}
}
else
{
result.Warnings.Add($"Property {oldProp.Key} was removed in new version");
}
}
// Identify new properties
foreach (var newProp in newProperties.Where(p => !oldProperties.ContainsKey(p.Key)))
{
result.Warnings.Add($"New property {newProp.Key} added with value {newProp.Value}");
}
return result;
}
}
// Original plugin deployment
var orderV1 = new OrderPluginV1();
var proxy = await pluginEngine.CreateViewModelAsync(orderV1);
await proxy.SetPropertyValueAsync("CustomerId", "CUST-001");
await proxy.SetPropertyValueAsync("ShippingAddress", "123 Main St");
// Events stored: PropertyChanged for CustomerId, PropertyChanged for ShippingAddress
// Business logic evolution - deploy new version
var migrationService = serviceProvider.GetService<PluginMigrationService>();
// Validate migration impact first
var validationResult = await migrationService.ValidateMigrationAsync<OrderPluginV1, OrderPluginV2>("order-123");
if (validationResult.HasWarnings)
{
Console.WriteLine($"Migration warnings: {string.Join(", ", validationResult.Warnings)}");
}
// Execute migration - replays events through new logic
await migrationService.MigrateAggregateAsync<OrderPluginV1, OrderPluginV2>("order-123",
"Enhanced address handling and state management");
// Result:
// - CustomerId remains "CUST-001"
// - ShippingAddress "123 Main St" gets parsed and triggers new cascading logic
// - ShippingCountry, ShippingState, ShippingCity get populated based on address parsing
// - New state machine gets initialized
// Rollback if needed
await eventSourcingEngine.ReplayToCheckpointAsync<OrderPluginV1>("order-123", "pre-migration-abc123");
// A/B testing - compare results
var stateWithOldLogic = await eventSourcingEngine.ReplayToCurrentAsync<OrderPluginV1>("order-123", userId, tenantId);
var stateWithNewLogic = await eventSourcingEngine.ReplayWithNewLogicAsync("order-123", new OrderPluginV2());
This event sourcing approach provides:
Business Logic Evolution - Update plugin rules without data loss
Safe Migrations - Validate changes before deploying
Rollback Capability - Return to previous logic versions instantly
A/B Testing - Compare results of different plugin versions
Audit Trail - Complete history of logic changes and their impacts
Zero Downtime - Migrate data while system remains operational
Data Integrity - Events are immutable, state can always be rebuilt
The plugin remains naive code-as-configuration, but the system becomes incredibly resilient to business rule changes over time. You can evolve your business logic while maintaining complete data history and rollback capability! CH also the need for plugin registry
Absolutely essential! A plugin registry is the backbone of this system - managing versioning, discovery, deployment, dependencies, and lifecycle. Here’s a comprehensive plugin registry architecture:
public interface IPluginRegistry
{
Task<PluginRegistrationResult> RegisterPluginAsync<T>(T pluginInstance, PluginMetadata metadata) where T : class;
Task<PluginDescriptor> GetPluginAsync(string pluginName, string version = "latest");
Task<PluginDescriptor[]> DiscoverPluginsAsync(PluginDiscoveryQuery query);
Task<bool> UnregisterPluginAsync(string pluginName, string version);
Task<PluginInstance<T>> CreatePluginInstanceAsync<T>(string pluginName, string version = "latest") where T : class, new();
Task<PluginValidationResult> ValidatePluginAsync<T>(T pluginInstance) where T : class;
Task<PluginDependencyGraph> GetDependencyGraphAsync(string pluginName);
Task<PluginCompatibilityReport> CheckCompatibilityAsync(string pluginName, string targetVersion);
}
public class PluginRegistry : IPluginRegistry
{
private readonly IPluginStore _pluginStore;
private readonly IPluginVersionManager _versionManager;
private readonly IPluginValidationService _validationService;
private readonly IDependencyResolver _dependencyResolver;
private readonly IEventPublisher _eventPublisher;
public async Task<PluginRegistrationResult> RegisterPluginAsync<T>(T pluginInstance, PluginMetadata metadata)
where T : class
{
var result = new PluginRegistrationResult();
try
{
// Extract plugin metadata from attributes
var extractedMetadata = ExtractPluginMetadata<T>(pluginInstance);
var finalMetadata = MergeMetadata(extractedMetadata, metadata);
// Validate plugin
var validationResult = await _validationService.ValidatePluginAsync(pluginInstance);
if (!validationResult.IsValid)
{
result.IsSuccess = false;
result.Errors = validationResult.Errors;
return result;
}
// Check dependencies
var dependencyCheck = await _dependencyResolver.ValidateDependenciesAsync(finalMetadata.Dependencies);
if (!dependencyCheck.AllDependenciesResolved)
{
result.IsSuccess = false;
result.Errors.Add($"Unresolved dependencies: {string.Join(", ", dependencyCheck.MissingDependencies)}");
return result;
}
// Create plugin descriptor
var descriptor = new PluginDescriptor
{
PluginName = finalMetadata.Name,
Version = finalMetadata.Version,
Description = finalMetadata.Description,
Author = finalMetadata.Author,
AssemblyName = typeof(T).Assembly.GetName().Name,
TypeName = typeof(T).FullName,
Metadata = finalMetadata,
Dependencies = finalMetadata.Dependencies,
RegisteredAt = DateTime.UtcNow,
Status = PluginStatus.Active
};
// Store in registry
await _pluginStore.StorePluginAsync(descriptor);
// Register with version manager
await _versionManager.RegisterVersionAsync(finalMetadata.Name, finalMetadata.Version, descriptor);
// Publish registration event
await _eventPublisher.PublishAsync(new PluginRegisteredEvent
{
PluginName = finalMetadata.Name,
Version = finalMetadata.Version,
RegisteredBy = finalMetadata.Author,
Timestamp = DateTime.UtcNow
});
result.IsSuccess = true;
result.PluginDescriptor = descriptor;
result.RegistrationId = Guid.NewGuid().ToString();
return result;
}
catch (Exception ex)
{
result.IsSuccess = false;
result.Errors.Add($"Registration failed: {ex.Message}");
return result;
}
}
public async Task<PluginDescriptor[]> DiscoverPluginsAsync(PluginDiscoveryQuery query)
{
return await _pluginStore.QueryPluginsAsync(query);
}
public async Task<PluginInstance<T>> CreatePluginInstanceAsync<T>(string pluginName, string version = "latest")
where T : class, new()
{
var descriptor = await GetPluginAsync(pluginName, version);
if (descriptor == null)
{
throw new PluginNotFoundException($"Plugin {pluginName}:{version} not found");
}
// Resolve dependencies
var dependencyContext = await _dependencyResolver.ResolveDependenciesAsync(descriptor.Dependencies);
// Create instance
var instance = Activator.CreateInstance<T>();
// Apply dependency injection
await InjectDependencies(instance, dependencyContext);
return new PluginInstance<T>
{
Instance = instance,
Descriptor = descriptor,
DependencyContext = dependencyContext,
CreatedAt = DateTime.UtcNow
};
}
}
[AttributeUsage(AttributeTargets.Class)]
public class PluginAttribute : Attribute
{
public string Name { get; set; }
public string Version { get; set; }
public string Description { get; set; }
public string Author { get; set; }
public string[] Tags { get; set; } = new string[0];
public string[] Dependencies { get; set; } = new string[0];
public PluginLifecycle Lifecycle { get; set; } = PluginLifecycle.Transient;
public bool IsVisible { get; set; } = true;
}
[AttributeUsage(AttributeTargets.Class)]
public class PluginDependencyAttribute : Attribute
{
public string DependsOn { get; set; }
public string MinVersion { get; set; }
public string MaxVersion { get; set; }
public bool Optional { get; set; } = false;
}
public class PluginDescriptor
{
public string PluginName { get; set; }
public string Version { get; set; }
public string Description { get; set; }
public string Author { get; set; }
public string[] Tags { get; set; }
public string AssemblyName { get; set; }
public string TypeName { get; set; }
public PluginDependency[] Dependencies { get; set; }
public PluginMetadata Metadata { get; set; }
public DateTime RegisteredAt { get; set; }
public DateTime? LastUsed { get; set; }
public PluginStatus Status { get; set; }
public Dictionary<string, object> Configuration { get; set; } = new();
public PluginStatistics Statistics { get; set; } = new();
}
public class PluginDependency
{
public string Name { get; set; }
public string MinVersion { get; set; }
public string MaxVersion { get; set; }
public bool Optional { get; set; }
public DependencyType Type { get; set; } = DependencyType.Plugin;
}
public enum DependencyType
{
Plugin,
Service,
DataProvider,
ExternalApi
}
public enum PluginStatus
{
Active,
Deprecated,
Disabled,
Draft
}
// Example plugin with registry metadata
[Plugin(Name = "advanced-order-management",
Version = "2.1.0",
Description = "Advanced order management with multi-stage approval workflow",
Author = "Business Logic Team",
Tags = new[] { "order", "workflow", "approval", "enterprise" })]
[PluginDependency(DependsOn = "customer-service", MinVersion = "1.0.0")]
[PluginDependency(DependsOn = "inventory-service", MinVersion = "2.0.0", Optional = true)]
public class AdvancedOrderPlugin
{
[Storage(Persistable = true)]
[ContextAware(RequiredRoles = new[] { "OrderManager", "Admin" })]
public string OrderId { get; set; }
[StateMachine(StateEnumType = typeof(OrderState), StateProperty = nameof(Status))]
[Storage(Persistable = true)]
public OrderState Status { get; set; }
[CascadingSelection(DependsOn = nameof(CustomerId), DataSourceKey = "customer-credit-limits")]
[StateDependentAttribute(ValidStates = new[] { "Draft", "Submitted" })]
public decimal CreditLimit { get; set; }
}
public class PluginDiscoveryQuery
{
public string[] Names { get; set; }
public string[] Tags { get; set; }
public string[] Authors { get; set; }
public string MinVersion { get; set; }
public string MaxVersion { get; set; }
public PluginStatus[] Status { get; set; }
public DateTime? RegisteredAfter { get; set; }
public DateTime? RegisteredBefore { get; set; }
public bool IncludeDeprecated { get; set; } = false;
public string SearchText { get; set; }
public int Skip { get; set; } = 0;
public int Take { get; set; } = 100;
public string OrderBy { get; set; } = "RegisteredAt";
}
public class PluginDiscoveryService : IPluginDiscoveryService
{
private readonly IPluginRegistry _registry;
public async Task<PluginDescriptor[]> FindPluginsByTagAsync(params string[] tags)
{
return await _registry.DiscoverPluginsAsync(new PluginDiscoveryQuery
{
Tags = tags,
Status = new[] { PluginStatus.Active }
});
}
public async Task<PluginDescriptor[]> FindPluginsForBusinessDomainAsync(string domain)
{
return await _registry.DiscoverPluginsAsync(new PluginDiscoveryQuery
{
SearchText = domain,
Status = new[] { PluginStatus.Active },
OrderBy = "LastUsed DESC"
});
}
public async Task<PluginDescriptor[]> GetRecommendedPluginsAsync(string userId, string tenantId)
{
// AI-powered recommendations based on usage patterns
var userUsagePatterns = await GetUserUsagePatterns(userId);
var tenantPopularPlugins = await GetTenantPopularPlugins(tenantId);
return await _registry.DiscoverPluginsAsync(new PluginDiscoveryQuery
{
Tags = userUsagePatterns.PreferredTags,
Status = new[] { PluginStatus.Active },
OrderBy = "Statistics.UsageCount DESC"
});
}
}
public interface IPluginLifecycleManager
{
Task<PluginDeploymentResult> DeployPluginAsync<T>(T pluginInstance, DeploymentOptions options) where T : class;
Task<bool> RetirePluginAsync(string pluginName, string version, RetirementPlan plan);
Task<MigrationPlan> CreateMigrationPlanAsync(string pluginName, string fromVersion, string toVersion);
Task<PluginHealthReport> HealthCheckAsync(string pluginName, string version = null);
Task<PluginUsageReport> GetUsageReportAsync(string pluginName, TimeSpan period);
}
public class PluginLifecycleManager : IPluginLifecycleManager
{
private readonly IPluginRegistry _registry;
private readonly IEventSourcingEngine _eventSourcingEngine;
private readonly IPluginMigrationService _migrationService;
public async Task<PluginDeploymentResult> DeployPluginAsync<T>(T pluginInstance, DeploymentOptions options)
where T : class
{
var result = new PluginDeploymentResult();
try
{
// Register plugin
var registrationResult = await _registry.RegisterPluginAsync(pluginInstance, options.Metadata);
if (!registrationResult.IsSuccess)
{
result.IsSuccess = false;
result.Errors = registrationResult.Errors;
return result;
}
// Handle deployment strategy
switch (options.Strategy)
{
case DeploymentStrategy.BlueGreen:
await ExecuteBlueGreenDeployment(pluginInstance, options);
break;
case DeploymentStrategy.Canary:
await ExecuteCanaryDeployment(pluginInstance, options);
break;
case DeploymentStrategy.RollingUpdate:
await ExecuteRollingUpdate(pluginInstance, options);
break;
case DeploymentStrategy.Immediate:
default:
await ExecuteImmediateDeployment(pluginInstance, options);
break;
}
result.IsSuccess = true;
result.DeploymentId = Guid.NewGuid().ToString();
return result;
}
catch (Exception ex)
{
result.IsSuccess = false;
result.Errors.Add($"Deployment failed: {ex.Message}");
return result;
}
}
public async Task<bool> RetirePluginAsync(string pluginName, string version, RetirementPlan plan)
{
// Find all aggregates using this plugin version
var affectedAggregates = await FindAffectedAggregates(pluginName, version);
// Migrate aggregates to newer versions
foreach (var aggregateId in affectedAggregates)
{
if (!string.IsNullOrEmpty(plan.MigrationTarget))
{
await _migrationService.MigrateAggregateAsync(aggregateId, plan.MigrationTarget);
}
}
// Mark plugin as deprecated
await _registry.UpdatePluginStatusAsync(pluginName, version, PluginStatus.Deprecated);
// Schedule for removal after grace period
if (plan.RemovalDate.HasValue)
{
await SchedulePluginRemoval(pluginName, version, plan.RemovalDate.Value);
}
return true;
}
}
public class DeploymentOptions
{
public PluginMetadata Metadata { get; set; }
public DeploymentStrategy Strategy { get; set; } = DeploymentStrategy.Immediate;
public Dictionary<string, object> Configuration { get; set; } = new();
public string[] TargetTenants { get; set; }
public double CanaryPercentage { get; set; } = 0.1;
public TimeSpan RolloutDuration { get; set; } = TimeSpan.FromHours(24);
}
public enum DeploymentStrategy
{
Immediate,
BlueGreen,
Canary,
RollingUpdate
}
public interface IPluginStore
{
Task StorePluginAsync(PluginDescriptor descriptor);
Task<PluginDescriptor> GetPluginAsync(string pluginName, string version);
Task<PluginDescriptor[]> QueryPluginsAsync(PluginDiscoveryQuery query);
Task UpdatePluginStatusAsync(string pluginName, string version, PluginStatus status);
Task<bool> DeletePluginAsync(string pluginName, string version);
Task UpdatePluginStatisticsAsync(string pluginName, string version, PluginStatistics statistics);
}
// SQL-based implementation
public class SqlPluginStore : IPluginStore
{
private readonly IDbConnection _connection;
public async Task<PluginDescriptor[]> QueryPluginsAsync(PluginDiscoveryQuery query)
{
var sql = BuildQuerySql(query);
var parameters = BuildQueryParameters(query);
var results = await _connection.QueryAsync<PluginDescriptor>(sql, parameters);
return results.ToArray();
}
private string BuildQuerySql(PluginDiscoveryQuery query)
{
var sql = new StringBuilder();
sql.AppendLine("SELECT * FROM Plugins WHERE 1=1");
if (query.Names?.Any() == true)
sql.AppendLine("AND PluginName IN @Names");
if (query.Tags?.Any() == true)
sql.AppendLine("AND EXISTS (SELECT 1 FROM PluginTags WHERE PluginId = Plugins.Id AND Tag IN @Tags)");
if (query.Status?.Any() == true)
sql.AppendLine("AND Status IN @Status");
if (!string.IsNullOrEmpty(query.SearchText))
sql.AppendLine("AND (PluginName LIKE @SearchPattern OR Description LIKE @SearchPattern)");
if (query.RegisteredAfter.HasValue)
sql.AppendLine("AND RegisteredAt >= @RegisteredAfter");
sql.AppendLine($"ORDER BY {query.OrderBy}");
sql.AppendLine($"OFFSET {query.Skip} ROWS FETCH NEXT {query.Take} ROWS ONLY");
return sql.ToString();
}
}
[ApiController]
[Route("api/plugins")]
public class PluginRegistryController : ControllerBase
{
private readonly IPluginRegistry _pluginRegistry;
private readonly IPluginLifecycleManager _lifecycleManager;
[HttpGet]
public async Task<IActionResult> DiscoverPlugins([FromQuery] PluginDiscoveryQuery query)
{
var plugins = await _pluginRegistry.DiscoverPluginsAsync(query);
return Ok(plugins);
}
[HttpGet("{pluginName}")]
public async Task<IActionResult> GetPlugin(string pluginName, [FromQuery] string version = "latest")
{
var plugin = await _pluginRegistry.GetPluginAsync(pluginName, version);
if (plugin == null)
return NotFound();
return Ok(plugin);
}
[HttpPost("{pluginName}/deploy")]
public async Task<IActionResult> DeployPlugin(string pluginName, [FromBody] DeploymentRequest request)
{
// Dynamic plugin loading and deployment
var assembly = await LoadPluginAssembly(request.AssemblyData);
var pluginType = assembly.GetType(request.TypeName);
var pluginInstance = Activator.CreateInstance(pluginType);
var result = await _lifecycleManager.DeployPluginAsync(pluginInstance, request.Options);
if (result.IsSuccess)
return Ok(result);
else
return BadRequest(result.Errors);
}
[HttpPost("{pluginName}/retire")]
public async Task<IActionResult> RetirePlugin(string pluginName, [FromBody] RetirementPlan plan)
{
var success = await _lifecycleManager.RetirePluginAsync(pluginName, plan.Version, plan);
return success ? Ok() : BadRequest("Retirement failed");
}
[HttpGet("{pluginName}/health")]
public async Task<IActionResult> HealthCheck(string pluginName, [FromQuery] string version = null)
{
var health = await _lifecycleManager.HealthCheckAsync(pluginName, version);
return Ok(health);
}
[HttpGet("{pluginName}/usage")]
public async Task<IActionResult> GetUsageReport(string pluginName, [FromQuery] int days = 30)
{
var report = await _lifecycleManager.GetUsageReportAsync(pluginName, TimeSpan.FromDays(days));
return Ok(report);
}
}
// Register a plugin
var orderPlugin = new AdvancedOrderPlugin();
var registrationResult = await pluginRegistry.RegisterPluginAsync(orderPlugin, new PluginMetadata
{
Name = "advanced-order-management",
Version = "2.1.0",
Description = "Advanced order management with approval workflow"
});
// Discover plugins by business domain
var orderPlugins = await discoveryService.FindPluginsByTagAsync("order", "workflow");
// Deploy with canary strategy
await lifecycleManager.DeployPluginAsync(orderPlugin, new DeploymentOptions
{
Strategy = DeploymentStrategy.Canary,
CanaryPercentage = 0.05, // 5% of traffic
TargetTenants = new[] { "enterprise-customers" }
});
// Create plugin instance at runtime
var pluginInstance = await pluginRegistry.CreatePluginInstanceAsync<AdvancedOrderPlugin>(
"advanced-order-management", "2.1.0");
// Get recommendations for user
var recommendedPlugins = await discoveryService.GetRecommendedPluginsAsync(userId, tenantId);
// Health check
var health = await lifecycleManager.HealthCheckAsync("advanced-order-management");
if (health.Status == HealthStatus.Degraded)
{
// Handle degraded plugin
}
The plugin registry provides:
Centralized Plugin Management - Single source of truth for all plugins
Version Management - Track multiple versions, dependencies, compatibility
Discovery and Search - Find plugins by tags, business domain, popularity
Lifecycle Management - Deploy, migrate, retire plugins safely
Health Monitoring - Track plugin performance and usage
Dependency Resolution - Automatic dependency injection and validation
Deployment Strategies - Blue/green, canary, rolling updates
Security and Access Control - Plugin-level permissions and visibility
This creates a robust ecosystem where naive plugin developers can focus on business logic while the registry handles all the enterprise infrastructure concerns! CH give me a bullet list of some of our discoveries and modules needed to accomplish this task. keep it ultra high level and brief
Plugin-as-Configuration Pattern - Naive .NET POCOs with declarative attributes become fully functional view models through engine transformation
Cross-Cutting Concerns Externalization - Middleware pipeline handles logging, caching, validation, security while plugins remain pure business logic
Event Sourcing with Logic Replay - When plugin logic changes, replay historical events through new rules to rebuild state at any point
Context-Aware Everything - User, tenant, location, security context flows through all operations automatically
Cascading Dependencies - Properties trigger dependent data loading and state changes declaratively
State Machine Integration - Business states control property visibility, validation rules, and available operations
Plugin Engine - Transforms POCOs into intelligent view models with middleware pipeline
Attribute Framework - Storage, Context, Cascading, State, Validation, Security attributes
Middleware Pipeline - Logging, metrics, caching, authorization, auditing, events, undo/redo
Event Sourcing Engine - Event store, checkpoint management, replay with new logic
Context Management - Execution context with user/tenant/location injection
Serialization Engine - Multiple consumers (storage, UI, API) with filtered data
State Machine Framework - Declarative workflows with context-aware transitions
Plugin Registry - Discovery, versioning, lifecycle, deployment, health monitoring
Cascading Manager - Dependent property updates with context-aware data loading
Migration Service - Safe plugin version upgrades with rollback capability
Naive developers write simple POCOs with attributes. System provides enterprise-grade capabilities through composition and middleware - zero infrastructure code in plugins. CH these plugins would need to support external services as dependencies, maybe via DI, thoughts
Absolutely! External service dependencies are crucial for real-world plugins. The key is keeping plugins naive while providing rich dependency injection capabilities. Here’s the approach:
// Plugin remains naive - just declares what it needs
[Plugin("order-management", Version = "1.0")]
[ServiceDependency("customer-service", Required = true)]
[ServiceDependency("inventory-service", Optional = true)]
[ServiceDependency("payment-gateway", Required = true, ConfigKey = "payment.provider")]
public class OrderPlugin
{
// Plugin doesn't know about DI - just declares dependencies
[InjectService]
public ICustomerService CustomerService { get; set; }
[InjectService(Optional = true)]
public IInventoryService InventoryService { get; set; }
[InjectService("payment-gateway")]
public IPaymentProcessor PaymentProcessor { get; set; }
[Storage(Persistable = true)]
public string CustomerId { get; set; }
// Plugin logic uses services naturally - no service location patterns
[ContextDependentData(DataSourceKey = "customer-orders", UseService = "customer-service")]
[CascadingSelection(DependsOn = nameof(CustomerId))]
public string[] CustomerOrderHistory { get; set; }
[Computed] // Calculated property using injected services
public decimal CustomerCreditLimit => CustomerService?.GetCreditLimit(CustomerId) ?? 0;
[Computed]
public bool HasInventory => InventoryService?.CheckAvailability(ProductId) ?? true;
}
public interface IServiceDependencyResolver
{
Task<ServiceResolutionResult> ResolveServicesAsync<T>(T pluginInstance) where T : class;
Task RegisterServiceProviderAsync(string serviceName, IServiceProvider provider);
Task RegisterServiceFactoryAsync<TService>(string serviceName, Func<IServiceProvider, TService> factory);
Task<bool> ValidateServiceDependenciesAsync<T>() where T : class;
}
public class ServiceDependencyResolver : IServiceDependencyResolver
{
private readonly Dictionary<string, IServiceProvider> _serviceProviders = new();
private readonly Dictionary<string, Func<IServiceProvider, object>> _serviceFactories = new();
private readonly IPluginContextProvider _contextProvider;
public async Task<ServiceResolutionResult> ResolveServicesAsync<T>(T pluginInstance) where T : class
{
var result = new ServiceResolutionResult();
var serviceProperties = GetServiceProperties<T>();
foreach (var prop in serviceProperties)
{
var injectAttr = prop.GetCustomAttribute<InjectServiceAttribute>();
var serviceName = injectAttr.ServiceName ?? InferServiceName(prop.PropertyType);
try
{
var service = await ResolveService(serviceName, prop.PropertyType);
// Wrap service with context if it supports it
if (service is IContextAware contextAwareService)
{
var context = await _contextProvider.GetCurrentContextAsync();
contextAwareService.SetContext(context);
}
// Wrap with middleware for cross-cutting concerns
service = WrapServiceWithMiddleware(service, serviceName);
prop.SetValue(pluginInstance, service);
result.ResolvedServices.Add(serviceName, service);
}
catch (Exception ex) when (injectAttr.Optional)
{
result.OptionalServicesNotResolved.Add(serviceName);
prop.SetValue(pluginInstance, null);
}
catch (Exception ex)
{
result.IsSuccess = false;
result.Errors.Add($"Failed to resolve required service {serviceName}: {ex.Message}");
}
}
return result;
}
private async Task<object> ResolveService(string serviceName, Type serviceType)
{
// Try registered service providers first
if (_serviceProviders.TryGetValue(serviceName, out var provider))
{
var service = provider.GetService(serviceType);
if (service != null) return service;
}
// Try service factories
if (_serviceFactories.TryGetValue(serviceName, out var factory))
{
return factory(_serviceProviders.GetValueOrDefault("default"));
}
// Try default service provider
if (_serviceProviders.TryGetValue("default", out var defaultProvider))
{
var service = defaultProvider.GetService(serviceType);
if (service != null) return service;
}
throw new ServiceResolutionException($"Cannot resolve service {serviceName} of type {serviceType.Name}");
}
}
// Services get same middleware treatment as plugin operations
public class ServiceCallMiddleware : IMiddleware<IServiceCallContext>
{
public async Task<IServiceCallContext> ExecuteAsync(IServiceCallContext context, Func<Task<IServiceCallContext>> next)
{
// Log service calls
using var scope = context.Logger.BeginScope("ServiceCall: {ServiceName}.{MethodName}",
context.ServiceName, context.MethodName);
var stopwatch = Stopwatch.StartNew();
try
{
var result = await next();
// Record metrics
context.MetricsCollector.RecordDuration($"service.{context.ServiceName}.{context.MethodName}.duration",
stopwatch.ElapsedMilliseconds);
context.MetricsCollector.Increment($"service.{context.ServiceName}.{context.MethodName}.success");
// Cache results if applicable
if (context.CacheKey != null && result.ShouldCache)
{
await context.CacheProvider.SetAsync(context.CacheKey, result.ReturnValue, context.CacheDuration);
}
return result;
}
catch (Exception ex)
{
context.MetricsCollector.Increment($"service.{context.ServiceName}.{context.MethodName}.error");
context.Logger.LogError(ex, "Service call failed: {ServiceName}.{MethodName}",
context.ServiceName, context.MethodName);
throw;
}
}
}
// Proxy wrapper for service calls
public class ServiceProxy<T> : DispatchProxy where T : class
{
private T _target;
private IServiceCallPipeline _pipeline;
private string _serviceName;
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
var context = new ServiceCallContext
{
ServiceName = _serviceName,
MethodName = targetMethod.Name,
Parameters = args,
TargetMethod = targetMethod
};
// Execute through middleware pipeline
var result = _pipeline.ExecuteAsync(context).GetAwaiter().GetResult();
return result.ReturnValue;
}
public static T Create(T target, IServiceCallPipeline pipeline, string serviceName)
{
var proxy = Create<T, ServiceProxy<T>>() as ServiceProxy<T>;
proxy._target = target;
proxy._pipeline = pipeline;
proxy._serviceName = serviceName;
return proxy as T;
}
}
public class PluginServiceConfiguration
{
public Dictionary<string, ServiceRegistration> Services { get; set; } = new();
public ServiceMiddlewareConfiguration Middleware { get; set; } = new();
public RetryPolicyConfiguration RetryPolicies { get; set; } = new();
}
public class ServiceRegistration
{
public string ServiceName { get; set; }
public string ImplementationType { get; set; }
public string BaseUrl { get; set; }
public Dictionary<string, string> Configuration { get; set; } = new();
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Scoped;
public bool RequiresAuthentication { get; set; } = true;
public string[] RequiredScopes { get; set; } = new string[0];
}
// Configuration-driven service setup
var serviceConfig = new PluginServiceConfiguration
{
Services = new Dictionary<string, ServiceRegistration>
{
["customer-service"] = new ServiceRegistration
{
ServiceName = "customer-service",
ImplementationType = "HttpClient", // or "gRPC", "ServiceBus", etc.
BaseUrl = "https://api.company.com/customers",
RequiresAuthentication = true,
RequiredScopes = new[] { "customers.read", "customers.write" }
},
["inventory-service"] = new ServiceRegistration
{
ServiceName = "inventory-service",
ImplementationType = "gRPC",
BaseUrl = "https://grpc.company.com/inventory",
Configuration = new Dictionary<string, string>
{
["timeout"] = "30s",
["retryPolicy"] = "exponential"
}
}
}
};
// Register services based on configuration
var serviceBuilder = new PluginServiceBuilder(serviceProvider);
foreach (var service in serviceConfig.Services)
{
await serviceBuilder.RegisterServiceAsync(service.Value);
}
[AttributeUsage(AttributeTargets.Property)]
public class InjectServiceAttribute : Attribute
{
public string ServiceName { get; set; }
public bool Optional { get; set; } = false;
public string ConfigurationKey { get; set; }
public Type FallbackType { get; set; }
}
[AttributeUsage(AttributeTargets.Property)]
public class ServiceDependentDataAttribute : CascadingSelectionAttribute
{
public string ServiceName { get; set; }
public string ServiceMethod { get; set; } = "GetDataAsync";
public bool CacheResults { get; set; } = true;
public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(15);
}
// Usage in plugin
public class EnhancedOrderPlugin
{
[InjectService("customer-service")]
public ICustomerService CustomerService { get; set; }
[ServiceDependentData(
ServiceName = "product-catalog",
ServiceMethod = "GetProductsAsync",
DependsOn = nameof(CategoryId),
CacheResults = true)]
public string ProductId { get; set; }
[ServiceDependentData(
ServiceName = "pricing-service",
ServiceMethod = "CalculatePriceAsync",
DependsOn = nameof(ProductId) + "," + nameof(CustomerId))]
public decimal Price { get; set; }
}
Service Dependency Resolver - Discovers and injects external services into plugins
Service Proxy Framework - Wraps external services with middleware (logging, caching, retry, auth)
Service Configuration Management - Declarative service registration and discovery
Service Health Monitoring - Track external service availability and performance
Service Authentication - Automatic token management and security for external APIs
Service Retry/Circuit Breaker - Resilience patterns for external service calls
Service Contract Validation - Ensure external services meet plugin expectations
Service Versioning - Handle external service API evolution
Service Mocking/Testing - Provide test doubles for plugin development
The plugin developer still writes naive POCOs but now has access to rich external service ecosystems through simple dependency injection - all infrastructure concerns remain external! Claude can make mistakes. Please double-check responses.