using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Raven.Client.Documents.Indexes; namespace zero.Api.Abstractions; public class ZeroApiEntityStoreOperations where TModel : ZeroEntity, new() where TStore : IEntityStore { protected TStore Store { get; set; } IZeroMapper _mapper; protected IZeroMapper Mapper => _mapper ?? (_mapper = Controller.HttpContext?.RequestServices?.GetService()); protected ZeroApiController Controller { get; private set; } public ZeroApiEntityStoreOperations(ZeroApiController controller, TStore store) { Controller = controller; Store = store; Store.Config.IncludeInactive = true; } protected string GetActionMethod { get; set; } = "Get"; protected virtual string GetAction(TModel model) { return "/"; // TODO Url.Action does not work anymore, // probably due to ZeroApiControllerModelConvention, which rewrites AttributeRouteModel //return Url.Action(GetActionMethod, ControllerContext.ActionDescriptor.ControllerName, new { id = model.Id, changeVector = default(string) }); } public async Task> EmptyModel(string flavorAlias = null, Action modify = null) { TModel model = await Store.Empty(flavorAlias); if (model == null) { return Controller.NotFound(); } T result = Mapper.Map(model); modify?.Invoke(result); return result; } public async Task> EmptyModel(string flavorAlias = null, Action modify = null) { TModel model = await Store.Empty(flavorAlias); if (model == null) { return Controller.NotFound(); } modify?.Invoke(model); return model; } public async Task> GetModel(string id, string changeVector = null) { TModel model = await Store.Load(id, changeVector); if (model == null) { return Controller.NotFound(); } Controller.HttpContext.Items[ApiConstants.ChangeToken] = Store.GetChangeToken(model); return Mapper.Map(model); } public async Task> GetModel(string id, string changeVector = null) { TModel model = await Store.Load(id, changeVector); if (model == null) { return Controller.NotFound(); } Controller.HttpContext.Items[ApiConstants.ChangeToken] = Store.GetChangeToken(model); return model; } public async Task> GetModels(ListQuery query) { query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate); query.SearchSelector ??= x => x.Name; Paged result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query)); return Mapper.Map(result); } public async Task> GetModels(ListQuery query) { query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate); query.SearchSelector ??= x => x.Name; Paged result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query)); return result; } public async Task> GetModelsByIndex(ListQuery query) where TIndex : AbstractCommonApiForIndexes, new() { query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate); query.SearchSelector ??= x => x.Name; Paged result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query)); return Mapper.Map(result); } public async Task> GetModelsByIndex(ListQuery query) where TIndex : AbstractCommonApiForIndexes, new() { query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate); query.SearchSelector ??= x => x.Name; Paged result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query)); return result; } public async Task> CreateModel(T saveModel) where T : ISupportsFlavors { TModel emptyModel = saveModel.Flavor.IsNullOrEmpty() ? await Store.Empty() : await Store.Empty(saveModel.Flavor); TModel model = Mapper.Map(saveModel, emptyModel); Result result = await Store.Create(model); bool minimalResponse = Controller.Hints.ResponsePreference == ApiResponsePreference.Minimal; if (result.IsSuccess) { Result mappedResult = Mapper.Map(result); return Controller.Created(GetAction(model), minimalResponse ? null : mappedResult); } return result.WithoutModel(); } public async Task> CreateModel(TModel saveModel) { Result result = await Store.Create(saveModel); bool minimalResponse = Controller.Hints.ResponsePreference == ApiResponsePreference.Minimal; if (result.IsSuccess) { return Controller.Created(GetAction(saveModel), minimalResponse ? null : saveModel); } return result.WithoutModel(); } public async Task> UpdateModel(string id, T updateModel, string changeToken = null) where T : ZeroIdEntity { if (id != updateModel.Id) { return Controller.BadRequest(Result.Fail(nameof(id), "@errors.onupdate.noidmatch")); } TModel model = await Store.Load(id); if (model == null) { return Controller.BadRequest(Result.Fail(nameof(id), "@errors.idnotfound")); } string storedChangeToken = Store.GetChangeToken(model); if (!changeToken.IsNullOrEmpty() && storedChangeToken != changeToken) { return Controller.BadRequest(Result.Fail("@errors.onupdate.changetokenmismatch")); } Mapper.Map(updateModel, model); Result result = await Store.Update(model); if (Controller.Hints.ResponsePreference == ApiResponsePreference.Minimal) { return result.WithoutModel(); } // TODO add Preference-Applied header, see https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#76-standard-response-headers return Mapper.Map(result); } public async Task> UpdateModel(string id, TModel updateModel, string changeToken = null) { if (id != updateModel.Id) { return Controller.BadRequest(Result.Fail(nameof(id), "@errors.onupdate.noidmatch")); } // TODO throws error on save: Attempted to associate a different object with id ... // we need to map props to new object //TModel model = await Store.Load(id); //if (model == null) //{ // return BadRequest(Result.Fail(nameof(id), "@errors.idnotfound")); //} //string storedChangeToken = Store.GetChangeToken(model); //if (!changeToken.IsNullOrEmpty() && storedChangeToken != changeToken) //{ // return BadRequest(Result.Fail("@errors.onupdate.changetokenmismatch")); //} Result result = await Store.Update(updateModel); if (Controller.Hints.ResponsePreference == ApiResponsePreference.Minimal) { return result.WithoutModel(); } return result; } public async Task> SortModels(string[] ids) { return await Store.Sort(ids); } public async Task> DeleteModel(string id) { TModel model = await Store.Load(id); if (model == null) { return Controller.NotFound(); } Result result = await Store.Delete(model); return result.WithoutModel(); } }