backoffice login loads! 👍

This commit is contained in:
2021-12-03 14:45:49 +01:00
parent 58ea4acffa
commit b0c958ffb3
53 changed files with 783 additions and 489 deletions
+2
View File
@@ -11,5 +11,7 @@ public static class ApiConstants
public const string IdNotFound = "Could not find persisted model for the given Id";
public const string ChangeTokenMismatch = "The change token is not valid anymore";
public const string ChildNotAllowed = "Not allowed as a child entity";
}
}
@@ -0,0 +1,6 @@
namespace zero.Api.Endpoints.Pages;
public class PageBasic : BasicModel<Page>
{
}
+13
View File
@@ -0,0 +1,13 @@
namespace zero.Api.Endpoints.Pages;
public class PageEdit : ZeroIdEntity
{
// we can't use Page as property type here
// cause that would serialize flavors to Page and remove additional properties.
// according to the System.Text.Json docs using "object" will serialize to the implementing type
public object Page { get; set; }
public FlavorConfig PageType { get; set; }
public List<string> Urls { get; set; } = new();
}
@@ -0,0 +1,23 @@
namespace zero.Api.Endpoints.Pages;
public class PageMapperProfile : ZeroMapperProfile
{
public override void Configure(IZeroMapper mapper)
{
mapper.Define<Page, PageBasic>((source, ctx) => new(), Map);
// mapper.Define<CountrySave, Country>((source, ctx) => new(), Map);
}
protected virtual void Map(Page source, PageBasic target, IZeroMapperContext ctx)
{
this.MapBasicData(source, target);
}
//protected virtual void Map(CountrySave source, Country target, IZeroMapperContext ctx)
//{
// this.MapSaveData(source, target);
// target.Code = source.Code;
// target.IsPreferred = source.IsPreferred;
//}
}
@@ -0,0 +1,40 @@
//using Microsoft.AspNetCore.Mvc;
//using System;
//using System.Collections.Generic;
//using System.Threading.Tasks;
//using zero.Core.Api;
//using zero.Core.Entities;
//using zero.Core.Utils;
//using zero.Web.Models;
//namespace zero.Web.Controllers
//{
// public class ModulesController : BackofficeController
// {
// IModulesApi Api;
// public ModulesController(IModulesApi api)
// {
// Api = api;
// }
// public async Task<IList<ModuleType>> GetModuleTypes([FromQuery] string[] tags = default, [FromQuery] string pageId = default) => await Api.GetModuleTypes(tags, pageId);
// public ModuleType GetModuleType([FromQuery] string alias) => Api.GetModuleType(alias);
// public EditModel<Module> GetEmpty(string alias)
// {
// ModuleType moduleType = Api.GetModuleType(alias);
// Module module = Activator.CreateInstance(moduleType.ContentType) as Module;
// module.ModuleTypeAlias = moduleType.Alias;
// module.Id = IdGenerator.Create(8);
// module.IsActive = true;
// return Edit(module);
// }
// }
//}
+18
View File
@@ -0,0 +1,18 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Api.Endpoints.Pages;
public class PageModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IPermissionProvider, PagePermissions>();
services.AddSingleton<IMapperProfile, PageMapperProfile>();
//services.Configure<RavenOptions>(opts =>
//{
// opts.Indexes.Add<zero_Api_MailTemplates_Listing>();
//});
}
}
@@ -0,0 +1,31 @@
namespace zero.Api.Endpoints.Pages;
public class PagePermissions : PermissionProvider
{
public const string Group = "zero.page";
public const string Create = "zero.page.create";
public const string Read = "zero.page.read";
public const string Update = "zero.page.update";
public const string Delete = "zero.page.delete";
public override Task Configure(IPermissionContext context)
{
PermissionGroup group = new(Group, "@page.list");
group.Permissions.Add(new Permission("zero.page.defaults", "Default permissions")
{
Children = new()
{
new(Create, "@permission.states.create"),
new(Read, "@permission.states.read"),
new(Update, "@permission.states.update"),
new(Delete, "@permission.states.delete")
}
});
context.AddGroup(group);
return Task.CompletedTask;
}
}
@@ -4,6 +4,7 @@ using Raven.Client.Documents.Session;
namespace zero.Api.Endpoints.Pages;
[Obsolete("Move to zero.Backoffice")]
public class PageTreeService : IPageTreeService
{
protected IPagesStore Pages { get; private set; }
@@ -0,0 +1,91 @@
using Microsoft.AspNetCore.Mvc;
namespace zero.Api.Endpoints.Pages;
public class PagesController : ZeroApiTreeEntityStoreController<Page, IPagesStore>
{
IPageTypeService PageTypeService;
IRoutes Routes;
public PagesController(IPagesStore store, IPageTypeService pageTypeService, IRoutes routes) : base(store)
{
PageTypeService = pageTypeService;
Routes = routes;
}
[HttpGet("empty")]
[ZeroAuthorize(PagePermissions.Create)]
public virtual async Task<ActionResult<PageEdit>> Empty(string flavor, string parentId = null)
{
Page page = await Store.Empty(flavor, parentId);
if (page == null)
{
return NotFound();
}
if (!await Store.IsAllowedAsChild(page, parentId))
{
return BadRequest(ApiConstants.HttpErrors.ChildNotAllowed);
}
PageEdit model = new()
{
Id = page.Id,
Page = page,
PageType = PageTypeService.GetByAlias(page.Flavor)
};
return model;
}
[HttpGet("{id}")]
[ZeroAuthorize(PagePermissions.Read)]
public virtual async Task<ActionResult<PageEdit>> Get(string id, string changeVector = null)
{
Page page = await Store.Load(id, changeVector);
if (page == null)
{
return NotFound();
}
string url = await Routes.GetUrl(page);
PageEdit model = new()
{
Id = page.Id,
Page = page,
PageType = PageTypeService.GetByAlias(page.Flavor),
Urls = url.HasValue() ? new() { url } : new() { }
};
return model;
}
[HttpGet("")]
[ZeroAuthorize(PagePermissions.Read)]
public virtual Task<ActionResult<Paged>> Get([FromQuery] ListQuery<Page> query)
{
query.SearchFor(entity => entity.Name);
return GetModels<PageBasic>(query);
}
//[HttpPost("")]
//[ZeroAuthorize(PagePermissions.Create)]
//public virtual Task<ActionResult<Result>> Create(MailSave saveModel) => CreateModel<MailSave, MailEdit>(saveModel);
//[HttpPut("{id}")]
//[ZeroAuthorize(PagePermissions.Update)]
//public virtual Task<ActionResult<Result>> Update(string id, MailSave updateModel, [FromQuery] string changeToken = null) => UpdateModel<MailSave, MailEdit>(id, updateModel, changeToken);
//[HttpDelete("{id}")]
//[ZeroAuthorize(PagePermissions.Delete)]
//public virtual Task<ActionResult<Result>> Delete(string id) => DeleteModel(id);
}
@@ -0,0 +1,104 @@
//using Microsoft.AspNetCore.Mvc;
//using Raven.Client.Documents;
//using System.Collections.Generic;
//using System.Linq;
//using System.Threading.Tasks;
//using zero.Core.Api;
//using zero.Core.Collections;
//using zero.Core.Entities;
//using zero.Core.Extensions;
//using zero.Core.Routing;
//using zero.Web.Models;
//namespace zero.Web.Controllers
//{
// public class PagesController : BackofficeCollectionController<Page, IPageService>
// {
// IRoutes Routes;
// public PagesController(IPageService collection, IRoutes routes) : base(collection)
// {
// Routes = routes;
// }
// public override async Task<EditModel<Page>> GetById([FromQuery] string id, [FromQuery] string changeVector = null)
// {
// Page entity = changeVector.IsNullOrEmpty() ? await Collection.GetById(id) : await Collection.GetRevision(changeVector);
// return entity == null ? null : Edit<Page, PageEditModel<Page>>(new PageEditModel<Page>()
// {
// Entity = entity,
// PageType = Collection.GetPageType(entity.PageTypeAlias),
// Urls = new List<string>() { await Routes.GetUrl(entity) }
// });
// }
// public override async Task<IList<PickerPreviewModel>> GetPreviews([FromQuery] List<string> ids)
// {
// IReadOnlyCollection<PageType> pageTypes = Options.Pages.GetAllItems();
// Dictionary<string, Page> pages = await Collection.GetByIds(ids.ToArray());
// Dictionary<Page, Route> routes = await Routes.GetRoutes(pages.Where(x => x.Value != null).Select(x => x.Value).ToArray());
// return Previews(pages, item =>
// {
// routes.TryGetValue(item, out Route route);
// PickerPreviewModel model = new()
// {
// Id = item.Id,
// Icon = pageTypes.FirstOrDefault(x => x.Alias == item.PageTypeAlias)?.Icon ?? "fth-folder",
// Name = item.Name,
// Text = route?.Url.Or("No URL found")
// };
// PreviewTransform?.Invoke(item, model);
// return model;
// });
// }
// public async Task<IList<TreeItem>> GetChildren([FromQuery] string parent = null, [FromQuery] string active = null, [FromQuery] string search = null)
// {
// return await Collection.GetChildren(parent, active, search);
// }
// public PageType GetPageType([FromQuery] string alias) => Collection.GetPageType(alias);
// public async Task<List<string>> GetUrls([FromQuery] string pageId)
// {
// string url = await Routes.GetUrl<Page>(pageId);
// return url.HasValue() ? new List<string>() { url } : new List<string>();
// }
// public async Task<IList<PageType>> GetAllowedPageTypes([FromQuery] string parent = null) => await Collection.GetAllowedPageTypes(parent);
// public async Task<EditModel<Page>> GetEmptyByType([FromQuery] string type, [FromQuery] string parent = null) => Edit(await Collection.GetEmpty(type, parent));
// [HttpPost]
// public async Task<Result<IList<Page>>> SaveSorting([FromBody] string[] ids) => await Collection.SaveSorting(ids);
// [HttpPost]
// public async Task<Result<Page>> Move([FromBody] ActionCopyModel model) => await Collection.Move(model.Id, model.DestinationId);
// [HttpPost]
// public async Task<Result<Page>> Copy([FromBody] ActionCopyModel model) => await Collection.Copy(model.Id, model.DestinationId, model.IncludeDescendants);
// [HttpPost]
// public async Task<Result<string[]>> Restore([FromBody] ActionCopyModel model) => await Collection.Restore(model.Id, model.IncludeDescendants);
// [HttpDelete]
// public async Task<Result<string[]>> DeleteRecursive([FromQuery] string id) => await Collection.Delete(id, recursive: true, moveToRecycleBin: true);
// }
//}
-11
View File
@@ -1,11 +0,0 @@
namespace zero.Web.Models
{
public class LoginModel
{
public string Email { get; set; }
public string Password { get; set; }
public bool IsPersistent { get; set; } = true;
}
}
@@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using zero.Core.Api;
using zero.Core.Entities;
using zero.Core.Utils;
using zero.Web.Models;
namespace zero.Web.Controllers
{
public class ModulesController : BackofficeController
{
IModulesApi Api;
public ModulesController(IModulesApi api)
{
Api = api;
}
public async Task<IList<ModuleType>> GetModuleTypes([FromQuery] string[] tags = default, [FromQuery] string pageId = default) => await Api.GetModuleTypes(tags, pageId);
public ModuleType GetModuleType([FromQuery] string alias) => Api.GetModuleType(alias);
public EditModel<Module> GetEmpty(string alias)
{
ModuleType moduleType = Api.GetModuleType(alias);
Module module = Activator.CreateInstance(moduleType.ContentType) as Module;
module.ModuleTypeAlias = moduleType.Alias;
module.Id = IdGenerator.Create(8);
module.IsActive = true;
return Edit(module);
}
}
}
@@ -1,66 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace zero.Backoffice.Modules;
/// <summary>
/// | GET /zero/api/pages/empty
/// | GET /zero/api/pages/{id}
/// | GET /zero/api/pages/{id}/revisions
/// | GET /zero/api/pages[type,query,filter,...]
/// | POST /zero/api/pages
/// | PUT /zero/api/pages/{id}
/// | DELETE /zero/api/pages/{id}
/// </summary>
public class PagesController : ZeroBackofficeApiController
{
protected IPagesStore Store { get; set; }
public PagesController(IPagesStore store)
{
Store = store;
}
[HttpGet("empty")]
public virtual async Task<Page> New()
{
return await Store.Empty();
}
[HttpGet("{id}")]
public virtual async Task<Page> Get(string id, string changeVector = null)
{
return await Store.Load(id, changeVector);
}
[HttpGet]
public virtual async Task<Paged<Page>> Get(ListQuery<Page> query)
{
return await Store.Load<zero_Backoffice_Countries_Listing>(query.Page, query.PageSize, q => q.OrderByDescending(x => x.CreatedDate));
}
//[HttpGet("{id}/revisions")]
//public virtual async Task<List<Revision<Country>>> GetRevisions(string id)
//{
// return await Collection.GetRevisions(id);
//}
[HttpPost]
public virtual async Task<Result<Page>> Create(Country model)
{
return await Store.Create(model);
}
[HttpPut("{id}")]
public virtual async Task<Result<Page>> Update(string id, Country model)
{
return await Store.Update(model);
}
[HttpDelete("{id}")]
public virtual async Task<Result<Page>> Delete(string id)
{
return await Store.Delete(id);
}
}
@@ -1,104 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Raven.Client.Documents;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using zero.Core.Api;
using zero.Core.Collections;
using zero.Core.Entities;
using zero.Core.Extensions;
using zero.Core.Routing;
using zero.Web.Models;
namespace zero.Web.Controllers
{
public class PagesController : BackofficeCollectionController<Page, IPageService>
{
IRoutes Routes;
public PagesController(IPageService collection, IRoutes routes) : base(collection)
{
Routes = routes;
}
public override async Task<EditModel<Page>> GetById([FromQuery] string id, [FromQuery] string changeVector = null)
{
Page entity = changeVector.IsNullOrEmpty() ? await Collection.GetById(id) : await Collection.GetRevision(changeVector);
return entity == null ? null : Edit<Page, PageEditModel<Page>>(new PageEditModel<Page>()
{
Entity = entity,
PageType = Collection.GetPageType(entity.PageTypeAlias),
Urls = new List<string>() { await Routes.GetUrl(entity) }
});
}
public override async Task<IList<PickerPreviewModel>> GetPreviews([FromQuery] List<string> ids)
{
IReadOnlyCollection<PageType> pageTypes = Options.Pages.GetAllItems();
Dictionary<string, Page> pages = await Collection.GetByIds(ids.ToArray());
Dictionary<Page, Route> routes = await Routes.GetRoutes(pages.Where(x => x.Value != null).Select(x => x.Value).ToArray());
return Previews(pages, item =>
{
routes.TryGetValue(item, out Route route);
PickerPreviewModel model = new()
{
Id = item.Id,
Icon = pageTypes.FirstOrDefault(x => x.Alias == item.PageTypeAlias)?.Icon ?? "fth-folder",
Name = item.Name,
Text = route?.Url.Or("No URL found")
};
PreviewTransform?.Invoke(item, model);
return model;
});
}
public async Task<IList<TreeItem>> GetChildren([FromQuery] string parent = null, [FromQuery] string active = null, [FromQuery] string search = null)
{
return await Collection.GetChildren(parent, active, search);
}
public PageType GetPageType([FromQuery] string alias) => Collection.GetPageType(alias);
public async Task<List<string>> GetUrls([FromQuery] string pageId)
{
string url = await Routes.GetUrl<Page>(pageId);
return url.HasValue() ? new List<string>() { url } : new List<string>();
}
public async Task<IList<PageType>> GetAllowedPageTypes([FromQuery] string parent = null) => await Collection.GetAllowedPageTypes(parent);
public async Task<EditModel<Page>> GetEmptyByType([FromQuery] string type, [FromQuery] string parent = null) => Edit(await Collection.GetEmpty(type, parent));
[HttpPost]
public async Task<Result<IList<Page>>> SaveSorting([FromBody] string[] ids) => await Collection.SaveSorting(ids);
[HttpPost]
public async Task<Result<Page>> Move([FromBody] ActionCopyModel model) => await Collection.Move(model.Id, model.DestinationId);
[HttpPost]
public async Task<Result<Page>> Copy([FromBody] ActionCopyModel model) => await Collection.Copy(model.Id, model.DestinationId, model.IncludeDescendants);
[HttpPost]
public async Task<Result<string[]>> Restore([FromBody] ActionCopyModel model) => await Collection.Restore(model.Id, model.IncludeDescendants);
[HttpDelete]
public async Task<Result<string[]>> DeleteRecursive([FromQuery] string id) => await Collection.Delete(id, recursive: true, moveToRecycleBin: true);
}
}
+1
View File
@@ -31,6 +31,7 @@ public class ZeroApiPlugin : ZeroPlugin
ZeroModuleCollection.AddModule<Endpoints.Search.SearchModule>(services, configuration);
ZeroModuleCollection.AddModule<Endpoints.Translations.TranslationModule>(services, configuration);
ZeroModuleCollection.AddModule<Endpoints.Mails.MailModule>(services, configuration);
ZeroModuleCollection.AddModule<Endpoints.Pages.PageModule>(services, configuration);
PostConfigureServices?.Invoke(services, configuration);
}
+1 -1
View File
@@ -27,7 +27,7 @@ public class ZeroApiControllerModelConvention : IControllerModelConvention
/// <summary>
/// Configure routing model for all backoffice controllers
/// </summary>
public void Apply(ControllerModel controller)
public virtual void Apply(ControllerModel controller)
{
bool hasAttributeRouteModels = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
+5 -3
View File
@@ -3,7 +3,9 @@ using Raven.Client.Documents.Indexes;
namespace zero.Api.Controllers;
public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiController where TModel : ZeroEntity, new() where TStore : IEntityStore<TModel>
public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiController
where TModel : ZeroEntity, new()
where TStore : IEntityStore<TModel>
{
protected TStore Store { get; set; }
@@ -21,9 +23,9 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
}
protected async Task<ActionResult<T>> EmptyModel<T>(string flavor) where T : DisplayModel<TModel>
protected async Task<ActionResult<T>> EmptyModel<T>(string flavorAlias = null) where T : DisplayModel<TModel>
{
TModel model = await Store.Empty(flavor);
TModel model = await Store.Empty(flavorAlias);
if (model == null)
{
@@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Mvc;
using Raven.Client.Documents.Indexes;
namespace zero.Api.Controllers;
public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApiEntityStoreController<TModel, TStore>
where TModel : ZeroEntity, ISupportsTrees, new()
where TStore : ITreeEntityStore<TModel>
{
public ZeroApiTreeEntityStoreController(TStore store) : base(store) { }
protected async Task<ActionResult<Paged>> GetChildModels<T>(string parentId, ListQuery<TModel> query) where T : BasicModel<TModel>
{
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
Paged<TModel> result = await Store.LoadChildren(parentId, query.Page, query.PageSize, q => q.Filter(query));
return Mapper.Map<TModel, T>(result, (src, dest) =>
{
dest.Link = GetAction(src);
});
}
protected async Task<ActionResult<Paged>> GetChildModels<T, TIndex>(string parentId, ListQuery<TModel> query) where T : BasicModel<TModel> where TIndex : AbstractCommonApiForIndexes, new()
{
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
Paged<TModel> result = await Store.LoadChildren<TIndex>(parentId, query.Page, query.PageSize, q => q.Filter(query));
return Mapper.Map<TModel, T>(result, (src, dest) =>
{
dest.Link = GetAction(src);
});
}
protected async Task<ActionResult<Result>> SortModels(string[] ids)
{
return await Store.Sort(ids);
}
protected async Task<ActionResult<Result>> MoveModel<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
{
return await PutOperation<TEdit>(async () => await Store.Move(pageId, newParentId));
}
protected async Task<ActionResult<Result>> CopyModel<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
{
return await PutOperation<TEdit>(async () => await Store.Copy(pageId, newParentId));
}
protected async Task<ActionResult<Result>> CopyModelWithDescendants<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
{
return await PutOperation<TEdit>(async () => await Store.CopyWithDescendants(pageId, newParentId));
}
protected async Task<ActionResult<Result>> DeleteModelWithDescendants(string id)
{
TModel model = await Store.Load(id);
if (model == null)
{
return NotFound();
}
Result<string[]> result = await Store.DeleteWithDescendants(model);
return result.WithoutModel();
}
async Task<ActionResult<Result>> PutOperation<TEdit>(Func<Task<Result<TModel>>> action) where TEdit : DisplayModel<TModel>
{
Result<TModel> result = await action();
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
{
return result.WithoutModel();
}
return Mapper.Map<TModel, TEdit>(result);
}
}
+5
View File
@@ -14,6 +14,11 @@
<None Remove="Modules_legacy\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Endpoints\Pages\ModulesController.cs" />
<Compile Remove="Endpoints\Pages\_PagesController.cs" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
@@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using zero.Api.Controllers;
namespace zero.Backoffice.Controllers;
[ZeroAuthorize(false)]
[DisableBrowserCache]
public class ZeroIndexController : Controller
{
IZeroVue ZeroVue { get; set; }
IZeroOptions Options { get; set; }
public ZeroIndexController(IZeroVue zeroVue, IZeroOptions options)
{
ZeroVue = zeroVue;
Options = options;
}
public IActionResult Index()
{
if (Options.Version.IsNullOrEmpty())
{
return RedirectToAction("ZeroBackoffice", "Setup");
}
return View("Views/Zero/Index.cshtml", new ZeroBackofficeModel()
{
Port = Options.For<BackofficeOptions>().DevServer.Port,
Vue = ZeroVue
});
}
}
public class ZeroBackofficeModel
{
public int Port { get; set; }
public IZeroVue Vue { get; set; }
}
@@ -1,71 +0,0 @@
//using Microsoft.AspNetCore.Mvc;
//using Raven.Client.Documents.Linq;
//using System;
//using System.Collections.Generic;
//using System.Threading.Tasks;
//using zero.Core;
//using zero.Core.Entities;
//namespace zero.Backoffice;
//public abstract class _ZeroBackofficeCollectionController<TEntity, TCollection> : ZeroBackofficeController
// where TEntity : ZeroIdEntity, new()
// where TCollection : IStoreOperations<TEntity>
//{
// protected TCollection Collection { get; private set; }
// [Obsolete]
// protected Func<IRavenQueryable<TEntity>, IRavenQueryable<TEntity>> DefaultQuery { get; set; }
// protected Action<TEntity, PickerPreviewModel> PreviewTransform { get; set; }
// protected Action<TEntity, PickerModel> PickerTransform { get; set; }
// public ZeroBackofficeCollectionController(TCollection collection)
// {
// Collection = collection;
// }
// public override void OnScopeChanged(string scope)
// {
// //Collection.ApplyScope(scope);
// }
// public virtual async Task<DisplayModel<TEntity>> GetById([FromQuery] string id, [FromQuery] string changeVector = null) => Edit(await Collection.Load(id, changeVector));
// public virtual async Task<Dictionary<string, TEntity>> GetByIds([FromQuery] string[] ids) => await Collection.Load(ids);
// public virtual async Task<DisplayModel<TEntity>> GetEmpty() => Edit(await Collection.Empty());
// public virtual async Task<Paged<TEntity>> GetByQuery([FromQuery] ListQuery<TEntity> query)
// {
// return await Collection.Load(query);
// }
// public virtual async Task<Paged<Revision>> GetRevisions([FromQuery] string id, [FromQuery] ListQuery<TEntity> query)
// {
// return null; // TODO
// //return await Collection.GetRevisions(id, query.Page, query.PageSize);
// }
// public virtual async Task<IEnumerable<PickerModel>> GetForPicker() => await SelectList(Collection.Stream(), PickerTransform);
// public virtual async Task<IList<PickerPreviewModel>> GetPreviews([FromQuery] List<string> ids) => Previews(await Collection.Load(ids), PreviewTransform);
// [HttpPost]
// public virtual async Task<Result<TEntity>> Save([FromBody] TEntity model) => await Collection.Save(model);
// [HttpDelete]
// public virtual async Task<Result<TEntity>> Delete([FromQuery] string id) => await Collection.Delete(id);
//}
@@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Mvc;
using zero.Api.Filters;
using zero.Identity.Models;
namespace zero.Backoffice.Endpoints.Account;
[ZeroSystemApi]
public class AccountController : ZeroBackofficeController
{
readonly IAuthenticationService AuthService;
public AccountController(IAuthenticationService authService)
{
AuthService = authService;
}
[HttpGet("user")]
public async Task<ActionResult<UserModel>> GetUser()
{
ZeroUser user = await AuthService.GetUser();
if (user == null)
{
return BadRequest("notfound");
}
return Mapper.Map<ZeroUser, UserModel>(user);
}
[HttpGet("loggedin")]
[ZeroAuthorize(false)]
public ActionResult<bool> LoggedIn() => AuthService.IsLoggedIn();
[HttpPost("login")]
[ZeroAuthorize(false)]
//[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginModel model)
{
LoginResult result = await AuthService.Login(model.Email, model.Password, model.IsPersistent);
if (result != LoginResult.Success)
{
return BadRequest(result.ToString()); // TODO translate resource
}
return Ok();
}
[HttpPost("logout")]
public async Task<ActionResult> Logout()
{
await AuthService.Logout();
return Ok();
}
//[HttpPost, ZeroAuthorize]
//public async Task<Result> SwitchApp(string appId)
//{
// return Result.Maybe(await Api.TrySwitchApp(appId));
//}
}
@@ -0,0 +1,23 @@
namespace zero.Backoffice.Endpoints.Account;
public class AccountMapperProfile : ZeroMapperProfile
{
public override void Configure(IZeroMapper mapper)
{
mapper.Define<ZeroUser, UserModel>((source, ctx) => new(), Map);
}
protected virtual void Map(ZeroUser source, UserModel target, IZeroMapperContext ctx)
{
target.Id = source.Id;
target.CurrentAppId = source.CurrentAppId;
target.IsSuper = source.IsSuper;
target.IsActive = source.IsActive;
target.AvatarId = source.AvatarId;
target.Name = source.Name;
target.Email = source.Email;
target.CreatedDate = source.CreatedDate;
target.Flavor = source.Flavor;
}
}
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace zero.Backoffice.Endpoints.Account;
public class LoginModel
{
[Required, EmailAddress]
public string Email { get; set; }
[Required]
public string Password { get; set; }
public bool IsPersistent { get; set; } = true;
}
@@ -0,0 +1,20 @@
namespace zero.Backoffice.Endpoints.Account;
public class UserModel : ZeroIdEntity
{
public string CurrentAppId { get; set; }
public bool IsSuper { get; set; }
public string AvatarId { get; set; }
public string Email { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public DateTimeOffset CreatedDate { get; set; }
public string Flavor { get; set; }
}
@@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Mvc;
using zero.Api.Controllers;
namespace zero.Backoffice.Endpoints.Links;
public class LinksController : ZeroApiController
public class LinksController : ZeroBackofficeController
{
readonly IZeroStore Store;
readonly ILinks Links;
+1 -2
View File
@@ -1,11 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections;
using zero.Api.Controllers;
using zero.Api.Endpoints.Applications;
namespace zero.Backoffice.Endpoints.UI;
public class UIController : ZeroApiController
public class UIController : ZeroBackofficeController
{
readonly IEnumerable<IBackofficeSection> Sections;
readonly IEnumerable<ISettingsGroup> SettingsGroups;
@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.StaticFiles;
using System.IO;
using zero.Api.Controllers;
namespace zero.Backoffice.Controllers;
namespace zero.Backoffice.Extensions;
public static class ZeroBackofficeControllerExtensions
{
+5
View File
@@ -2,7 +2,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System.IO;
using zero.Backoffice.Endpoints.Account;
namespace zero.Backoffice;
@@ -21,8 +24,10 @@ public class ZeroBackofficePlugin : ZeroPlugin
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<BackofficeOptions>().Bind(configuration.GetSection("Zero:Backoffice")).Configure<IWebHostEnvironment>(ConfigureOptions);
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, ZeroBackofficeMvcOptions>());
services.AddHostedService<ZeroDevService>();
services.AddTransient<IZeroVue, ZeroVue>();
services.AddSingleton<IMapperProfile, AccountMapperProfile>();
services.AddZeroBackofficeUIComposition();
@@ -405,6 +405,7 @@
"password_placeholder": "Enter your password"
},
"errors": {
"unknown": "An unknown login error happened",
"wrongcredentials": "Email or password is wrong",
"lockedout": "The user has been locked out due to many failed password attempts",
"disabled": "The user has been deactivated",
@@ -725,46 +726,5 @@
"errors": {
"cannotDeleteChild": "Entities which are synchronized with a blueprint cannot be deleted"
}
},
"_test": {
"fields": {
"name": "Name",
"position": "Position",
"image": "Image",
"email": "Email",
"videoUri": "Video URL",
"isvisible": "Visible",
"facebook": "Facebook",
"facebook_text": "Link to facebook page",
"youtube": "Youtube",
"youtube_text": "Link to youtube channel",
"twitter": "Twitter",
"twitter_text": "Link to twitter profile",
"xIconPicker": "Icon",
"xTextarea": "Description",
"addresses": "Addresses",
"addresses_text": "Enter as many addresses as you wish",
"xCustom": "Generate",
"xRte": "Description as HTML",
"xMedia": "Images",
"xState": "Select media type",
"address": {
"city": "City",
"street": "Street",
"no": "No",
"countries": "Country",
"name": "Name",
"iso": "ISO Code"
},
"hideInNavigation": "Hide in navigation",
"hideInTitle": "Hide in title",
"titleOverride": "Override title",
"titleOverrideAll": "Override full title",
"seoDescription": "SEO Description",
"seoImage": "Image",
"noFollow": "No follow",
"noIndex": "No index"
}
}
}
+32
View File
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex,nofollow">
<meta name="author" content="brothers">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="expires" content="0">
<link rel="icon" type="image/png" href="/assets/zero-icon.png" sizes="32x32">
<link rel="image_src" type="image/png" href="/assets/zero-icon.png" sizes="192x192" />
<link rel="shortcut icon" type="image/png" href="/assets/zero-icon.png" sizes="196x196">
<base href="~/">
{css}
<title>zero</title>
</head>
<body class="theme-light theme-rounded">
<div id="app"></div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">{svg}</svg>
<script id="zeroconfig" type="application/json">{config}</script>
<script>
window.__zero = JSON.parse(document.getElementById('zeroconfig').innerHTML);
window.zero = window.__zero;
</script>
{js}
</body>
</html>
-27
View File
@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex,nofollow">
<meta name="author" content="brothers">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="expires" content="0">
<link rel="icon" type="image/png" href="/assets/zero-icon.png" sizes="32x32">
<link rel="image_src" type="image/png" href="/assets/zero-icon.png" sizes="192x192" />
<link rel="shortcut icon" type="image/png" href="/assets/zero-icon.png" sizes="196x196">
<base href="~/">
{{styles}}
<title>zero</title>
</head>
<body class="theme-light theme-roundedx">
<div id="app"></div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">{{svg}}</svg>
<script id="zeroconfig" type="application/json">{{config}}</script>
<script>
window.__zero = {{config}};
window.zero = window.__zero;
</script>
{{scripts}}
</body>
</html>
@@ -1,34 +1,41 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Reflection;
using zero.Api.Controllers;
using zero.Api.Filters;
namespace zero.Backoffice;
public class ZeroBackofficeControllerModelConvention : IApplicationModelConvention
public class ZeroBackofficeControllerModelConvention : ZeroApiControllerModelConvention
{
readonly AttributeRouteModel RouteModel;
readonly AttributeRouteModel AppAwareRouteModel;
readonly AttributeRouteModel AppUnawareRouteModel;
readonly Type BaseClass = typeof(ZeroBackofficeController);
readonly bool RuntimeIsAppAware = false;
public ZeroBackofficeControllerModelConvention(string backofficePath)
public ZeroBackofficeControllerModelConvention(string zeroPath, string backofficeApiPath = "backoffice", bool isAppAware = false) : base(zeroPath, backofficeApiPath, isAppAware)
{
RouteModel = new AttributeRouteModel(new RouteAttribute(backofficePath.TrimEnd('/') + "/api/[controller]"));
RuntimeIsAppAware = isAppAware;
AppAwareRouteModel = BuildRouteModel(zeroPath, backofficeApiPath, true);
AppUnawareRouteModel = BuildRouteModel(zeroPath, backofficeApiPath, false);
}
/// <summary>
/// Configure routing model for all backoffice controllers
/// </summary>
public void Apply(ApplicationModel application)
public override void Apply(ControllerModel controller)
{
foreach (var controller in application.Controllers)
{
bool hasAttributeRouteModels = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
bool hasAttributeRouteModels = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
if (controller.ControllerType.IsSubclassOf(BaseClass))
{
controller.Selectors[0].AttributeRouteModel = RouteModel;
}
if (controller.ControllerType.IsSubclassOf(BaseClass))
{
bool isAppAware = RuntimeIsAppAware && controller.ControllerType.GetCustomAttribute<ZeroSystemApiAttribute>() == null;
controller.Selectors[0].AttributeRouteModel = isAppAware ? AppAwareRouteModel : AppUnawareRouteModel;
}
}
}
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace zero.Backoffice;
+64
View File
@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Mvc;
using System.IO;
using zero.Api.Controllers;
namespace zero.Backoffice.Controllers;
[ZeroAuthorize(false)]
[DisableBrowserCache]
public class ZeroIndexController : Controller
{
IZeroVue ZeroVue { get; set; }
IZeroOptions Options { get; set; }
string[] JsDevModulePaths = new[] { "/vite/client", "/app/app.js" };
public ZeroIndexController(IZeroVue zeroVue, IZeroOptions options)
{
ZeroVue = zeroVue;
Options = options;
}
public async Task<ActionResult> Index()
{
if (Options.Version.IsNullOrEmpty())
{
return RedirectToAction("ZeroBackoffice", "Setup");
}
string config = await ZeroVue.ConfigAsJson();
int port = Options.For<BackofficeOptions>().DevServer.Port;
string resourceName = $"zero.Backoffice.Resources.index.tpl.html";
using Stream stream = GetType().Assembly.GetManifestResourceStream(resourceName);
using StreamReader reader = new(stream);
string template = await reader.ReadToEndAsync();
string content = TokenReplacement.Apply(template, new()
{
{ "css", String.Empty },
{ "js", String.Join(String.Empty, GetJsModules("http://localhost:" + port)) },
{ "svg", ZeroVue.GetIconSvg() },
{ "config", config }
});
return Content(content, "text/html");
}
IEnumerable<string> GetJsModules(string domain)
{
return JsDevModulePaths.Select(path => $"<script type='module' src='{domain.TrimEnd('/')}{path.EnsureStartsWith('/')}'></script>");
}
}
public class ZeroBackofficeModel
{
public int Port { get; set; }
public IZeroVue Vue { get; set; }
}
+4
View File
@@ -17,6 +17,10 @@
<None Remove="Modules_legacy\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\index.tpl.html" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
+16
View File
@@ -0,0 +1,16 @@
namespace zero.Identity.Models;
public enum LoginResult
{
[Localize("@login.errors.unknown")]
Unknown = 0,
[Localize("@login.errors.wrongcredentials")]
WrongCredentials = 1,
[Localize("@login.errors.lockedout")]
LockedOut = 2,
[Localize("@login.errors.notallowed")]
NotAllowed = 3,
[Localize("@login.errors.requirestwofactor")]
RequiresTwoFactor = 4,
Success = 10
}
@@ -27,6 +27,18 @@ public class SchemedSignInManager<TUser> : SignInManager<TUser> where TUser : Ze
}
/// <inheritdoc />
public override Task<bool> CanSignInAsync(TUser user)
{
if (!user.IsActive)
{
return Task.FromResult(false);
}
return base.CanSignInAsync(user);
}
/// <inheritdoc />
public override bool IsSignedIn(ClaimsPrincipal principal)
{
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Identity;
using zero.Identity.Models;
namespace zero.Identity;
@@ -54,50 +55,29 @@ public class AuthenticationService : IAuthenticationService
/// <inheritdoc />
public async Task<Result> Login(string email, string password, bool isPersistent)
public async Task<LoginResult> Login(string email, string password, bool isPersistent)
{
Result result = new();
ZeroUser user = await SignInManager.UserManager.FindByNameAsync(email);
if (user == null)
{
result.AddError("@login.errors.wrongcredentials"); // TODO we don't need translations here, but return an enum, so the app itself can translate the error
return result;
}
// TODO probably move this logic into a custom SignInManager which overrides CanSignInAsync()
// see https://stackoverflow.com/a/35484758/670860
else if (!user.IsActive)
{
result.AddError("@login.errors.disabled");
return result;
}
SignInResult signInResult = await SignInManager.PasswordSignInAsync(user, password, isPersistent, true);
SignInResult signInResult = await SignInManager.PasswordSignInAsync(email, password, isPersistent, true);
if (!signInResult.Succeeded)
{
if (signInResult.IsLockedOut)
{
result.AddError("@login.errors.lockedout");
return LoginResult.LockedOut;
}
else if (signInResult.IsNotAllowed)
{
result.AddError("@login.errors.notallowed");
return LoginResult.NotAllowed;
}
else if (signInResult.RequiresTwoFactor)
{
result.AddError("@login.errors.requirestwofactor");
return LoginResult.RequiresTwoFactor;
}
else
{
result.AddError("@login.errors.wrongcredentials");
}
return result;
return LoginResult.WrongCredentials;
}
return Result.Success();
return LoginResult.Success;
}
@@ -141,7 +121,7 @@ public interface IAuthenticationService
/// <summary>
/// Logs a zero-user in and sets cookie
/// </summary>
Task<Result> Login(string email, string password, bool isPersistent);
Task<LoginResult> Login(string email, string password, bool isPersistent);
/// <summary>
/// Logs out the current user
+4 -3
View File
@@ -1,4 +1,6 @@
using System.Reflection;
using Raven.Client.Documents;
using Raven.Client.Documents.Session;
using System.Reflection;
namespace zero.Localization;
@@ -8,7 +10,6 @@ public class Localizer : ILocalizer
protected IZeroStore Store { get; private set; }
public Localizer(IZeroStore store)
{
Store = store;
@@ -82,7 +83,7 @@ public class Localizer : ILocalizer
/// </summary>
protected virtual Translation LoadTranslation(string key)
{
return Store.Session().Query<Translation>().FirstOrDefault(x => x.Key == key); // TODO I guess this throws ^^
return Store.Session().Synchronous.Query<Translation>().FirstOrDefault(x => x.Key == key);
}
}
+6
View File
@@ -68,6 +68,12 @@ public class ZeroMapper : IZeroMapper
}
Type destinationType = typeof(TDestination);
if (destinationType == source.GetType())
{
return (TDestination)source;
}
ZeroMapperContext mapperContext = new(this);
var constructor = GetConstructor(sourceType, destinationType);
+2 -2
View File
@@ -35,7 +35,7 @@ public class PageTypeService : IPageTypeService
/// <inheritdoc />
public async Task<IEnumerable<FlavorConfig>> GetAllowedFlavors(string pageParentId = null)
public async Task<IEnumerable<FlavorConfig>> GetAllowedTypes(string pageParentId = null)
{
List<Page> parents = new();
@@ -80,7 +80,7 @@ public interface IPageTypeService
/// <summary>
/// Get all page types which are allowed below a selected parent page
/// </summary>
Task<IEnumerable<FlavorConfig>> GetAllowedFlavors(string pageParentId = null);
Task<IEnumerable<FlavorConfig>> GetAllowedTypes(string pageParentId = null);
/// <summary>
/// Get a specific page type by alias
+2 -3
View File
@@ -16,7 +16,7 @@ public class PagesStore : TreeEntityStore<Page>, IPagesStore
/// <inheritdoc />
public override async Task<bool> IsAllowedAsChild(Page model, string parentId)
{
IEnumerable<FlavorConfig> pageTypes = await PageTypes.GetAllowedFlavors(parentId);
IEnumerable<FlavorConfig> pageTypes = await PageTypes.GetAllowedTypes(parentId);
return pageTypes.Any(x => x.Alias == model.Flavor);
}
@@ -31,8 +31,7 @@ public class PagesStore : TreeEntityStore<Page>, IPagesStore
return null;
}
Page model = await Empty();
model.Flavor = type.Alias;
Page model = await base.Empty(pageTypeAlias);
model.ParentId = parentId;
if (!await IsAllowedAsChild(model, parentId))
@@ -1,12 +0,0 @@
namespace zero.Persistence;
/// <summary>
/// A change token holds a reference to a database entity
/// This is used to verify change requests for entities in the zero backoffice
/// </summary>
public class ChangeToken : ISupportsDbConventions
{
public string Id { get; set; }
public string ReferenceId { get; set; }
}
@@ -7,6 +7,10 @@ public class ZeroDocumentSession : AsyncDocumentSession, IZeroDocumentSession
{
public IZeroDocumentSession Core { get; private set; }
public IDocumentSession Synchronous => _synchronous ?? (_synchronous = DocumentStore.OpenSession(DatabaseName));
IDocumentSession _synchronous;
public ZeroDocumentSession(DocumentStore documentStore, Guid id, SessionOptions options, string coreDatabase = null) : base(documentStore, id, options)
{
if (coreDatabase.HasValue())
@@ -28,6 +32,12 @@ public class ZeroDocumentSession : AsyncDocumentSession, IZeroDocumentSession
}
public bool IsDisposed { get; set; }
public override void Dispose()
{
_synchronous?.Dispose();
base.Dispose();
}
}
@@ -35,5 +45,7 @@ public interface IZeroDocumentSession : IAsyncDocumentSession
{
IZeroDocumentSession Core { get; }
IDocumentSession Synchronous { get; }
bool IsDisposed { get; }
}
+10 -1
View File
@@ -15,7 +15,16 @@ public class ZeroStore : IZeroStore
protected Dictionary<string, IZeroDocumentSession> ScopedSessions { get; set; } = new();
/// <inheritdoc />
public string ResolvedDatabase { get; set; }
string _resolvedDatabase = null;
public string ResolvedDatabase
{
get => _resolvedDatabase;
set
{
_resolvedDatabase = value;
Raven.ResolvedDatabase = value;
}
}
/// <inheritdoc />
+4 -4
View File
@@ -33,10 +33,10 @@ public abstract class EntityStore<T> : IEntityStore<T> where T : ZeroIdEntity, I
/// <inheritdoc />
public virtual Task<T> Empty(string flavor = null) => Operations.Empty<T>(flavor);
public virtual Task<T> Empty(string flavorAlias = null) => Operations.Empty<T>(flavorAlias);
/// <inheritdoc />
public virtual Task<TFlavor> Empty<TFlavor>(string flavor) where TFlavor : T, new() => Operations.Empty<T, TFlavor>(flavor);
public virtual Task<TFlavor> Empty<TFlavor>(string flavorAlias = null) where TFlavor : T, new() => Operations.Empty<T, TFlavor>(flavorAlias);
/// <inheritdoc />
public virtual Task<T> Load(string id, string changeVector = null) => Operations.Load<T>(id, changeVector);
@@ -106,12 +106,12 @@ public interface IEntityStore<T> where T : ZeroIdEntity, new()
/// <summary>
/// Get new instance of an entity (with an optional flavor)
/// </summary>
Task<T> Empty(string flavor = null);
Task<T> Empty(string flavorAlias = null);
/// <summary>
/// Get new instance of an entity with a specific flavor
/// </summary>
Task<TFlavor> Empty<TFlavor>(string flavor) where TFlavor : T, new();
Task<TFlavor> Empty<TFlavor>(string flavorAlias = null) where TFlavor : T, new();
/// <summary>
/// Get an entity by Id
+5 -1
View File
@@ -1,4 +1,6 @@
namespace zero.Stores;
using System.Text.Json.Serialization;
namespace zero.Stores;
/// <summary>
/// A flavor config holds information about a flavor implementation
@@ -8,6 +10,7 @@ public class FlavorConfig
/// <summary>
/// Type of the associated entity
/// </summary>
[JsonIgnore]
public Type FlavorType { get; private set; }
/// <summary>
@@ -31,6 +34,7 @@ public class FlavorConfig
public string Icon { get; set; }
[JsonIgnore]
public Func<FlavorConfig, object> Construct { get; set; }
+1 -1
View File
@@ -34,7 +34,7 @@ public class FlavorOptions
return provider.DefaultFlavor;
}
return provider.Flavors.First().Alias;
return null;
}
+14 -7
View File
@@ -3,28 +3,35 @@
public partial class StoreOperations : IStoreOperations
{
/// <inheritdoc />
public virtual Task<T> Empty<T>(string flavor = null) where T : ZeroIdEntity, ISupportsFlavors, new() => Empty<T, T>(flavor);
public virtual Task<T> Empty<T>(string flavorAlias = null) where T : ZeroIdEntity, ISupportsFlavors, new() => Empty<T, T>(flavorAlias);
/// <inheritdoc />
public virtual Task<TFlavor> Empty<T, TFlavor>(string flavor)
public virtual Task<TFlavor> Empty<T, TFlavor>(string flavorAlias = null)
where T : ZeroIdEntity, ISupportsFlavors, new()
where TFlavor : T, new()
{
// throw if this entity is not allowed to be created without a flavor
if (flavor.IsNullOrEmpty() && !Flavors.CanUseWithoutFlavors<T>())
if (flavorAlias.IsNullOrEmpty() && !Flavors.CanUseWithoutFlavors<T>())
{
throw new ArgumentException("Can not create instance of an entity which is configured to to be only used as a flavor", nameof(flavor));
string defaultFlavor = Flavors.DefaultFlavorFor<T>();
if (defaultFlavor.IsNullOrEmpty())
{
throw new ArgumentException("Can not create instance of an entity which is configured to to be only used as a flavor", nameof(flavorAlias));
}
flavorAlias = defaultFlavor;
}
// return default instance if no flavor is required
if (flavor.IsNullOrEmpty())
if (flavorAlias.IsNullOrEmpty())
{
return Task.FromResult<TFlavor>(new());
}
// try to load and construct a specific flavor
FlavorConfig config = Flavors.Get<T>(flavor);
FlavorConfig config = Flavors.Get<T>(flavorAlias);
TFlavor result = config?.Construct(config) as TFlavor;
if (result == null)
@@ -32,7 +39,7 @@ public partial class StoreOperations : IStoreOperations
return Task.FromResult<TFlavor>(default);
}
result.Flavor = flavor;
result.Flavor = flavorAlias;
return Task.FromResult(result);
}
}
+3 -2
View File
@@ -114,12 +114,13 @@ public interface IStoreOperations
/// <summary>
/// Get new instance of an entity (with an optional flavor)
/// </summary>
Task<T> Empty<T>(string flavor = null) where T : ZeroIdEntity, ISupportsFlavors, new();
Task<T> Empty<T>(string flavorAlias = null) where T : ZeroIdEntity, ISupportsFlavors, new();
/// <summary>
/// Get new instance of an entity with a specific flavor
/// </summary>
Task<TFlavor> Empty<T, TFlavor>(string flavor)
/// <param name="flavorAlias">Optional alias. If left out the default flavor is used (if configured)</param>
Task<TFlavor> Empty<T, TFlavor>(string flavorAlias = null)
where T : ZeroIdEntity, ISupportsFlavors, new()
where TFlavor : T, new();