diff --git a/zero.Core/Api/PageTreeApi.cs b/zero.Core/Api/PageTreeApi.cs index d48f1aaf..e0f0bda1 100644 --- a/zero.Core/Api/PageTreeApi.cs +++ b/zero.Core/Api/PageTreeApi.cs @@ -12,7 +12,7 @@ using zero.Core.Options; namespace zero.Core.Api { - public class PageTreeApi : AppAwareBackofficeApi, IPageTreeApi where T : IPage + public class PageTreeApi : AppAwareBackofficeApi, IPageTreeApi { public PageTreeApi(IBackofficeStore store) : base(store) { } @@ -26,8 +26,8 @@ namespace zero.Core.Api using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) { - IList pages = await session - .Query() + IList pages = await session + .Query() .Scope(Scope) .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) .ToListAsync(); @@ -60,7 +60,7 @@ namespace zero.Core.Api // build tree - foreach (T page in pages) + foreach (IPage page in pages) { PageType pageType = pageTypes.FirstOrDefault(x => x.Alias == page.PageTypeAlias); @@ -112,7 +112,7 @@ namespace zero.Core.Api } - public interface IPageTreeApi where T : IPage + public interface IPageTreeApi { /// /// Get all children for the current parent page (or root if empty) diff --git a/zero.Core/Api/PagesApi.cs b/zero.Core/Api/PagesApi.cs index e03968be..13f81ac0 100644 --- a/zero.Core/Api/PagesApi.cs +++ b/zero.Core/Api/PagesApi.cs @@ -13,7 +13,7 @@ using zero.Core.Options; namespace zero.Core.Api { - public class PagesApi : AppAwareBackofficeApi, IPagesApi where T : IPage + public class PagesApi : AppAwareBackofficeApi, IPagesApi { const string RECYCLE_BIN_GROUP = "pages"; @@ -30,10 +30,9 @@ namespace zero.Core.Api /// - public async Task GetById(string id) + public async Task GetById(string id) { - var res = await GetById(id); // this works - return await GetById(id); + return await GetById(id); } @@ -79,16 +78,16 @@ namespace zero.Core.Api /// - public async Task> Save(T model) + public async Task> Save(IPage model) { return await SaveModel(model, null); } /// - public async Task> SaveSorting(string[] sortedIds) + public async Task> SaveSorting(string[] sortedIds) { - Dictionary items = await GetByIds(sortedIds); + Dictionary items = await GetByIds(sortedIds); uint index = 0; // TODO check if all items are valid @@ -108,23 +107,23 @@ namespace zero.Core.Api await session.SaveChangesAsync(); } - return EntityResult.Success(); + return EntityResult.Success(); } /// - public async Task> Move(string id, string parentId) + public async Task> Move(string id, string parentId) { - T model = await GetById(id); + IPage model = await GetById(id); model.ParentId = parentId; return await Save(model); } /// - public async Task> Copy(string id, string destinationId, bool includeDescendants = false) + public async Task> Copy(string id, string destinationId, bool includeDescendants = false) { - T model = await GetById(id); + IPage model = await GetById(id); string baseId = model.Id; @@ -143,7 +142,7 @@ namespace zero.Core.Api { Pages_WithChildren.Result childrenResult = await session.Query() .ProjectInto() - .Include(x => x.Id) + .Include(x => x.Id) .Scope(Scope) .Where(x => x.Id == oldParentId) .FirstOrDefaultAsync(); @@ -153,11 +152,11 @@ namespace zero.Core.Api return; } - Dictionary childrenPages = await session.LoadAsync(childrenResult.ChildrenIds); + Dictionary childrenPages = await session.LoadAsync(childrenResult.ChildrenIds); foreach (var child in childrenPages) { - T childPage = child.Value.Clone(); + IPage childPage = child.Value.Clone(); childPage.Id = null; childPage.IsActive = false; childPage.ParentId = newParentId; @@ -178,14 +177,14 @@ namespace zero.Core.Api await session.SaveChangesAsync(); } - return EntityResult.Success(model); + return EntityResult.Success(model); } /// public async Task> Delete(string id, bool moveToRecycleBin = true) { - IList pages = await GetByIdWithDescendants(id); + IList pages = await GetByIdWithDescendants(id); string[] ids = pages.Select(x => x.Id).ToArray(); if (moveToRecycleBin) @@ -193,7 +192,7 @@ namespace zero.Core.Api await RecycleBinApi.Add(pages, RECYCLE_BIN_GROUP); } - await DeleteByIds(ids); + await DeleteByIds(ids); return EntityResult.Success(ids); } @@ -268,11 +267,11 @@ namespace zero.Core.Api /// /// Get a page with all its descendants /// - async Task> GetByIdWithDescendants(string id) + async Task> GetByIdWithDescendants(string id) { - List items = new List(); + List items = new List(); - T model = await GetById(id); + IPage model = await GetById(id); if (model == null) { @@ -288,7 +287,7 @@ namespace zero.Core.Api { Pages_WithChildren.Result childrenResult = await session.Query() .ProjectInto() - .Include(x => x.Id) + .Include(x => x.Id) .Scope(Scope) .Where(x => x.Id == parentId) .FirstOrDefaultAsync(); @@ -298,7 +297,7 @@ namespace zero.Core.Api return; } - Dictionary childrenPages = await session.LoadAsync(childrenResult.ChildrenIds); + Dictionary childrenPages = await session.LoadAsync(childrenResult.ChildrenIds); foreach (var child in childrenPages) { @@ -315,12 +314,12 @@ namespace zero.Core.Api } - public interface IPagesApi where T : IPage + public interface IPagesApi { /// /// Get page by Id /// - Task GetById(string id); + Task GetById(string id); /// /// Get all available page types @@ -340,22 +339,22 @@ namespace zero.Core.Api /// /// Creates or updates a page /// - Task> Save(T model); + Task> Save(IPage model); /// /// Update sorting of pages on a specific level /// - Task> SaveSorting(string[] sortedIds); + Task> SaveSorting(string[] sortedIds); /// /// Move a page to a new parent /// - Task> Move(string id, string parentId); + Task> Move(string id, string parentId); /// /// Copies a page (with optional descendants) to a new location /// - Task> Copy(string id, string destinationId, bool includeDescendants = false); + Task> Copy(string id, string destinationId, bool includeDescendants = false); /// /// Deletes a page by Id (with all it's descendants) diff --git a/zero.Core/Entities/Pages/Page.cs b/zero.Core/Entities/Pages/Page.cs index dbc3e493..52ce6b0a 100644 --- a/zero.Core/Entities/Pages/Page.cs +++ b/zero.Core/Entities/Pages/Page.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.AspNetCore.Mvc; +using System; using zero.Core.Attributes; namespace zero.Core.Entities diff --git a/zero.Core/Entities/ZeroEntityInterfaceBinder.cs b/zero.Core/Entities/ZeroEntityInterfaceBinder.cs new file mode 100644 index 00000000..cdb0c28c --- /dev/null +++ b/zero.Core/Entities/ZeroEntityInterfaceBinder.cs @@ -0,0 +1,81 @@ +//using Microsoft.AspNetCore.Mvc.ModelBinding; +//using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +//using System; +//using System.Collections.Generic; +//using System.Text; +//using System.Threading.Tasks; + +//namespace zero.Core.Entities +//{ +// public class ZeroEntityInterfaceBinderProvider : IModelBinderProvider +// { +// public IModelBinder GetBinder(ModelBinderProviderContext context) +// { +// if (context == null) +// { +// throw new ArgumentNullException(nameof(context)); +// } + +// if (context.Metadata.ModelType == typeof(IPage)) +// { +// return new BinderTypeModelBinder(typeof(ZeroEntityInterfaceBinder)); +// } + +// return null; +// } +// } + + +// public class ZeroEntityInterfaceBinder : IModelBinder +// { +// //private readonly AuthorContext _context; + +// //public ZeroEntityInterfaceBinder(AuthorContext context) +// //{ +// // _context = context; +// //} + +// public Task BindModelAsync(ModelBindingContext bindingContext) +// { +// if (bindingContext == null) +// { +// throw new ArgumentNullException(nameof(bindingContext)); +// } + +// var modelName = bindingContext.OriginalModelName; + +// // Try to fetch the value of the argument by name +// var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + +// if (valueProviderResult == ValueProviderResult.None) +// { +// return Task.CompletedTask; +// } + +// bindingContext.ModelState.SetModelValue(modelName, valueProviderResult); + +// var value = valueProviderResult.FirstValue; + +// // Check if the argument value is null or empty +// //if (string.IsNullOrEmpty(value)) +// //{ +// // return Task.CompletedTask; +// //} + +// //if (!int.TryParse(value, out var id)) +// //{ +// // // Non-integer arguments result in model state errors +// // bindingContext.ModelState.TryAddModelError( +// // modelName, "Author Id must be an integer."); + +// // return Task.CompletedTask; +// //} + +// // Model will be null if not found, including for +// // out of range id values (0, -3, etc.) +// //var model = _context.Authors.Find(id); +// bindingContext.Result = ModelBindingResult.Success(new { name = "tobi" }); +// return Task.CompletedTask; +// } +// } +//} diff --git a/zero.Debug/Controllers/TestController.cs b/zero.Debug/Controllers/TestController.cs index 30be5b1a..3a08071a 100644 --- a/zero.Debug/Controllers/TestController.cs +++ b/zero.Debug/Controllers/TestController.cs @@ -9,9 +9,11 @@ using zero.Core.Api; using zero.Core.Entities; using zero.Core.Extensions; using zero.Core.Utils; +using zero.Web.Filters; namespace zero.Debug.Controllers { + [ServiceFilter(typeof(ModelStateValidationFilterAttribute))] public class TestController : Controller { private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; @@ -75,6 +77,10 @@ namespace zero.Debug.Controllers } + [HttpPost] + public IActionResult SaveTest([FromBody] IPage model) => Json(model); + + [HttpGet] diff --git a/zero.Web/Controllers/BackofficeController.cs b/zero.Web/Controllers/BackofficeController.cs index 30c87b4f..ebeda02f 100644 --- a/zero.Web/Controllers/BackofficeController.cs +++ b/zero.Web/Controllers/BackofficeController.cs @@ -28,30 +28,14 @@ namespace zero.Web.Controllers protected IZeroOptions Options => _options ?? (_options = HttpContext?.RequestServices?.GetService()); protected IToken Token => _token ?? (_token = HttpContext?.RequestServices?.GetService()); - static JsonSerializerSettings JsonSettings; - static JsonSerializerSettings TypedJsonSettings; - static Type AppAwareType = typeof(IAppAwareEntity); static Type AppAwareShareableType = typeof(IAppAwareShareableEntity); - static BackofficeController() - { - JsonSettings = new BackofficeJsonSerlializerSettings(false); - TypedJsonSettings = new BackofficeJsonSerlializerSettings(true); - } - - /// /// Creates a Microsoft.AspNetCore.Mvc.JsonResult object that serializes the specified data object to JSON. /// - public override JsonResult Json(object data) => Json(data, false); - - - /// - /// Creates a Microsoft.AspNetCore.Mvc.JsonResult object that serializes the specified data object to JSON. - /// - public JsonResult Json(object data, bool typed) => Json(data, typed ? TypedJsonSettings : JsonSettings); + public JsonResult Json(object data, bool typed) => Json(data); /// diff --git a/zero.Web/Controllers/PageTreeController.cs b/zero.Web/Controllers/PageTreeController.cs index aa79fd68..e37d7bb8 100644 --- a/zero.Web/Controllers/PageTreeController.cs +++ b/zero.Web/Controllers/PageTreeController.cs @@ -5,11 +5,11 @@ using zero.Core.Entities; namespace zero.Web.Controllers { - public class PageTreeController : BackofficeController where T : IPage + public class PageTreeController : BackofficeController { - IPageTreeApi Api; + IPageTreeApi Api; - public PageTreeController(IPageTreeApi api) + public PageTreeController(IPageTreeApi api) { Api = api; } diff --git a/zero.Web/Controllers/PagesController.cs b/zero.Web/Controllers/PagesController.cs index 747ab580..e21d7c71 100644 --- a/zero.Web/Controllers/PagesController.cs +++ b/zero.Web/Controllers/PagesController.cs @@ -2,19 +2,22 @@ using System.Threading.Tasks; using zero.Core.Api; using zero.Core.Entities; +using zero.Core.Extensions; using zero.Web.Models; namespace zero.Web.Controllers { - public class PagesController : BackofficeController where T : IPage, new() + public class PagesController : BackofficeController { - IPagesApi Api; + IPagesApi Api; IRevisionsApi RevisionsApi; + IPage Blueprint; - public PagesController(IPagesApi api, IRevisionsApi revisionsApi) + public PagesController(IPagesApi api, IRevisionsApi revisionsApi, IPage blueprint) { Api = api; RevisionsApi = revisionsApi; + Blueprint = blueprint; } @@ -25,25 +28,28 @@ namespace zero.Web.Controllers public async Task GetById([FromQuery] string id) { - T entity = await Api.GetById(id); + IPage entity = await Api.GetById(id); - return Edit>(new PageEditModel() + return Edit>(new PageEditModel() { Entity = entity, - Revisions = await RevisionsApi.GetPaged(id), + Revisions = await RevisionsApi.GetPaged(id), PageType = Api.GetPageType(entity.PageTypeAlias) }); } - public IActionResult GetEmpty(string type, string parent = null) => Edit(new T() + public IActionResult GetEmpty(string type, string parent = null) { - PageTypeAlias = type, - ParentId = parent - }); + IPage entity = Blueprint.Clone(); + entity.PageTypeAlias = type; + entity.ParentId = parent; - public async Task GetRevisions([FromQuery] string id, [FromQuery] int page = 1) => Json(await RevisionsApi.GetPaged(id, page)); + return Edit(entity); + } - public async Task Save([FromBody] T model) => Json(await Api.Save(model)); + public async Task GetRevisions([FromQuery] string id, [FromQuery] int page = 1) => Json(await RevisionsApi.GetPaged(id, page)); + + public async Task Save([FromBody] IPage model) => Json(await Api.Save(model)); [HttpPost] public async Task SaveSorting([FromBody] string[] ids) => Json(await Api.SaveSorting(ids)); diff --git a/zero.Web/Defaults/ZeroBackofficePlugin.cs b/zero.Web/Defaults/ZeroBackofficePlugin.cs index 7a5ed78d..9b63b8a7 100644 --- a/zero.Web/Defaults/ZeroBackofficePlugin.cs +++ b/zero.Web/Defaults/ZeroBackofficePlugin.cs @@ -32,11 +32,14 @@ namespace zero.Web.Defaults services.AddTransient(typeof(ITranslationsApi), typeof(TranslationsApi)); services.AddTransient(typeof(ITranslationsApi<>), typeof(TranslationsApi<>)); services.AddTransient(typeof(ITranslationsApiFacade), typeof(TranslationsApiFacade)); - services.AddTransient(typeof(IPagesApi<>), typeof(PagesApi<>)); - services.AddTransient(typeof(IPageTreeApi<>), typeof(PageTreeApi<>)); + //services.AddTransient(typeof(IPagesApi<>), typeof(PagesApi<>)); + //services.AddTransient(typeof(IPageTreeApi<>), typeof(PageTreeApi<>)); services.AddTransient(typeof(IUserApi<>), typeof(UserApi<>)); services.AddTransient(typeof(IRecycleBinApi), typeof(RecycleBinApi)); - // services.AddTransient(typeof(IRecycleBinApi), typeof(RecycleBinApi)); + // services.AddTransient(typeof(IRecycleBinApi), typeof(RecycleBinApi)); + + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/zero.Web/ZeroBuilder.cs b/zero.Web/ZeroBuilder.cs index 6581dd33..aab3c978 100644 --- a/zero.Web/ZeroBuilder.cs +++ b/zero.Web/ZeroBuilder.cs @@ -4,6 +4,9 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; using Raven.Client.Documents; using Raven.Client.Documents.Indexes; using System; @@ -42,7 +45,10 @@ namespace zero.Web public ZeroBuilder(IServiceCollection services, IConfiguration configuration, Action setupAction) { Services = services; - Mvc = services.AddMvc(); + Mvc = services.AddMvc(opts => + { + //opts.ModelBinderProviders.Insert(0, new ZeroEntityInterfaceBinderProvider()); + }); Configuration = configuration; // create startup options @@ -74,7 +80,15 @@ namespace zero.Web // configure MVC - Mvc.AddNewtonsoftJson(); + Mvc.AddNewtonsoftJson(x => + { + // TODO this shall only be configurated for backoffice controllers + x.SerializerSettings.Converters.Add(new IsoDateTimeConverter() { DateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" }); + x.SerializerSettings.Converters.Add(new StringEnumConverter(new CamelCaseNamingStrategy())); + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.TypeNameHandling = TypeNameHandling.Objects; + }); if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") { Mvc.AddRazorRuntimeCompilation();