diff --git a/.gitignore b/.gitignore index dcab5b24b..3bf6448e3 100644 --- a/.gitignore +++ b/.gitignore @@ -135,8 +135,6 @@ DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html -# Click-Once directory -publish/ # Publish Web Output *.[Pp]ublish.xml diff --git a/Olive.Entities.Data.Replication/Publish/ExportDataAttribute.cs b/Olive.Entities.Data.Replication/Publish/ExportDataAttribute.cs new file mode 100644 index 000000000..9418ac300 --- /dev/null +++ b/Olive.Entities.Data.Replication/Publish/ExportDataAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace Olive.Entities.Replication +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class ExportDataAttribute : Attribute + { + public Type Type { get; } + + public ExportDataAttribute(Type type) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + + if (!type.IsA()) + throw new ArgumentException(type.FullName + " is not a subclass of " + typeof(ReplicatedData).FullName); + } + } +} diff --git a/Olive.Entities.Data.Replication/Publish/ExportedField.cs b/Olive.Entities.Data.Replication/Publish/ExportedField.cs new file mode 100644 index 000000000..ab958ee79 --- /dev/null +++ b/Olive.Entities.Data.Replication/Publish/ExportedField.cs @@ -0,0 +1,37 @@ +using System.Reflection; + +namespace Olive.Entities.Replication +{ + public class ExportedField + { + string title; + public PropertyInfo Property { get; } + + public bool IsAssociation => Property.PropertyType.IsA(); + + public bool IsInverseAssociation => Property.PropertyType.IsA() && Property.Defines(); + + public ExportedField(PropertyInfo property) + { + Property = property; + title = property.GetCustomAttribute()?.DisplayName; + if (title.IsEmpty()) + title = property.Name.ToLiteralFromPascalCase(); + } + + public string GetTitle() => title; + + public ExportedField Title(string exportTitle) + { + title = exportTitle; + return this; + } + + public object GetValue(IEntity entity) + { + var result = Property.GetValue(entity); + if (result is IEntity ent) return ent.GetId(); + else return result; + } + } +} \ No newline at end of file diff --git a/Olive.Entities.Data.Replication/Publish/ReplicatedData-T.cs b/Olive.Entities.Data.Replication/Publish/ReplicatedData-T.cs new file mode 100644 index 000000000..3932834ca --- /dev/null +++ b/Olive.Entities.Data.Replication/Publish/ReplicatedData-T.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace Olive.Entities.Replication +{ + public abstract class ReplicatedData : ReplicatedData + where TDomain : IEntity + { + Type domainType; + + public override Type DomainType => domainType ?? (domainType = GetType().BaseType.GenericTypeArguments.Single()); + + protected virtual string QueueUrlConfigKey => string.Empty; + + IEventBusQueue Queue + { + get + { + if (QueueUrlConfigKey.HasValue()) + return EventBus.Queue(Config.GetOrThrow(QueueUrlConfigKey)); + else + return EventBus.Queue(QueueUrl); + } + } + + internal override void Start() + { + GlobalEntityEvents.InstanceSaved.Handle(async x => + { + if (!x.Entity.GetType().IsA(DomainType)) return; + await Queue.Publish(ToMessage(x.Entity)); + }); + } + + public ExportedField Export(Expression> field) + { + var result = new ExportedField(field.GetProperty()); + Fields.Add(result); + return result; + } + + public void ExportAll() + { + var properties = DomainType.GetProperties() + .Where(x => x.CanRead && x.CanWrite) + .Where(x => x.DeclaringType.Assembly == DomainType.Assembly) + .ToArray(); + + foreach (var p in properties) + { + if (p.PropertyType == typeof(Guid?) && p.Name.EndsWith("Id") && properties.Any(x => x.Name == + p.Name.TrimEnd(2))) continue; + + Fields.Add(new ExportedField(p)); + } + } + + internal override async Task UploadAll() + { + foreach (var item in await Context.Current.Database().GetList()) + await Queue.Publish(ToMessage(item)); // TODO: Should this be done in parallel batches? + } + } +} \ No newline at end of file diff --git a/Olive.Entities.Data.Replication/Publish/ReplicatedData.cs b/Olive.Entities.Data.Replication/Publish/ReplicatedData.cs new file mode 100644 index 000000000..48e89e913 --- /dev/null +++ b/Olive.Entities.Data.Replication/Publish/ReplicatedData.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Olive.Entities.Replication +{ + public abstract class ReplicatedData + { + internal string QueueUrl { get; set; } + + public List Fields = new List(); + + public abstract Type DomainType { get; } + + public ReplicateDataMessage ToMessage(IEntity entity) + { + var properties = new Dictionary(); + + properties["ID"] = entity.GetId(); + + foreach (var f in Fields.Except(x => x.IsInverseAssociation)) + properties[f.Property.Name] = f.GetValue(entity); + + var serialized = JsonConvert.SerializeObject(properties); + + return new ReplicateDataMessage + { + TypeFullName = GetType().FullName, + Entity = serialized, + CreationUtc = DateTime.UtcNow + }; + } + + internal abstract void Start(); + + internal abstract Task UploadAll(); + + protected internal abstract void Define(); + } +} \ No newline at end of file diff --git a/Olive.Entities.Data.Replication/Publish/SourceEndpoint.cs b/Olive.Entities.Data.Replication/Publish/SourceEndpoint.cs new file mode 100644 index 000000000..e667ddb9d --- /dev/null +++ b/Olive.Entities.Data.Replication/Publish/SourceEndpoint.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Olive.Entities.Replication +{ + public abstract partial class SourceEndpoint + { + Dictionary Agents = new Dictionary(); + + string UrlPattern => Config.GetOrThrow("DataReplication:" + GetType().FullName + ":Url"); + + /// + /// Starts publishing an end point for the specified data types. + /// It handles all save events on such objects, and publishes them on the event bus. + /// + public void Publish() + { + var types = GetType().GetCustomAttributes().Select(x => x.Type).Distinct(); + + if (types.None()) + throw new Exception("No data is exported on " + GetType().FullName); + + foreach (var type in types) + { + var agent = type.CreateInstance(); + agent.Define(); + + agent.QueueUrl = UrlPattern; + Agents.Add(type.FullName, agent); + agent.Start(); + } + + HandleRefreshRequests(); + } + + void HandleRefreshRequests() + { + EventBus.Queue(UrlPattern.TrimEnd(".fifo") + "-REFRESH.fifo").Subscribe(message => + { + Agents[message.TypeName].UploadAll(); + return Task.CompletedTask; + }); + } + } +} \ No newline at end of file