new API project

This commit is contained in:
2021-11-29 18:25:50 +01:00
parent 192a0d795a
commit 346a674306
85 changed files with 940 additions and 410 deletions
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Abstractions;
namespace zero.Api.Abstractions;
public class PickerProvider<T> : IPickerProvider<T> where T : ZeroIdEntity, new()
{
@@ -83,7 +83,7 @@ public class PickerProvider<T> : IPickerProvider<T> where T : ZeroIdEntity, new(
{
return new()
{
HasError = true,
//HasError = true,
Icon = "fth-alert-circle color-red",
Id = id,
Name = "@errors.preview.notfound",
+23
View File
@@ -0,0 +1,23 @@
namespace zero.Api;
public struct ApiRequestHints
{
public ApiResponsePreference ResponsePreference { get; set; }
}
/// <summary>
/// Preference for POST + PUT requests
/// see https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#75-standard-request-headers
/// </summary>
public enum ApiResponsePreference
{
/// <summary>
/// Returns a minimal repsonse for inserts and updates
/// </summary>
Minimal = 0,
/// <summary>
/// Returns status as well as model for inserts and updates
/// </summary>
Representation = 1
}
+11
View File
@@ -0,0 +1,11 @@
using zero.Api.Modules.Search;
namespace zero.Api.Configuration;
public class ApiOptions
{
/// <summary>
/// Configure search maps
/// </summary>
public SearchOptions Search { get; set; } = new();
}
@@ -0,0 +1,11 @@
namespace zero.Api.Configuration;
public static class BackofficeConstants
{
public static class HttpErrors
{
public const string NoIdMatchOnUpdate = "The Id as part of the URL does not match the Id of the model";
public const string IdNotFound = "Could not find persisted model for the given Id";
}
}
@@ -0,0 +1,57 @@
namespace zero.Api.Extensions;
public static class IMapperProfileExtensions
{
/// <summary>
/// Map data for a zero entity BasicModel
/// </summary>
public static void MapBasicData<TSource, TDestination>(this IMapperProfile profile, TSource source, TDestination target)
where TSource : ZeroEntity
where TDestination : BasicModel<TSource>
{
target.Id = source.Id;
target.Name = source.Name;
target.Alias = source.Alias;
target.Key = source.Key;
target.IsActive = source.IsActive;
target.CreatedDate = source.CreatedDate;
}
/// <summary>
/// Map data for a zero entity DiplayModel
/// </summary>
public static void MapDisplayData<TSource, TDestination>(this IMapperProfile profile, TSource source, TDestination target)
where TSource : ZeroEntity
where TDestination : DisplayModel<TSource>
{
target.Id = source.Id;
target.Name = source.Name;
target.Alias = source.Alias;
target.Key = source.Key;
target.IsActive = source.IsActive;
target.CreatedDate = source.CreatedDate;
target.Sort = source.Sort;
target.Hash = source.Hash;
target.LastModifiedById = source.LastModifiedById;
target.LastModifiedDate = source.LastModifiedDate;
target.CreatedById = source.CreatedById;
target.LanguageId = source.LanguageId;
}
/// <summary>
/// Map data for a zero entity SaveModel
/// </summary>
public static void MapSaveData<TSource, TDestination>(this IMapperProfile profile, TSource source, TDestination target)
where TSource : SaveModel<TDestination>
where TDestination : ZeroEntity
{
target.Name = source.Name;
target.Alias = source.Alias;
target.Key = source.Key;
target.Sort = source.Sort;
target.IsActive = source.IsActive;
}
}
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc.Filters;
namespace zero.Backoffice.Controllers;
namespace zero.Api.Controllers;
public class BackofficeFilterAttribute : IActionFilter
{
@@ -11,7 +11,7 @@ public class BackofficeFilterAttribute : IActionFilter
{
Type type = context.Controller.GetType();
if (typeof(ZeroBackofficeController).IsAssignableFrom(type))
if (typeof(ZeroApiController).IsAssignableFrom(type))
{
if (context.HttpContext.Request.Query.TryGetValue(SCOPE_KEY, out var scope))
{
@@ -2,7 +2,7 @@
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
namespace zero.Backoffice;
namespace zero.Api;
internal class BackofficeJsonSerlializerSettings : JsonSerializerSettings
{
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace zero.Backoffice.Controllers;
namespace zero.Api.Controllers;
public class CanEditAttribute : TypeFilterAttribute
{
@@ -2,7 +2,7 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Net.Http.Headers;
namespace zero.Backoffice.Controllers;
namespace zero.Api.Controllers;
/// <summary>
/// Ensures that the request is not cached by the browser
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace zero.Backoffice.Controllers;
namespace zero.Api.Controllers;
public class ModelStateValidationFilterAttribute : IActionFilter
{
+29
View File
@@ -0,0 +1,29 @@
namespace zero.Api.Models;
public abstract class BasicModel<T> : ZeroIdEntity where T : ZeroEntity
{
/// <summary>
/// Full name of the entity
/// </summary>
public string Name { get; set; }
/// <summary>
/// Alias (non-unique) which can be used in the frontend and URLs
/// </summary>
public string Alias { get; set; }
/// <summary>
/// A key which can be used to query this entity in code
/// </summary>
public string Key { get; set; }
/// <summary>
/// Whether the entity is visible in the frontend
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// Date of creation
/// </summary>
public DateTimeOffset CreatedDate { get; set; }
}
+120
View File
@@ -0,0 +1,120 @@
namespace zero.Api.Models;
public abstract class DisplayModel : ZeroIdEntity
{
/// <summary>
/// Meta data
/// </summary>
public DisplayModelConfiguration Configuration { get; set; } = new();
/// <summary>
/// Permissions for this entity
/// </summary>
public DisplayModelPermissions Permissions { get; set; } = new();
}
public abstract class DisplayModel<T> : DisplayModel where T : ZeroEntity
{
/// <summary>
/// Full name of the entity
/// </summary>
public string Name { get; set; }
/// <summary>
/// Alias (non-unique) which can be used in the frontend and URLs
/// </summary>
public string Alias { get; set; }
/// <summary>
/// A key which can be used to query this entity in code
/// </summary>
public string Key { get; set; }
/// <summary>
/// Sort order
/// </summary>
public uint Sort { get; set; }
/// <summary>
/// Whether the entity is visible in the frontend
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// Unique hash for this entity (primarily used for routing)
/// </summary>
public string Hash { get; set; }
/// <summary>
/// Backoffice user who last modified this content
/// </summary>
public string LastModifiedById { get; set; }
/// <summary>
/// Date of last modification
/// </summary>
public DateTimeOffset LastModifiedDate { get; set; }
/// <summary>
/// Backoffice user who created this content
/// </summary>
public string CreatedById { get; set; }
/// <summary>
/// Date of creation
/// </summary>
public DateTimeOffset CreatedDate { get; set; }
/// <summary>
/// Language of the entity
/// </summary>
public string LanguageId { get; set; }
}
public class DisplayModelConfiguration
{
/// <summary>
/// Wehther this entity is application aware
/// </summary>
public bool IsAppAware { get; set; }
/// <summary>
/// Whether this entity can be shared across applications (only for IsAppAware=true)
/// </summary>
public bool CanBeShared { get; set; }
public bool IsShared { get; set; }
/// <summary>
/// The change token maps to a database entity which holds ID and collection of the model to edit
/// If these values do not match the entity on save it is rejected
/// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60);
/// </summary>
public string Token { get; set; }
}
public class DisplayModelPermissions
{
/// <summary>
/// Whether an entity of this type can be created
/// </summary>
public bool CanCreate { get; set; }
/// <summary>
/// Whether an entity of this type can be created in the shared app space
/// </summary>
public bool CanCreateShared { get; set; }
/// <summary>
/// Whether this entity can be edited or only viewed
/// </summary>
public bool CanEdit { get; set; }
/// <summary>
/// Whether this entity can be deleted
/// </summary>
public bool CanDelete { get; set; }
}
@@ -1,6 +1,6 @@
using System.Linq.Expressions;
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
public class ListQuery<T>
{
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
public enum ListQueryDisplayType
{
@@ -2,7 +2,7 @@
using Raven.Client.Documents.Linq;
using Raven.Client.Documents.Session;
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
public static class ListQueryExtensions
{
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
public enum ListQueryOrderType
{
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
public class ListQueryDateRange
{
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
public interface IListSpecificQuery { }
@@ -1,6 +1,6 @@
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
public struct PickerModel
public class PickerModel
{
public string Id { get; set; }
+21
View File
@@ -0,0 +1,21 @@
namespace zero.Api.Models;
public class PickerPreviewModel
{
public string Id { get; set; }
public string Icon { get; set; }
public string Text { get; set; }
public string Name { get; set; }
//public static PickerPreviewModel NotFound(string id) => new()
//{
// HasError = true,
// Icon = "fth-alert-circle color-red",
// Id = id,
// Name = "@errors.preview.notfound",
// Text = "@errors.preview.notfound_text"
//};
}
+29
View File
@@ -0,0 +1,29 @@
namespace zero.Api.Models;
public abstract class SaveModel<T> : ZeroIdEntity where T : ZeroEntity
{
/// <summary>
/// Full name of the entity
/// </summary>
public string Name { get; set; }
/// <summary>
/// Alias (non-unique) which can be used in the frontend and URLs
/// </summary>
public string Alias { get; set; }
/// <summary>
/// A key which can be used to query this entity in code
/// </summary>
public string Key { get; set; }
/// <summary>
/// Sort order
/// </summary>
public uint Sort { get; set; }
/// <summary>
/// Whether the entity is visible in the frontend
/// </summary>
public bool IsActive { get; set; }
}
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
/// <summary>
/// Represents an item in a tree
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Models;
namespace zero.Api.Models;
/// <summary>
/// The modifier displays a small icon (with hover text) next to the main item icon
@@ -1,8 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections;
namespace zero.Backoffice.Modules.Countries;
namespace zero.Api.Modules.Countries;
public class CountriesController : ZeroBackofficeApiController
public class CountriesController : ZeroApiController
{
protected ICountryStore Store { get; set; }
@@ -18,7 +19,7 @@ public class CountriesController : ZeroBackofficeApiController
[HttpGet("empty")]
[ZeroAuthorize(CountryPermissions.Create)]
public virtual async Task<ActionResult<CountryDisplay>> Empty()
public virtual async Task<ActionResult<DisplayModel>> Empty()
{
Country model = await Store.Empty();
@@ -33,24 +34,26 @@ public class CountriesController : ZeroBackofficeApiController
[HttpGet("pick")]
[ZeroAuthorize(CountryPermissions.Read)]
public virtual async Task<ActionResult<Paged>> Pick(ListQuery<Country> query)
public virtual async Task<ActionResult<Paged>> Pick([FromQuery] ListQuery<Country> query)
{
query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name);
return await Picker.PickFrom(query.Page, query.PageSize, query);
Paged<Country> result = await Store.Load<zero_Backoffice_Countries_Listing>(query.Page, query.PageSize, q => q.Filter(query));
return Mapper.Map<Country, PickerModel>(result);
}
[HttpGet("pick")]
[HttpGet("pickpreview")]
[ZeroAuthorize(CountryPermissions.Read)]
public virtual async Task<ActionResult<List<PickerPreviewModel>>> Pick(string[] ids)
public virtual async Task<ActionResult<IEnumerable>> Pick([FromQuery] string[] ids)
{
return await Picker.GetPreviews(ids);
Dictionary<string, Country> model = await Store.Load(ids);
return Mapper.Map<Country, PickerPreviewModel>(model);
}
[HttpGet("{id}")]
[ZeroAuthorize(CountryPermissions.Read)]
public virtual async Task<ActionResult<CountryDisplay>> Get(string id, string changeVector = null)
public virtual async Task<ActionResult<DisplayModel>> Get(string id, string changeVector = null)
{
Country model = await Store.Load(id, changeVector);
@@ -65,7 +68,7 @@ public class CountriesController : ZeroBackofficeApiController
[HttpGet("")]
[ZeroAuthorize(CountryPermissions.Read)]
public virtual async Task<ActionResult<Paged>> Get(ListQuery<Country> query)
public virtual async Task<ActionResult<Paged>> Get([FromQuery] ListQuery<Country> query)
{
query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name);
Paged<Country> result = await Store.Load<zero_Backoffice_Countries_Listing>(query.Page, query.PageSize, q => q.Filter(query));
@@ -86,7 +89,12 @@ public class CountriesController : ZeroBackofficeApiController
return CreatedAtAction(nameof(CountriesController.Get), new { id = model.Id }, mappedResult);
}
return result.WithoutModel();
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
{
return result.WithoutModel();
}
return Mapper.Map<Country, CountryDisplay>(result);
}
@@ -99,11 +107,24 @@ public class CountriesController : ZeroBackofficeApiController
return BadRequest(BackofficeConstants.HttpErrors.NoIdMatchOnUpdate);
}
Country model = Mapper.Map<CountrySave, Country>(updateModel);
model.Id = id;
Country model = await Store.Load(id);
if (model == null)
{
return BadRequest(BackofficeConstants.HttpErrors.IdNotFound);
}
Mapper.Map(updateModel, model);
Result<Country> result = await Store.Update(model);
return result.WithoutModel();
if (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<Country, CountryDisplay>(result);
}
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Modules.Countries;
namespace zero.Api.Modules.Countries;
public class CountryPermissions : PermissionProvider
{
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Modules.Countries;
namespace zero.Api.Modules.Countries;
public class CountryPickerProvider : PickerProvider<Country>
{
@@ -1,6 +1,6 @@
using Raven.Client.Documents.Indexes;
namespace zero.Backoffice.Modules.Countries;
namespace zero.Api.Modules.Countries;
public class zero_Backoffice_Countries_Listing : ZeroIndex<Country>
{
@@ -1,9 +1,7 @@
namespace zero.Backoffice.Modules.Countries;
namespace zero.Api.Modules.Countries;
public class CountryBasic : BasicModel<Country>
{
public bool IsActive { get; set; }
public bool IsPreferred { get; set; }
public string Code { get; set; }
@@ -1,12 +1,8 @@
namespace zero.Backoffice.Modules.Countries;
namespace zero.Api.Modules.Countries;
public class CountryDisplay : DisplayModel<Country>
{
public string Name { get; set; }
public bool IsPreferred { get; set; }
public string Code { get; set; }
public string LanguageId { get; set; }
}
@@ -0,0 +1,49 @@
namespace zero.Api.Modules.Countries;
public class CountryMapperProfile : ZeroMapperProfile
{
public override void Configure(IZeroMapper mapper)
{
mapper.Define<Country, CountryBasic>((source, ctx) => new(), Map);
mapper.Define<Country, CountryDisplay>((source, ctx) => new(), Map);
mapper.Define<Country, PickerModel>((source, ctx) => new(), Map);
mapper.Define<Country, PickerPreviewModel>((source, ctx) => new(), Map);
mapper.Define<CountrySave, Country>((source, ctx) => new(), Map);
}
void Map(Country source, CountryBasic target, IZeroMapperContext ctx)
{
this.MapBasicData(source, target);
target.Code = source.Code;
target.IsPreferred = source.IsPreferred;
}
void Map(Country source, CountryDisplay target, IZeroMapperContext ctx)
{
this.MapDisplayData(source, target);
target.Code = source.Code;
target.IsPreferred = source.IsPreferred;
}
void Map(CountrySave source, Country target, IZeroMapperContext ctx)
{
this.MapSaveData(source, target);
target.Code = source.Code;
target.IsPreferred = source.IsPreferred;
}
void Map(Country source, PickerModel target, IZeroMapperContext ctx)
{
target.Id = source.Id;
target.Name = source.Name;
target.IsActive = source.IsActive;
}
void Map(Country source, PickerPreviewModel target, IZeroMapperContext ctx)
{
target.Id = source.Id;
target.Name = source.Name;
target.Icon = "flag-" + source.Code.ToLowerInvariant();
}
}
@@ -0,0 +1,8 @@
namespace zero.Api.Modules.Countries;
public class CountrySave : SaveModel<Country>
{
public bool IsPreferred { get; set; }
public string Code { get; set; }
}
@@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
namespace zero.Api.Modules.Countries;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddZeroBackofficeCountries(this IServiceCollection services)
{
services.AddScoped<IPickerProvider<Country>, CountryPickerProvider>();
services.AddSingleton<IPermissionProvider, CountryPermissions>();
services.AddSingleton<IMapperProfile, CountryMapperProfile>();
services.Configure<RavenOptions>(opts =>
{
opts.Indexes.Add<zero_Backoffice_Countries_Listing>();
});
return services;
}
}
@@ -2,7 +2,7 @@
using Raven.Client.Documents.Linq;
using Raven.Client.Documents.Session;
namespace zero.Backoffice.Modules.Pages;
namespace zero.Api.Modules.Pages;
public class PageTreeService : IPageTreeService
{
@@ -2,7 +2,7 @@
using Raven.Client.Documents.Queries;
using Raven.Client.Documents.Session;
namespace zero.Backoffice.Modules.Search;
namespace zero.Api.Modules.Search;
public class BackofficeSearchService : IBackofficeSearchService
{
@@ -1,6 +1,6 @@
//using Microsoft.AspNetCore.Mvc;
//namespace zero.Backoffice.Modules;
//namespace zero.Api.Modules;
//[ZeroAuthorize]
//public class SearchController : BackofficeController
@@ -1,6 +1,6 @@
using Raven.Client.Documents;
namespace zero.Backoffice.Modules.Search;
namespace zero.Api.Modules.Search;
public class SearchIndexMap
{
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Modules.Search;
namespace zero.Api.Modules.Search;
public class SearchOptions : List<SearchIndexMap>
{
@@ -1,4 +1,4 @@
namespace zero.Backoffice.Modules.Search;
namespace zero.Api.Modules.Search;
public class SearchResult
{
@@ -1,6 +1,6 @@
using Raven.Client.Documents;
namespace zero.Backoffice.Modules.Search;
namespace zero.Api.Modules.Search;
public class zero_Backoffice_Search : ZeroJavascriptIndex
{
@@ -0,0 +1,25 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using zero.Api.Modules.Countries;
using zero.Api.Modules.Pages;
using zero.Api.Modules.Search;
namespace zero.Api.Modules;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddZeroBackofficeModules(this IServiceCollection services, IConfiguration config)
{
services.AddZeroBackofficeCountries();
services.AddScoped<IPageTreeService, PageTreeService>();
services.AddScoped<IBackofficeSearchService, BackofficeSearchService>();
//services.Configure<RavenOptions>(opts =>
//{
// opts.Indexes.Add<zero_Backoffice_Search>();
//});
return services;
}
}
+60
View File
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace zero.Api;
public class ZeroApiPlugin : ZeroPlugin
{
internal Action<IServiceCollection, IConfiguration> PostConfigureServices = null;
public ZeroApiPlugin()
{
Options.Name = "zero.Api";
}
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<ApiOptions>().Bind(configuration.GetSection("Zero:Api")).Configure<IWebHostEnvironment>(ConfigureOptions);
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, ZeroBackofficeMvcOptions>());
//Mvc.AddNewtonsoftJson(x =>
//{
// // TODO this shall only be configurated for backoffice controllers
// BackofficeJsonSerlializerSettings.Setup(x.SerializerSettings);
//});
services.AddScoped<IZeroMapper, ZeroMapper>();
services.AddZeroBackofficeModules(configuration);
//services.AddTransient<ISectionsApi, SectionsApi>();
//services.AddTransient<ISettingsApi, SettingsApi>();
//services.AddScoped<IBackofficeSearchService, BackofficeSearchService>();
PostConfigureServices?.Invoke(services, configuration);
}
protected void ConfigureOptions(ApiOptions options, IWebHostEnvironment env)
{
options.Search.Enabled = true;
//Map<Page>().Display((x, res, opts) =>
//{
// PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias);
// if (pageType != null)
// {
// res.Icon = pageType.Icon;
// }
// res.Url = "/pages/edit/" + x.Id;
//});
//Map<MediaFolder>("fth-image");
}
}
+30
View File
@@ -0,0 +1,30 @@
using Microsoft.Extensions.DependencyInjection;
namespace zero.Api;
public static class ServiceCollectionExtensions
{
public static ZeroBuilder AddApi<T>(this ZeroBuilder builder) where T : ZeroApiPlugin, IZeroPlugin, new()
{
return builder.AddApi<T>(_ => { });
}
public static ZeroBuilder AddApi(this ZeroBuilder builder)
{
return builder.AddApi<ZeroApiPlugin>();
}
public static ZeroBuilder AddApi<T>(this ZeroBuilder builder, Action<ApiOptions> options) where T : ZeroApiPlugin, IZeroPlugin, new()
{
return builder.AddPlugin(services =>
{
T plugin = new();
plugin.PostConfigureServices = (services, configuration) =>
{
services.Configure<ApiOptions>(opts => options(opts));
};
return plugin;
});
}
}
+30
View File
@@ -0,0 +1,30 @@
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using zero.Applications;
global using zero.Architecture;
global using zero.Stores;
global using zero.Configuration;
global using zero.Context;
global using zero.Extensions;
global using zero.FileStorage;
global using zero.Identity;
global using zero.Localization;
global using zero.Mapper;
global using zero.Media;
global using zero.Models;
global using zero.Pages;
global using zero.Persistence;
global using zero.Routing;
global using zero.Utils;
global using zero.Api.Configuration;
global using zero.Api.Controllers;
global using zero.Api.Models;
global using zero.Api.Modules;
global using zero.Api.Abstractions;
global using zero.Api.Extensions;
+18
View File
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Api.Controllers;
[ApiController]
[ZeroAuthorize]
[DisableBrowserCache]
//[ServiceFilter(typeof(ModelStateValidationFilterAttribute))]
//[ServiceFilter(typeof(BackofficeFilterAttribute))]
public abstract class ZeroApiController : ControllerBase
{
IZeroMapper _mapper;
protected IZeroMapper Mapper => _mapper ?? (_mapper = HttpContext?.RequestServices?.GetService<IZeroMapper>());
protected ApiRequestHints Hints { get; set; } = new();
}
@@ -1,13 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace zero.Backoffice.Controllers;
namespace zero.Api.Controllers;
public class ZeroBackofficeControllerModelConvention : IApplicationModelConvention
{
readonly AttributeRouteModel RouteModel;
readonly Type BaseClass = typeof(ZeroBackofficeController);
readonly Type BaseClass = typeof(ZeroApiController);
public ZeroBackofficeControllerModelConvention(string backofficePath)
+19
View File
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace zero.Api;
internal class ZeroBackofficeMvcOptions : IConfigureOptions<MvcOptions>
{
IZeroOptions Options { get; set; }
public ZeroBackofficeMvcOptions(IZeroOptions options)
{
Options = options;
}
public void Configure(MvcOptions options)
{
options.Conventions.Add(new ZeroBackofficeControllerModelConvention(Options.ZeroPath));
}
}
+24
View File
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>zero.Api</PackageId>
<Version>0.1.0</Version>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Modules_legacy\**" />
<EmbeddedResource Remove="Modules_legacy\**" />
<None Remove="Modules_legacy\**" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\zero.Core\zero.Core.csproj" />
</ItemGroup>
</Project>
@@ -5,5 +5,7 @@ public static class BackofficeConstants
public static class HttpErrors
{
public const string NoIdMatchOnUpdate = "The Id as part of the URL does not match the Id of the model";
public const string IdNotFound = "Could not find persisted model for the given Id";
}
}
@@ -1,28 +0,0 @@
using AutoMapper;
namespace zero.Backoffice.Configuration;
/// <summary>
///
/// </summary>
public class BackofficeMaps
{
public MapperConfiguration MapperConfiguration { get; internal set; }
internal List<Action<IMapperConfigurationExpression>> Expressions { get; }
public void Configure(Action<IMapperConfigurationExpression> expression)
{
Expressions.Add(expression);
}
internal void Run(IMapperConfigurationExpression mapperConfig)
{
foreach (var expression in Expressions)
{
expression(mapperConfig);
}
}
}
@@ -1,8 +1,4 @@
using AutoMapper;
using System.Linq.Expressions;
using zero.Backoffice.Modules.Search;
namespace zero.Backoffice.Configuration;
namespace zero.Backoffice.Configuration;
public class BackofficeOptions
{
@@ -21,11 +17,6 @@ public class BackofficeOptions
/// </summary>
public ExternalServicesOptions ExternalServices { get; set; } = new();
/// <summary>
/// Configure search maps
/// </summary>
public SearchOptions Search { get; set; } = new();
/// <summary>
/// Options for configuring the vite development server
/// </summary>
@@ -40,9 +31,4 @@ public class BackofficeOptions
/// Language ISO codes which are supported by the zero backoffice
/// </summary>
public string[] SupportedLanguages { get; internal set; }
/// <summary>
/// Configuration for AutoMapper
/// </summary>
public BackofficeMaps Mapper { get; private set; } = new();
}
@@ -1,12 +0,0 @@
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
namespace zero.Backoffice.Controllers;
[ApiController]
//[ServiceFilter(typeof(ModelStateValidationFilterAttribute))]
//[ServiceFilter(typeof(BackofficeFilterAttribute))]
public abstract class ZeroBackofficeApiController : ZeroBackofficeController
{
protected IMapper Mapper { get; private set; }
}
@@ -1,10 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace zero.Backoffice.Controllers;
[ZeroAuthorize]
[DisableBrowserCache]
public abstract class ZeroBackofficeController : ControllerBase
{
}
@@ -1,81 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using System.IO;
using zero.Api.Controllers;
namespace zero.Backoffice.Controllers;
public static class ZeroBackofficeControllerExtensions
{
/// <summary>
/// Transform entities to a preview list
/// </summary>
public static List<PickerPreviewModel> TransformToPreviewModels<T>(this ZeroBackofficeController controller, Dictionary<string, T> items, Action<T, PickerPreviewModel> transform = null) where T : ZeroIdEntity
{
List<PickerPreviewModel> previews = new();
foreach (var item in items)
{
if (item.Value == null)
{
previews.Add(new PickerPreviewModel()
{
HasError = true,
Icon = "fth-alert-circle color-red",
Id = item.Key,
Name = "@errors.preview.notfound",
Text = "@errors.preview.notfound_text"
});
continue;
}
PickerPreviewModel model = new() { Id = item.Value.Id };
if (item.Value is ZeroEntity)
{
model.Name = (item.Value as ZeroEntity).Name;
}
transform?.Invoke(item.Value, model);
previews.Add(model);
}
return previews;
}
/// <summary>
/// Transform entities to a select list
/// </summary>
public static List<PickerModel> TransformToSelectModels<T>(this ZeroBackofficeController controller, IEnumerable<T> enumerable, Action<T, PickerModel> transform = null) where T : ZeroIdEntity
{
List<PickerModel> items = new();
foreach (T item in enumerable)
{
PickerModel model = new()
{
Id = item.Id
};
if (item is ZeroEntity)
{
model.Name = (item as ZeroEntity).Name;
model.IsActive = (item as ZeroEntity).IsActive;
}
transform?.Invoke(item, model);
items.Add(model);
}
return items;
}
/// <summary>
/// Provides a file stream for download in the browser
/// </summary>
public static IActionResult DownloadFile(this ZeroBackofficeController controller, Stream stream, string filename)
public static IActionResult DownloadFile(this ZeroApiController controller, Stream stream, string filename)
{
if (stream == null)
{
@@ -102,7 +37,7 @@ public static class ZeroBackofficeControllerExtensions
/// <summary>
/// Provides a file stream to the browser
/// </summary>
public static IActionResult File(this ZeroBackofficeController controller, FileStorage.FileResult file)
public static IActionResult File(this ZeroApiController controller, FileStorage.FileResult file)
{
if (file == null)
{
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using zero.Api.Controllers;
namespace zero.Backoffice.Controllers;
@@ -1,17 +0,0 @@
using AutoMapper;
namespace zero.Backoffice.Mapper;
public static class MapperExtensions
{
public static Paged<TDestination> Map<TSource, TDestination>(this IMapper mapper, Paged<TSource> source)
{
return source.MapTo(srcItem => mapper.Map<TSource, TDestination>(srcItem));
}
public static Result<TDestination> Map<TSource, TDestination>(this IMapper mapper, Result<TSource> source)
{
TDestination model = mapper.Map<TSource, TDestination>(source.Model);
return source.ConvertTo(model);
}
}
-16
View File
@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace zero.Backoffice.Mapper
{
public class ZeroMapper : AutoMapper.Mapper
{
public ZeroMapper(AutoMapper.IConfigurationProvider provider) : base(provider)
{
}
}
}
@@ -1,8 +0,0 @@
using AutoMapper;
namespace zero.Backoffice.Mapper;
public class ZeroMapperProfile : Profile
{
}
-6
View File
@@ -1,6 +0,0 @@
namespace zero.Backoffice.Models;
public abstract class BasicModel<T> : ZeroIdEntity where T : ZeroIdEntity
{
public string Name { get; set; }
}
-61
View File
@@ -1,61 +0,0 @@
namespace zero.Backoffice.Models;
public abstract class DisplayModel<T> : ZeroIdEntity where T : ZeroIdEntity
{
/// <summary>
/// Meta data
/// </summary>
public DisplayModelMeta Meta { get; set; } = new();
/// <summary>
/// Permissions for this entity
/// </summary>
public DisplayModelPermissions Permissions { get; set; } = new();
}
public class DisplayModelMeta
{
/// <summary>
/// Wehther this entity is application aware
/// </summary>
public bool IsAppAware { get; set; }
/// <summary>
/// Whether this entity can be shared across applications (only for IsAppAware=true)
/// </summary>
public bool CanBeShared { get; set; }
public bool IsShared { get; set; }
/// <summary>
/// The change token maps to a database entity which holds ID and collection of the model to edit
/// If these values do not match the entity on save it is rejected
/// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60);
/// </summary>
public string Token { get; set; }
}
public class DisplayModelPermissions
{
/// <summary>
/// Whether an entity of this type can be created
/// </summary>
public bool CanCreate { get; set; }
/// <summary>
/// Whether an entity of this type can be created in the shared app space
/// </summary>
public bool CanCreateShared { get; set; }
/// <summary>
/// Whether this entity can be edited or only viewed
/// </summary>
public bool CanEdit { get; set; }
/// <summary>
/// Whether this entity can be deleted
/// </summary>
public bool CanDelete { get; set; }
}
@@ -1,14 +0,0 @@
namespace zero.Backoffice.Models;
public struct PickerPreviewModel
{
public string Id { get; set; }
public string Icon { get; set; }
public string Text { get; set; }
public string Name { get; set; }
public bool HasError { get; set; }
}
-6
View File
@@ -1,6 +0,0 @@
namespace zero.Backoffice.Models;
public abstract class SaveModel<T> : ZeroIdEntity where T : ZeroIdEntity
{
}
@@ -1,11 +0,0 @@
namespace zero.Backoffice.Modules.Countries;
public class CountryMapperProfile : ZeroMapperProfile
{
public CountryMapperProfile()
{
CreateMap<Country, CountryBasic>();
CreateMap<Country, CountryDisplay>();
CreateMap<CountrySave, Country>();
}
}
@@ -1,14 +0,0 @@
using System;
namespace zero.Backoffice.Modules.Countries;
public class CountrySave : SaveModel<Country>
{
public string Name { get; set; }
public bool IsPreferred { get; set; }
public string Code { get; set; }
public string LanguageId { get; set; }
}
@@ -1,36 +0,0 @@
using AutoMapper;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using zero.Backoffice.Modules.Countries;
using zero.Backoffice.Modules.Pages;
using zero.Backoffice.Modules.Search;
namespace zero.Backoffice.Modules;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddZeroBackofficeModules(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<IPageTreeService, PageTreeService>();
services.AddScoped<IBackofficeSearchService, BackofficeSearchService>();
services.AddScoped<IPickerProvider<Country>, CountryPickerProvider>();
services.AddSingleton<IPermissionProvider, CountryPermissions>();
services.Configure<RavenOptions>(opts =>
{
opts.Indexes.Add<zero_Backoffice_Countries_Listing>();
opts.Indexes.Add<zero_Backoffice_Search>();
});
services.Configure<BackofficeOptions>(opts =>
{
opts.Mapper.Configure(maps =>
{
maps.AddProfile<CountryMapperProfile>();
});
});
return services;
}
}
+1 -20
View File
@@ -1,12 +1,7 @@
using AutoMapper;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System.IO;
using zero.Backoffice.Modules;
namespace zero.Backoffice;
@@ -25,7 +20,6 @@ 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>();
@@ -36,7 +30,6 @@ public class ZeroBackofficePlugin : ZeroPlugin
//});
services.AddZeroBackofficeUIComposition();
services.AddZeroBackofficeModules(configuration);
//services.AddTransient<ISectionsApi, SectionsApi>();
//services.AddTransient<ISettingsApi, SettingsApi>();
@@ -50,7 +43,6 @@ public class ZeroBackofficePlugin : ZeroPlugin
protected void ConfigureOptions(BackofficeOptions options, IWebHostEnvironment env)
{
options.Search.Enabled = true;
options.DevServer.WorkingDirectory = Path.Combine(env.ContentRootPath, "..", "Zero.Web.UI", "App");
options.IconSets.Add(new BackofficeIconSet()
@@ -63,16 +55,5 @@ public class ZeroBackofficePlugin : ZeroPlugin
options.SupportedLanguages = new string[2] { "en-US", "de-DE" };
options.DefaultLanguage = options.SupportedLanguages[0];
//Map<Page>().Display((x, res, opts) =>
//{
// PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias);
// if (pageType != null)
// {
// res.Icon = pageType.Icon;
// }
// res.Url = "/pages/edit/" + x.Id;
//});
//Map<MediaFolder>("fth-image");
}
}
+2 -5
View File
@@ -20,12 +20,9 @@ global using zero.Pages;
global using zero.Persistence;
global using zero.Routing;
global using zero.Utils;
global using zero.Mapper;
global using zero.Backoffice.Configuration;
global using zero.Backoffice.Controllers;
global using zero.Backoffice.Models;
global using zero.Backoffice.Modules;
global using zero.Backoffice.UIComposition;
global using zero.Backoffice.DevServer;
global using zero.Backoffice.Mapper;
global using zero.Backoffice.Abstractions;
global using zero.Backoffice.DevServer;
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using zero.Api.Controllers;
namespace zero.Backoffice;
+2 -9
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>zero.Backoffice</PackageId>
@@ -18,15 +18,8 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\zero.Core\zero.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Modules\Search\" />
<ProjectReference Include="..\zero.Api\zero.Api.csproj" />
</ItemGroup>
</Project>
+50
View File
@@ -0,0 +1,50 @@
namespace zero.Mapper;
public static class MapperExtensions
{
//public static TDestination Register<TSource, TDestination>(this IZeroMapper mapper, Action<TSource, TDestination, IZeroMapperContext> map) where TDestination : class, new()
//{
//}
//public static TDestination Register<TSource, TDestination>(this IZeroMapper mapper, Action<TSource, TDestination, IZeroMapperContext> map, TDestination destination)
//{
//}
public static TDestination Map<TSource, TDestination>(this IZeroMapper mapper, TSource source, TDestination destination)
{
return mapper.Map(source, typeof(TSource), destination);
}
public static TDestination Map<TSource, TDestination>(this IZeroMapper mapper, TSource source)
{
return mapper.Map<TDestination>(source, typeof(TSource), default);
}
public static Paged<TDestination> Map<TSource, TDestination>(this IZeroMapper mapper, Paged<TSource> source)
{
return source.MapTo(srcItem => mapper.Map<TSource, TDestination>(srcItem));
}
public static Result<TDestination> Map<TSource, TDestination>(this IZeroMapper mapper, Result<TSource> source)
{
TDestination model = mapper.Map<TSource, TDestination>(source.Model);
return source.ConvertTo(model);
}
public static Dictionary<string, TDestination> Map<TSource, TDestination>(this IZeroMapper mapper, Dictionary<string, TSource> source)
{
Dictionary<string, TDestination> model = new();
foreach ((string key, TSource sourceItem) in source)
{
model.Add(key, sourceItem == null ? default : mapper.Map<TSource, TDestination>(sourceItem));
}
return model;
}
}
@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
namespace zero.Mapper;
internal static class ServiceCollectionExtensions
{
public static IServiceCollection AddZeroMapper(this IServiceCollection services)
{
services.AddScoped<IZeroMapper, ZeroMapper>();
return services;
}
}
+166
View File
@@ -0,0 +1,166 @@
using System.Collections.Concurrent;
namespace zero.Mapper;
public class ZeroMapper : IZeroMapper
{
/// <summary>
/// Concurrent cache for all ctor definitions
/// </summary>
protected ConcurrentDictionary<Type, Dictionary<Type, Action<object, object, IZeroMapperContext>>> MapDefinitions { get; private set; } = new();
/// <summary>
/// Concurrent cache for all constructor definitions
/// </summary>
protected ConcurrentDictionary<Type, Dictionary<Type, Func<object, IZeroMapperContext, object>>> ConstructorDefinitions { get; private set; } = new();
public ZeroMapper(IEnumerable<IMapperProfile> profiles)
{
foreach (IMapperProfile profile in profiles)
{
profile.Configure(this);
}
}
/// <inheritdoc />
public void Define<TSource, TDestination>(Func<TSource, IZeroMapperContext, TDestination> ctor, Action<TSource, TDestination, IZeroMapperContext> map)
{
Type sourceType = typeof(TSource);
Type destinationType = typeof(TDestination);
var sourceMaps = MapDefinitions.GetOrAdd(sourceType, _ => new());
var sourceCtors = ConstructorDefinitions.GetOrAdd(sourceType, _ => new());
sourceCtors[destinationType] = (source, ctx) => ctor((TSource)source, ctx);
sourceMaps[destinationType] = (source, destination, ctx) => map((TSource)source, (TDestination)destination, ctx);
}
/// <inheritdoc />
public TDestination Map<TDestination>(object source, Type sourceType, TDestination destination = default)
{
if (source == null)
{
return default;
}
Type destinationType = typeof(TDestination);
ZeroMapperContext mapperContext = new(this);
var constructor = GetConstructor(sourceType, destinationType);
var map = GetMap(sourceType, destinationType);
if (constructor != null && map != null)
{
destination ??= (TDestination)constructor(source, mapperContext);
map(source, destination, mapperContext);
return destination;
}
// TODO enumerables
throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {destinationType.FullName}.");
}
protected virtual Func<object, IZeroMapperContext, object> GetConstructor(Type sourceType, Type targetType)
{
if (ConstructorDefinitions.TryGetValue(sourceType, out var sourceCtor) && sourceCtor.TryGetValue(targetType, out var ctor))
{
return ctor;
}
// we *may* run this more than once but it does not matter
ctor = null;
foreach (var (stype, sctors) in ConstructorDefinitions)
{
if (!stype.IsAssignableFrom(sourceType)) continue;
if (!sctors.TryGetValue(targetType, out ctor)) continue;
sourceCtor = sctors;
break;
}
if (ctor == null)
{
return null;
}
ConstructorDefinitions.AddOrUpdate(sourceType, sourceCtor, (k, v) =>
{
// Add missing constructors
foreach (var c in sourceCtor)
{
if (!v.ContainsKey(c.Key))
{
v.Add(c.Key, c.Value);
}
}
return v;
});
return ctor;
}
protected virtual Action<object, object, IZeroMapperContext> GetMap(Type sourceType, Type targetType)
{
if (MapDefinitions.TryGetValue(sourceType, out var sourceMap) && sourceMap.TryGetValue(targetType, out var map))
{
return map;
}
// we *may* run this more than once but it does not matter
map = null;
foreach (var (stype, smap) in MapDefinitions)
{
if (!stype.IsAssignableFrom(sourceType)) continue;
// TODO: consider looking for assignable types for target too?
if (!smap.TryGetValue(targetType, out map)) continue;
sourceMap = smap;
break;
}
if (map == null) return null;
if (MapDefinitions.ContainsKey(sourceType))
{
foreach (var m in sourceMap)
{
if (!MapDefinitions[sourceType].TryGetValue(m.Key, out _))
{
MapDefinitions[sourceType].Add(m.Key, m.Value);
}
}
}
else
{
MapDefinitions[sourceType] = sourceMap;
}
return map;
}
}
public interface IZeroMapper
{
/// <summary>
/// Register a new mapping
/// </summary>
void Define<TSource, TDestination>(Func<TSource, IZeroMapperContext, TDestination> ctor, Action<TSource, TDestination, IZeroMapperContext> map);
/// <summary>
/// Map a source type to the destination type
/// </summary>
TDestination Map<TDestination>(object source, Type sourceType, TDestination destination = default);
}
+22
View File
@@ -0,0 +1,22 @@
namespace zero.Mapper;
public class ZeroMapperContext : IZeroMapperContext
{
/// <inheritdoc />
public IZeroMapper Mapper { get; }
public ZeroMapperContext(IZeroMapper mapper)
{
Mapper = mapper;
}
}
public interface IZeroMapperContext
{
/// <summary>
/// Access to the runtime mapper
/// </summary>
IZeroMapper Mapper { get; }
}
+16
View File
@@ -0,0 +1,16 @@
namespace zero.Mapper;
public abstract class ZeroMapperProfile : IMapperProfile
{
/// <inheritdoc />
public abstract void Configure(IZeroMapper mapper);
}
public interface IMapperProfile
{
/// <summary>
/// Configure maps for this profile
/// </summary>
void Configure(IZeroMapper mapper);
}
+1
View File
@@ -16,6 +16,7 @@ global using zero.FileStorage;
global using zero.Identity;
global using zero.Localization;
global using zero.Mails;
global using zero.Mapper;
global using zero.Media;
global using zero.Models;
global using zero.Pages;
+1
View File
@@ -50,6 +50,7 @@ public class ZeroBuilder
services.AddZeroRendering();
services.AddZeroRouting(Configuration);
services.AddZeroStores();
services.AddZeroMapper();
//if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1")
//{
+8 -2
View File
@@ -5,7 +5,7 @@ VisualStudioVersion = 17.0.31912.275
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Commerce", "plugins\zero.Commerce\zero.Commerce.csproj", "{63A8AD15-15F0-4555-BD62-C38B4465FFC1}"
EndProject
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "zero.Backoffice.UI", "zero.Backoffice.UI\", "{8CE1A69A-35DB-4748-AE1C-86F5BC2E11D4}"
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "zero.Web.UI", "zero.Backoffice.UI\", "{8CE1A69A-35DB-4748-AE1C-86F5BC2E11D4}"
ProjectSection(WebsiteProperties) = preProject
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5"
Debug.AspNetCompiler.VirtualPath = "/localhost_56763"
@@ -43,7 +43,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Backoffice", "zero.Bac
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Core", "zero.Core\zero.Core.csproj", "{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Web", "zero.Web\zero.Web.csproj", "{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Web", "zero.Web\zero.Web.csproj", "{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Api", "zero.Api\zero.Api.csproj", "{C0442589-3915-43B5-9F78-2D5F9E8D8C94}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -79,6 +81,10 @@ Global
{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Release|Any CPU.Build.0 = Release|Any CPU
{C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE