diff --git a/zero.Core/Blueprints/Blueprint.cs b/zero.Core/Blueprints/Blueprint.cs
new file mode 100644
index 00000000..d01645c4
--- /dev/null
+++ b/zero.Core/Blueprints/Blueprint.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using zero.Core.Entities;
+
+namespace zero.Core.Blueprints
+{
+ ///
+ ///
+ ///
+ public class Blueprint : Blueprint where T : ZeroEntity, new()
+ {
+ public List> Fields { get; private set; } = new();
+
+
+ public Blueprint() : base(typeof(T))
+ {
+ Sync(x => x.Name);
+ Sync(x => x.Alias);
+ Sync(x => x.Sort);
+ Sync(x => x.IsActive);
+ Sync(x => x.LanguageId);
+ Sync(x => x.Key);
+ }
+
+
+ ///
+ /// Merges blueprint data into a model based on the configuration
+ ///
+ public virtual T Apply(T blueprint, T model)
+ {
+ // reset blueprint options for model in case no blueprint was found
+ if (blueprint == null)
+ {
+ model.Blueprint = null;
+ return model;
+ }
+
+ foreach (BlueprintField field in Fields)
+ {
+ // do not sync disabled fields
+ if (!field.IsSynced && !field.IsLocked)
+ {
+ continue;
+ }
+
+ // desynced fields are not synced with the blueprint
+ // (but only if they are not locked)
+ if (model.Blueprint.Desync.Contains(field.FieldName, StringComparer) && !field.IsLocked)
+ {
+ continue;
+ }
+
+ // apply new value to model property
+ field.Apply(blueprint, model);
+ }
+
+ ApplyDefaults(blueprint, model);
+
+ return model;
+ }
+
+
+ ///
+ public override ZeroEntity Apply(ZeroEntity blueprint, ZeroEntity model) => Apply(blueprint as T, model as T);
+
+
+ ///
+ /// Updates meta data for an entity which should always be synchronized
+ ///
+ protected virtual void ApplyDefaults(T blueprint, T model)
+ {
+ model.Hash = blueprint.Hash;
+ model.CreatedById = blueprint.CreatedById;
+ model.CreatedDate = blueprint.CreatedDate;
+ model.LastModifiedById = blueprint.LastModifiedById;
+ model.LastModifiedDate = blueprint.LastModifiedDate;
+ }
+
+
+ ///
+ /// Synchronize a field
+ ///
+ public BlueprintField Sync(Expression> selector)
+ {
+ BlueprintField field = Field(selector);
+ field.IsSynced = true;
+ return field;
+ }
+
+
+ ///
+ /// Lock a field so it always get synchronized no and cannot be changed
+ ///
+ public BlueprintField Lock(Expression> selector)
+ {
+ BlueprintField field = Field(selector);
+ field.IsLocked = true;
+ return field;
+ }
+
+
+ ///
+ /// Remove a field from synchronisation
+ ///
+ public void Remove(Expression> selector)
+ {
+ BlueprintField field = new(selector);
+ Fields.RemoveAll(x => x.FieldName.Equals(field.FieldName, StringComparison.InvariantCultureIgnoreCase));
+ }
+
+
+ ///
+ /// Get existing field or create a new one
+ ///
+ protected BlueprintField Field(Expression> selector)
+ {
+ BlueprintField tempField = new(selector);
+ BlueprintField storedField = Fields.FirstOrDefault(x => x.FieldName.Equals(tempField.FieldName, StringComparison.InvariantCultureIgnoreCase));
+
+ if (storedField != null)
+ {
+ return storedField;
+ }
+
+ Fields.Add(tempField);
+ return tempField;
+ }
+ }
+
+
+ ///
+ ///
+ ///
+ public abstract class Blueprint
+ {
+ ///
+ /// Type of the associated entity
+ ///
+ public Type ContentType { get; private set; }
+
+ ///
+ /// String comparer for property name comparison
+ ///
+ protected readonly StringComparer StringComparer = StringComparer.InvariantCultureIgnoreCase;
+
+
+ public Blueprint(Type type)
+ {
+ ContentType = type;
+ }
+
+ ///
+ /// Merges blueprint data into a model based on the configuration
+ ///
+ public abstract ZeroEntity Apply(ZeroEntity blueprint, ZeroEntity model);
+ }
+
+
+ public class DefaultBlueprint : Blueprint where T : ZeroEntity, new()
+ {
+ public DefaultBlueprint(Action> expression)
+ {
+ expression(this);
+ }
+ }
+}
diff --git a/zero.Core/Blueprints/BlueprintField.cs b/zero.Core/Blueprints/BlueprintField.cs
new file mode 100644
index 00000000..0150abad
--- /dev/null
+++ b/zero.Core/Blueprints/BlueprintField.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using zero.Core.Entities;
+using zero.Core.Extensions;
+
+namespace zero.Core.Blueprints
+{
+ public class BlueprintField where T : ZeroEntity
+ {
+ public Expression> Expression { get; private set; }
+
+ public PropertyInfo PropertyInfo { get; private set; }
+
+ public string FieldName { get; private set; }
+
+ ///
+ /// This field is synced as long as it's not desynced for a certain entity
+ ///
+ public bool IsSynced { get; set; }
+
+ ///
+ /// This field is synced and cannot be desynced
+ ///
+ public bool IsLocked { get; set; }
+
+ Action _applyHook { get; set; }
+
+ Func _selectorFunc = null;
+
+
+ internal BlueprintField(Expression> selector)
+ {
+ Expression = selector;
+ FieldName = GetPropertyPath(selector, out MemberExpression member);
+ PropertyInfo = member?.Member as PropertyInfo;
+
+ if (PropertyInfo == null)
+ {
+ throw new ArgumentException("selector parameter has to be a property selector");
+ }
+
+ if (FieldName.IsNullOrEmpty())
+ {
+ throw new Exception("Could not find a matching field name");
+ }
+ }
+
+
+ public virtual void Apply(T blueprint, T model)
+ {
+ if (_applyHook != null)
+ {
+ _applyHook(blueprint, model);
+ return;
+ }
+
+ _selectorFunc ??= Expression.Compile();
+ PropertyInfo.SetValue(model, _selectorFunc(blueprint));
+ }
+
+
+ public virtual void OnUpdate(Action onUpdate)
+ {
+ _applyHook = onUpdate;
+ }
+
+
+ static string GetPropertyPath(Expression expr, out MemberExpression member)
+ {
+ StringBuilder path = new();
+ MemberExpression memberExpression = GetMemberExpression(expr);
+ do
+ {
+ member = memberExpression;
+
+ if (path.Length > 0)
+ {
+ path.Insert(0, ".");
+ }
+ path.Insert(0, memberExpression.Member.Name);
+ memberExpression = GetMemberExpression(memberExpression.Expression);
+ }
+ while (memberExpression != null);
+
+ return path.ToString();
+ }
+
+
+ static MemberExpression GetMemberExpression(Expression expression)
+ {
+ if (expression is MemberExpression)
+ {
+ return (MemberExpression)expression;
+ }
+ else if (expression is LambdaExpression)
+ {
+ var lambdaExpression = expression as LambdaExpression;
+ if (lambdaExpression.Body is MemberExpression)
+ {
+ return (MemberExpression)lambdaExpression.Body;
+ }
+ else if (lambdaExpression.Body is UnaryExpression)
+ {
+ return ((MemberExpression)((UnaryExpression)lambdaExpression.Body).Operand);
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/zero.Core/Blueprints/BlueprintInterceptor.cs b/zero.Core/Blueprints/BlueprintInterceptor.cs
new file mode 100644
index 00000000..096eed8b
--- /dev/null
+++ b/zero.Core/Blueprints/BlueprintInterceptor.cs
@@ -0,0 +1,141 @@
+using Microsoft.Extensions.Logging;
+using Raven.Client.Documents;
+using Raven.Client.Documents.Session;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using zero.Core.Blueprints;
+using zero.Core.Collections;
+using zero.Core.Database;
+using zero.Core.Entities;
+using zero.Core.Extensions;
+
+namespace zero.Core.Routing
+{
+ public class BlueprintInterceptor : CollectionInterceptor
+ {
+ protected IZeroContext Context { get; set; }
+
+ protected IZeroStore Store { get; set; }
+
+ protected ILogger Logger { get; set; }
+
+ protected IBlueprintService BlueprintService { get; set; }
+
+ IList Apps { get; set; }
+
+
+ public BlueprintInterceptor(IZeroContext context, IZeroStore store, ILogger logger, IBlueprintService blueprintService)
+ {
+ Context = context;
+ Store = store;
+ Logger = logger;
+ BlueprintService = blueprintService;
+ }
+
+ ///
+ public override bool CanRun(Parameters args)
+ {
+ // we do only update children if we operate on the shared database
+ return (args.Session as AsyncDocumentSession)?.DatabaseName == Context.Options.Raven.Database;
+ }
+
+
+ ///
+ public override async Task Saved(SaveParameters args)
+ {
+ if (!BlueprintService.TryGetBlueprint(args.Model, out Blueprint blueprint))
+ {
+ return;
+ }
+
+ int count = 0;
+
+ foreach (Application app in await GetApplications())
+ {
+ using ZeroContextScope scope = Context.CreateScope(app);
+ IZeroDocumentSession session = scope.Store.Session(scope.Database);
+
+ ZeroEntity child = default;
+
+ if (args.IsUpdate)
+ {
+ child = await session.LoadAsync(args.Model.Id);
+ }
+
+ if (child == null)
+ {
+ child = args.Model.Clone();
+ child.Blueprint = new() { Id = args.Model.Id };
+ }
+ else
+ {
+ blueprint.Apply(args.Model, child);
+ }
+
+ count += 1;
+
+ // now we have to store the child
+ // but this will not work with the session as we need to run the scoped interceptors,
+ // therefore we need access to the collection
+ await session.StoreAsync(child);
+ await session.SaveChangesAsync();
+ }
+
+ Logger.LogDebug("Blueprint: Synced {count} children for {name} ({id})", count, args.Model.Name, args.Id);
+ }
+
+
+ ///
+ public override async Task Deleted(DeleteParameters args)
+ {
+ if (!BlueprintService.TryGetBlueprint(args.Model, out Blueprint blueprint))
+ {
+ return;
+ }
+
+ int count = 0;
+
+ foreach (Application app in await GetApplications())
+ {
+ using ZeroContextScope scope = Context.CreateScope(app);
+ IZeroDocumentSession session = scope.Store.Session(scope.Database);
+
+ count += 1;
+
+ // now we have to delete the child
+ // but this will not work with the session as we need to run the scoped interceptors,
+ // therefore we need access to the collection
+ session.Delete(args.Model.Id);
+ await session.SaveChangesAsync();
+ }
+
+ Logger.LogDebug("Blueprint: Deleted {count} children for {name} ({id})", count, args.Model.Name, args.Id);
+ }
+
+
+ ///
+ /// Get all applications to choose from
+ ///
+ async Task> GetApplications()
+ {
+ if (Apps != null)
+ {
+ return Apps;
+ }
+
+ IAsyncDocumentSession session = Store.Session(global: true);
+ Apps = await session.Query().ToListAsync();
+ return Apps;
+ }
+
+
+
+
+ ///
+ //public override async Task Saved(SaveParameters args)
+ //{
+
+ // //Logger.LogInformation("Route updates completed (+{added}/~{updated}/-{removed}) for {model} (id: {id})", countRoutes - countUpdatedRoutes, countUpdatedRoutes, obsoleteRoutes.Count, args.Model.Name, args.Model.Id);
+ //}
+ }
+}
diff --git a/zero.Core/Blueprints/BlueprintService.cs b/zero.Core/Blueprints/BlueprintService.cs
new file mode 100644
index 00000000..1d260676
--- /dev/null
+++ b/zero.Core/Blueprints/BlueprintService.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using zero.Core.Entities;
+using zero.Core.Options;
+
+namespace zero.Core.Blueprints
+{
+ public class BlueprintService : IBlueprintService
+ {
+ protected IZeroOptions Options { get; set; }
+
+ protected IReadOnlyCollection Blueprints { get; set; }
+
+
+ public BlueprintService(IZeroOptions options)
+ {
+ Options = options;
+ Blueprints = Options.Blueprints.GetAllItems();
+ }
+
+
+ ///
+ public bool IsEnabled(T model) where T : ZeroEntity => IsEnabled(model.GetType());
+
+
+ ///
+ public bool IsEnabled() where T : ZeroEntity => IsEnabled(typeof(T));
+
+
+ ///
+ public bool IsEnabled(Type type) => Blueprints.Any(x => x.ContentType.IsAssignableFrom(type));
+
+
+ ///
+ public bool TryGetBlueprint(T model, out Blueprint blueprint) where T : ZeroEntity => TryGetBlueprint(model.GetType(), out blueprint);
+
+
+ ///
+ public bool TryGetBlueprint(out Blueprint blueprint) where T : ZeroEntity => TryGetBlueprint(typeof(T), out blueprint);
+
+
+ ///
+ public bool TryGetBlueprint(Type type, out Blueprint blueprint)
+ {
+ blueprint = Blueprints.FirstOrDefault(x => x.ContentType.IsAssignableFrom(type));
+ return blueprint != null;
+ }
+ }
+
+
+ public interface IBlueprintService
+ {
+ ///
+ /// Check whether blueprinting functionality is enabled for a certain entity
+ ///
+ bool IsEnabled() where T : ZeroEntity;
+
+ ///
+ /// Check whether blueprinting functionality is enabled for a certain entity
+ ///
+ bool IsEnabled(T model) where T : ZeroEntity;
+
+ bool IsEnabled(Type type);
+
+ bool TryGetBlueprint(T model, out Blueprint blueprint) where T : ZeroEntity;
+
+ bool TryGetBlueprint(out Blueprint blueprint) where T : ZeroEntity;
+
+ bool TryGetBlueprint(Type type, out Blueprint blueprint);
+ }
+}
diff --git a/zero.Core/Blueprints/Implementations/CountryBlueprint.cs b/zero.Core/Blueprints/Implementations/CountryBlueprint.cs
new file mode 100644
index 00000000..47853b2b
--- /dev/null
+++ b/zero.Core/Blueprints/Implementations/CountryBlueprint.cs
@@ -0,0 +1,13 @@
+using zero.Core.Entities;
+
+namespace zero.Core.Blueprints
+{
+ public class CountryBlueprint : Blueprint
+ {
+ public CountryBlueprint()
+ {
+ Sync(x => x.Code);
+ Sync(x => x.IsPreferred);
+ }
+ }
+}
\ No newline at end of file
diff --git a/zero.Core/Collections/Interceptors/CollectionInterceptor.cs b/zero.Core/Collections/Interceptors/CollectionInterceptor.cs
index 76805979..215d9c2e 100644
--- a/zero.Core/Collections/Interceptors/CollectionInterceptor.cs
+++ b/zero.Core/Collections/Interceptors/CollectionInterceptor.cs
@@ -1,11 +1,13 @@
-using System;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
using zero.Core.Entities;
namespace zero.Core.Collections
{
public abstract partial class CollectionInterceptor : ICollectionInterceptor where T : ZeroEntity
{
+ ///
+ public virtual bool CanRun(Parameters args) => true;
+
///
public virtual Task> Creating(CreateParameters args) => Task.FromResult>(default);
@@ -38,12 +40,21 @@ namespace zero.Core.Collections
}
- public abstract partial class CollectionInterceptor : CollectionInterceptor, ICollectionInterceptor { }
+ public abstract partial class CollectionInterceptor : CollectionInterceptor, ICollectionInterceptor
+ {
+ ///
+ public override bool CanRun(Parameters args) => base.CanRun(args);
+ }
public interface ICollectionInterceptor : ICollectionInterceptor { }
public interface ICollectionInterceptor where T : ZeroEntity
{
+ ///
+ /// Whether any of the interceptor methods is allowed to run based on the parameters
+ ///
+ bool CanRun(CollectionInterceptor.Parameters args);
+
///
/// Called after an entity has been stored but before the session has saved its changes
///
diff --git a/zero.Core/Collections/Interceptors/CollectionInterceptorHandler.cs b/zero.Core/Collections/Interceptors/CollectionInterceptorHandler.cs
index a3ce376e..c22c89cd 100644
--- a/zero.Core/Collections/Interceptors/CollectionInterceptorHandler.cs
+++ b/zero.Core/Collections/Interceptors/CollectionInterceptorHandler.cs
@@ -61,6 +61,11 @@ namespace zero.Core.Collections
continue;
}
+ if (!interceptor.CanRun(instruction.Parameters))
+ {
+ continue;
+ }
+
if (func == default)
{
func = expression.Compile();
@@ -113,6 +118,11 @@ namespace zero.Core.Collections
continue;
}
+ if (!interceptor.CanRun(instruction.Parameters))
+ {
+ continue;
+ }
+
InterceptorResult beforeResult = instruction?.Results.FirstOrDefault(res => res.InterceptorHash == registration.Hash);
if (func == default)
diff --git a/zero.Core/Collections/Interceptors/CollectionInterceptorParameters.cs b/zero.Core/Collections/Interceptors/CollectionInterceptorParameters.cs
index 986dc03b..ecf6f155 100644
--- a/zero.Core/Collections/Interceptors/CollectionInterceptorParameters.cs
+++ b/zero.Core/Collections/Interceptors/CollectionInterceptorParameters.cs
@@ -31,6 +31,11 @@ namespace zero.Core.Collections
///
public IValidator Validator { get; set; }
+ ///
+ /// Access to the collection
+ ///
+ public ICollectionBase Collection { get; set; }
+
///
/// Parameters from the interceptor which ran on before the operation (only available for completed operations)
///
diff --git a/zero.Core/Collections/Interceptors/CollectionInterceptorShim.cs b/zero.Core/Collections/Interceptors/CollectionInterceptorShim.cs
index ff14d2be..d61b3a0b 100644
--- a/zero.Core/Collections/Interceptors/CollectionInterceptorShim.cs
+++ b/zero.Core/Collections/Interceptors/CollectionInterceptorShim.cs
@@ -20,7 +20,8 @@ namespace zero.Core.Collections
Context = args.Context,
Properties = args.Properties,
Session = args.Session,
- Store = args.Store
+ Store = args.Store,
+ Collection = null, //args.Collection
//Validator = args.Validator
};
@@ -46,6 +47,16 @@ namespace zero.Core.Collections
};
}
+ ///
+ public override bool CanRun(Parameters args)
+ {
+ if (args is ParametersWithModel)
+ {
+ return _base.CanRun(Args.ParametersWithModel>(args as ParametersWithModel));
+ }
+ return _base.CanRun(Args.Parameters>(args));
+ }
+
///
public override async Task> Creating(CreateParameters args) => Result(await _base.Creating(Args.CreateParameters>(args)));
diff --git a/zero.Core/Collections/Interceptors/InterceptorHelper.cs b/zero.Core/Collections/Interceptors/InterceptorHelper.cs
new file mode 100644
index 00000000..61cf1b40
--- /dev/null
+++ b/zero.Core/Collections/Interceptors/InterceptorHelper.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using zero.Core.Entities;
+
+namespace zero.Core.Collections
+{
+ public class InterceptorHelper
+ {
+ protected ICollectionInterceptorHandler Handler { get; set; }
+
+
+ public InterceptorHelper(ICollectionInterceptorHandler handler)
+ {
+ Handler = handler;
+ }
+
+
+ public async Task.UpdateParameters>> Updating(T model) where T : ZeroEntity
+ {
+ CollectionInterceptor.UpdateParameters parameters = new();
+ InterceptorInstruction.UpdateParameters> instruction = Handler.Create.UpdateParameters>("update", parameters) ?? new();
+
+ await instruction.HandleBefore(x => x.Updating(instruction.Parameters));
+
+ return instruction;
+ }
+ }
+}
diff --git a/zero.Core/Options/BlueprintOptions.cs b/zero.Core/Options/BlueprintOptions.cs
new file mode 100644
index 00000000..b16c1ed4
--- /dev/null
+++ b/zero.Core/Options/BlueprintOptions.cs
@@ -0,0 +1,32 @@
+using System;
+using zero.Core.Blueprints;
+using zero.Core.Entities;
+
+namespace zero.Core.Options
+{
+ public class BlueprintOptions : ZeroBackofficeCollection, IZeroCollectionOptions
+ {
+ public BlueprintOptions()
+ {
+ Add();
+ }
+
+
+ public void Add() where T : Blueprint, new()
+ {
+ Items.Add(new T());
+ }
+
+
+ public void Add(Blueprint implementation) where T : ZeroEntity, new()
+ {
+ Items.Add(implementation);
+ }
+
+
+ public void Add(Action> createExpression) where T : ZeroEntity, new()
+ {
+ Items.Add(new DefaultBlueprint(createExpression));
+ }
+ }
+}
diff --git a/zero.Core/Options/ZeroOptions.cs b/zero.Core/Options/ZeroOptions.cs
index d6878ca3..593ebbbc 100644
--- a/zero.Core/Options/ZeroOptions.cs
+++ b/zero.Core/Options/ZeroOptions.cs
@@ -28,6 +28,7 @@ namespace zero.Core.Options
Integrations = new();
Icons = new();
Services = new();
+ Blueprints = new();
Routing = new();
Interceptors = new();
Applications = new();
@@ -116,6 +117,9 @@ namespace zero.Core.Options
///
public ServiceOptions Services { get; private set; }
+ ///
+ public BlueprintOptions Blueprints { get; private set; }
+
///
public RoutingOptions Routing { get; private set; }
@@ -225,6 +229,11 @@ namespace zero.Core.Options
///
ServiceOptions Services { get; }
+ ///
+ ///
+ ///
+ BlueprintOptions Blueprints { get; }
+
///
///
///
diff --git a/zero.Web.UI/vite.config.js b/zero.Web.UI/vite.config.js
index 35c6eae3..e6bb9b36 100644
--- a/zero.Web.UI/vite.config.js
+++ b/zero.Web.UI/vite.config.js
@@ -79,7 +79,7 @@ const myPlugin = () => ({
*/
let config = {
server: {
- port: process.env.PORT,
+ port: process.env.PORT || 3399,
cors: true
},
plugins: [createVuePlugin(), ...zeroPlugins, myPlugin()],
diff --git a/zero.Web/Defaults/ZeroBackofficePlugin.cs b/zero.Web/Defaults/ZeroBackofficePlugin.cs
index 971854d3..0ee4f7b6 100644
--- a/zero.Web/Defaults/ZeroBackofficePlugin.cs
+++ b/zero.Web/Defaults/ZeroBackofficePlugin.cs
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using zero.Core;
using zero.Core.Api;
+using zero.Core.Blueprints;
using zero.Core.Collections;
using zero.Core.Entities;
using zero.Core.Integrations;
@@ -50,7 +51,8 @@ namespace zero.Web.Defaults
zero.Icons.AddSet("feather", "Feather", "/assets/icons/feather.svg", "fth");
zero.Pages.Add(Constants.Pages.FolderAlias, "@page.folder.name", "@page.folder.description", "fth-folder");
- zero.Interceptors.Add(gravity: 1000);
+ zero.Interceptors.Add(gravity: 100);
+ zero.Interceptors.Add(gravity: -1);
}
@@ -134,6 +136,9 @@ namespace zero.Web.Defaults
services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
services.AddScoped();
}
}
diff --git a/zero.Web/DevServer/ZeroDevOptions.cs b/zero.Web/DevServer/ZeroDevOptions.cs
index a7cc1631..bb566ca1 100644
--- a/zero.Web/DevServer/ZeroDevOptions.cs
+++ b/zero.Web/DevServer/ZeroDevOptions.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Zero.Web.DevServer
+namespace Zero.Web.DevServer
{
public class ZeroDevOptions
{
@@ -12,5 +7,7 @@ namespace Zero.Web.DevServer
public bool ForwardLog { get; set; } = false;
public string WorkingDirectory { get; set; }
+
+ public bool Enabled { get; set; } = true;
}
}
diff --git a/zero.Web/DevServer/ZeroDevService.cs b/zero.Web/DevServer/ZeroDevService.cs
index 7e633975..2741ce12 100644
--- a/zero.Web/DevServer/ZeroDevService.cs
+++ b/zero.Web/DevServer/ZeroDevService.cs
@@ -42,7 +42,7 @@ namespace Zero.Web.DevServer
{
// this is a development-time service,
// therefore no way to enable it in production
- if (!env.IsDevelopment())
+ if (!env.IsDevelopment() || !options.Value.Enabled)
{
return;
}