Merge branch 'v8/dev' into v8/contrib

This commit is contained in:
Sebastiaan Janssen
2020-01-06 14:29:05 +01:00
112 changed files with 2265 additions and 3185 deletions

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Logging;
@@ -43,8 +44,15 @@ namespace Umbraco.Core.Composing
var componentType = component.GetType();
using (_logger.DebugDuration<ComponentCollection>($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
component.Terminate();
component.DisposeIfDisposable();
try
{
component.Terminate();
component.DisposeIfDisposable();
}
catch (Exception ex)
{
_logger.Error(componentType, ex, "Error while terminating component {ComponentType}.", componentType.FullName);
}
}
}
}

View File

@@ -185,6 +185,7 @@ namespace Umbraco.Core.Migrations.Upgrade
// to 8.6.0
To<AddPropertyTypeValidationMessageColumns>("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}");
To<MissingContentVersionsIndexes>("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
//FINAL
}

View File

@@ -0,0 +1,24 @@
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
{
public class MissingContentVersionsIndexes : MigrationBase
{
public MissingContentVersionsIndexes(IMigrationContext context) : base(context)
{
}
public override void Migrate()
{
Create
.Index("IX_" + ContentVersionDto.TableName + "_NodeId")
.OnTable(ContentVersionDto.TableName)
.OnColumn("nodeId")
.Ascending()
.OnColumn("current")
.Ascending()
.WithOptions().NonClustered()
.Do();
}
}
}

View File

@@ -19,6 +19,7 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("nodeId")]
[ForeignKey(typeof(ContentDto))]
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current")]
public int NodeId { get; set; }
[Column("versionDate")] // TODO: db rename to 'updateDate'
@@ -30,7 +31,6 @@ namespace Umbraco.Core.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.Null)]
public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero
// TODO: we need an index on this it is used almost always in querying and sorting
[Column("current")]
public bool Current { get; set; }

View File

@@ -16,7 +16,6 @@ using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -506,7 +505,7 @@ AND umbracoNode.id <> @id",
/// <summary>
/// Corrects the property type variations for the given entity
/// to make sure the property type variation is compatible with the
/// variation set on the entity itself.
/// variation set on the entity itself.
/// </summary>
/// <param name="entity">Entity to correct properties for</param>
private void CorrectPropertyTypeVariations(IContentTypeComposition entity)
@@ -754,7 +753,7 @@ AND umbracoNode.id <> @id",
//we don't need to move the names! this is because we always keep the invariant names with the name of the default language.
//however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :(
// if we want these SQL statements back, look into GIT history
// if we want these SQL statements back, look into GIT history
}
}
@@ -1033,7 +1032,7 @@ AND umbracoNode.id <> @id",
//keep track of this node/lang to mark or unmark a culture as edited
var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>();
//keep track of which node to mark or unmark as edited
//keep track of which node to mark or unmark as edited
var editedDocument = new Dictionary<int, bool>();
var nodeId = -1;
var propertyTypeId = -1;

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using NPoco;
using Umbraco.Core.Cache;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
@@ -75,7 +74,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (ids.Any())
sql.WhereIn<NodeDto>(x => x.NodeId, ids);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql));
return MapDtosToContent(Database.Fetch<DocumentDto>(sql), false,
// load everything
true, true, true, true);
}
protected override IEnumerable<IContent> PerformGetByQuery(IQuery<IContent> query)
@@ -87,7 +88,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
AddGetByQueryOrderBy(sql);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql));
return MapDtosToContent(Database.Fetch<DocumentDto>(sql), false,
// load everything
true, true, true, true);
}
private void AddGetByQueryOrderBy(Sql<ISqlContext> sql)
@@ -226,7 +229,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.OrderByDescending<ContentVersionDto>(x => x.Current)
.AndByDescending<ContentVersionDto>(x => x.VersionDate);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql), true);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql), true, true, true, true, true);
}
public override IEnumerable<IContent> GetAllVersionsSlim(int nodeId, int skip, int take)
@@ -236,7 +239,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.OrderByDescending<ContentVersionDto>(x => x.Current)
.AndByDescending<ContentVersionDto>(x => x.VersionDate);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql), true, true).Skip(skip).Take(take);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql), true,
// load bare minimum
false, false, false, false).Skip(skip).Take(take);
}
public override IContent GetVersion(int versionId)
@@ -832,7 +837,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
return GetPage<DocumentDto>(query, pageIndex, pageSize, out totalRecords,
x => MapDtosToContent(x),
x => MapDtosToContent(x, false,
// load properties but nothing else
true, false, false, true),
filterSql,
ordering);
}
@@ -919,7 +926,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (ids.Length > 0)
sql.WhereIn<NodeDto>(x => x.UniqueId, ids);
return _outerRepo.MapDtosToContent(Database.Fetch<DocumentDto>(sql));
return _outerRepo.MapDtosToContent(Database.Fetch<DocumentDto>(sql), false,
// load everything
true, true, true, true);
}
protected override IEnumerable<IContent> PerformGetByQuery(IQuery<IContent> query)
@@ -977,7 +986,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
AddGetByQueryOrderBy(sql);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql));
return MapDtosToContent(Database.Fetch<DocumentDto>(sql),
// load the bare minimum
false, false, false, false, false);
}
/// <inheritdoc />
@@ -993,7 +1004,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
AddGetByQueryOrderBy(sql);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql));
return MapDtosToContent(Database.Fetch<DocumentDto>(sql),
// load the bare minimum
false, false, false, false, false);
}
#endregion
@@ -1056,7 +1069,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return base.ApplySystemOrdering(ref sql, ordering);
}
private IEnumerable<IContent> MapDtosToContent(List<DocumentDto> dtos, bool withCache = false, bool slim = false)
private IEnumerable<IContent> MapDtosToContent(List<DocumentDto> dtos,
bool withCache,
bool loadProperties,
bool loadTemplates,
bool loadSchedule,
bool loadVariants)
{
var temps = new List<TempContent<Content>>();
var contentTypes = new Dictionary<int, IContentType>();
@@ -1089,7 +1107,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType);
if (!slim)
if (loadTemplates)
{
// need templates
var templateId = dto.DocumentVersionDto.TemplateId;
@@ -1114,49 +1132,71 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
temps.Add(temp);
}
if (!slim)
Dictionary<int, ITemplate> templates = null;
if (loadTemplates)
{
// load all required templates in 1 query, and index
var templates = _templateRepository.GetMany(templateIds.ToArray())
templates = _templateRepository.GetMany(templateIds.ToArray())
.ToDictionary(x => x.Id, x => x);
}
IDictionary<int, PropertyCollection> properties = null;
if (loadProperties)
{
// load all properties for all documents from database in 1 query - indexed by version id
var properties = GetPropertyCollections(temps);
var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray());
properties = GetPropertyCollections(temps);
}
// assign templates and properties
foreach (var temp in temps)
var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray());
// assign templates and properties
foreach (var temp in temps)
{
if (loadTemplates)
{
// set the template ID if it matches an existing template
if (temp.Template1Id.HasValue && templates.ContainsKey(temp.Template1Id.Value))
temp.Content.TemplateId = temp.Template1Id;
if (temp.Template2Id.HasValue && templates.ContainsKey(temp.Template2Id.Value))
temp.Content.PublishTemplateId = temp.Template2Id;
}
// set properties
// set properties
if (loadProperties)
{
if (properties.ContainsKey(temp.VersionId))
temp.Content.Properties = properties[temp.VersionId];
else
throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'.");
}
if (loadSchedule)
{
// load in the schedule
if (schedule.TryGetValue(temp.Content.Id, out var s))
temp.Content.ContentSchedule = s;
}
}
// set variations, if varying
temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList();
if (temps.Count > 0)
if (loadVariants)
{
// load all variations for all documents from database, in one query
var contentVariations = GetContentVariations(temps);
var documentVariations = GetDocumentVariations(temps);
foreach (var temp in temps)
SetVariations(temp.Content, contentVariations, documentVariations);
// set variations, if varying
temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList();
if (temps.Count > 0)
{
// load all variations for all documents from database, in one query
var contentVariations = GetContentVariations(temps);
var documentVariations = GetDocumentVariations(temps);
foreach (var temp in temps)
SetVariations(temp.Content, contentVariations, documentVariations);
}
}
foreach(var c in content)
foreach (var c in content)
c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946)
return content;

View File

@@ -14,6 +14,7 @@ using System.Runtime.InteropServices;
[assembly: InternalsVisibleTo("Umbraco.Web")]
[assembly: InternalsVisibleTo("Umbraco.Web.UI")]
[assembly: InternalsVisibleTo("Umbraco.Examine")]
[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")]
[assembly: InternalsVisibleTo("Umbraco.Tests")]
[assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")]

View File

@@ -128,6 +128,7 @@
</Compile>
-->
<Compile Include="AssemblyExtensions.cs" />
<Compile Include="Migrations\Upgrade\V_8_6_0\MissingContentVersionsIndexes.cs" />
<Compile Include="SystemLock.cs" />
<Compile Include="Attempt.cs" />
<Compile Include="AttemptOfTResult.cs" />

View File

@@ -0,0 +1,36 @@
using System;
using System.Reflection;
using Semver;
namespace Umbraco.ModelsBuilder.Embedded
{
/// <summary>
/// Manages API version handshake between client and server.
/// </summary>
public class ApiVersion
{
/// <summary>
/// Initializes a new instance of the <see cref="ApiVersion"/> class.
/// </summary>
/// <param name="executingVersion">The currently executing version.</param>
/// <exception cref="ArgumentNullException"></exception>
internal ApiVersion(SemVersion executingVersion)
{
Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion));
}
private static SemVersion CurrentAssemblyVersion
=> SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion);
/// <summary>
/// Gets the currently executing API version.
/// </summary>
public static ApiVersion Current { get; }
= new ApiVersion(CurrentAssemblyVersion);
/// <summary>
/// Gets the executing version of the API.
/// </summary>
public SemVersion Version { get; }
}
}

View File

@@ -0,0 +1,17 @@
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.ModelsBuilder.Embedded.BackOffice
{
/// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used
/// </summary>
// ReSharper disable once UnusedMember.Global - This is typed scanned
public class ContentTypeModelValidator : ContentTypeModelValidatorBase<DocumentTypeSave, PropertyTypeBasic>
{
public ContentTypeModelValidator(IModelsBuilderConfig config) : base(config)
{
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web.Editors;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.ModelsBuilder.Embedded.BackOffice
{
public abstract class ContentTypeModelValidatorBase<TModel, TProperty> : EditorValidator<TModel>
where TModel : ContentTypeSave<TProperty>
where TProperty : PropertyTypeBasic
{
private readonly IModelsBuilderConfig _config;
public ContentTypeModelValidatorBase(IModelsBuilderConfig config)
{
_config = config;
}
protected override IEnumerable<ValidationResult> Validate(TModel model)
{
//don't do anything if we're not enabled
if (!_config.Enable) yield break;
var properties = model.Groups.SelectMany(x => x.Properties)
.Where(x => x.Inherited == false)
.ToArray();
foreach (var prop in properties)
{
var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop));
if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant())
yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[]
{
$"Groups[{model.Groups.IndexOf(propertyGroup)}].Properties[{propertyGroup.Properties.IndexOf(prop)}].Alias"
});
//we need to return the field name with an index so it's wired up correctly
var groupIndex = model.Groups.IndexOf(propertyGroup);
var propertyIndex = propertyGroup.Properties.IndexOf(prop);
var validationResult = ValidateProperty(prop, groupIndex, propertyIndex);
if (validationResult != null)
yield return validationResult;
}
}
private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex)
{
//don't let them match any properties or methods in IPublishedContent
//TODO: There are probably more!
var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray();
var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray();
var alias = property.Alias;
if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias))
return new ValidationResult(
$"The alias {alias} is a reserved term and cannot be used", new[]
{
$"Groups[{groupIndex}].Properties[{propertyIndex}].Alias"
});
return null;
}
}
}

View File

@@ -0,0 +1,61 @@
using System.Text;
using Umbraco.ModelsBuilder.Embedded.Configuration;
namespace Umbraco.ModelsBuilder.Embedded.BackOffice
{
internal class DashboardReport
{
private readonly IModelsBuilderConfig _config;
private readonly OutOfDateModelsStatus _outOfDateModels;
private readonly ModelsGenerationError _mbErrors;
public DashboardReport(IModelsBuilderConfig config, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors)
{
_config = config;
_outOfDateModels = outOfDateModels;
_mbErrors = mbErrors;
}
public bool CanGenerate() => _config.ModelsMode.SupportsExplicitGeneration();
public bool AreModelsOutOfDate() => _outOfDateModels.IsOutOfDate;
public string LastError() => _mbErrors.GetLastError();
public string Text()
{
if (!_config.Enable)
return "Version: " + ApiVersion.Current.Version + "<br />&nbsp;<br />ModelsBuilder is disabled<br />(the .Enable key is missing, or its value is not 'true').";
var sb = new StringBuilder();
sb.Append("Version: ");
sb.Append(ApiVersion.Current.Version);
sb.Append("<br />&nbsp;<br />");
sb.Append("ModelsBuilder is enabled, with the following configuration:");
sb.Append("<ul>");
sb.Append("<li>The <strong>models factory</strong> is ");
sb.Append(_config.EnableFactory || _config.ModelsMode == ModelsMode.PureLive
? "enabled"
: "not enabled. Umbraco will <em>not</em> use models");
sb.Append(".</li>");
sb.Append(_config.ModelsMode != ModelsMode.Nothing
? $"<li><strong>{_config.ModelsMode} models</strong> are enabled.</li>"
: "<li>No models mode is specified: models will <em>not</em> be generated.</li>");
sb.Append($"<li>Models namespace is {_config.ModelsNamespace}.</li>");
sb.Append("<li>Tracking of <strong>out-of-date models</strong> is ");
sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled");
sb.Append(".</li>");
sb.Append("</ul>");
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,17 @@
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.ModelsBuilder.Embedded.BackOffice
{
/// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used
/// </summary>
// ReSharper disable once UnusedMember.Global - This is typed scanned
public class MediaTypeModelValidator : ContentTypeModelValidatorBase<MediaTypeSave, PropertyTypeBasic>
{
public MediaTypeModelValidator(IModelsBuilderConfig config) : base(config)
{
}
}
}

View File

@@ -0,0 +1,17 @@
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.ModelsBuilder.Embedded.BackOffice
{
/// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used
/// </summary>
// ReSharper disable once UnusedMember.Global - This is typed scanned
public class MemberTypeModelValidator : ContentTypeModelValidatorBase<MemberTypeSave, MemberPropertyTypeBasic>
{
public MemberTypeModelValidator(IModelsBuilderConfig config) : base(config)
{
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Web.Hosting;
using Umbraco.Core.Exceptions;
using Umbraco.ModelsBuilder.Embedded.Building;
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web.Editors;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.ModelsBuilder.Embedded.BackOffice
{
/// <summary>
/// API controller for use in the Umbraco back office with Angular resources
/// </summary>
/// <remarks>
/// We've created a different controller for the backoffice/angular specifically this is to ensure that the
/// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to
/// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats.
/// </remarks>
[UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)]
public class ModelsBuilderDashboardController : UmbracoAuthorizedJsonController
{
private readonly IModelsBuilderConfig _config;
private readonly ModelsGenerator _modelGenerator;
private readonly OutOfDateModelsStatus _outOfDateModels;
private readonly ModelsGenerationError _mbErrors;
private readonly DashboardReport _dashboardReport;
public ModelsBuilderDashboardController(IModelsBuilderConfig config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors)
{
//_umbracoServices = umbracoServices;
_config = config;
_modelGenerator = modelsGenerator;
_outOfDateModels = outOfDateModels;
_mbErrors = mbErrors;
_dashboardReport = new DashboardReport(config, outOfDateModels, mbErrors);
}
// invoked by the dashboard
// requires that the user is logged into the backoffice and has access to the settings section
// beware! the name of the method appears in modelsbuilder.controller.js
[System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers!
public HttpResponseMessage BuildModels()
{
try
{
var config = _config;
if (!config.ModelsMode.SupportsExplicitGeneration())
{
var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." };
return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter);
}
var bin = HostingEnvironment.MapPath("~/bin");
if (bin == null)
throw new PanicException("bin is null.");
// EnableDllModels will recycle the app domain - but this request will end properly
_modelGenerator.GenerateModels();
_mbErrors.Clear();
}
catch (Exception e)
{
_mbErrors.Report("Failed to build models.", e);
}
return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter);
}
// invoked by the back-office
// requires that the user is logged into the backoffice and has access to the settings section
[System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers!
public HttpResponseMessage GetModelsOutOfDateStatus()
{
var status = _outOfDateModels.IsEnabled
? _outOfDateModels.IsOutOfDate
? new OutOfDateStatus { Status = OutOfDateType.OutOfDate }
: new OutOfDateStatus { Status = OutOfDateType.Current }
: new OutOfDateStatus { Status = OutOfDateType.Unknown };
return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter);
}
// invoked by the back-office
// requires that the user is logged into the backoffice and has access to the settings section
// beware! the name of the method appears in modelsbuilder.controller.js
[System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers!
public HttpResponseMessage GetDashboard()
{
return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter);
}
private Dashboard GetDashboardResult()
{
return new Dashboard
{
Enable = _config.Enable,
Text = _dashboardReport.Text(),
CanGenerate = _dashboardReport.CanGenerate(),
OutOfDateModels = _dashboardReport.AreModelsOutOfDate(),
LastError = _dashboardReport.LastError(),
};
}
[DataContract]
internal class BuildResult
{
[DataMember(Name = "success")]
public bool Success;
[DataMember(Name = "message")]
public string Message;
}
[DataContract]
internal class Dashboard
{
[DataMember(Name = "enable")]
public bool Enable;
[DataMember(Name = "text")]
public string Text;
[DataMember(Name = "canGenerate")]
public bool CanGenerate;
[DataMember(Name = "outOfDateModels")]
public bool OutOfDateModels;
[DataMember(Name = "lastError")]
public string LastError;
}
internal enum OutOfDateType
{
OutOfDate,
Current,
Unknown = 100
}
[DataContract]
internal class OutOfDateStatus
{
[DataMember(Name = "status")]
public OutOfDateType Status { get; set; }
}
}
}

View File

@@ -1,14 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Embedded.Configuration;
namespace Umbraco.ModelsBuilder.Building
namespace Umbraco.ModelsBuilder.Embedded.Building
{
// NOTE
// The idea was to have different types of builder, because I wanted to experiment with
@@ -22,10 +17,10 @@ namespace Umbraco.ModelsBuilder.Building
/// </summary>
internal abstract class Builder
{
private readonly IList<TypeModel> _typeModels;
protected Dictionary<string, string> ModelsMap { get; } = new Dictionary<string, string>();
protected ParseResult ParseResult { get; }
// the list of assemblies that will be 'using' by default
protected readonly IList<string> TypesUsing = new List<string>
@@ -37,8 +32,7 @@ namespace Umbraco.ModelsBuilder.Building
"Umbraco.Core.Models",
"Umbraco.Core.Models.PublishedContent",
"Umbraco.Web",
"Umbraco.ModelsBuilder",
"Umbraco.ModelsBuilder.Umbraco",
"Umbraco.ModelsBuilder.Embedded"
};
/// <summary>
@@ -55,10 +49,10 @@ namespace Umbraco.ModelsBuilder.Building
/// <summary>
/// Gets the list of models to generate.
/// </summary>
/// <returns>The models to generate, ie those that are not ignored.</returns>
/// <returns>The models to generate</returns>
public IEnumerable<TypeModel> GetModelsToGenerate()
{
return _typeModels.Where(x => !x.IsContentIgnored);
return _typeModels;
}
/// <summary>
@@ -67,90 +61,39 @@ namespace Umbraco.ModelsBuilder.Building
/// <remarks>Includes those that are ignored.</remarks>
internal IList<TypeModel> TypeModels => _typeModels;
/// <summary>
/// Initializes a new instance of the <see cref="Builder"/> class with a list of models to generate
/// and the result of code parsing.
/// </summary>
/// <param name="typeModels">The list of models to generate.</param>
/// <param name="parseResult">The result of code parsing.</param>
protected Builder(IList<TypeModel> typeModels, ParseResult parseResult)
{
_typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels));
ParseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult));
Prepare();
}
/// <summary>
/// Initializes a new instance of the <see cref="Builder"/> class with a list of models to generate,
/// the result of code parsing, and a models namespace.
/// </summary>
/// <param name="typeModels">The list of models to generate.</param>
/// <param name="parseResult">The result of code parsing.</param>
/// <param name="modelsNamespace">The models namespace.</param>
protected Builder(IList<TypeModel> typeModels, ParseResult parseResult, string modelsNamespace)
: this(typeModels, parseResult)
protected Builder(IModelsBuilderConfig config, IList<TypeModel> typeModels)
{
_typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels));
Config = config ?? throw new ArgumentNullException(nameof(config));
// can be null or empty, we'll manage
ModelsNamespace = modelsNamespace;
ModelsNamespace = Config.ModelsNamespace;
// but we want it to prepare
Prepare();
}
// for unit tests only
protected Builder()
{ }
protected IModelsBuilderConfig Config { get; }
/// <summary>
/// Prepares generation by processing the result of code parsing.
/// </summary>
/// <remarks>
/// Preparation includes figuring out from the existing code which models or properties should
/// be ignored or renamed, etc. -- anything that comes from the attributes in the existing code.
/// </remarks>
private void Prepare()
{
var pureLive = UmbracoConfig.For.ModelsBuilder().ModelsMode == ModelsMode.PureLive;
TypeModel.MapModelTypes(_typeModels, ModelsNamespace);
// mark IsContentIgnored models that we discovered should be ignored
// then propagate / ignore children of ignored contents
// ignore content = don't generate a class for it, don't generate children
foreach (var typeModel in _typeModels.Where(x => ParseResult.IsIgnored(x.Alias)))
typeModel.IsContentIgnored = true;
foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored && x.EnumerateBaseTypes().Any(xx => xx.IsContentIgnored)))
typeModel.IsContentIgnored = true;
// handle model renames
foreach (var typeModel in _typeModels.Where(x => ParseResult.IsContentRenamed(x.Alias)))
{
typeModel.ClrName = ParseResult.ContentClrName(typeModel.Alias);
typeModel.IsRenamed = true;
ModelsMap[typeModel.Alias] = typeModel.ClrName;
}
// handle implement
foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentImplement(x.Alias)))
{
typeModel.HasImplement = true;
}
// mark OmitBase models that we discovered already have a base class
foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentBase(ParseResult.ContentClrName(x.Alias) ?? x.ClrName)))
typeModel.HasBase = true;
foreach (var typeModel in _typeModels)
{
// mark IsRemoved properties that we discovered should be ignored
// ie is marked as ignored on type, or on any parent type
var tm = typeModel;
foreach (var property in typeModel.Properties
.Where(property => tm.EnumerateBaseTypes(true).Any(x => ParseResult.IsPropertyIgnored(ParseResult.ContentClrName(x.Alias) ?? x.ClrName, property.Alias))))
{
property.IsIgnored = true;
}
// handle property renames
foreach (var property in typeModel.Properties)
property.ClrName = ParseResult.PropertyClrName(ParseResult.ContentClrName(typeModel.Alias) ?? typeModel.ClrName, property.Alias) ?? property.ClrName;
}
var pureLive = Config.ModelsMode == ModelsMode.PureLive;
// for the first two of these two tests,
// always throw, even in purelive: cannot happen unless ppl start fidling with attributes to rename
@@ -158,22 +101,22 @@ namespace Umbraco.ModelsBuilder.Building
// for the last one, don't throw in purelive, see comment
// ensure we have no duplicates type names
foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1))
foreach (var xx in _typeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1))
throw new InvalidOperationException($"Type name \"{xx.Key}\" is used"
+ $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique."
+ " Consider using an attribute to assign different names to conflicting types.");
// ensure we have no duplicates property names
foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored))
foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1))
foreach (var typeModel in _typeModels)
foreach (var xx in typeModel.Properties.GroupBy(x => x.ClrName).Where(x => x.Count() > 1))
throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\""
+ $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique."
+ " Consider using an attribute to assign different names to conflicting properties.");
// ensure content & property type don't have identical name (csharp hates it)
foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored))
foreach (var typeModel in _typeModels)
{
foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored && x.ClrName == typeModel.ClrName))
foreach (var xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName))
{
if (!pureLive)
throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"."
@@ -204,7 +147,7 @@ namespace Umbraco.ModelsBuilder.Building
// collect all the (non-removed) types implemented at parent level
// ie the parent content types and the mixins content types, recursively
var parentImplems = new List<TypeModel>();
if (typeModel.BaseType != null && !typeModel.BaseType.IsContentIgnored)
if (typeModel.BaseType != null)
TypeModel.CollectImplems(parentImplems, typeModel.BaseType);
// interfaces we must declare we implement (initially empty)
@@ -212,7 +155,6 @@ namespace Umbraco.ModelsBuilder.Building
// and except those that are already declared at the parent level
// in other words, DeclaringInterfaces is "local mixins"
var declaring = typeModel.MixinTypes
.Where(x => !x.IsContentIgnored)
.Except(parentImplems);
typeModel.DeclaringInterfaces.AddRange(declaring);
@@ -227,43 +169,16 @@ namespace Umbraco.ModelsBuilder.Building
typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems));
}
// register using types
foreach (var usingNamespace in ParseResult.UsingNamespaces)
// ensure elements don't inherit from non-elements
foreach (var typeModel in _typeModels.Where(x => x.IsElement))
{
if (!TypesUsing.Contains(usingNamespace))
TypesUsing.Add(usingNamespace);
if (typeModel.BaseType != null && !typeModel.BaseType.IsElement)
throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not.");
var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList();
if (errs.Count > 0)
throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not.");
}
// discover static mixin methods
foreach (var typeModel in _typeModels)
typeModel.StaticMixinMethods.AddRange(ParseResult.StaticMixinMethods(typeModel.ClrName));
// handle ctor
foreach (var typeModel in _typeModels.Where(x => ParseResult.HasCtor(x.ClrName)))
typeModel.HasCtor = true;
}
private SemanticModel _ambiguousSymbolsModel;
private int _ambiguousSymbolsPos;
// internal for tests
internal void PrepareAmbiguousSymbols()
{
var codeBuilder = new StringBuilder();
foreach (var t in TypesUsing)
codeBuilder.AppendFormat("using {0};\n", t);
codeBuilder.AppendFormat("namespace {0}\n{{ }}\n", GetModelsNamespace());
var compiler = new Compiler();
SyntaxTree[] trees;
var compilation = compiler.GetCompilation("MyCompilation", new Dictionary<string, string> { { "code", codeBuilder.ToString() } }, out trees);
var tree = trees[0];
_ambiguousSymbolsModel = compilation.GetSemanticModel(tree);
var namespaceSyntax = tree.GetRoot().DescendantNodes().OfType<NamespaceDeclarationSyntax>().First();
//var namespaceSymbol = model.GetDeclaredSymbol(namespaceSyntax);
_ambiguousSymbolsPos = namespaceSyntax.OpenBraceToken.SpanStart;
}
// looking for a simple symbol eg 'Umbraco' or 'String'
@@ -273,20 +188,12 @@ namespace Umbraco.ModelsBuilder.Building
// - 1 symbol is found BUT not matching (implicitely ambiguous)
protected bool IsAmbiguousSymbol(string symbol, string match)
{
if (_ambiguousSymbolsModel == null)
PrepareAmbiguousSymbols();
if (_ambiguousSymbolsModel == null)
throw new Exception("Could not prepare ambiguous symbols.");
var symbols = _ambiguousSymbolsModel.LookupNamespacesAndTypes(_ambiguousSymbolsPos, null, symbol);
// cannot figure out is a symbol is ambiguous without Roslyn
// so... let's say everything is ambiguous - code won't be
// pretty but it'll work
if (symbols.Length > 1) return true;
if (symbols.Length == 0) return false; // what else?
// only 1 - ensure it matches
var found = symbols[0].ToDisplayString();
var pos = found.IndexOf('<'); // generic?
if (pos > 0) found = found.Substring(0, pos); // strip
return found != match; // and compare
// Essentially this means that a `global::` syntax will be output for the generated models
return true;
}
internal string ModelsNamespaceForTests;
@@ -296,25 +203,18 @@ namespace Umbraco.ModelsBuilder.Building
if (ModelsNamespaceForTests != null)
return ModelsNamespaceForTests;
// code attribute overrides everything
if (ParseResult.HasModelsNamespace)
return ParseResult.ModelsNamespace;
// if builder was initialized with a namespace, use that one
if (!string.IsNullOrWhiteSpace(ModelsNamespace))
return ModelsNamespace;
// default
// fixme - should NOT reference config here, should make ModelsNamespace mandatory
return UmbracoConfig.For.ModelsBuilder().ModelsNamespace;
// use configured else fallback to default
return string.IsNullOrWhiteSpace(Config.ModelsNamespace)
? ModelsBuilderConfig.DefaultModelsNamespace
: Config.ModelsNamespace;
}
protected string GetModelsBaseClassName(TypeModel type)
{
// code attribute overrides everything
if (ParseResult.HasModelsBaseClassName)
return ParseResult.ModelsBaseClassName;
// default
return type.IsElement ? "PublishedElementModel" : "PublishedContentModel";
}

View File

@@ -0,0 +1,54 @@
using System.IO;
using System.Text;
using Umbraco.ModelsBuilder.Embedded.Configuration;
namespace Umbraco.ModelsBuilder.Embedded.Building
{
public class ModelsGenerator
{
private readonly UmbracoServices _umbracoService;
private readonly IModelsBuilderConfig _config;
private readonly OutOfDateModelsStatus _outOfDateModels;
public ModelsGenerator(UmbracoServices umbracoService, IModelsBuilderConfig config, OutOfDateModelsStatus outOfDateModels)
{
_umbracoService = umbracoService;
_config = config;
_outOfDateModels = outOfDateModels;
}
internal void GenerateModels()
{
if (!Directory.Exists(_config.ModelsDirectory))
Directory.CreateDirectory(_config.ModelsDirectory);
foreach (var file in Directory.GetFiles(_config.ModelsDirectory, "*.generated.cs"))
File.Delete(file);
var typeModels = _umbracoService.GetAllTypes();
var builder = new TextBuilder(_config, typeModels);
foreach (var typeModel in builder.GetModelsToGenerate())
{
var sb = new StringBuilder();
builder.Generate(sb, typeModel);
var filename = Path.Combine(_config.ModelsDirectory, typeModel.ClrName + ".generated.cs");
File.WriteAllText(filename, sb.ToString());
}
// the idea was to calculate the current hash and to add it as an extra file to the compilation,
// in order to be able to detect whether a DLL is consistent with an environment - however the
// environment *might not* contain the local partial files, and thus it could be impossible to
// calculate the hash. So... maybe that's not a good idea after all?
/*
var currentHash = HashHelper.Hash(ourFiles, typeModels);
ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder;
[assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")]
";
*/
_outOfDateModels.Clear();
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace Umbraco.ModelsBuilder.Building
namespace Umbraco.ModelsBuilder.Embedded.Building
{
/// <summary>
/// Represents a model property.
@@ -41,11 +41,6 @@ namespace Umbraco.ModelsBuilder.Building
/// </summary>
public string ClrTypeName;
/// <summary>
/// Gets a value indicating whether this property should be excluded from generation.
/// </summary>
public bool IsIgnored;
/// <summary>
/// Gets the generation errors for the property.
/// </summary>

View File

@@ -3,11 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Embedded.Configuration;
namespace Umbraco.ModelsBuilder.Building
namespace Umbraco.ModelsBuilder.Embedded.Building
{
/// <summary>
/// Implements a builder that works by writing text.
@@ -19,20 +17,8 @@ namespace Umbraco.ModelsBuilder.Building
/// and the result of code parsing.
/// </summary>
/// <param name="typeModels">The list of models to generate.</param>
/// <param name="parseResult">The result of code parsing.</param>
public TextBuilder(IList<TypeModel> typeModels, ParseResult parseResult)
: base(typeModels, parseResult)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TextBuilder"/> class with a list of models to generate,
/// the result of code parsing, and a models namespace.
/// </summary>
/// <param name="typeModels">The list of models to generate.</param>
/// <param name="parseResult">The result of code parsing.</param>
/// <param name="modelsNamespace">The models namespace.</param>
public TextBuilder(IList<TypeModel> typeModels, ParseResult parseResult, string modelsNamespace)
: base(typeModels, parseResult, modelsNamespace)
public TextBuilder(IModelsBuilderConfig config, IList<TypeModel> typeModels)
: base(config, typeModels)
{ }
// internal for unit tests only
@@ -97,6 +83,20 @@ namespace Umbraco.ModelsBuilder.Building
TextHeaderWriter.WriteHeader(sb);
}
// writes an attribute that identifies code generated by a tool
// (helps reduce warnings, tools such as FxCop use it)
// see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107
// see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute
// see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/
//
// note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
// and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
//
private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs)
{
sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder\", \"{1}\")]\n", tabs, ApiVersion.Current.Version);
}
private void WriteContentType(StringBuilder sb, TypeModel type)
{
string sep;
@@ -104,11 +104,11 @@ namespace Umbraco.ModelsBuilder.Building
if (type.IsMixin)
{
// write the interface declaration
sb.AppendFormat("\t// Mixin content Type {0} with alias \"{1}\"\n", type.Id, type.Alias);
sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias);
if (!string.IsNullOrWhiteSpace(type.Name))
sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name));
sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName);
var implements = type.BaseType == null || type.BaseType.IsContentIgnored
var implements = type.BaseType == null
? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent"))
: type.BaseType.ClrName;
if (implements != null)
@@ -126,7 +126,7 @@ namespace Umbraco.ModelsBuilder.Building
// write the properties - only the local (non-ignored) ones, we're an interface
var more = false;
foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName))
foreach (var prop in type.Properties.OrderBy(x => x.ClrName))
{
if (more) sb.Append("\n");
more = true;
@@ -137,8 +137,6 @@ namespace Umbraco.ModelsBuilder.Building
}
// write the class declaration
if (type.IsRenamed)
sb.AppendFormat("\t// Content Type {0} with alias \"{1}\"\n", type.Id, type.Alias);
if (!string.IsNullOrWhiteSpace(type.Name))
sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name));
// cannot do it now. see note in ImplementContentTypeAttribute
@@ -148,7 +146,7 @@ namespace Umbraco.ModelsBuilder.Building
sb.AppendFormat("\tpublic partial class {0}", type.ClrName);
var inherits = type.HasBase
? null // has its own base already
: (type.BaseType == null || type.BaseType.IsContentIgnored
: (type.BaseType == null
? GetModelsBaseClassName(type)
: type.BaseType.ClrName);
if (inherits != null)
@@ -178,22 +176,25 @@ namespace Umbraco.ModelsBuilder.Building
// as 'new' since parent has its own - or maybe not - disable warning
sb.Append("\t\t// helpers\n");
sb.Append("#pragma warning disable 0109 // new is redundant\n");
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n",
type.Alias);
var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n",
itemType);
sb.Append("\t\tpublic new static PublishedContentType GetModelContentType()\n");
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType()\n");
sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n");
sb.AppendFormat("\t\tpublic static PublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<{0}, TValue>> selector)\n",
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<{0}, TValue>> selector)\n",
type.ClrName);
sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n");
sb.Append("#pragma warning restore 0109\n\n");
// write the ctor
if (!type.HasCtor)
sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n",
type.ClrName, type.IsElement ? "Element" : "Content");
sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n",
type.ClrName, type.IsElement ? "Element" : "Content");
// write the properties
sb.Append("\t\t// properties\n");
@@ -205,10 +206,10 @@ namespace Umbraco.ModelsBuilder.Building
private void WriteContentTypeProperties(StringBuilder sb, TypeModel type)
{
var staticMixinGetters = UmbracoConfig.For.ModelsBuilder().StaticMixinGetters;
var staticMixinGetters = true;
// write the properties
foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName))
foreach (var prop in type.Properties.OrderBy(x => x.ClrName))
WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null);
// no need to write the parent properties since we inherit from the parent
@@ -217,7 +218,7 @@ namespace Umbraco.ModelsBuilder.Building
// write the mixins properties
foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName))
foreach (var prop in mixinType.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName))
foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName))
if (staticMixinGetters)
WriteMixinProperty(sb, prop, mixinType.ClrName);
else
@@ -242,6 +243,7 @@ namespace Umbraco.ModelsBuilder.Building
sb.Append("\t\t///</summary>\n");
}
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);
sb.Append("\t\tpublic ");
@@ -256,7 +258,7 @@ namespace Umbraco.ModelsBuilder.Building
private static string MixinStaticGetterName(string clrName)
{
return string.Format(UmbracoConfig.For.ModelsBuilder().StaticMixinGetterPattern, clrName);
return string.Format("Get{0}", clrName);
}
private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null)
@@ -300,6 +302,7 @@ namespace Umbraco.ModelsBuilder.Building
sb.Append("\t\t///</summary>\n");
}
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);
if (mixinStatic)
@@ -336,13 +339,14 @@ namespace Umbraco.ModelsBuilder.Building
var mixinStaticGetterName = MixinStaticGetterName(property.ClrName);
if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return;
//if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return;
sb.Append("\n");
if (!string.IsNullOrWhiteSpace(property.Name))
sb.AppendFormat("\t\t/// <summary>Static getter for {0}</summary>\n", XmlCommentString(property.Name));
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.Append("\t\tpublic static ");
WriteClrType(sb, property.ClrTypeName);
sb.AppendFormat(" {0}(I{1} that) => that.Value",
@@ -397,6 +401,7 @@ namespace Umbraco.ModelsBuilder.Building
if (!string.IsNullOrWhiteSpace(property.Name))
sb.AppendFormat("\t\t/// <summary>{0}</summary>\n", XmlCommentString(property.Name));
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.Append("\t\t");
WriteClrType(sb, property.ClrTypeName);
sb.AppendFormat(" {0} {{ get; }}\n",
@@ -461,7 +466,7 @@ namespace Umbraco.ModelsBuilder.Building
s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]);
// takes care eg of "System.Int32" vs. "int"
if (TypesMap.TryGetValue(s.ToLowerInvariant(), out string typeName))
if (TypesMap.TryGetValue(s, out string typeName))
{
sb.Append(typeName);
return;
@@ -481,6 +486,11 @@ namespace Umbraco.ModelsBuilder.Building
typeName = typeName.Substring(p + 1);
typeUsing = x;
}
else if (x == ModelsNamespace) // that one is used by default
{
typeName = typeName.Substring(p + 1);
typeUsing = ModelsNamespace;
}
}
// nested types *after* using
@@ -531,24 +541,24 @@ namespace Umbraco.ModelsBuilder.Building
return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' ');
}
private static readonly IDictionary<string, string> TypesMap = new Dictionary<string, string>
private static readonly IDictionary<string, string> TypesMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "system.int16", "short" },
{ "system.int32", "int" },
{ "system.int64", "long" },
{ "system.string", "string" },
{ "system.object", "object" },
{ "system.boolean", "bool" },
{ "system.void", "void" },
{ "system.char", "char" },
{ "system.byte", "byte" },
{ "system.uint16", "ushort" },
{ "system.uint32", "uint" },
{ "system.uint64", "ulong" },
{ "system.sbyte", "sbyte" },
{ "system.single", "float" },
{ "system.double", "double" },
{ "system.decimal", "decimal" }
{ "System.Int16", "short" },
{ "System.Int32", "int" },
{ "System.Int64", "long" },
{ "System.String", "string" },
{ "System.Object", "object" },
{ "System.Boolean", "bool" },
{ "System.Void", "void" },
{ "System.Char", "char" },
{ "System.Byte", "byte" },
{ "System.UInt16", "ushort" },
{ "System.UInt32", "uint" },
{ "System.UInt64", "ulong" },
{ "System.SByte", "sbyte" },
{ "System.Single", "float" },
{ "System.Double", "double" },
{ "System.Decimal", "decimal" }
};
}
}

View File

@@ -1,9 +1,8 @@
using System.Text;
using Umbraco.ModelsBuilder.Api;
namespace Umbraco.ModelsBuilder.Building
namespace Umbraco.ModelsBuilder.Embedded.Building
{
public static class TextHeaderWriter
internal static class TextHeaderWriter
{
/// <summary>
/// Outputs an "auto-generated" header to a string builder.

View File

@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.ModelsBuilder.Building
namespace Umbraco.ModelsBuilder.Embedded.Building
{
/// <summary>
/// Represents a model.
@@ -76,10 +77,10 @@ namespace Umbraco.ModelsBuilder.Building
/// </summary>
public readonly List<TypeModel> ImplementingInterfaces = new List<TypeModel>();
/// <summary>
/// Gets the list of existing static mixin method candidates.
/// </summary>
public readonly List<string> StaticMixinMethods = new List<string>();
///// <summary>
///// Gets the list of existing static mixin method candidates.
///// </summary>
//public readonly List<string> StaticMixinMethods = new List<string>(); //TODO: Do we need this? it isn't used
/// <summary>
/// Gets a value indicating whether this model has a base class.
@@ -88,16 +89,6 @@ namespace Umbraco.ModelsBuilder.Building
/// or because the existing user's code declares a base class for this model.</remarks>
public bool HasBase;
/// <summary>
/// Gets a value indicating whether this model has been renamed.
/// </summary>
public bool IsRenamed;
/// <summary>
/// Gets a value indicating whether this model has [ImplementContentType] already.
/// </summary>
public bool HasImplement;
/// <summary>
/// Gets a value indicating whether this model is used as a mixin by another model.
/// </summary>
@@ -108,16 +99,6 @@ namespace Umbraco.ModelsBuilder.Building
/// </summary>
public bool IsParent;
/// <summary>
/// Gets a value indicating whether this model should be excluded from generation.
/// </summary>
public bool IsContentIgnored;
/// <summary>
/// Gets a value indicating whether the ctor is already defined in a partial.
/// </summary>
public bool HasCtor;
/// <summary>
/// Gets a value indicating whether the type is an element.
/// </summary>
@@ -181,11 +162,11 @@ namespace Umbraco.ModelsBuilder.Building
/// <remarks>Includes the specified type.</remarks>
internal static void CollectImplems(ICollection<TypeModel> types, TypeModel type)
{
if (!type.IsContentIgnored && types.Contains(type) == false)
if (types.Contains(type) == false)
types.Add(type);
if (type.BaseType != null && !type.BaseType.IsContentIgnored)
if (type.BaseType != null)
CollectImplems(types, type.BaseType);
foreach (var mixin in type.MixinTypes.Where(x => !x.IsContentIgnored))
foreach (var mixin in type.MixinTypes)
CollectImplems(types, mixin);
}
@@ -204,5 +185,21 @@ namespace Umbraco.ModelsBuilder.Building
typeModel = typeModel.BaseType;
}
}
/// <summary>
/// Maps ModelType.
/// </summary>
public static void MapModelTypes(IList<TypeModel> typeModels, string ns)
{
var hasNs = !string.IsNullOrWhiteSpace(ns);
var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? (ns + "." + x.ClrName) : x.ClrName);
foreach (var typeModel in typeModels)
{
foreach (var propertyModel in typeModel.Properties)
{
propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map);
}
}
}
}
}

View File

@@ -1,18 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.ModelsBuilder.Building;
namespace Umbraco.ModelsBuilder.Umbraco
namespace Umbraco.ModelsBuilder.Embedded.Building
{
class HashHelper
internal class TypeModelHasher
{
public static string Hash(IDictionary<string, string> ourFiles, IEnumerable<TypeModel> typeModels)
public static string Hash(IEnumerable<TypeModel> typeModels)
{
var hash = new HashCombiner();
foreach (var kvp in ourFiles)
hash.Add(kvp.Key + "::" + kvp.Value);
// see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash
// ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference
@@ -39,6 +35,9 @@ namespace Umbraco.ModelsBuilder.Umbraco
}
}
// Include the MB version in the hash so that if the MB version changes, models are rebuilt
hash.Add(ApiVersion.Current.Version.ToString());
return hash.GetCombinedHashCode();
}
}

View File

@@ -0,0 +1,30 @@
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.ModelsBuilder.Embedded.BackOffice;
using Umbraco.Web.Features;
namespace Umbraco.ModelsBuilder.Embedded.Compose
{
/// <summary>
/// Special component used for when MB is disabled with the legacy MB is detected
/// </summary>
internal class DisabledModelsBuilderComponent : IComponent
{
private readonly UmbracoFeatures _features;
public DisabledModelsBuilderComponent(UmbracoFeatures features)
{
_features = features;
}
public void Initialize()
{
//disable the embedded dashboard controller
_features.Disabled.Controllers.Add<ModelsBuilderDashboardController>();
}
public void Terminate()
{
}
}
}

View File

@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.ModelsBuilder.Embedded.BackOffice;
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web;
using Umbraco.Web.JavaScript;
using Umbraco.Web.Mvc;
namespace Umbraco.ModelsBuilder.Embedded.Compose
{
internal class ModelsBuilderComponent : IComponent
{
private readonly IModelsBuilderConfig _config;
private readonly LiveModelsProvider _liveModelsProvider;
private readonly OutOfDateModelsStatus _outOfDateModels;
public ModelsBuilderComponent(IModelsBuilderConfig config, LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels)
{
_config = config;
_liveModelsProvider = liveModelsProvider;
_outOfDateModels = outOfDateModels;
}
public void Initialize()
{
// always setup the dashboard
// note: UmbracoApiController instances are automatically registered
InstallServerVars();
ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException;
if (_config.Enable)
FileService.SavingTemplate += FileService_SavingTemplate;
if (_config.ModelsMode.IsLiveNotPure())
_liveModelsProvider.Install();
if (_config.FlagOutOfDateModels)
_outOfDateModels.Install();
}
public void Terminate()
{ }
private void InstallServerVars()
{
// register our url - for the backoffice api
ServerVariablesParser.Parsing += (sender, serverVars) =>
{
if (!serverVars.ContainsKey("umbracoUrls"))
throw new ArgumentException("Missing umbracoUrls.");
var umbracoUrlsObject = serverVars["umbracoUrls"];
if (umbracoUrlsObject == null)
throw new ArgumentException("Null umbracoUrls");
if (!(umbracoUrlsObject is Dictionary<string, object> umbracoUrls))
throw new ArgumentException("Invalid umbracoUrls");
if (!serverVars.ContainsKey("umbracoPlugins"))
throw new ArgumentException("Missing umbracoPlugins.");
if (!(serverVars["umbracoPlugins"] is Dictionary<string, object> umbracoPlugins))
throw new ArgumentException("Invalid umbracoPlugins");
if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null");
var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()));
umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl<ModelsBuilderDashboardController>(controller => controller.BuildModels());
umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings();
};
}
private Dictionary<string, object> GetModelsBuilderSettings()
{
var settings = new Dictionary<string, object>
{
{"enabled", _config.Enable}
};
return settings;
}
/// <summary>
/// Used to check if a template is being created based on a document type, in this case we need to
/// ensure the template markup is correct based on the model name of the document type
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs<Core.Models.ITemplate> e)
{
// don't do anything if the factory is not enabled
// because, no factory = no models (even if generation is enabled)
if (!_config.EnableFactory) return;
// don't do anything if this special key is not found
if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return;
// ensure we have the content type alias
if (!e.AdditionalData.ContainsKey("ContentTypeAlias"))
throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found");
foreach (var template in e.SavedEntities)
// if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key
// is found, then it means a new template is being created based on the creation of a document type
if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content))
{
// ensure is safe and always pascal cased, per razor standard
// + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application
var alias = e.AdditionalData["ContentTypeAlias"].ToString();
var name = template.Name; // will be the name of the content type since we are creating
var className = UmbracoServices.GetClrName(name, alias);
var modelNamespace = _config.ModelsNamespace;
// we do not support configuring this at the moment, so just let Umbraco use its default value
//var modelNamespaceAlias = ...;
var markup = ViewHelper.GetDefaultFileContent(
modelClassName: className,
modelNamespace: modelNamespace/*,
modelNamespaceAlias: modelNamespaceAlias*/);
//set the template content to the new markup
template.Content = markup;
}
}
private void ContentModelBinder_ModelBindingException(object sender, ContentModelBinder.ModelBindingArgs args)
{
var sourceAttr = args.SourceType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
var modelAttr = args.ModelType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
// if source or model is not a ModelsBuider type...
if (sourceAttr == null || modelAttr == null)
{
// if neither are ModelsBuilder types, give up entirely
if (sourceAttr == null && modelAttr == null)
return;
// else report, but better not restart (loops?)
args.Message.Append(" The ");
args.Message.Append(sourceAttr == null ? "view model" : "source");
args.Message.Append(" is a ModelsBuilder type, but the ");
args.Message.Append(sourceAttr != null ? "view model" : "source");
args.Message.Append(" is not. The application is in an unstable state and should be restarted.");
return;
}
// both are ModelsBuilder types
var pureSource = sourceAttr.PureLive;
var pureModel = modelAttr.PureLive;
if (sourceAttr.PureLive || modelAttr.PureLive)
if (pureSource == false || pureModel == false)
{
// only one is pure - report, but better not restart (loops?)
args.Message.Append(pureSource
? " The content model is PureLive, but the view model is not."
: " The view model is PureLive, but the content model is not.");
args.Message.Append(" The application is in an unstable state and should be restarted.");
}
else
{
// both are pure - report, and if different versions, restart
// if same version... makes no sense... and better not restart (loops?)
var sourceVersion = args.SourceType.Assembly.GetName().Version;
var modelVersion = args.ModelType.Assembly.GetName().Version;
args.Message.Append(" Both view and content models are PureLive, with ");
args.Message.Append(sourceVersion == modelVersion
? "same version. The application is in an unstable state and should be restarted."
: "different versions. The application is in an unstable state and is going to be restarted.");
args.Restart = sourceVersion != modelVersion;
}
}
}
}

View File

@@ -0,0 +1,100 @@
using System.Linq;
using System.Reflection;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.ModelsBuilder.Embedded.Building;
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web;
using Umbraco.Web.PublishedCache.NuCache;
using Umbraco.Web.Features;
namespace Umbraco.ModelsBuilder.Embedded.Compose
{
[ComposeBefore(typeof(NuCacheComposer))]
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public sealed class ModelsBuilderComposer : ICoreComposer
{
public void Compose(Composition composition)
{
var isLegacyModelsBuilderInstalled = IsLegacyModelsBuilderInstalled();
if (isLegacyModelsBuilderInstalled)
{
ComposeForLegacyModelsBuilder(composition);
return;
}
composition.Components().Append<ModelsBuilderComponent>();
composition.Register<UmbracoServices>(Lifetime.Singleton);
composition.Configs.Add<IModelsBuilderConfig>(() => new ModelsBuilderConfig());
composition.RegisterUnique<ModelsGenerator>();
composition.RegisterUnique<LiveModelsProvider>();
composition.RegisterUnique<OutOfDateModelsStatus>();
composition.RegisterUnique<ModelsGenerationError>();
if (composition.Configs.ModelsBuilder().ModelsMode == ModelsMode.PureLive)
ComposeForLiveModels(composition);
else if (composition.Configs.ModelsBuilder().EnableFactory)
ComposeForDefaultModelsFactory(composition);
}
private static bool IsLegacyModelsBuilderInstalled()
{
Assembly legacyMbAssembly = null;
try
{
legacyMbAssembly = Assembly.Load("Umbraco.ModelsBuilder");
}
catch (System.Exception)
{
//swallow exception, DLL must not be there
}
return legacyMbAssembly != null;
}
private void ComposeForLegacyModelsBuilder(Composition composition)
{
composition.Logger.Info<ModelsBuilderComposer>("ModelsBuilder.Embedded is disabled, the external ModelsBuilder was detected.");
composition.Components().Append<DisabledModelsBuilderComponent>();
composition.Dashboards().Remove<ModelsBuilderDashboard>();
}
private void ComposeForDefaultModelsFactory(Composition composition)
{
composition.RegisterUnique<IPublishedModelFactory>(factory =>
{
var typeLoader = factory.GetInstance<TypeLoader>();
var types = typeLoader
.GetTypes<PublishedElementModel>() // element models
.Concat(typeLoader.GetTypes<PublishedContentModel>()); // content models
return new PublishedModelFactory(types);
});
}
private void ComposeForLiveModels(Composition composition)
{
composition.RegisterUnique<IPublishedModelFactory, PureLiveModelFactory>();
// the following would add @using statement in every view so user's don't
// have to do it - however, then noone understands where the @using statement
// comes from, and it cannot be avoided / removed --- DISABLED
//
/*
// no need for @using in views
// note:
// we are NOT using the in-code attribute here, config is required
// because that would require parsing the code... and what if it changes?
// we can AddGlobalImport not sure we can remove one anyways
var modelsNamespace = Configuration.Config.ModelsNamespace;
if (string.IsNullOrWhiteSpace(modelsNamespace))
modelsNamespace = Configuration.Config.DefaultModelsNamespace;
System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace);
*/
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Web;
using System.Web.Compilation;
using Umbraco.ModelsBuilder.Embedded.Compose;
[assembly: PreApplicationStartMethod(typeof(ModelsBuilderInitializer), "Initialize")]
namespace Umbraco.ModelsBuilder.Embedded.Compose
{
public static class ModelsBuilderInitializer
{
public static void Initialize()
{
// for some reason, netstandard is missing from BuildManager.ReferencedAssemblies and yet, is part of
// the references that CSharpCompiler receives - in some cases eg when building views - but not when
// using BuildManager to build the PureLive models - where is it coming from? cannot figure it out
// so... cheating here
// this is equivalent to adding
// <add assembly="netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51" />
// to web.config system.web/compilation/assemblies
var netStandard = ReferencedAssemblies.GetNetStandardAssembly();
if (netStandard != null)
BuildManager.AddReferencedAssembly(netStandard);
}
}
}

View File

@@ -0,0 +1,20 @@
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Embedded.Configuration;
namespace Umbraco.ModelsBuilder.Embedded
{
/// <summary>
/// Provides extension methods for the <see cref="Configs"/> class.
/// </summary>
public static class ConfigsExtensions
{
/// <summary>
/// Gets the models builder configuration.
/// </summary>
/// <remarks>Getting the models builder configuration freezes its state,
/// and any attempt at modifying the configuration using the Setup method
/// will be ignored.</remarks>
public static IModelsBuilderConfig ModelsBuilder(this Configs configs)
=> configs.GetConfig<IModelsBuilderConfig>();
}
}

View File

@@ -0,0 +1,15 @@
namespace Umbraco.ModelsBuilder.Embedded.Configuration
{
public interface IModelsBuilderConfig
{
bool Enable { get; }
bool AcceptUnsafeModelsDirectory { get; }
int DebugLevel { get; }
bool EnableFactory { get; }
bool FlagOutOfDateModels { get; }
bool IsDebug { get; }
string ModelsDirectory { get; }
ModelsMode ModelsMode { get; }
string ModelsNamespace { get; }
}
}

View File

@@ -1,53 +1,24 @@
using System;
using System.Configuration;
using System.IO;
using System.Reflection;
using System.Web.Configuration;
using System.Web.Hosting;
using Microsoft.CodeAnalysis.CSharp;
using Umbraco.Core;
using Umbraco.Core.IO;
namespace Umbraco.ModelsBuilder.Configuration
namespace Umbraco.ModelsBuilder.Embedded.Configuration
{
/// <summary>
/// Represents the models builder configuration.
/// </summary>
public class Config
public class ModelsBuilderConfig : IModelsBuilderConfig
{
private static Config _value;
public const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels";
public const string DefaultModelsDirectory = "~/App_Data/Models";
/// <summary>
/// Gets the configuration - internal so that the UmbracoConfig extension
/// can get the value to initialize its own value. Either a value has
/// been provided via the Setup method, or a new instance is created, which
/// will load settings from the config file.
/// Initializes a new instance of the <see cref="ModelsBuilderConfig"/> class.
/// </summary>
internal static Config Value => _value ?? new Config();
/// <summary>
/// Sets the configuration programmatically.
/// </summary>
/// <param name="config">The configuration.</param>
/// <remarks>
/// <para>Once the configuration has been accessed via the UmbracoConfig extension,
/// it cannot be changed anymore, and using this method will achieve nothing.</para>
/// <para>For tests, see UmbracoConfigExtensions.ResetConfig().</para>
/// </remarks>
public static void Setup(Config config)
{
_value = config;
}
internal const string DefaultStaticMixinGetterPattern = "Get{0}";
internal const LanguageVersion DefaultLanguageVersion = LanguageVersion.CSharp6;
internal const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels";
internal const ClrNameSource DefaultClrNameSource = ClrNameSource.Alias; // for legacy reasons
internal const string DefaultModelsDirectory = "~/App_Data/Models";
/// <summary>
/// Initializes a new instance of the <see cref="Config"/> class.
/// </summary>
private Config()
public ModelsBuilderConfig()
{
const string prefix = "Umbraco.ModelsBuilder.";
@@ -56,13 +27,8 @@ namespace Umbraco.ModelsBuilder.Configuration
Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true";
// ensure defaults are initialized for tests
StaticMixinGetterPattern = DefaultStaticMixinGetterPattern;
LanguageVersion = DefaultLanguageVersion;
ModelsNamespace = DefaultModelsNamespace;
ClrNameSource = DefaultClrNameSource;
ModelsDirectory = HostingEnvironment.IsHosted
? HostingEnvironment.MapPath(DefaultModelsDirectory)
: DefaultModelsDirectory.TrimStart("~/");
ModelsDirectory = IOHelper.MapPath(DefaultModelsDirectory);
DebugLevel = 0;
// stop here, everything is false
@@ -80,12 +46,6 @@ namespace Umbraco.ModelsBuilder.Configuration
case nameof(ModelsMode.PureLive):
ModelsMode = ModelsMode.PureLive;
break;
case nameof(ModelsMode.Dll):
ModelsMode = ModelsMode.Dll;
break;
case nameof(ModelsMode.LiveDll):
ModelsMode = ModelsMode.LiveDll;
break;
case nameof(ModelsMode.AppData):
ModelsMode = ModelsMode.AppData;
break;
@@ -94,17 +54,15 @@ namespace Umbraco.ModelsBuilder.Configuration
break;
default:
throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode."
+ " Note that modes are case-sensitive.");
+ " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode))));
}
}
// default: false
EnableApi = ConfigurationManager.AppSettings[prefix + "EnableApi"].InvariantEquals("true");
AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true");
// default: true
EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false");
StaticMixinGetters = !ConfigurationManager.AppSettings[prefix + "StaticMixinGetters"].InvariantEquals("false");
FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false");
// default: initialized above with DefaultModelsNamespace const
@@ -112,52 +70,11 @@ namespace Umbraco.ModelsBuilder.Configuration
if (!string.IsNullOrWhiteSpace(value))
ModelsNamespace = value;
// default: initialized above with DefaultStaticMixinGetterPattern const
value = ConfigurationManager.AppSettings[prefix + "StaticMixinGetterPattern"];
if (!string.IsNullOrWhiteSpace(value))
StaticMixinGetterPattern = value;
// default: initialized above with DefaultLanguageVersion const
value = ConfigurationManager.AppSettings[prefix + "LanguageVersion"];
if (!string.IsNullOrWhiteSpace(value))
{
LanguageVersion lv;
if (!Enum.TryParse(value, true, out lv))
throw new ConfigurationErrorsException($"Invalid language version \"{value}\".");
LanguageVersion = lv;
}
// default: initialized above with DefaultClrNameSource const
value = ConfigurationManager.AppSettings[prefix + "ClrNameSource"];
if (!string.IsNullOrWhiteSpace(value))
{
switch (value)
{
case nameof(ClrNameSource.Nothing):
ClrNameSource = ClrNameSource.Nothing;
break;
case nameof(ClrNameSource.Alias):
ClrNameSource = ClrNameSource.Alias;
break;
case nameof(ClrNameSource.RawAlias):
ClrNameSource = ClrNameSource.RawAlias;
break;
case nameof(ClrNameSource.Name):
ClrNameSource = ClrNameSource.Name;
break;
default:
throw new ConfigurationErrorsException($"ClrNameSource \"{value}\" is not a valid source."
+ " Note that sources are case-sensitive.");
}
}
// default: initialized above with DefaultModelsDirectory const
value = ConfigurationManager.AppSettings[prefix + "ModelsDirectory"];
if (!string.IsNullOrWhiteSpace(value))
{
var root = HostingEnvironment.IsHosted
? HostingEnvironment.MapPath("~/")
: Directory.GetCurrentDirectory();
var root = IOHelper.MapPath("~/");
if (root == null)
throw new ConfigurationErrorsException("Could not determine root directory.");
@@ -181,19 +98,14 @@ namespace Umbraco.ModelsBuilder.Configuration
}
/// <summary>
/// Initializes a new instance of the <see cref="Config"/> class.
/// Initializes a new instance of the <see cref="ModelsBuilderConfig"/> class.
/// </summary>
public Config(
public ModelsBuilderConfig(
bool enable = false,
ModelsMode modelsMode = ModelsMode.Nothing,
bool enableApi = true,
string modelsNamespace = null,
bool enableFactory = true,
LanguageVersion languageVersion = DefaultLanguageVersion,
bool staticMixinGetters = true,
string staticMixinGetterPattern = null,
bool flagOutOfDateModels = true,
ClrNameSource clrNameSource = DefaultClrNameSource,
string modelsDirectory = null,
bool acceptUnsafeModelsDirectory = false,
int debugLevel = 0)
@@ -201,14 +113,9 @@ namespace Umbraco.ModelsBuilder.Configuration
Enable = enable;
ModelsMode = modelsMode;
EnableApi = enableApi;
ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace;
EnableFactory = enableFactory;
LanguageVersion = languageVersion;
StaticMixinGetters = staticMixinGetters;
StaticMixinGetterPattern = string.IsNullOrWhiteSpace(staticMixinGetterPattern) ? DefaultStaticMixinGetterPattern : staticMixinGetterPattern;
FlagOutOfDateModels = flagOutOfDateModels;
ClrNameSource = clrNameSource;
ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory;
AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory;
DebugLevel = debugLevel;
@@ -259,27 +166,6 @@ namespace Umbraco.ModelsBuilder.Configuration
/// </summary>
public ModelsMode ModelsMode { get; }
/// <summary>
/// Gets a value indicating whether to serve the API.
/// </summary>
public bool ApiServer => EnableApi && ApiInstalled && IsDebug;
/// <summary>
/// Gets a value indicating whether to enable the API.
/// </summary>
/// <remarks>
/// <para>Default value is <c>true</c>.</para>
/// <para>The API is used by the Visual Studio extension and the console tool to talk to Umbraco
/// and retrieve the content types. It needs to be enabled so the extension & tool can work.</para>
/// </remarks>
public bool EnableApi { get; }
/// <summary>
/// Gets a value indicating whether the API is installed.
/// </summary>
// fixme - this is now always true as the API is part of Core
public bool ApiInstalled => true;
/// <summary>
/// Gets a value indicating whether system.web/compilation/@debug is true.
/// </summary>
@@ -287,7 +173,7 @@ namespace Umbraco.ModelsBuilder.Configuration
{
get
{
var section = (CompilationSection) ConfigurationManager.GetSection("system.web/compilation");
var section = (CompilationSection)ConfigurationManager.GetSection("system.web/compilation");
return section != null && section.Debug;
}
}
@@ -304,24 +190,6 @@ namespace Umbraco.ModelsBuilder.Configuration
/// <remarks>Default value is <c>true</c> because no factory is enabled by default in Umbraco.</remarks>
public bool EnableFactory { get; }
/// <summary>
/// Gets the Roslyn parser language version.
/// </summary>
/// <remarks>Default value is <c>CSharp6</c>.</remarks>
public LanguageVersion LanguageVersion { get; }
/// <summary>
/// Gets a value indicating whether to generate static mixin getters.
/// </summary>
/// <remarks>Default value is <c>false</c> for backward compat reaons.</remarks>
public bool StaticMixinGetters { get; }
/// <summary>
/// Gets the string pattern for mixin properties static getter name.
/// </summary>
/// <remarks>Default value is "GetXxx". Standard string format.</remarks>
public string StaticMixinGetterPattern { get; }
/// <summary>
/// Gets a value indicating whether we should flag out-of-date models.
/// </summary>
@@ -330,11 +198,6 @@ namespace Umbraco.ModelsBuilder.Configuration
/// generated through the dashboard, the files is cleared. Default value is <c>false</c>.</remarks>
public bool FlagOutOfDateModels { get; }
/// <summary>
/// Gets the CLR name source.
/// </summary>
public ClrNameSource ClrNameSource { get; }
/// <summary>
/// Gets the models directory.
/// </summary>

View File

@@ -1,4 +1,4 @@
namespace Umbraco.ModelsBuilder.Configuration
namespace Umbraco.ModelsBuilder.Embedded.Configuration
{
/// <summary>
/// Defines the models generation modes.
@@ -8,7 +8,7 @@
/// <summary>
/// Do not generate models.
/// </summary>
Nothing = 0, // default value
Nothing = 0, // default value
/// <summary>
/// Generate models in memory.
@@ -31,22 +31,6 @@
/// </summary>
/// <remarks>Generation can be triggered from the dashboard. The app does not restart.
/// Models are not compiled and thus are not available to the project.</remarks>
LiveAppData,
/// <summary>
/// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts).
/// When: generation is triggered.
/// </summary>
/// <remarks>Generation can be triggered from the dashboard. The app does restart. Models
/// are available to the entire project.</remarks>
Dll,
/// <summary>
/// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts).
/// When: a content type change occurs, or generation is triggered.
/// </summary>
/// <remarks>Generation can be triggered from the dashboard. The app does restart. Models
/// are available to the entire project.</remarks>
LiveDll
LiveAppData
}
}

View File

@@ -1,4 +1,4 @@
namespace Umbraco.ModelsBuilder.Configuration
namespace Umbraco.ModelsBuilder.Embedded.Configuration
{
/// <summary>
/// Provides extensions for the <see cref="ModelsMode"/> enumeration.
@@ -12,7 +12,6 @@
{
return
modelsMode == ModelsMode.PureLive
|| modelsMode == ModelsMode.LiveDll
|| modelsMode == ModelsMode.LiveAppData;
}
@@ -22,18 +21,7 @@
public static bool IsLiveNotPure(this ModelsMode modelsMode)
{
return
modelsMode == ModelsMode.LiveDll
|| modelsMode == ModelsMode.LiveAppData;
}
/// <summary>
/// Gets a value indicating whether the mode is [Live]Dll.
/// </summary>
public static bool IsAnyDll(this ModelsMode modelsMode)
{
return
modelsMode == ModelsMode.Dll
|| modelsMode == ModelsMode.LiveDll;
modelsMode == ModelsMode.LiveAppData;
}
/// <summary>
@@ -42,10 +30,8 @@
public static bool SupportsExplicitGeneration(this ModelsMode modelsMode)
{
return
modelsMode == ModelsMode.Dll
|| modelsMode == ModelsMode.LiveDll
|| modelsMode == ModelsMode.AppData
modelsMode == ModelsMode.AppData
|| modelsMode == ModelsMode.LiveAppData;
}
}
}
}

View File

@@ -1,17 +1,17 @@
using System;
using System.Globalization;
namespace Umbraco.ModelsBuilder.Umbraco
namespace Umbraco.ModelsBuilder.Embedded
{
// because, of course, it's internal in Umbraco
// see also System.Web.Util.HashCodeCombiner
class HashCombiner
internal class HashCombiner
{
private long _combinedHash = 5381L;
public void Add(int i)
{
_combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i;
_combinedHash = (_combinedHash << 5) + _combinedHash ^ i;
}
public void Add(object o)
@@ -27,7 +27,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
public void Add(string s)
{
if (s == null) return;
Add((StringComparer.InvariantCulture).GetHashCode(s));
Add(StringComparer.InvariantCulture.GetHashCode(s));
}
public string GetCombinedHashCode()

View File

@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.ModelsBuilder
namespace Umbraco.ModelsBuilder.Embedded
{
/// <summary>
/// Indicates that a property implements a given property alias.

View File

@@ -0,0 +1,110 @@
using System;
using System.Threading;
using System.Web.Hosting;
using Umbraco.Core.Logging;
using Umbraco.ModelsBuilder.Embedded.Building;
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web.Cache;
namespace Umbraco.ModelsBuilder.Embedded
{
// supports LiveAppData - but not PureLive
public sealed class LiveModelsProvider
{
private static Mutex _mutex;
private static int _req;
private readonly ILogger _logger;
private readonly IModelsBuilderConfig _config;
private readonly ModelsGenerator _modelGenerator;
private readonly ModelsGenerationError _mbErrors;
// we do not manage pure live here
internal bool IsEnabled => _config.ModelsMode.IsLiveNotPure();
public LiveModelsProvider(ILogger logger, IModelsBuilderConfig config, ModelsGenerator modelGenerator, ModelsGenerationError mbErrors)
{
_logger = logger;
_config = config ?? throw new ArgumentNullException(nameof(config));
_modelGenerator = modelGenerator;
_mbErrors = mbErrors;
}
internal void Install()
{
// just be sure
if (!IsEnabled)
return;
// initialize mutex
// ApplicationId will look like "/LM/W3SVC/1/Root/AppName"
// name is system-wide and must be less than 260 chars
var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider";
_mutex = new Mutex(false, name); //TODO: Replace this with MainDom? Seems we now have 2x implementations of almost the same thing
// anything changes, and we want to re-generate models.
ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
// at the end of a request since we're restarting the pool
// NOTE - this does NOT trigger - see module below
//umbracoApplication.EndRequest += GenerateModelsIfRequested;
}
// NOTE
// Using HttpContext Items fails because CacheUpdated triggers within
// some asynchronous backend task where we seem to have no HttpContext.
// So we use a static (non request-bound) var to register that models
// need to be generated. Could be by another request. Anyway. We could
// have collisions but... you know the risk.
private void RequestModelsGeneration(object sender, EventArgs args)
{
//HttpContext.Current.Items[this] = true;
_logger.Debug<LiveModelsProvider>("Requested to generate models.");
Interlocked.Exchange(ref _req, 1);
}
public void GenerateModelsIfRequested(object sender, EventArgs args)
{
//if (HttpContext.Current.Items[this] == null) return;
if (Interlocked.Exchange(ref _req, 0) == 0) return;
// cannot use a simple lock here because we don't want another AppDomain
// to generate while we do... and there could be 2 AppDomains if the app restarts.
try
{
_logger.Debug<LiveModelsProvider>("Generate models...");
const int timeout = 2 * 60 * 1000; // 2 mins
_mutex.WaitOne(timeout); // wait until it is safe, and acquire
_logger.Info<LiveModelsProvider>("Generate models now.");
GenerateModels();
_mbErrors.Clear();
_logger.Info<LiveModelsProvider>("Generated.");
}
catch (TimeoutException)
{
_logger.Warn<LiveModelsProvider>("Timeout, models were NOT generated.");
}
catch (Exception e)
{
_mbErrors.Report("Failed to build Live models.", e);
_logger.Error<LiveModelsProvider>("Failed to generate models.", e);
}
finally
{
_mutex.ReleaseMutex(); // release
}
}
private void GenerateModels()
{
// EnableDllModels will recycle the app domain - but this request will end properly
_modelGenerator.GenerateModels();
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Web;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.ModelsBuilder.Embedded;
// will install only if configuration says it needs to be installed
[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")]
namespace Umbraco.ModelsBuilder.Embedded
{
// have to do this because it's the only way to subscribe to EndRequest,
// module is installed by assembly attribute at the top of this file
public class LiveModelsProviderModule : IHttpModule
{
private static LiveModelsProvider _liveModelsProvider;
public void Init(HttpApplication app)
{
app.EndRequest += App_EndRequest;
}
private void App_EndRequest(object sender, EventArgs e)
{
if (((HttpApplication)sender).Request.Url.IsClientSideRequest())
return;
// here we're using "Current." since we're in a module, it is possible in a round about way to inject into a module but for now we'll just use Current
if (_liveModelsProvider == null)
_liveModelsProvider = Current.Factory.TryGetInstance<LiveModelsProvider>(); // will be null in upgrade mode or if embedded MB is disabled
if (_liveModelsProvider?.IsEnabled ?? false)
_liveModelsProvider.GenerateModelsIfRequested(sender, e);
}
public void Dispose()
{
// nothing
}
public static void Install()
{
// always - don't read config in PreApplicationStartMethod
HttpApplication.RegisterModule(typeof(LiveModelsProviderModule));
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
namespace Umbraco.ModelsBuilder
namespace Umbraco.ModelsBuilder.Embedded
{
/// <summary>
/// Indicates that an Assembly is a Models Builder assembly.

View File

@@ -0,0 +1,19 @@
using System;
using Umbraco.Core.Composing;
using Umbraco.Core.Dashboards;
namespace Umbraco.ModelsBuilder.Embedded
{
[Weight(40)]
public class ModelsBuilderDashboard : IDashboard
{
public string Alias => "settingsModelsBuilder";
public string[] Sections => new [] { "settings" };
public string View => "views/dashboard/settings/modelsbuildermanagement.html";
public IAccessRule[] AccessRules => Array.Empty<IAccessRule>();
}
}

View File

@@ -1,14 +1,20 @@
using System;
using System.IO;
using System.Text;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Embedded.Configuration;
namespace Umbraco.ModelsBuilder.Umbraco
namespace Umbraco.ModelsBuilder.Embedded
{
internal static class ModelsGenerationError
public sealed class ModelsGenerationError
{
public static void Clear()
private readonly IModelsBuilderConfig _config;
public ModelsGenerationError(IModelsBuilderConfig config)
{
_config = config;
}
public void Clear()
{
var errFile = GetErrFile();
if (errFile == null) return;
@@ -17,7 +23,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
File.Delete(errFile);
}
public static void Report(string message, Exception e)
public void Report(string message, Exception e)
{
var errFile = GetErrFile();
if (errFile == null) return;
@@ -33,7 +39,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
File.WriteAllText(errFile, sb.ToString());
}
public static string GetLastError()
public string GetLastError()
{
var errFile = GetErrFile();
if (errFile == null) return null;
@@ -48,9 +54,9 @@ namespace Umbraco.ModelsBuilder.Umbraco
}
}
private static string GetErrFile()
private string GetErrFile()
{
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory))
return null;

View File

@@ -1,55 +1,58 @@
using System;
using System.IO;
using System.Web.Hosting;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
using System.IO;
using Umbraco.ModelsBuilder.Embedded.Configuration;
using Umbraco.Web.Cache;
namespace Umbraco.ModelsBuilder.Umbraco
namespace Umbraco.ModelsBuilder.Embedded
{
public sealed class OutOfDateModelsStatus
{
internal static void Install()
private readonly IModelsBuilderConfig _config;
public OutOfDateModelsStatus(IModelsBuilderConfig config)
{
_config = config;
}
internal void Install()
{
// just be sure
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false)
if (_config.FlagOutOfDateModels == false)
return;
ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write();
DataTypeCacheRefresher.CacheUpdated += (sender, args) => Write();
}
private static string GetFlagPath()
private string GetFlagPath()
{
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory);
return Path.Combine(modelsDirectory, "ood.flag");
}
private static void Write()
private void Write()
{
var path = GetFlagPath();
if (path == null || File.Exists(path)) return;
File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n");
}
public static void Clear()
public void Clear()
{
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return;
if (_config.FlagOutOfDateModels == false) return;
var path = GetFlagPath();
if (path == null || !File.Exists(path)) return;
File.Delete(path);
}
public static bool IsEnabled => UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels;
public bool IsEnabled => _config.FlagOutOfDateModels;
public static bool IsOutOfDate
public bool IsOutOfDate
{
get
{
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return false;
if (_config.FlagOutOfDateModels == false) return false;
var path = GetFlagPath();
return path != null && File.Exists(path);
}

View File

@@ -0,0 +1,13 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Umbraco.ModelsBuilder")]
[assembly: AssemblyDescription("Umbraco ModelsBuilder")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyProduct("Umbraco CMS")]
[assembly: ComVisible(false)]
[assembly: Guid("52ac0ba8-a60e-4e36-897b-e8b97a54ed1c")]
[assembly: InternalsVisibleTo("Umbraco.Tests")]

View File

@@ -2,9 +2,12 @@
using System.Linq.Expressions;
using System.Reflection;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
using Umbraco.ModelsBuilder;
using Umbraco.ModelsBuilder.Embedded;
namespace Umbraco.ModelsBuilder
// same namespace as original Umbraco.Web PublishedElementExtensions
// ReSharper disable once CheckNamespace
namespace Umbraco.Web
{
/// <summary>
/// Provides extension methods to models.
@@ -14,13 +17,14 @@ namespace Umbraco.ModelsBuilder
/// <summary>
/// Gets the value of a property.
/// </summary>
public static TValue Value<TModel, TValue>(this TModel model, Expression<Func<TModel, TValue>> property, string culture = ".", string segment = ".")
public static TValue Value<TModel, TValue>(this TModel model, Expression<Func<TModel, TValue>> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default)
where TModel : IPublishedElement
{
var alias = GetAlias(model, property);
return model.Value<TValue>(alias, culture, segment);
return model.Value<TValue>(alias, culture, segment, fallback, defaultValue);
}
// fixme that one should be public so ppl can use it
private static string GetAlias<TModel, TValue>(TModel model, Expression<Func<TModel, TValue>> property)
{
if (property.NodeType != ExpressionType.Lambda)

View File

@@ -1,11 +1,17 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using Umbraco.Web.Composing;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Composing;
namespace Umbraco.ModelsBuilder.Umbraco
namespace Umbraco.ModelsBuilder.Embedded
{
/// <summary>
/// This is called from within the generated model classes
/// </summary>
/// <remarks>
/// DO NOT REMOVE - although there are not code references this is used directly by the generated models.
/// </remarks>
public static class PublishedModelUtility
{
// looks safer but probably useless... ppl should not call these methods directly
@@ -24,7 +30,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
// // etc...
//}
public static PublishedContentType GetModelContentType(PublishedItemType itemType, string alias)
public static IPublishedContentType GetModelContentType(PublishedItemType itemType, string alias)
{
var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject!
switch (itemType)
@@ -40,8 +46,8 @@ namespace Umbraco.ModelsBuilder.Umbraco
}
}
public static PublishedPropertyType GetModelPropertyType<TModel, TValue>(PublishedContentType contentType, Expression<Func<TModel, TValue>> selector)
//where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel
public static IPublishedPropertyType GetModelPropertyType<TModel, TValue>(IPublishedContentType contentType, Expression<Func<TModel, TValue>> selector)
//where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel
{
// fixme therefore, missing a check on TModel here
@@ -54,7 +60,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
// see note above : accepted risk...
var attr = expr.Member
.GetCustomAttributes(typeof (ImplementPropertyTypeAttribute), false)
.GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false)
.OfType<ImplementPropertyTypeAttribute>()
.SingleOrDefault();

View File

@@ -3,59 +3,60 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using System.Web.Compilation;
using System.Web.Hosting;
using System.Web.WebPages.Razor;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Cache;
using Umbraco.ModelsBuilder.Building;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Embedded.Building;
using Umbraco.ModelsBuilder.Embedded.Configuration;
using File = System.IO.File;
namespace Umbraco.ModelsBuilder.Umbraco
namespace Umbraco.ModelsBuilder.Embedded
{
internal class PureLiveModelFactory : IPublishedModelFactory, IRegisteredObject
internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject
{
private Assembly _modelsAssembly;
private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
private volatile bool _hasModels; // volatile 'cos reading outside lock
private bool _pendingRebuild;
private readonly ProfilingLogger _logger;
private readonly IProfilingLogger _logger;
private readonly FileSystemWatcher _watcher;
private int _ver, _skipver;
private readonly int _debugLevel;
private BuildManager _theBuildManager;
private readonly Lazy<UmbracoServices> _umbracoServices;
private readonly Lazy<UmbracoServices> _umbracoServices; // fixme: this is because of circular refs :(
private UmbracoServices UmbracoServices => _umbracoServices.Value;
private static readonly Regex AssemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled);
private const string ProjVirt = "~/App_Data/Models/all.generated.cs";
private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" };
public PureLiveModelFactory(Lazy<UmbracoServices> umbracoServices, ProfilingLogger logger)
private readonly IModelsBuilderConfig _config;
private readonly ModelsGenerationError _errors;
public PureLiveModelFactory(Lazy<UmbracoServices> umbracoServices, IProfilingLogger logger, IModelsBuilderConfig config)
{
_umbracoServices = umbracoServices;
_logger = logger;
_config = config;
_errors = new ModelsGenerationError(config);
_ver = 1; // zero is for when we had no version
_skipver = -1; // nothing to skip
ContentTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels();
DataTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels();
RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted;
if (!HostingEnvironment.IsHosted) return;
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory);
@@ -68,9 +69,23 @@ namespace Umbraco.ModelsBuilder.Umbraco
_watcher.EnableRaisingEvents = true;
// get it here, this need to be fast
_debugLevel = UmbracoConfig.For.ModelsBuilder().DebugLevel;
_debugLevel = _config.DebugLevel;
}
#region ILivePublishedModelFactory
/// <inheritdoc />
public object SyncRoot { get; } = new object();
/// <inheritdoc />
public void Refresh()
{
ResetModels();
EnsureModels();
}
#endregion
#region IPublishedModelFactory
public IPublishedElement CreateModel(IPublishedElement element)
@@ -84,7 +99,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
var contentTypeAlias = element.ContentType.Alias;
// lookup model constructor (else null)
infos.TryGetValue(contentTypeAlias, out ModelInfo info);
infos.TryGetValue(contentTypeAlias, out var info);
// create model
return info == null ? element : info.Ctor(element);
@@ -115,7 +130,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (ctor != null) return ctor();
var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType);
ctor = modelInfo.ListCtor = ReflectionUtilities.EmitCtor<Func<IList>>(declaring: listType);
ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor<Func<IList>>(declaring: listType);
return ctor();
}
@@ -163,7 +178,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (_modelsAssembly == null) return;
if (_debugLevel > 0)
_logger.Logger.Debug<PureLiveModelFactory>("RazorBuildProvider.CodeGenerationStarted");
_logger.Debug<PureLiveModelFactory>("RazorBuildProvider.CodeGenerationStarted");
if (!(sender is RazorBuildProvider provider)) return;
// add the assembly, and add a dependency to a text file that will change on each
@@ -182,7 +197,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
// tells the factory that it should build a new generation of models
private void ResetModels()
{
_logger.Logger.Debug<PureLiveModelFactory>("Resetting models.");
_logger.Debug<PureLiveModelFactory>("Resetting models.");
try
{
@@ -190,6 +205,19 @@ namespace Umbraco.ModelsBuilder.Umbraco
_hasModels = false;
_pendingRebuild = true;
var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory);
// clear stuff
var modelsHashFile = Path.Combine(modelsDirectory, "models.hash");
//var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs");
//var projFile = Path.Combine(modelsDirectory, "all.generated.cs");
var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path");
if (File.Exists(dllPathFile)) File.Delete(dllPathFile);
if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile);
}
finally
{
@@ -204,10 +232,10 @@ namespace Umbraco.ModelsBuilder.Umbraco
get
{
if (_theBuildManager != null) return _theBuildManager;
var prop = typeof (BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static);
var prop = typeof(BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static);
if (prop == null)
throw new InvalidOperationException("Could not get BuildManager.TheBuildManager property.");
_theBuildManager = (BuildManager) prop.GetValue(null);
_theBuildManager = (BuildManager)prop.GetValue(null);
return _theBuildManager;
}
}
@@ -216,7 +244,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
internal Infos EnsureModels()
{
if (_debugLevel > 0)
_logger.Logger.Debug<PureLiveModelFactory>("Ensuring models.");
_logger.Debug<PureLiveModelFactory>("Ensuring models.");
// don't use an upgradeable lock here because only 1 thread at a time could enter it
try
@@ -264,15 +292,15 @@ namespace Umbraco.ModelsBuilder.Umbraco
var types = assembly.ExportedTypes.Where(x => x.Inherits<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
_infos = RegisterModels(types);
ModelsGenerationError.Clear();
_errors.Clear();
}
catch (Exception e)
{
try
{
_logger.Logger.Error<PureLiveModelFactory>("Failed to build models.", e);
_logger.Logger.Warn<PureLiveModelFactory>("Running without models."); // be explicit
ModelsGenerationError.Report("Failed to build PureLive models.", e);
_logger.Error<PureLiveModelFactory>("Failed to build models.", e);
_logger.Warn<PureLiveModelFactory>("Running without models."); // be explicit
_errors.Report("Failed to build PureLive models.", e);
}
finally
{
@@ -300,19 +328,12 @@ namespace Umbraco.ModelsBuilder.Umbraco
private Assembly GetModelsAssembly(bool forceRebuild)
{
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory);
// must filter out *.generated.cs because we haven't deleted them yet!
var ourFiles = Directory.Exists(modelsDirectory)
? Directory.GetFiles(modelsDirectory, "*.cs")
.Where(x => !x.EndsWith(".generated.cs"))
.ToDictionary(x => x, File.ReadAllText)
: new Dictionary<string, string>();
var typeModels = UmbracoServices.GetAllTypes();
var currentHash = HashHelper.Hash(ourFiles, typeModels);
var currentHash = TypeModelHasher.Hash(typeModels);
var modelsHashFile = Path.Combine(modelsDirectory, "models.hash");
var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs");
var projFile = Path.Combine(modelsDirectory, "all.generated.cs");
@@ -323,31 +344,48 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (!forceRebuild)
{
_logger.Logger.Debug<PureLiveModelFactory>("Looking for cached models.");
_logger.Debug<PureLiveModelFactory>("Looking for cached models.");
if (File.Exists(modelsHashFile) && File.Exists(projFile))
{
var cachedHash = File.ReadAllText(modelsHashFile);
if (currentHash != cachedHash)
{
_logger.Logger.Debug<PureLiveModelFactory>("Found obsolete cached models.");
_logger.Debug<PureLiveModelFactory>("Found obsolete cached models.");
forceRebuild = true;
}
// else cachedHash matches currentHash, we can try to load an existing dll
}
else
{
_logger.Logger.Debug<PureLiveModelFactory>("Could not find cached models.");
_logger.Debug<PureLiveModelFactory>("Could not find cached models.");
forceRebuild = true;
}
}
Assembly assembly;
if (forceRebuild == false)
if (!forceRebuild)
{
// try to load the dll directly (avoid rebuilding)
//
// ensure that the .dll file does not have a corresponding .dll.delete file
// as that would mean the the .dll file is going to be deleted and should not
// be re-used - that should not happen in theory, but better be safe
//
// ensure that the .dll file is in the current codegen directory - when IIS
// or Express does a full restart, it can switch to an entirely new codegen
// directory, and then we end up referencing a dll which is *not* in that
// directory, and BuildManager fails to instantiate views ("the view found
// at ... was not created").
//
if (File.Exists(dllPathFile))
{
var dllPath = File.ReadAllText(dllPathFile);
if (File.Exists(dllPath))
var codegen = HttpRuntime.CodegenDir;
_logger.Debug<PureLiveModelFactory>($"Cached models dll at {dllPath}.");
if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete") && dllPath.StartsWith(codegen))
{
assembly = Assembly.LoadFile(dllPath);
var attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
@@ -359,13 +397,23 @@ namespace Umbraco.ModelsBuilder.Umbraco
// with the "same but different" version of the assembly in memory
_skipver = assembly.GetName().Version.Revision;
_logger.Logger.Debug<PureLiveModelFactory>("Loading cached models (dll).");
_logger.Debug<PureLiveModelFactory>("Loading cached models (dll).");
return assembly;
}
_logger.Debug<PureLiveModelFactory>("Cached models dll cannot be loaded (invalid assembly).");
}
else if (!File.Exists(dllPath))
_logger.Debug<PureLiveModelFactory>("Cached models dll does not exist.");
else if (File.Exists(dllPath + ".delete"))
_logger.Debug<PureLiveModelFactory>("Cached models dll is marked for deletion.");
else if (!dllPath.StartsWith(codegen))
_logger.Debug<PureLiveModelFactory>("Cached models dll is in a different codegen directory.");
else
_logger.Debug<PureLiveModelFactory>("Cached models dll cannot be loaded (why?).");
}
// mmust reset the version in the file else it would keep growing
// must reset the version in the file else it would keep growing
// loading cached modules only happens when the app restarts
var text = File.ReadAllText(projFile);
var match = AssemblyVersionRegex.Match(text);
@@ -381,47 +429,80 @@ namespace Umbraco.ModelsBuilder.Umbraco
//File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver);
_ver++;
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
File.WriteAllText(dllPathFile, assembly.Location);
try
{
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
File.WriteAllText(dllPathFile, assembly.Location);
}
catch
{
ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile);
throw;
}
_logger.Logger.Debug<PureLiveModelFactory>("Loading cached models (source).");
_logger.Debug<PureLiveModelFactory>("Loading cached models (source).");
return assembly;
}
// need to rebuild
_logger.Logger.Debug<PureLiveModelFactory>("Rebuilding models.");
_logger.Debug<PureLiveModelFactory>("Rebuilding models.");
// generate code, save
var code = GenerateModelsCode(ourFiles, typeModels);
var code = GenerateModelsCode(typeModels);
// add extra attributes,
// PureLiveAssembly helps identifying Assemblies that contain PureLive models
// AssemblyVersion is so that we have a different version for each rebuild
var ver = _ver == _skipver ? ++_ver : _ver;
_ver++;
code = code.Replace("//ASSATTR", $@"[assembly: PureLiveAssembly]
[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")]
code = code.Replace("//ASSATTR", $@"[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")]
[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]");
File.WriteAllText(modelsSrcFile, code);
// generate proj, save
ourFiles["models.generated.cs"] = code;
var proj = GenerateModelsProj(ourFiles);
var projFiles = new Dictionary<string, string>
{
{ "models.generated.cs", code }
};
var proj = GenerateModelsProj(projFiles);
File.WriteAllText(projFile, proj);
// compile and register
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
File.WriteAllText(dllPathFile, assembly.Location);
try
{
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
File.WriteAllText(dllPathFile, assembly.Location);
File.WriteAllText(modelsHashFile, currentHash);
}
catch
{
ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile);
throw;
}
// assuming we can write and it's not going to cause exceptions...
File.WriteAllText(modelsHashFile, currentHash);
_logger.Logger.Debug<PureLiveModelFactory>("Done rebuilding.");
_logger.Debug<PureLiveModelFactory>("Done rebuilding.");
return assembly;
}
private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile)
{
_logger.Debug<PureLiveModelFactory>("Failed to compile.");
// the dll file reference still points to the previous dll, which is obsolete
// now and will be deleted by ASP.NET eventually, so better clear that reference.
// also touch the proj file to force views to recompile - don't delete as it's
// useful to have the source around for debugging.
try
{
if (File.Exists(dllPathFile)) File.Delete(dllPathFile);
if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile);
if (File.Exists(projFile)) File.SetLastWriteTime(projFile, DateTime.Now);
}
catch { /* enough */ }
}
private static Infos RegisterModels(IEnumerable<Type> types)
{
var ctorArgTypes = new[] { typeof (IPublishedElement) };
var ctorArgTypes = new[] { typeof(IPublishedElement) };
var modelInfos = new Dictionary<string, ModelInfo>(StringComparer.InvariantCultureIgnoreCase);
var map = new Dictionary<string, Type>();
@@ -433,7 +514,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
foreach (var ctor in type.GetConstructors())
{
var parms = ctor.GetParameters();
if (parms.Length == 1 && typeof (IPublishedElement).IsAssignableFrom(parms[0].ParameterType))
if (parms.Length == 1 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType))
{
if (constructor != null)
throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet.");
@@ -448,16 +529,17 @@ namespace Umbraco.ModelsBuilder.Umbraco
var attribute = type.GetCustomAttribute<PublishedModelAttribute>(false);
var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias;
if (modelInfos.TryGetValue(typeName, out ModelInfo modelInfo))
if (modelInfos.TryGetValue(typeName, out var modelInfo))
throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\".");
// fixme use Core's ReflectionUtilities.EmitCtor !!
var meth = new DynamicMethod(string.Empty, typeof (IPublishedElement), ctorArgTypes, type.Module, true);
// Yes .. DynamicMethod is uber slow
var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true);
var gen = meth.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Newobj, constructor);
gen.Emit(OpCodes.Ret);
var func = (Func<IPublishedElement, IPublishedElement>) meth.CreateDelegate(typeof (Func<IPublishedElement, IPublishedElement>));
var func = (Func<IPublishedElement, IPublishedElement>)meth.CreateDelegate(typeof(Func<IPublishedElement, IPublishedElement>));
modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type };
map[typeName] = type;
@@ -466,26 +548,16 @@ namespace Umbraco.ModelsBuilder.Umbraco
return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map };
}
private static string GenerateModelsCode(IDictionary<string, string> ourFiles, IList<TypeModel> typeModels)
private string GenerateModelsCode(IList<TypeModel> typeModels)
{
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory);
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
File.Delete(file);
var map = typeModels.ToDictionary(x => x.Alias, x => x.ClrName);
foreach (var typeModel in typeModels)
{
foreach (var propertyModel in typeModel.Properties)
{
propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map);
}
}
var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles);
var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace);
var builder = new TextBuilder(_config, typeModels);
var codeBuilder = new StringBuilder();
builder.Generate(codeBuilder, builder.GetModelsToGenerate());
@@ -577,7 +649,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
//if (_building && OurFiles.Contains(changed))
//{
// //_logger.Logger.Info<PureLiveModelFactory>("Ignoring files self-changes.");
// //_logger.Info<PureLiveModelFactory>("Ignoring files self-changes.");
// return;
//}
@@ -585,9 +657,10 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (OurFiles.Contains(changed))
return;
_logger.Logger.Info<PureLiveModelFactory>("Detected files changes.");
_logger.Info<PureLiveModelFactory>("Detected files changes.");
ResetModels();
lock (SyncRoot) // don't reset while being locked
ResetModels();
}
public void Stop(bool immediate)

View File

@@ -4,25 +4,19 @@ using System.Linq;
using System.Reflection;
using System.Web.Compilation;
using System.Web.Hosting;
using Microsoft.CodeAnalysis;
using Umbraco.Core;
namespace Umbraco.ModelsBuilder
namespace Umbraco.ModelsBuilder.Embedded
{
internal static class ReferencedAssemblies
{
private static readonly Lazy<IEnumerable<string>> LazyLocations;
private static readonly Lazy<IEnumerable<PortableExecutableReference>> LazyReferences;
static ReferencedAssemblies()
{
LazyLocations = new Lazy<IEnumerable<string>>(() => HostingEnvironment.IsHosted
? GetAllReferencedAssembliesLocationFromBuildManager()
: GetAllReferencedAssembliesFromDomain());
LazyReferences = new Lazy<IEnumerable<PortableExecutableReference>>(() => Locations
.Select(x => MetadataReference.CreateFromFile(x))
.ToArray());
}
/// <summary>
@@ -31,19 +25,68 @@ namespace Umbraco.ModelsBuilder
/// </summary>
public static IEnumerable<string> Locations => LazyLocations.Value;
/// <summary>
/// Gets the metadata reference of all the referenced assemblies.
/// </summary>
public static IEnumerable<PortableExecutableReference> References => LazyReferences.Value;
public static Assembly GetNetStandardAssembly(List<Assembly> assemblies)
{
if (assemblies == null)
assemblies = BuildManager.GetReferencedAssemblies().Cast<Assembly>().ToList();
// hosted, get referenced assemblies from the BuildManader and filter
// for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of
// the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out
try
{
// so, resorting to an ugly trick
// we should have System.Reflection.Metadata around, and it should reference netstandard
var someAssembly = assemblies.First(x => x.FullName.StartsWith("System.Reflection.Metadata,"));
var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,"));
var netStandard = Assembly.Load(netStandardAssemblyName.FullName);
return netStandard;
}
catch { /* never mind */ }
return null;
}
public static Assembly GetNetStandardAssembly()
{
// in PreApplicationStartMethod we cannot get BuildManager.Referenced assemblies, do it differently
try
{
var someAssembly = Assembly.Load("System.Reflection.Metadata");
var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,"));
var netStandard = Assembly.Load(netStandardAssemblyName.FullName);
return netStandard;
}
catch { /* never mind */ }
return null;
}
// hosted, get referenced assemblies from the BuildManager and filter
private static IEnumerable<string> GetAllReferencedAssembliesLocationFromBuildManager()
{
return BuildManager.GetReferencedAssemblies()
.Cast<Assembly>()
var assemblies = BuildManager.GetReferencedAssemblies().Cast<Assembly>().ToList();
assemblies.Add(typeof(ReferencedAssemblies).Assembly); // always include ourselves
// see https://github.com/aspnet/RoslynCodeDomProvider/blob/master/src/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/CSharpCompiler.cs:
// mentions "Bug 913691: Explicitly add System.Runtime as a reference."
// and explicitly adds System.Runtime to references before invoking csc.exe
// so, doing the same here
try
{
var systemRuntime = Assembly.Load("System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
assemblies.Add(systemRuntime);
}
catch { /* never mind */ }
// for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of
// the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out
var netStandard = GetNetStandardAssembly(assemblies);
if (netStandard != null) assemblies.Add(netStandard);
return assemblies
.Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace())
.Select(x => x.Location)
.And(typeof(ReferencedAssemblies).Assembly.Location) // always include ourselves
.Distinct()
.ToList();
}
@@ -99,26 +142,6 @@ namespace Umbraco.ModelsBuilder
// ----
private static IEnumerable<Assembly> GetDeepReferencedAssemblies(Assembly assembly)
{
var visiting = new Stack<Assembly>();
var visited = new HashSet<Assembly>();
visiting.Push(assembly);
visited.Add(assembly);
while (visiting.Count > 0)
{
var visAsm = visiting.Pop();
foreach (var refAsm in visAsm.GetReferencedAssemblies()
.Select(TryLoad)
.Where(x => x != null && visited.Contains(x) == false))
{
yield return refAsm;
visiting.Push(refAsm);
visited.Add(refAsm);
}
}
}
private static Assembly TryLoad(AssemblyName name)
{
@@ -132,6 +155,5 @@ namespace Umbraco.ModelsBuilder
return null;
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
namespace Umbraco.ModelsBuilder
namespace Umbraco.ModelsBuilder.Embedded
{
internal static class TypeExtensions
{

View File

@@ -4,13 +4,15 @@
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7020A059-C0D1-43A0-8EFD-23591A0C9AF6}</ProjectGuid>
<ProjectGuid>{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Umbraco.ModelsBuilder</RootNamespace>
<AssemblyName>Umbraco.ModelsBuilder</AssemblyName>
<RootNamespace>Umbraco.ModelsBuilder.Embedded</RootNamespace>
<AssemblyName>Umbraco.ModelsBuilder.Embedded</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@@ -28,13 +30,14 @@
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Release\Umbraco.ModelsBuilder.xml</DocumentationFile>
<DocumentationFile>bin\Release\Umbraco.ModelsBuilder.Embedded.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Web" />
<Reference Include="System.Web.ApplicationServices" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
@@ -46,62 +49,48 @@
<Compile Include="..\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="Api\ApiBasicAuthFilter.cs" />
<Compile Include="Api\ApiClient.cs" />
<Compile Include="Api\ApiHelper.cs" />
<Compile Include="Api\ApiVersion.cs" />
<Compile Include="Api\GetModelsData.cs" />
<Compile Include="Api\ModelsBuilderApiController.cs" />
<Compile Include="Api\TokenData.cs" />
<Compile Include="Api\ValidateClientVersionData.cs" />
<Compile Include="ApiVersion.cs" />
<Compile Include="BackOffice\ContentTypeModelValidatorBase.cs" />
<Compile Include="BackOffice\MediaTypeModelValidator.cs" />
<Compile Include="BackOffice\MemberTypeModelValidator.cs" />
<Compile Include="Building\Builder.cs" />
<Compile Include="Building\CodeDomBuilder.cs" />
<Compile Include="Building\CodeParser.cs" />
<Compile Include="Building\Compiler.cs" />
<Compile Include="Building\CompilerException.cs" />
<Compile Include="Building\ParseResult.cs" />
<Compile Include="Building\PropertyModel.cs" />
<Compile Include="Building\TextBuilder.cs" />
<Compile Include="Building\TextHeaderWriter.cs" />
<Compile Include="Building\TypeModel.cs" />
<Compile Include="Configuration\ClrNameSource.cs" />
<Compile Include="Configuration\Config.cs" />
<Compile Include="Compose\DisabledModelsBuilderComponent.cs" />
<Compile Include="ConfigsExtensions.cs" />
<Compile Include="Configuration\IModelsBuilderConfig.cs" />
<Compile Include="Configuration\ModelsBuilderConfig.cs" />
<Compile Include="Configuration\ModelsMode.cs" />
<Compile Include="Configuration\ModelsModeExtensions.cs" />
<Compile Include="Configuration\UmbracoConfigExtensions.cs" />
<Compile Include="Dashboard\BuilderDashboardHelper.cs" />
<Compile Include="EnumerableExtensions.cs" />
<Compile Include="IgnoreContentTypeAttribute.cs" />
<Compile Include="IgnorePropertyTypeAttribute.cs" />
<Compile Include="ImplementContentTypeAttribute.cs" />
<Compile Include="BackOffice\DashboardReport.cs" />
<Compile Include="ImplementPropertyTypeAttribute.cs" />
<Compile Include="ModelsBaseClassAttribute.cs" />
<Compile Include="ModelsBuilderAssemblyAttribute.cs" />
<Compile Include="ModelsNamespaceAttribute.cs" />
<Compile Include="ModelsUsingAttribute.cs" />
<Compile Include="ModelsBuilderDashboard.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PublishedElementExtensions.cs" />
<Compile Include="PublishedPropertyTypeExtensions.cs" />
<Compile Include="PureLiveAssemblyAttribute.cs" />
<Compile Include="ReferencedAssemblies.cs" />
<Compile Include="RenameContentTypeAttribute.cs" />
<Compile Include="RenamePropertyTypeAttribute.cs" />
<Compile Include="TypeExtensions.cs" />
<Compile Include="Umbraco\UmbracoServices.cs" />
<Compile Include="Umbraco\HashCombiner.cs" />
<Compile Include="Umbraco\HashHelper.cs" />
<Compile Include="Umbraco\LiveModelsProvider.cs" />
<Compile Include="Umbraco\ModelsBuilderBackOfficeController.cs" />
<Compile Include="Umbraco\ModelsBuilderComponent.cs" />
<Compile Include="Umbraco\ModelsGenerationError.cs" />
<Compile Include="Umbraco\OutOfDateModelsStatus.cs" />
<Compile Include="Umbraco\PublishedModelUtility.cs" />
<Compile Include="Umbraco\PureLiveModelFactory.cs" />
<Compile Include="Validation\ContentTypeModelValidator.cs" />
<Compile Include="HashCombiner.cs" />
<Compile Include="LiveModelsProvider.cs" />
<Compile Include="LiveModelsProviderModule.cs" />
<Compile Include="Building\ModelsGenerator.cs" />
<Compile Include="BackOffice\ModelsBuilderDashboardController.cs" />
<Compile Include="Compose\ModelsBuilderComponent.cs" />
<Compile Include="Compose\ModelsBuilderComposer.cs" />
<Compile Include="Building\TypeModelHasher.cs" />
<Compile Include="Compose\ModelsBuilderInitializer.cs" />
<Compile Include="ModelsGenerationError.cs" />
<Compile Include="OutOfDateModelsStatus.cs" />
<Compile Include="PublishedModelUtility.cs" />
<Compile Include="PureLiveModelFactory.cs" />
<Compile Include="UmbracoServices.cs" />
<Compile Include="BackOffice\ContentTypeModelValidator.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp">
<Version>2.8.0</Version>
<Version>2.10.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub">
<Version>1.0.0-beta2-19324-01</Version>
@@ -111,7 +100,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj">
<Project>{31785bc3-256c-4613-b2f5-a1b0bdded8c1}</Project>
<Project>{31785BC3-256C-4613-B2F5-A1B0BDDED8C1}</Project>
<Name>Umbraco.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
@@ -119,5 +108,11 @@
<Name>Umbraco.Web</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.Mvc">
<Version>5.2.7</Version>
</PackageReference>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -2,17 +2,16 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.ModelsBuilder.Building;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Embedded.Building;
namespace Umbraco.ModelsBuilder.Umbraco
namespace Umbraco.ModelsBuilder.Embedded
{
public class UmbracoServices
public sealed class UmbracoServices
{
private readonly IContentTypeService _contentTypeService;
private readonly IMediaTypeService _mediaTypeService;
@@ -33,6 +32,10 @@ namespace Umbraco.ModelsBuilder.Umbraco
{
var types = new List<TypeModel>();
// TODO: this will require 3 rather large SQL queries on startup in PureLive. I know that these will be cached after lookup but it will slow
// down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can
// load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup
// in more than one place.
types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast<IContentTypeComposition>().ToArray()));
types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast<IContentTypeComposition>().ToArray()));
types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast<IContentTypeComposition>().ToArray()));
@@ -60,38 +63,8 @@ namespace Umbraco.ModelsBuilder.Umbraco
public static string GetClrName(string name, string alias)
{
// ideally we should just be able to re-use Umbraco's alias,
// just upper-casing the first letter, however in v7 for backward
// compatibility reasons aliases derive from names via ToSafeAlias which is
// PreFilter = ApplyUrlReplaceCharacters,
// IsTerm = (c, leading) => leading
// ? char.IsLetter(c) // only letters
// : (char.IsLetterOrDigit(c) || c == '_'), // letter, digit or underscore
// StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase,
// BreakTermsOnUpper = false
//
// but that is not ideal with acronyms and casing
// however we CANNOT change Umbraco
// so, adding a way to "do it right" deriving from name, here
switch (UmbracoConfig.For.ModelsBuilder().ClrNameSource)
{
case ClrNameSource.RawAlias:
// use Umbraco's alias
return alias;
case ClrNameSource.Alias:
// ModelsBuilder's legacy - but not ideal
return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase);
case ClrNameSource.Name:
// derive from name
var source = name.TrimStart('_'); // because CleanStringType.ConvertCase accepts them
return source.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase | CleanStringType.Ascii);
default:
throw new Exception("Invalid ClrNameSource.");
}
// ModelsBuilder's legacy - but not ideal
return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase);
}
private IList<TypeModel> GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes)
@@ -116,36 +89,26 @@ namespace Umbraco.ModelsBuilder.Umbraco
// of course this should never happen, but when it happens, better detect it
// else we end up with weird nullrefs everywhere
if (uniqueTypes.Contains(typeModel.ClrName))
throw new Exception($"Panic: duplicate type ClrName \"{typeModel.ClrName}\".");
throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\".");
uniqueTypes.Add(typeModel.ClrName);
// fixme - we need a better way at figuring out what's an element type!
// and then we should not do the alias filtering below
bool IsElement(PublishedContentType x)
{
return x.Alias.InvariantEndsWith("Element");
}
var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType);
switch (itemType)
{
case PublishedItemType.Content:
if (IsElement(publishedContentType))
{
typeModel.ItemType = TypeModel.ItemTypes.Element;
if (typeModel.ClrName.InvariantEndsWith("Element"))
typeModel.ClrName = typeModel.ClrName.Substring(0, typeModel.ClrName.Length - "Element".Length);
}
else
{
typeModel.ItemType = TypeModel.ItemTypes.Content;
}
typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element
? TypeModel.ItemTypes.Element
: TypeModel.ItemTypes.Content;
break;
case PublishedItemType.Media:
typeModel.ItemType = TypeModel.ItemTypes.Media;
typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element
? TypeModel.ItemTypes.Element
: TypeModel.ItemTypes.Media;
break;
case PublishedItemType.Member:
typeModel.ItemType = TypeModel.ItemTypes.Member;
typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element
? TypeModel.ItemTypes.Element
: TypeModel.ItemTypes.Member;
break;
default:
throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType));
@@ -166,7 +129,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias);
if (publishedPropertyType == null)
throw new Exception($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}.");
throw new PanicException($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}.");
propertyModel.ModelClrType = publishedPropertyType.ModelClrType;
@@ -188,7 +151,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
foreach (var contentType in contentTypes)
{
var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id);
if (typeModel == null) throw new Exception("Panic: no type model matching content type.");
if (typeModel == null) throw new PanicException("Panic: no type model matching content type.");
IEnumerable<IContentTypeComposition> compositionTypes;
var contentTypeAsMedia = contentType as IMediaType;
@@ -197,12 +160,12 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition;
else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition;
else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition;
else throw new Exception(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName));
else throw new PanicException(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName));
foreach (var compositionType in compositionTypes)
{
var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id);
if (compositionModel == null) throw new Exception("Panic: composition type does not exist.");
if (compositionModel == null) throw new PanicException("Panic: composition type does not exist.");
if (compositionType.Id == contentType.ParentId) continue;
@@ -223,11 +186,9 @@ namespace Umbraco.ModelsBuilder.Umbraco
{
var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant());
foreach (var group in groups.Where(x => x.Count() > 1))
{
throw new NotSupportedException($"Alias \"{group.Key}\" is used by types"
+ $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique."
+ " One of the aliases must be modified in order to use the ModelsBuilder.");
}
return typeModels;
}

View File

@@ -1,86 +0,0 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http.Controllers;
using System.Web.Security;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Membership;
namespace Umbraco.ModelsBuilder.Api
{
//TODO: This needs to be changed:
// * Authentication cannot happen in a filter, only Authorization
// * The filter must be an AuthorizationFilter, not an ActionFilter
// * Authorization must be done using the Umbraco logic - it is very specific for claim checking for ASP.Net Identity
// * Theoretically this shouldn't be required whatsoever because when we authenticate a request that has Basic Auth (i.e. for
// VS to work, it will add the correct Claims to the Identity and it will automatically be authorized.
//
// we *do* have POC supporting ASP.NET identity, however they require some config on the server
// we'll keep using this quick-and-dirty method for the time being
public class ApiBasicAuthFilter : System.Web.Http.Filters.ActionFilterAttribute // use the http one, not mvc, with api controllers!
{
private static readonly char[] Separator = ":".ToCharArray();
private readonly string _section;
public ApiBasicAuthFilter(string section)
{
_section = section;
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var user = Authenticate(actionContext.Request);
if (user == null || !user.AllowedSections.Contains(_section))
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
//else
//{
// // note - would that be a proper way to pass data to the controller?
// // see http://stevescodingblog.co.uk/basic-authentication-with-asp-net-webapi/
// actionContext.ControllerContext.RouteData.Values["umbraco-user"] = user;
//}
base.OnActionExecuting(actionContext);
}
catch
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
}
private static IUser Authenticate(HttpRequestMessage request)
{
var ah = request.Headers.Authorization;
if (ah == null || ah.Scheme != "Basic")
return null;
var token = ah.Parameter;
var credentials = Encoding.ASCII
.GetString(Convert.FromBase64String(token))
.Split(Separator);
if (credentials.Length != 2)
return null;
var username = ApiClient.DecodeTokenElement(credentials[0]);
var password = ApiClient.DecodeTokenElement(credentials[1]);
var providerKey = UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider;
var provider = Membership.Providers[providerKey];
if (provider == null || !provider.ValidateUser(username, password))
return null;
var user = Current.Services.UserService.GetByUsername(username);
if (!user.IsApproved || user.IsLockedOut)
return null;
return user;
}
}
}

View File

@@ -1,159 +0,0 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Text;
namespace Umbraco.ModelsBuilder.Api
{
public class ApiClient
{
private readonly string _url;
private readonly string _user;
private readonly string _password;
private readonly JsonMediaTypeFormatter _formatter;
private readonly MediaTypeFormatter[] _formatters;
// fixme hardcoded?
// could be options - but we cannot "discover" them as the API client runs outside of the web app
// in addition, anything that references the controller forces API clients to reference Umbraco.Core
private const string ApiControllerUrl = "/Umbraco/BackOffice/ModelsBuilder/ModelsBuilderApi/";
public ApiClient(string url, string user, string password)
{
_url = url.TrimEnd('/');
_user = user;
_password = password;
_formatter = new JsonMediaTypeFormatter();
_formatters = new MediaTypeFormatter[] { _formatter };
}
private void SetBaseAddress(HttpClient client, string url)
{
try
{
client.BaseAddress = new Uri(url);
}
catch
{
throw new UriFormatException($"Invalid URI: the format of the URI \"{url}\" could not be determined.");
}
}
public void ValidateClientVersion()
{
// FIXME - add proxys support
var hch = new HttpClientHandler();
using (var client = new HttpClient(hch))
{
SetBaseAddress(client, _url);
Authorize(client);
var data = new ValidateClientVersionData
{
ClientVersion = ApiVersion.Current.Version,
MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient,
};
var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.ValidateClientVersion),
data, _formatter).Result;
// this is not providing enough details in case of an error - do our own reporting
//result.EnsureSuccessStatusCode();
EnsureSuccess(result);
}
}
public IDictionary<string, string> GetModels(Dictionary<string, string> ourFiles, string modelsNamespace)
{
// FIXME - add proxys support
var hch = new HttpClientHandler();
//hch.Proxy = new WebProxy("path.to.proxy", 8888);
//hch.UseProxy = true;
using (var client = new HttpClient(hch))
{
SetBaseAddress(client, _url);
Authorize(client);
var data = new GetModelsData
{
Namespace = modelsNamespace,
ClientVersion = ApiVersion.Current.Version,
MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient,
Files = ourFiles
};
var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.GetModels),
data, _formatter).Result;
// this is not providing enough details in case of an error - do our own reporting
//result.EnsureSuccessStatusCode();
EnsureSuccess(result);
var genFiles = result.Content.ReadAsAsync<IDictionary<string, string>>(_formatters).Result;
return genFiles;
}
}
private static void EnsureSuccess(HttpResponseMessage result)
{
if (result.IsSuccessStatusCode) return;
var text = result.Content.ReadAsStringAsync().Result;
throw new Exception($"Response status code does not indicate success ({result.StatusCode})\n{text}");
}
private void Authorize(HttpClient client)
{
AuthorizeBasic(client);
}
// fixme - for the time being, we don't cache the token and we auth on each API call
// not used at the moment
/*
private void AuthorizeIdentity(HttpClient client)
{
var formData = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("userName", _user),
new KeyValuePair<string, string>("password", _password),
});
var result = client.PostAsync(_url + UmbracoOAuthTokenUrl, formData).Result;
EnsureSuccess(result);
var token = result.Content.ReadAsAsync<TokenData>(_formatters).Result;
if (token.TokenType != "bearer")
throw new Exception($"Received invalid token type \"{token.TokenType}\", expected \"bearer\".");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
}
*/
private void AuthorizeBasic(HttpClient client)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes(EncodeTokenElement(_user) + ':' + EncodeTokenElement(_password))));
}
public static string EncodeTokenElement(string s)
{
return s.Replace("%", "%a").Replace(":", "%b");
}
public static string DecodeTokenElement(string s)
{
return s.Replace("%b", ":").Replace("%a", "%");
}
}
}

View File

@@ -1,27 +0,0 @@
using System.Collections.Generic;
using System.Text;
using Umbraco.ModelsBuilder.Building;
using Umbraco.ModelsBuilder.Umbraco;
namespace Umbraco.ModelsBuilder.Api
{
internal static class ApiHelper
{
public static Dictionary<string, string> GetModels(UmbracoServices umbracoServices, string modelsNamespace, IDictionary<string, string> files)
{
var typeModels = umbracoServices.GetAllTypes();
var parseResult = new CodeParser().ParseWithReferencedAssemblies(files);
var builder = new TextBuilder(typeModels, parseResult, modelsNamespace);
var models = new Dictionary<string, string>();
foreach (var typeModel in builder.GetModelsToGenerate())
{
var sb = new StringBuilder();
builder.Generate(sb, typeModel);
models[typeModel.ClrName] = sb.ToString();
}
return models;
}
}
}

View File

@@ -1,88 +0,0 @@
using System;
using System.Reflection;
namespace Umbraco.ModelsBuilder.Api
{
/// <summary>
/// Manages API version handshake between client and server.
/// </summary>
public class ApiVersion
{
#region Configure
// indicate the minimum version of the client API that is supported by this server's API.
// eg our Version = 4.8 but we support connections from VSIX down to version 3.2
// => as a server, we accept connections from client down to version ...
private static readonly Version MinClientVersionSupportedByServerConst = new Version(3, 0, 0, 0);
// indicate the minimum version of the server that can support the client API
// eg our Version = 4.8 and we know we're compatible with website server down to version 3.2
// => as a client, we tell the server down to version ... that it should accept us
private static readonly Version MinServerVersionSupportingClientConst = new Version(3, 0, 0, 0);
#endregion
/// <summary>
/// Initializes a new instance of the <see cref="ApiVersion"/> class.
/// </summary>
/// <param name="executingVersion">The currently executing version.</param>
/// <param name="minClientVersionSupportedByServer">The min client version supported by the server.</param>
/// <param name="minServerVersionSupportingClient">An opt min server version supporting the client.</param>
internal ApiVersion(Version executingVersion, Version minClientVersionSupportedByServer, Version minServerVersionSupportingClient = null)
{
if (executingVersion == null) throw new ArgumentNullException(nameof(executingVersion));
if (minClientVersionSupportedByServer == null) throw new ArgumentNullException(nameof(minClientVersionSupportedByServer));
Version = executingVersion;
MinClientVersionSupportedByServer = minClientVersionSupportedByServer;
MinServerVersionSupportingClient = minServerVersionSupportingClient;
}
/// <summary>
/// Gets the currently executing API version.
/// </summary>
public static ApiVersion Current { get; }
= new ApiVersion(Assembly.GetExecutingAssembly().GetName().Version,
MinClientVersionSupportedByServerConst, MinServerVersionSupportingClientConst);
/// <summary>
/// Gets the executing version of the API.
/// </summary>
public Version Version { get; }
/// <summary>
/// Gets the min client version supported by the server.
/// </summary>
public Version MinClientVersionSupportedByServer { get; }
/// <summary>
/// Gets the min server version supporting the client.
/// </summary>
public Version MinServerVersionSupportingClient { get; }
/// <summary>
/// Gets a value indicating whether the API server is compatible with a client.
/// </summary>
/// <param name="clientVersion">The client version.</param>
/// <param name="minServerVersionSupportingClient">An opt min server version supporting the client.</param>
/// <remarks>
/// <para>A client is compatible with a server if the client version is greater-or-equal _minClientVersionSupportedByServer
/// (ie client can be older than server, up to a point) AND the client version is lower-or-equal the server version
/// (ie client cannot be more recent than server) UNLESS the server .</para>
/// </remarks>
public bool IsCompatibleWith(Version clientVersion, Version minServerVersionSupportingClient = null)
{
// client cannot be older than server's min supported version
if (clientVersion < MinClientVersionSupportedByServer)
return false;
// if we know about this client (client is older than server), it is supported
if (clientVersion <= Version) // if we know about this client (client older than server)
return true;
// if we don't know about this client (client is newer than server),
// give server a chance to tell client it is, indeed, ok to support it
return minServerVersionSupportingClient != null && minServerVersionSupportingClient <= Version;
}
}
}

View File

@@ -1,17 +0,0 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Umbraco.ModelsBuilder.Api
{
[DataContract]
public class GetModelsData : ValidateClientVersionData
{
[DataMember]
public string Namespace { get; set; }
[DataMember]
public IDictionary<string, string> Files { get; set; }
public override bool IsValid => base.IsValid && Files != null;
}
}

View File

@@ -1,85 +0,0 @@
using System;
using System.Net;
using System.Net.Http;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Umbraco;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
namespace Umbraco.ModelsBuilder.Api
{
// read http://umbraco.com/follow-us/blog-archive/2014/1/17/heads-up,-breaking-change-coming-in-702-and-62.aspx
// read http://our.umbraco.org/forum/developers/api-questions/43025-Web-API-authentication
// UmbracoAuthorizedApiController :: /Umbraco/BackOffice/Zbu/ModelsBuilderApi/GetTypeModels
// UmbracoApiController :: /Umbraco/Zbu/ModelsBuilderApi/GetTypeModels ?? UNLESS marked with isbackoffice
//
// BEWARE! the controller url is hard-coded in ModelsBuilderApi and needs to be in sync!
[PluginController(ControllerArea)]
[IsBackOffice]
//[UmbracoApplicationAuthorize(Constants.Applications.Developer)] // see ApiBasicAuthFilter - that one would be for ASP.NET identity
public class ModelsBuilderApiController : UmbracoApiController // UmbracoAuthorizedApiController - for ASP.NET identity
{
public const string ControllerArea = "ModelsBuilder";
private readonly UmbracoServices _umbracoServices;
public ModelsBuilderApiController(UmbracoServices umbracoServices)
{
_umbracoServices = umbracoServices;
}
// invoked by the API
[System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers!
[ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth
public HttpResponseMessage ValidateClientVersion(ValidateClientVersionData data)
{
if (!UmbracoConfig.For.ModelsBuilder().ApiServer)
return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you.");
if (!ModelState.IsValid || data == null || !data.IsValid)
return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data.");
var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient);
return (checkResult.Success
? Request.CreateResponse(HttpStatusCode.OK, "OK", Configuration.Formatters.JsonFormatter)
: checkResult.Result);
}
// invoked by the API
[System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers!
[ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth
public HttpResponseMessage GetModels(GetModelsData data)
{
if (!UmbracoConfig.For.ModelsBuilder().ApiServer)
return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you.");
if (!ModelState.IsValid || data == null || !data.IsValid)
return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data.");
var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient);
if (!checkResult.Success)
return checkResult.Result;
var models = ApiHelper.GetModels(_umbracoServices, data.Namespace, data.Files);
return Request.CreateResponse(HttpStatusCode.OK, models, Configuration.Formatters.JsonFormatter);
}
private Attempt<HttpResponseMessage> CheckVersion(Version clientVersion, Version minServerVersionSupportingClient)
{
if (clientVersion == null)
return Attempt<HttpResponseMessage>.Fail(Request.CreateResponse(HttpStatusCode.Forbidden,
$"API version conflict: client version (<null>) is not compatible with server version({ApiVersion.Current.Version})."));
// minServerVersionSupportingClient can be null
var isOk = ApiVersion.Current.IsCompatibleWith(clientVersion, minServerVersionSupportingClient);
var response = isOk ? null : Request.CreateResponse(HttpStatusCode.Forbidden,
$"API version conflict: client version ({clientVersion}) is not compatible with server version({ApiVersion.Current.Version}).");
return Attempt<HttpResponseMessage>.If(isOk, response);
}
}
}

View File

@@ -1,17 +0,0 @@
using System.Runtime.Serialization;
namespace Umbraco.ModelsBuilder.Api
{
[DataContract]
class TokenData
{
[DataMember(Name = "access_token")]
public string AccessToken { get; set; }
[DataMember(Name = "token_type")]
public string TokenType { get; set; }
[DataMember(Name = "expires_in")]
public int ExpiresIn { get; set; }
}
}

View File

@@ -1,57 +0,0 @@
using System;
using System.Runtime.Serialization;
namespace Umbraco.ModelsBuilder.Api
{
[DataContract]
public class ValidateClientVersionData
{
// issues 32, 34... problems when serializing versions
//
// make sure System.Version objects are transfered as strings
// depending on the JSON serializer version, it looks like versions are causing issues
// see
// http://stackoverflow.com/questions/13170386/why-system-version-in-json-string-does-not-deserialize-correctly
//
// if the class is marked with [DataContract] then only properties marked with [DataMember]
// are serialized and the rest is ignored, see
// http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization
[DataMember]
public string ClientVersionString
{
get { return VersionToString(ClientVersion); }
set { ClientVersion = ParseVersion(value, false, "client"); }
}
[DataMember]
public string MinServerVersionSupportingClientString
{
get { return VersionToString(MinServerVersionSupportingClient); }
set { MinServerVersionSupportingClient = ParseVersion(value, true, "minServer"); }
}
// not serialized
public Version ClientVersion { get; set; }
public Version MinServerVersionSupportingClient { get; set; }
private static string VersionToString(Version version)
{
return version?.ToString() ?? "0.0.0.0";
}
private static Version ParseVersion(string value, bool canBeNull, string name)
{
if (string.IsNullOrWhiteSpace(value) && canBeNull)
return null;
Version version;
if (Version.TryParse(value, out version))
return version;
throw new ArgumentException($"Failed to parse \"{value}\" as {name} version.");
}
public virtual bool IsValid => ClientVersion != null;
}
}

View File

@@ -1,113 +0,0 @@
using System.CodeDom;
using System.Collections.Generic;
namespace Umbraco.ModelsBuilder.Building
{
// NOTE
// See nodes in Builder.cs class - that one does not work, is not complete,
// and was just some sort of experiment...
/// <summary>
/// Implements a builder that works by using CodeDom
/// </summary>
internal class CodeDomBuilder : Builder
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeDomBuilder"/> class with a list of models to generate.
/// </summary>
/// <param name="typeModels">The list of models to generate.</param>
public CodeDomBuilder(IList<TypeModel> typeModels)
: base(typeModels, null)
{ }
/// <summary>
/// Outputs a generated model to a code namespace.
/// </summary>
/// <param name="ns">The code namespace.</param>
/// <param name="typeModel">The model to generate.</param>
public void Generate(CodeNamespace ns, TypeModel typeModel)
{
// what about USING?
// what about references?
if (typeModel.IsMixin)
{
var i = new CodeTypeDeclaration("I" + typeModel.ClrName)
{
IsInterface = true,
IsPartial = true,
Attributes = MemberAttributes.Public
};
i.BaseTypes.Add(typeModel.BaseType == null ? "IPublishedContent" : "I" + typeModel.BaseType.ClrName);
foreach (var mixinType in typeModel.DeclaringInterfaces)
i.BaseTypes.Add(mixinType.ClrName);
i.Comments.Add(new CodeCommentStatement($"Mixin content Type {typeModel.Id} with alias \"{typeModel.Alias}\""));
foreach (var propertyModel in typeModel.Properties)
{
var p = new CodeMemberProperty
{
Name = propertyModel.ClrName,
Type = new CodeTypeReference(propertyModel.ModelClrType),
Attributes = MemberAttributes.Public,
HasGet = true,
HasSet = false
};
i.Members.Add(p);
}
}
var c = new CodeTypeDeclaration(typeModel.ClrName)
{
IsClass = true,
IsPartial = true,
Attributes = MemberAttributes.Public
};
c.BaseTypes.Add(typeModel.BaseType == null ? "PublishedContentModel" : typeModel.BaseType.ClrName);
// if it's a missing it implements its own interface
if (typeModel.IsMixin)
c.BaseTypes.Add("I" + typeModel.ClrName);
// write the mixins, if any, as interfaces
// only if not a mixin because otherwise the interface already has them
if (typeModel.IsMixin == false)
foreach (var mixinType in typeModel.DeclaringInterfaces)
c.BaseTypes.Add("I" + mixinType.ClrName);
foreach (var mixin in typeModel.MixinTypes)
c.BaseTypes.Add("I" + mixin.ClrName);
c.Comments.Add(new CodeCommentStatement($"Content Type {typeModel.Id} with alias \"{typeModel.Alias}\""));
foreach (var propertyModel in typeModel.Properties)
{
var p = new CodeMemberProperty
{
Name = propertyModel.ClrName,
Type = new CodeTypeReference(propertyModel.ModelClrType),
Attributes = MemberAttributes.Public,
HasGet = true,
HasSet = false
};
p.GetStatements.Add(new CodeMethodReturnStatement( // return
new CodeMethodInvokeExpression(
new CodeMethodReferenceExpression(
new CodeThisReferenceExpression(), // this
"Value", // .Value
new[] // <T>
{
new CodeTypeReference(propertyModel.ModelClrType)
}),
new CodeExpression[] // ("alias")
{
new CodePrimitiveExpression(propertyModel.Alias)
})));
c.Members.Add(p);
}
}
}
}

View File

@@ -1,238 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.ModelsBuilder.Building
{
/// <summary>
/// Implements code parsing.
/// </summary>
/// <remarks>Parses user's code and look for generator's instructions.</remarks>
internal class CodeParser
{
/// <summary>
/// Parses a set of file.
/// </summary>
/// <param name="files">A set of (filename,content) representing content to parse.</param>
/// <returns>The result of the code parsing.</returns>
/// <remarks>The set of files is a dictionary of name, content.</remarks>
public ParseResult Parse(IDictionary<string, string> files)
{
return Parse(files, Enumerable.Empty<PortableExecutableReference>());
}
/// <summary>
/// Parses a set of file.
/// </summary>
/// <param name="files">A set of (filename,content) representing content to parse.</param>
/// <param name="references">Assemblies to reference in compilations.</param>
/// <returns>The result of the code parsing.</returns>
/// <remarks>The set of files is a dictionary of name, content.</remarks>
public ParseResult Parse(IDictionary<string, string> files, IEnumerable<PortableExecutableReference> references)
{
SyntaxTree[] trees;
var compiler = new Compiler { References = references };
var compilation = compiler.GetCompilation("Umbraco.ModelsBuilder.Generated", files, out trees);
var disco = new ParseResult();
foreach (var tree in trees)
Parse(disco, compilation, tree);
return disco;
}
public ParseResult ParseWithReferencedAssemblies(IDictionary<string, string> files)
{
return Parse(files, ReferencedAssemblies.References);
}
private static void Parse(ParseResult disco, CSharpCompilation compilation, SyntaxTree tree)
{
var model = compilation.GetSemanticModel(tree);
//we quite probably have errors but that is normal
//var diags = model.GetDiagnostics();
var classDecls = tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>();
foreach (var classSymbol in classDecls.Select(x => model.GetDeclaredSymbol(x)))
{
ParseClassSymbols(disco, classSymbol);
var baseClassSymbol = classSymbol.BaseType;
if (baseClassSymbol != null)
//disco.SetContentBaseClass(SymbolDisplay.ToDisplayString(classSymbol), SymbolDisplay.ToDisplayString(baseClassSymbol));
disco.SetContentBaseClass(classSymbol.Name, baseClassSymbol.Name);
var interfaceSymbols = classSymbol.Interfaces;
disco.SetContentInterfaces(classSymbol.Name, //SymbolDisplay.ToDisplayString(classSymbol),
interfaceSymbols.Select(x => x.Name)); //SymbolDisplay.ToDisplayString(x)));
var hasCtor = classSymbol.Constructors
.Any(x =>
{
if (x.IsStatic) return false;
if (x.Parameters.Length != 1) return false;
var type1 = x.Parameters[0].Type;
var type2 = typeof (IPublishedContent);
return type1.ToDisplayString() == type2.FullName;
});
if (hasCtor)
disco.SetHasCtor(classSymbol.Name);
foreach (var propertySymbol in classSymbol.GetMembers().Where(x => x is IPropertySymbol))
ParsePropertySymbols(disco, classSymbol, propertySymbol);
foreach (var staticMethodSymbol in classSymbol.GetMembers().Where(x => x is IMethodSymbol))
ParseMethodSymbol(disco, classSymbol, staticMethodSymbol);
}
var interfaceDecls = tree.GetRoot().DescendantNodes().OfType<InterfaceDeclarationSyntax>();
foreach (var interfaceSymbol in interfaceDecls.Select(x => model.GetDeclaredSymbol(x)))
{
ParseClassSymbols(disco, interfaceSymbol);
var interfaceSymbols = interfaceSymbol.Interfaces;
disco.SetContentInterfaces(interfaceSymbol.Name, //SymbolDisplay.ToDisplayString(interfaceSymbol),
interfaceSymbols.Select(x => x.Name)); // SymbolDisplay.ToDisplayString(x)));
}
ParseAssemblySymbols(disco, compilation.Assembly);
}
private static void ParseClassSymbols(ParseResult disco, ISymbol symbol)
{
foreach (var attrData in symbol.GetAttributes())
{
var attrClassSymbol = attrData.AttributeClass;
// handle errors
if (attrClassSymbol is IErrorTypeSymbol) continue;
if (attrData.AttributeConstructor == null) continue;
var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol);
switch (attrClassName)
{
case "Umbraco.ModelsBuilder.IgnorePropertyTypeAttribute":
var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value;
disco.SetIgnoredProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToIgnore);
break;
case "Umbraco.ModelsBuilder.RenamePropertyTypeAttribute":
var propertyAliasToRename = (string)attrData.ConstructorArguments[0].Value;
var propertyRenamed = (string)attrData.ConstructorArguments[1].Value;
disco.SetRenamedProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToRename, propertyRenamed);
break;
// that one causes all sorts of issues with references to Umbraco.Core in Roslyn
//case "Umbraco.Core.Models.PublishedContent.PublishedContentModelAttribute":
// var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value;
// disco.SetRenamedContent(contentAliasToRename, symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/);
// break;
case "Umbraco.ModelsBuilder.ImplementContentTypeAttribute":
var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value;
disco.SetRenamedContent(contentAliasToRename, symbol.Name, true /*SymbolDisplay.ToDisplayString(symbol)*/);
break;
}
}
}
private static void ParsePropertySymbols(ParseResult disco, ISymbol classSymbol, ISymbol symbol)
{
foreach (var attrData in symbol.GetAttributes())
{
var attrClassSymbol = attrData.AttributeClass;
// handle errors
if (attrClassSymbol is IErrorTypeSymbol) continue;
if (attrData.AttributeConstructor == null) continue;
var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol);
// ReSharper disable once SwitchStatementMissingSomeCases
switch (attrClassName)
{
case "Umbraco.ModelsBuilder.ImplementPropertyTypeAttribute":
var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value;
disco.SetIgnoredProperty(classSymbol.Name /*SymbolDisplay.ToDisplayString(classSymbol)*/, propertyAliasToIgnore);
break;
}
}
}
private static void ParseAssemblySymbols(ParseResult disco, ISymbol symbol)
{
foreach (var attrData in symbol.GetAttributes())
{
var attrClassSymbol = attrData.AttributeClass;
// handle errors
if (attrClassSymbol is IErrorTypeSymbol) continue;
if (attrData.AttributeConstructor == null) continue;
var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol);
switch (attrClassName)
{
case "Umbraco.ModelsBuilder.IgnoreContentTypeAttribute":
var contentAliasToIgnore = (string)attrData.ConstructorArguments[0].Value;
// see notes in IgnoreContentTypeAttribute
//var ignoreContent = (bool)attrData.ConstructorArguments[1].Value;
//var ignoreMixin = (bool)attrData.ConstructorArguments[1].Value;
//var ignoreMixinProperties = (bool)attrData.ConstructorArguments[1].Value;
disco.SetIgnoredContent(contentAliasToIgnore /*, ignoreContent, ignoreMixin, ignoreMixinProperties*/);
break;
case "Umbraco.ModelsBuilder.RenameContentTypeAttribute":
var contentAliasToRename = (string) attrData.ConstructorArguments[0].Value;
var contentRenamed = (string)attrData.ConstructorArguments[1].Value;
disco.SetRenamedContent(contentAliasToRename, contentRenamed, false);
break;
case "Umbraco.ModelsBuilder.ModelsBaseClassAttribute":
var modelsBaseClass = (INamedTypeSymbol) attrData.ConstructorArguments[0].Value;
if (modelsBaseClass is IErrorTypeSymbol)
throw new Exception($"Invalid base class type \"{modelsBaseClass.Name}\".");
disco.SetModelsBaseClassName(SymbolDisplay.ToDisplayString(modelsBaseClass));
break;
case "Umbraco.ModelsBuilder.ModelsNamespaceAttribute":
var modelsNamespace= (string) attrData.ConstructorArguments[0].Value;
disco.SetModelsNamespace(modelsNamespace);
break;
case "Umbraco.ModelsBuilder.ModelsUsingAttribute":
var usingNamespace = (string)attrData.ConstructorArguments[0].Value;
disco.SetUsingNamespace(usingNamespace);
break;
}
}
}
private static void ParseMethodSymbol(ParseResult disco, ISymbol classSymbol, ISymbol symbol)
{
var methodSymbol = symbol as IMethodSymbol;
if (methodSymbol == null
|| !methodSymbol.IsStatic
|| methodSymbol.IsGenericMethod
|| methodSymbol.ReturnsVoid
|| methodSymbol.IsExtensionMethod
|| methodSymbol.Parameters.Length != 1)
return;
var returnType = methodSymbol.ReturnType;
var paramSymbol = methodSymbol.Parameters[0];
var paramType = paramSymbol.Type;
// cannot do this because maybe the param type is ISomething and we don't have
// that type yet - will be generated - so cannot put any condition on it really
//const string iPublishedContent = "Umbraco.Core.Models.IPublishedContent";
//var implements = paramType.AllInterfaces.Any(x => x.ToDisplayString() == iPublishedContent);
//if (!implements)
// return;
disco.SetStaticMixinMethod(classSymbol.Name, methodSymbol.Name, returnType.Name, paramType.Name);
}
}
}

View File

@@ -1,171 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
namespace Umbraco.ModelsBuilder.Building
{
// main Roslyn compiler
internal class Compiler
{
private readonly LanguageVersion _languageVersion;
public Compiler()
: this(UmbracoConfig.For.ModelsBuilder().LanguageVersion)
{ }
public Compiler(LanguageVersion languageVersion)
{
_languageVersion = languageVersion;
References = ReferencedAssemblies.References;
Debug = HttpContext.Current != null && HttpContext.Current.IsDebuggingEnabled;
}
// gets or sets the references
public IEnumerable<PortableExecutableReference> References { get; set; }
public bool Debug { get; set; }
// gets a compilation
public CSharpCompilation GetCompilation(string assemblyName, IDictionary<string, string> files)
{
SyntaxTree[] trees;
return GetCompilation(assemblyName, files, out trees);
}
// gets a compilation
// used by CodeParser to get a "compilation" of the existing files
public CSharpCompilation GetCompilation(string assemblyName, IDictionary<string, string> files, out SyntaxTree[] trees)
{
var options = new CSharpParseOptions(_languageVersion);
trees = files.Select(x =>
{
var text = x.Value;
var tree = CSharpSyntaxTree.ParseText(text, /*options:*/ options);
var diagnostic = tree.GetDiagnostics().FirstOrDefault(y => y.Severity == DiagnosticSeverity.Error);
if (diagnostic != null)
ThrowExceptionFromDiagnostic(x.Key, x.Value, diagnostic);
return tree;
}).ToArray();
var refs = References;
var compilationOptions = new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default,
optimizationLevel: Debug ? OptimizationLevel.Debug : OptimizationLevel.Release
);
var compilation = CSharpCompilation.Create(
assemblyName,
/*syntaxTrees:*/ trees,
/*references:*/ refs,
compilationOptions);
return compilation;
}
// compile files into a Dll
// used by ModelsBuilderBackOfficeController in [Live]Dll mode, to compile the models to disk
public void Compile(string assemblyName, IDictionary<string, string> files, string binPath)
{
var assemblyPath = Path.Combine(binPath, assemblyName + ".dll");
using (var stream = new FileStream(assemblyPath, FileMode.Create))
{
Compile(assemblyName, files, stream);
}
// this is how we'd create the pdb:
/*
var pdbPath = Path.Combine(binPath, assemblyName + ".pdb");
// create the compilation
var compilation = GetCompilation(assemblyName, files);
// check diagnostics for errors (not warnings)
foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error))
ThrowExceptionFromDiagnostic(files, diag);
// emit
var result = compilation.Emit(assemblyPath, pdbPath);
if (result.Success) return;
// deal with errors
var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error);
ThrowExceptionFromDiagnostic(files, diagnostic);
*/
}
// compile files into an assembly
public Assembly Compile(string assemblyName, IDictionary<string, string> files)
{
using (var stream = new MemoryStream())
{
Compile(assemblyName, files, stream);
return Assembly.Load(stream.GetBuffer());
}
}
// compile one file into an assembly
public Assembly Compile(string assemblyName, string path, string code)
{
using (var stream = new MemoryStream())
{
Compile(assemblyName, new Dictionary<string, string> { { path, code } }, stream);
return Assembly.Load(stream.GetBuffer());
}
}
// compiles files into a stream
public void Compile(string assemblyName, IDictionary<string, string> files, Stream stream)
{
// create the compilation
var compilation = GetCompilation(assemblyName, files);
// check diagnostics for errors (not warnings)
foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error))
ThrowExceptionFromDiagnostic(files, diag);
// emit
var result = compilation.Emit(stream);
if (result.Success) return;
// deal with errors
var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error);
ThrowExceptionFromDiagnostic(files, diagnostic);
}
// compiles one file into a stream
public void Compile(string assemblyName, string path, string code, Stream stream)
{
Compile(assemblyName, new Dictionary<string, string> { { path, code } }, stream);
}
private static void ThrowExceptionFromDiagnostic(IDictionary<string, string> files, Diagnostic diagnostic)
{
var message = diagnostic.GetMessage();
if (diagnostic.Location == Location.None)
throw new CompilerException(message);
var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1;
var path = diagnostic.Location.SourceTree.FilePath;
var code = files.ContainsKey(path) ? files[path] : string.Empty;
throw new CompilerException(message, path, code, position);
}
private static void ThrowExceptionFromDiagnostic(string path, string code, Diagnostic diagnostic)
{
var message = diagnostic.GetMessage();
if (diagnostic.Location == Location.None)
throw new CompilerException(message);
var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1;
throw new CompilerException(message, path, code, position);
}
}
}

View File

@@ -1,25 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder.Building
{
public class CompilerException : Exception
{
public CompilerException(string message)
: base(message)
{ }
public CompilerException(string message, string path, string sourceCode, int line)
: base(message)
{
Path = path;
SourceCode = sourceCode;
Line = line;
}
public string Path { get; } = string.Empty;
public string SourceCode { get; } = string.Empty;
public int Line { get; } = -1;
}
}

View File

@@ -1,275 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.ModelsBuilder.Building
{
/// <summary>
/// Contains the result of a code parsing.
/// </summary>
internal class ParseResult
{
// "alias" is the umbraco alias
// content "name" is the complete name eg Foo.Bar.Name
// property "name" is just the local name
// see notes in IgnoreContentTypeAttribute
private readonly HashSet<string> _ignoredContent
= new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
//private readonly HashSet<string> _ignoredMixin
// = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
//private readonly HashSet<string> _ignoredMixinProperties
// = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
private readonly Dictionary<string, string> _renamedContent
= new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
private readonly HashSet<string> _withImplementContent
= new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
private readonly Dictionary<string, HashSet<string>> _ignoredProperty
= new Dictionary<string, HashSet<string>>();
private readonly Dictionary<string, Dictionary<string, string>> _renamedProperty
= new Dictionary<string, Dictionary<string, string>>();
private readonly Dictionary<string, string> _contentBase
= new Dictionary<string, string>();
private readonly Dictionary<string, string[]> _contentInterfaces
= new Dictionary<string, string[]>();
private readonly List<string> _usingNamespaces
= new List<string>();
private readonly Dictionary<string, List<StaticMixinMethodInfo>> _staticMixins
= new Dictionary<string, List<StaticMixinMethodInfo>>();
private readonly HashSet<string> _withCtor
= new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
public static readonly ParseResult Empty = new ParseResult();
private class StaticMixinMethodInfo
{
public StaticMixinMethodInfo(string contentName, string methodName, string returnType, string paramType)
{
ContentName = contentName;
MethodName = methodName;
//ReturnType = returnType;
//ParamType = paramType;
}
// short name eg Type1
public string ContentName { get; private set; }
// short name eg GetProp1
public string MethodName { get; private set; }
// those types cannot be FQ because when parsing, some of them
// might not exist since we're generating them... and so prob.
// that info is worthless - not using it anyway at the moment...
//public string ReturnType { get; private set; }
//public string ParamType { get; private set; }
}
#region Declare
// content with that alias should not be generated
// alias can end with a * (wildcard)
public void SetIgnoredContent(string contentAlias /*, bool ignoreContent, bool ignoreMixin, bool ignoreMixinProperties*/)
{
//if (ignoreContent)
_ignoredContent.Add(contentAlias);
//if (ignoreMixin)
// _ignoredMixin.Add(contentAlias);
//if (ignoreMixinProperties)
// _ignoredMixinProperties.Add(contentAlias);
}
// content with that alias should be generated with a different name
public void SetRenamedContent(string contentAlias, string contentName, bool withImplement)
{
_renamedContent[contentAlias] = contentName;
if (withImplement)
_withImplementContent.Add(contentAlias);
}
// property with that alias should not be generated
// applies to content name and any content that implements it
// here, contentName may be an interface
// alias can end with a * (wildcard)
public void SetIgnoredProperty(string contentName, string propertyAlias)
{
HashSet<string> ignores;
if (!_ignoredProperty.TryGetValue(contentName, out ignores))
ignores = _ignoredProperty[contentName] = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
ignores.Add(propertyAlias);
}
// property with that alias should be generated with a different name
// applies to content name and any content that implements it
// here, contentName may be an interface
public void SetRenamedProperty(string contentName, string propertyAlias, string propertyName)
{
Dictionary<string, string> renames;
if (!_renamedProperty.TryGetValue(contentName, out renames))
renames = _renamedProperty[contentName] = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
renames[propertyAlias] = propertyName;
}
// content with that name has a base class so no need to generate one
public void SetContentBaseClass(string contentName, string baseName)
{
if (baseName.ToLowerInvariant() != "object")
_contentBase[contentName] = baseName;
}
// content with that name implements the interfaces
public void SetContentInterfaces(string contentName, IEnumerable<string> interfaceNames)
{
_contentInterfaces[contentName] = interfaceNames.ToArray();
}
public void SetModelsBaseClassName(string modelsBaseClassName)
{
ModelsBaseClassName = modelsBaseClassName;
}
public void SetModelsNamespace(string modelsNamespace)
{
ModelsNamespace = modelsNamespace;
}
public void SetUsingNamespace(string usingNamespace)
{
_usingNamespaces.Add(usingNamespace);
}
public void SetStaticMixinMethod(string contentName, string methodName, string returnType, string paramType)
{
if (!_staticMixins.ContainsKey(contentName))
_staticMixins[contentName] = new List<StaticMixinMethodInfo>();
_staticMixins[contentName].Add(new StaticMixinMethodInfo(contentName, methodName, returnType, paramType));
}
public void SetHasCtor(string contentName)
{
_withCtor.Add(contentName);
}
#endregion
#region Query
public bool IsIgnored(string contentAlias)
{
return IsContentOrMixinIgnored(contentAlias, _ignoredContent);
}
//public bool IsMixinIgnored(string contentAlias)
//{
// return IsContentOrMixinIgnored(contentAlias, _ignoredMixin);
//}
//public bool IsMixinPropertiesIgnored(string contentAlias)
//{
// return IsContentOrMixinIgnored(contentAlias, _ignoredMixinProperties);
//}
private static bool IsContentOrMixinIgnored(string contentAlias, HashSet<string> ignored)
{
if (ignored.Contains(contentAlias)) return true;
return ignored
.Where(x => x.EndsWith("*"))
.Select(x => x.Substring(0, x.Length - 1))
.Any(x => contentAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase));
}
public bool HasContentBase(string contentName)
{
return _contentBase.ContainsKey(contentName);
}
public bool IsContentRenamed(string contentAlias)
{
return _renamedContent.ContainsKey(contentAlias);
}
public bool HasContentImplement(string contentAlias)
{
return _withImplementContent.Contains(contentAlias);
}
public string ContentClrName(string contentAlias)
{
string name;
return (_renamedContent.TryGetValue(contentAlias, out name)) ? name : null;
}
public bool IsPropertyIgnored(string contentName, string propertyAlias)
{
HashSet<string> ignores;
if (_ignoredProperty.TryGetValue(contentName, out ignores))
{
if (ignores.Contains(propertyAlias)) return true;
if (ignores
.Where(x => x.EndsWith("*"))
.Select(x => x.Substring(0, x.Length - 1))
.Any(x => propertyAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase)))
return true;
}
string baseName;
if (_contentBase.TryGetValue(contentName, out baseName)
&& IsPropertyIgnored(baseName, propertyAlias)) return true;
string[] interfaceNames;
if (_contentInterfaces.TryGetValue(contentName, out interfaceNames)
&& interfaceNames.Any(interfaceName => IsPropertyIgnored(interfaceName, propertyAlias))) return true;
return false;
}
public string PropertyClrName(string contentName, string propertyAlias)
{
Dictionary<string, string> renames;
string name;
if (_renamedProperty.TryGetValue(contentName, out renames)
&& renames.TryGetValue(propertyAlias, out name)) return name;
string baseName;
if (_contentBase.TryGetValue(contentName, out baseName)
&& null != (name = PropertyClrName(baseName, propertyAlias))) return name;
string[] interfaceNames;
if (_contentInterfaces.TryGetValue(contentName, out interfaceNames)
&& null != (name = interfaceNames
.Select(interfaceName => PropertyClrName(interfaceName, propertyAlias))
.FirstOrDefault(x => x != null))) return name;
return null;
}
public bool HasModelsBaseClassName
{
get { return !string.IsNullOrWhiteSpace(ModelsBaseClassName); }
}
public string ModelsBaseClassName { get; private set; }
public bool HasModelsNamespace
{
get { return !string.IsNullOrWhiteSpace(ModelsNamespace); }
}
public string ModelsNamespace { get; private set; }
public IEnumerable<string> UsingNamespaces
{
get { return _usingNamespaces; }
}
public IEnumerable<string> StaticMixinMethods(string contentName)
{
return _staticMixins.ContainsKey(contentName)
? _staticMixins[contentName].Select(x => x.MethodName)
: Enumerable.Empty<string>() ;
}
public bool HasCtor(string contentName)
{
return _withCtor.Contains(contentName);
}
#endregion
}
}

View File

@@ -1,28 +0,0 @@
namespace Umbraco.ModelsBuilder.Configuration
{
/// <summary>
/// Defines the CLR name sources.
/// </summary>
public enum ClrNameSource
{
/// <summary>
/// No source.
/// </summary>
Nothing = 0,
/// <summary>
/// Use the name as source.
/// </summary>
Name,
/// <summary>
/// Use the alias as source.
/// </summary>
Alias,
/// <summary>
/// Use the alias directly.
/// </summary>
RawAlias
}
}

View File

@@ -1,34 +0,0 @@
using System.Threading;
using Umbraco.Core.Configuration;
namespace Umbraco.ModelsBuilder.Configuration
{
/// <summary>
/// Provides extension methods for the <see cref="UmbracoConfig"/> class.
/// </summary>
public static class UmbracoConfigExtensions
{
private static Config _config;
/// <summary>
/// Gets the models builder configuration.
/// </summary>
/// <param name="umbracoConfig">The umbraco configuration.</param>
/// <returns>The models builder configuration.</returns>
/// <remarks>Getting the models builder configuration freezes its state,
/// and any attempt at modifying the configuration using the Setup method
/// will be ignored.</remarks>
public static Config ModelsBuilder(this UmbracoConfig umbracoConfig)
{
// capture the current Config2.Default value, cannot change anymore
LazyInitializer.EnsureInitialized(ref _config, () => Config.Value);
return _config;
}
// internal for tests
internal static void ResetConfig()
{
_config = null;
}
}
}

View File

@@ -1,91 +0,0 @@
using System;
using System.Text;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Umbraco;
namespace Umbraco.ModelsBuilder.Dashboard
{
internal static class BuilderDashboardHelper
{
public static bool CanGenerate()
{
return UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration();
}
public static bool GenerateCausesRestart()
{
return UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll();
}
public static bool AreModelsOutOfDate()
{
return OutOfDateModelsStatus.IsOutOfDate;
}
public static string LastError()
{
return ModelsGenerationError.GetLastError();
}
public static string Text()
{
var config = UmbracoConfig.For.ModelsBuilder();
if (!config.Enable)
return "ModelsBuilder is disabled<br />(the .Enable key is missing, or its value is not 'true').";
var sb = new StringBuilder();
sb.Append("ModelsBuilder is enabled, with the following configuration:");
sb.Append("<ul>");
sb.Append("<li>The <strong>models factory</strong> is ");
sb.Append(config.EnableFactory || config.ModelsMode == ModelsMode.PureLive
? "enabled"
: "not enabled. Umbraco will <em>not</em> use models");
sb.Append(".</li>");
sb.Append("<li>The <strong>API</strong> is ");
if (config.ApiInstalled && config.EnableApi)
{
sb.Append("installed and enabled");
if (!config.IsDebug) sb.Append(".<br />However, the API runs only with <em>debug</em> compilation mode");
}
else if (config.ApiInstalled || config.EnableApi)
sb.Append(config.ApiInstalled ? "installed but not enabled" : "enabled but not installed");
else sb.Append("neither installed nor enabled");
sb.Append(".<br />");
if (!config.ApiServer)
sb.Append("External tools such as Visual Studio <em>cannot</em> use the API");
else
sb.Append("<span style=\"color:orange;font-weight:bold;\">The API endpoint is open on this server</span>");
sb.Append(".</li>");
sb.Append(config.ModelsMode != ModelsMode.Nothing
? $"<li><strong>{config.ModelsMode} models</strong> are enabled.</li>"
: "<li>No models mode is specified: models will <em>not</em> be generated.</li>");
sb.Append($"<li>Models namespace is {config.ModelsNamespace}.</li>");
sb.Append("<li>Static mixin getters are ");
sb.Append(config.StaticMixinGetters ? "enabled" : "disabled");
if (config.StaticMixinGetters)
{
sb.Append(". The pattern for getters is ");
sb.Append(string.IsNullOrWhiteSpace(config.StaticMixinGetterPattern)
? "not configured (will use default)"
: $"\"{config.StaticMixinGetterPattern}\"");
}
sb.Append(".</li>");
sb.Append("<li>Tracking of <strong>out-of-date models</strong> is ");
sb.Append(config.FlagOutOfDateModels ? "enabled" : "not enabled");
sb.Append(".</li>");
sb.Append("</ul>");
return sb.ToString();
}
}
}

View File

@@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
namespace Umbraco.ModelsBuilder
{
public static class EnumerableExtensions
{
public static void RemoveAll<T>(this IList<T> list, Func<T, bool> predicate)
{
for (var i = 0; i < list.Count; i++)
{
if (predicate(list[i]))
{
list.RemoveAt(i--); // i-- is important here!
}
}
}
public static IEnumerable<T> And<T>(this IEnumerable<T> enumerable, T item)
{
foreach (var x in enumerable) yield return x;
yield return item;
}
public static IEnumerable<T> AndIfNotNull<T>(this IEnumerable<T> enumerable, T item)
where T : class
{
foreach (var x in enumerable) yield return x;
if (item != null)
yield return item;
}
}
}

View File

@@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using Umbraco.ModelsBuilder;
namespace Umbraco.ModelsBuilder
{
// for the time being it's all-or-nothing
// when the content type is ignored then
// - no class is generated for that content type
// - no class is generated for any child of that class
// - no interface is generated for that content type as a mixin
// - and it is ignored as a mixin ie its properties are not generated
// in the future we may way to do
// [assembly:IgnoreContentType("foo", ContentTypeIgnorable.ContentType|ContentTypeIgnorable.Mixin|ContentTypeIgnorable.MixinProperties)]
// so that we can
// - generate a class for that content type, or not
// - if not generated, generate children or not
// - if generate children, include properties or not
// - generate an interface for that content type as a mixin
// - if not generated, still generate properties in content types implementing the mixin or not
// but... I'm not even sure it makes sense
// if we don't want it... we don't want it.
// about ignoring
// - content (don't generate the content, use as mixin)
// - mixin (don't generate the interface, use the properties)
// - mixin properties (generate the interface, not the properties)
// - mixin: local only or children too...
/// <summary>
/// Indicates that no model should be generated for a specified content type alias.
/// </summary>
/// <remarks>When a content type is ignored, its descendants are also ignored.</remarks>
/// <remarks>Supports trailing wildcard eg "foo*".</remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public sealed class IgnoreContentTypeAttribute : Attribute
{
public IgnoreContentTypeAttribute(string alias /*, bool ignoreContent = true, bool ignoreMixin = true, bool ignoreMixinProperties = true*/)
{}
}
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates that no model should be generated for a specified property type alias.
/// </summary>
/// <remarks>Supports trailing wildcard eg "foo*".</remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class IgnorePropertyTypeAttribute : Attribute
{
public IgnorePropertyTypeAttribute(string alias)
{}
}
}

View File

@@ -1,20 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder
{
// NOTE
// that attribute should inherit from PublishedModelAttribute
// so we do not have different syntaxes
// but... is sealed at the moment.
/// <summary>
/// Indicates that a (partial) class defines the model type for a specific alias.
/// </summary>
/// <remarks>Though a model will be generated - so that is the way to register a rename.</remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ImplementContentTypeAttribute : Attribute
{
public ImplementContentTypeAttribute(string alias)
{ }
}
}

View File

@@ -1,16 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates the default base class for models.
/// </summary>
/// <remarks>Otherwise it is PublishedContentModel. Would make sense to inherit from PublishedContentModel.</remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
public sealed class ModelsBaseClassAttribute : Attribute
{
public ModelsBaseClassAttribute(Type type)
{}
}
}

View File

@@ -1,16 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates the models namespace.
/// </summary>
/// <remarks>Will override anything else that might come from settings.</remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
public sealed class ModelsNamespaceAttribute : Attribute
{
public ModelsNamespaceAttribute(string modelsNamespace)
{}
}
}

View File

@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using Umbraco.ModelsBuilder;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates namespaces that should be used in generated models (in using clauses).
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public sealed class ModelsUsingAttribute : Attribute
{
public ModelsUsingAttribute(string usingNamespace)
{}
}
}

View File

@@ -1,14 +0,0 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Umbraco.ModelsBuilder")]
[assembly: AssemblyDescription("Umbraco ModelsBuilder")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyProduct("Umbraco CMS")]
[assembly: ComVisible(false)]
[assembly: Guid("7020a059-c0d1-43a0-8efd-23591a0c9af6")]
// code analysis
// IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "~_~")]

View File

@@ -1,20 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.ModelsBuilder
{
public static class PublishedPropertyTypeExtensions
{
// fixme - need to rewrite that one - we don't have prevalues anymore
//public static KeyValuePair<int, string>[] PreValues(this PublishedPropertyType propertyType)
//{
// return ApplicationContext.Current.Services.DataTypeService
// .GetPreValuesCollectionByDataTypeId(propertyType.DataType.Id)
// .PreValuesAsArray
// .Select(x => new KeyValuePair<int, string>(x.Id, x.Value))
// .ToArray();
//}
}
}

View File

@@ -1,15 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates that an Assembly is a PureLive models assembly.
/// </summary>
/// <remarks>Though technically not required, ie models will work without it, the attribute
/// can be used by Umbraco view models binder to figure out whether the model type comes
/// from a PureLive Assembly.</remarks>
[Obsolete("Should use ModelsBuilderAssemblyAttribute but that requires a change in Umbraco Core.")]
[AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)]
public sealed class PureLiveAssemblyAttribute : Attribute
{ }
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates a model name for a specified content alias.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public sealed class RenameContentTypeAttribute : Attribute
{
public RenameContentTypeAttribute(string alias, string name)
{}
}
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates a model name for a specified property alias.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class RenamePropertyTypeAttribute : Attribute
{
public RenamePropertyTypeAttribute(string alias, string name)
{}
}
}

View File

@@ -1,142 +0,0 @@
using System;
using System.Threading;
using System.Web;
using System.Web.Hosting;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Umbraco;
using Umbraco.Web.Cache;
// will install only if configuration says it needs to be installed
[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")]
namespace Umbraco.ModelsBuilder.Umbraco
{
// supports LiveDll and LiveAppData - but not PureLive
public sealed class LiveModelsProvider
{
private static UmbracoServices _umbracoServices;
private static Mutex _mutex;
private static int _req;
internal static bool IsEnabled
{
get
{
var config = UmbracoConfig.For.ModelsBuilder();
return config.ModelsMode.IsLiveNotPure();
// we do not manage pure live here
}
}
internal static void Install(UmbracoServices umbracoServices)
{
// just be sure
if (!IsEnabled)
return;
_umbracoServices = umbracoServices;
// initialize mutex
// ApplicationId will look like "/LM/W3SVC/1/Root/AppName"
// name is system-wide and must be less than 260 chars
var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider";
_mutex = new Mutex(false, name);
// anything changes, and we want to re-generate models.
ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
// at the end of a request since we're restarting the pool
// NOTE - this does NOT trigger - see module below
//umbracoApplication.EndRequest += GenerateModelsIfRequested;
}
// NOTE
// Using HttpContext Items fails because CacheUpdated triggers within
// some asynchronous backend task where we seem to have no HttpContext.
// So we use a static (non request-bound) var to register that models
// need to be generated. Could be by another request. Anyway. We could
// have collisions but... you know the risk.
private static void RequestModelsGeneration(object sender, EventArgs args)
{
//HttpContext.Current.Items[this] = true;
Current.Logger.Debug<LiveModelsProvider>("Requested to generate models.");
Interlocked.Exchange(ref _req, 1);
}
public static void GenerateModelsIfRequested(object sender, EventArgs args)
{
//if (HttpContext.Current.Items[this] == null) return;
if (Interlocked.Exchange(ref _req, 0) == 0) return;
// cannot use a simple lock here because we don't want another AppDomain
// to generate while we do... and there could be 2 AppDomains if the app restarts.
try
{
Current.Logger.Debug<LiveModelsProvider>("Generate models...");
const int timeout = 2*60*1000; // 2 mins
_mutex.WaitOne(timeout); // wait until it is safe, and acquire
Current.Logger.Info<LiveModelsProvider>("Generate models now.");
GenerateModels();
ModelsGenerationError.Clear();
Current.Logger.Info<LiveModelsProvider>("Generated.");
}
catch (TimeoutException)
{
Current.Logger.Warn<LiveModelsProvider>("Timeout, models were NOT generated.");
}
catch (Exception e)
{
ModelsGenerationError.Report("Failed to build Live models.", e);
Current.Logger.Error<LiveModelsProvider>("Failed to generate models.", e);
}
finally
{
_mutex.ReleaseMutex(); // release
}
}
private static void GenerateModels()
{
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
var bin = HostingEnvironment.MapPath("~/bin");
if (bin == null)
throw new Exception("Panic: bin is null.");
var config = UmbracoConfig.For.ModelsBuilder();
// EnableDllModels will recycle the app domain - but this request will end properly
ModelsBuilderBackOfficeController.GenerateModels(_umbracoServices, modelsDirectory, config.ModelsMode.IsAnyDll() ? bin : null);
}
}
// have to do this because it's the only way to subscribe to EndRequest,
// module is installed by assembly attribute at the top of this file
public class LiveModelsProviderModule : IHttpModule
{
public void Init(HttpApplication app)
{
app.EndRequest += LiveModelsProvider.GenerateModelsIfRequested;
}
public void Dispose()
{
// nothing
}
public static void Install()
{
if (!LiveModelsProvider.IsEnabled)
return;
HttpApplication.RegisterModule(typeof(LiveModelsProviderModule));
}
}
}

View File

@@ -1,194 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Text;
using System.Web.Hosting;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Building;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Dashboard;
using Umbraco.Web.Editors;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.ModelsBuilder.Umbraco
{
/// <summary>
/// API controller for use in the Umbraco back office with Angular resources
/// </summary>
/// <remarks>
/// We've created a different controller for the backoffice/angular specifically this is to ensure that the
/// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to
/// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats.
/// </remarks>
[UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)]
public class ModelsBuilderBackOfficeController : UmbracoAuthorizedJsonController
{
private readonly UmbracoServices _umbracoServices;
public ModelsBuilderBackOfficeController(UmbracoServices umbracoServices)
{
_umbracoServices = umbracoServices;
}
// invoked by the dashboard
// requires that the user is logged into the backoffice and has access to the developer section
// beware! the name of the method appears in modelsbuilder.controller.js
[System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers!
public HttpResponseMessage BuildModels()
{
try
{
if (!UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration())
{
var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." };
return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter);
}
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
var bin = HostingEnvironment.MapPath("~/bin");
if (bin == null)
throw new Exception("Panic: bin is null.");
// EnableDllModels will recycle the app domain - but this request will end properly
GenerateModels(modelsDirectory, UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll() ? bin : null);
ModelsGenerationError.Clear();
}
catch (Exception e)
{
ModelsGenerationError.Report("Failed to build models.", e);
}
return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter);
}
// invoked by the back-office
// requires that the user is logged into the backoffice and has access to the developer section
[System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers!
public HttpResponseMessage GetModelsOutOfDateStatus()
{
var status = OutOfDateModelsStatus.IsEnabled
? (OutOfDateModelsStatus.IsOutOfDate
? new OutOfDateStatus { Status = OutOfDateType.OutOfDate }
: new OutOfDateStatus { Status = OutOfDateType.Current })
: new OutOfDateStatus { Status = OutOfDateType.Unknown };
return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter);
}
// invoked by the back-office
// requires that the user is logged into the backoffice and has access to the developer section
// beware! the name of the method appears in modelsbuilder.controller.js
[System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers!
public HttpResponseMessage GetDashboard()
{
return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter);
}
private Dashboard GetDashboardResult()
{
return new Dashboard
{
Enable = UmbracoConfig.For.ModelsBuilder().Enable,
Text = BuilderDashboardHelper.Text(),
CanGenerate = BuilderDashboardHelper.CanGenerate(),
GenerateCausesRestart = BuilderDashboardHelper.GenerateCausesRestart(),
OutOfDateModels = BuilderDashboardHelper.AreModelsOutOfDate(),
LastError = BuilderDashboardHelper.LastError(),
};
}
private void GenerateModels(string modelsDirectory, string bin)
{
GenerateModels(_umbracoServices, modelsDirectory, bin);
}
internal static void GenerateModels(UmbracoServices umbracoServices, string modelsDirectory, string bin)
{
if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory);
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
File.Delete(file);
var typeModels = umbracoServices.GetAllTypes();
var ourFiles = Directory.GetFiles(modelsDirectory, "*.cs").ToDictionary(x => x, File.ReadAllText);
var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles);
var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace);
foreach (var typeModel in builder.GetModelsToGenerate())
{
var sb = new StringBuilder();
builder.Generate(sb, typeModel);
var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs");
File.WriteAllText(filename, sb.ToString());
}
// the idea was to calculate the current hash and to add it as an extra file to the compilation,
// in order to be able to detect whether a DLL is consistent with an environment - however the
// environment *might not* contain the local partial files, and thus it could be impossible to
// calculate the hash. So... maybe that's not a good idea after all?
/*
var currentHash = HashHelper.Hash(ourFiles, typeModels);
ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder;
[assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")]
";
*/
if (bin != null)
{
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
ourFiles[file] = File.ReadAllText(file);
var compiler = new Compiler();
compiler.Compile(builder.GetModelsNamespace(), ourFiles, bin);
}
OutOfDateModelsStatus.Clear();
}
[DataContract]
internal class BuildResult
{
[DataMember(Name = "success")]
public bool Success;
[DataMember(Name = "message")]
public string Message;
}
[DataContract]
internal class Dashboard
{
[DataMember(Name = "enable")]
public bool Enable;
[DataMember(Name = "text")]
public string Text;
[DataMember(Name = "canGenerate")]
public bool CanGenerate;
[DataMember(Name = "generateCausesRestart")]
public bool GenerateCausesRestart;
[DataMember(Name = "outOfDateModels")]
public bool OutOfDateModels;
[DataMember(Name = "lastError")]
public string LastError;
}
internal enum OutOfDateType
{
OutOfDate,
Current,
Unknown = 100
}
[DataContract]
internal class OutOfDateStatus
{
[DataMember(Name = "status")]
public OutOfDateType Status { get; set; }
}
}
}

View File

@@ -1,178 +0,0 @@
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using LightInject;
using Umbraco.Core;
using Umbraco.Core.Components;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.ModelsBuilder.Api;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.Web;
using Umbraco.Web.PublishedCache.NuCache;
using Umbraco.Web.UI.JavaScript;
namespace Umbraco.ModelsBuilder.Umbraco
{
[RequiredComponent(typeof(NuCacheComponent))]
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class ModelsBuilderComponent : UmbracoComponentBase, IUmbracoCoreComponent
{
public override void Compose(Composition composition)
{
base.Compose(composition);
composition.Container.Register<UmbracoServices>(new PerContainerLifetime());
var config = UmbracoConfig.For.ModelsBuilder();
if (config.ModelsMode == ModelsMode.PureLive)
ComposeForLiveModels(composition.Container);
else if (config.EnableFactory)
ComposeForDefaultModelsFactory(composition.Container);
// always setup the dashboard
InstallServerVars(composition.Container.GetInstance<IRuntimeState>().Level);
composition.Container.Register(typeof(ModelsBuilderBackOfficeController), new PerRequestLifeTime());
// setup the API if enabled (and in debug mode)
if (config.ApiServer)
composition.Container.Register(typeof(ModelsBuilderApiController), new PerRequestLifeTime());
}
public void Initialize(UmbracoServices umbracoServices)
{
var config = UmbracoConfig.For.ModelsBuilder();
if (config.Enable)
FileService.SavingTemplate += FileService_SavingTemplate;
// fixme LiveModelsProvider should not be static
if (config.ModelsMode.IsLiveNotPure())
LiveModelsProvider.Install(umbracoServices);
// fixme OutOfDateModelsStatus should not be static
if (config.FlagOutOfDateModels)
OutOfDateModelsStatus.Install();
}
private void ComposeForDefaultModelsFactory(IServiceContainer container)
{
container.RegisterSingleton<IPublishedModelFactory>(factory
=> new PublishedModelFactory(factory.GetInstance<TypeLoader>().GetTypes<PublishedContentModel>()));
}
private void ComposeForLiveModels(IServiceContainer container)
{
container.RegisterSingleton<IPublishedModelFactory, PureLiveModelFactory>();
// the following would add @using statement in every view so user's don't
// have to do it - however, then noone understands where the @using statement
// comes from, and it cannot be avoided / removed --- DISABLED
//
/*
// no need for @using in views
// note:
// we are NOT using the in-code attribute here, config is required
// because that would require parsing the code... and what if it changes?
// we can AddGlobalImport not sure we can remove one anyways
var modelsNamespace = Configuration.Config.ModelsNamespace;
if (string.IsNullOrWhiteSpace(modelsNamespace))
modelsNamespace = Configuration.Config.DefaultModelsNamespace;
System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace);
*/
}
private void InstallServerVars(RuntimeLevel level)
{
// register our url - for the backoffice api
ServerVariablesParser.Parsing += (sender, serverVars) =>
{
if (!serverVars.ContainsKey("umbracoUrls"))
throw new Exception("Missing umbracoUrls.");
var umbracoUrlsObject = serverVars["umbracoUrls"];
if (umbracoUrlsObject == null)
throw new Exception("Null umbracoUrls");
if (!(umbracoUrlsObject is Dictionary<string, object> umbracoUrls))
throw new Exception("Invalid umbracoUrls");
if (!serverVars.ContainsKey("umbracoPlugins"))
throw new Exception("Missing umbracoPlugins.");
if (!(serverVars["umbracoPlugins"] is Dictionary<string, object> umbracoPlugins))
throw new Exception("Invalid umbracoPlugins");
if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null");
var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()));
umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl<ModelsBuilderBackOfficeController>(controller => controller.BuildModels());
umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(level);
};
}
private Dictionary<string, object> GetModelsBuilderSettings(RuntimeLevel level)
{
if (level != RuntimeLevel.Run)
return null;
var settings = new Dictionary<string, object>
{
{"enabled", UmbracoConfig.For.ModelsBuilder().Enable}
};
return settings;
}
/// <summary>
/// Used to check if a template is being created based on a document type, in this case we need to
/// ensure the template markup is correct based on the model name of the document type
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs<Core.Models.ITemplate> e)
{
// don't do anything if the factory is not enabled
// because, no factory = no models (even if generation is enabled)
if (!UmbracoConfig.For.ModelsBuilder().EnableFactory) return;
// don't do anything if this special key is not found
if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return;
// ensure we have the content type alias
if (!e.AdditionalData.ContainsKey("ContentTypeAlias"))
throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found");
foreach (var template in e.SavedEntities)
{
// if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key
// is found, then it means a new template is being created based on the creation of a document type
if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content))
{
// ensure is safe and always pascal cased, per razor standard
// + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application
var alias = e.AdditionalData["ContentTypeAlias"].ToString();
var name = template.Name; // will be the name of the content type since we are creating
var className = UmbracoServices.GetClrName(name, alias);
var modelNamespace = UmbracoConfig.For.ModelsBuilder().ModelsNamespace;
// we do not support configuring this at the moment, so just let Umbraco use its default value
//var modelNamespaceAlias = ...;
var markup = ViewHelper.GetDefaultFileContent(
modelClassName: className,
modelNamespace: modelNamespace/*,
modelNamespaceAlias: modelNamespaceAlias*/);
//set the template content to the new markup
template.Content = markup;
}
}
}
}
}

View File

@@ -1,95 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.Web.Editors;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.ModelsBuilder.Validation
{
/// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used
/// </summary>
internal class ContentTypeModelValidator : ContentTypeModelValidatorBase<DocumentTypeSave, PropertyTypeBasic>
{
}
/// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used
/// </summary>
internal class MediaTypeModelValidator : ContentTypeModelValidatorBase<MediaTypeSave, PropertyTypeBasic>
{
}
/// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used
/// </summary>
internal class MemberTypeModelValidator : ContentTypeModelValidatorBase<MemberTypeSave, MemberPropertyTypeBasic>
{
}
internal abstract class ContentTypeModelValidatorBase<TModel, TProperty> : EditorValidator<TModel>
where TModel: ContentTypeSave<TProperty>
where TProperty: PropertyTypeBasic
{
protected override IEnumerable<ValidationResult> Validate(TModel model)
{
//don't do anything if we're not enabled
if (UmbracoConfig.For.ModelsBuilder().Enable)
{
var properties = model.Groups.SelectMany(x => x.Properties)
.Where(x => x.Inherited == false)
.ToArray();
foreach (var prop in properties)
{
var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop));
if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant())
yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[]
{
string.Format("Groups[{0}].Properties[{1}].Alias", model.Groups.IndexOf(propertyGroup), propertyGroup.Properties.IndexOf(prop))
});
//we need to return the field name with an index so it's wired up correctly
var groupIndex = model.Groups.IndexOf(propertyGroup);
var propertyIndex = propertyGroup.Properties.IndexOf(prop);
var validationResult = ValidateProperty(prop, groupIndex, propertyIndex);
if (validationResult != null)
{
yield return validationResult;
}
}
}
}
private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex)
{
//don't let them match any properties or methods in IPublishedContent
//TODO: There are probably more!
var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray();
var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray();
var alias = property.Alias;
if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias))
{
return new ValidationResult(
string.Format("The alias {0} is a reserved term and cannot be used", alias), new[]
{
string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex)
});
}
return null;
}
}
}

View File

@@ -0,0 +1,429 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.ModelsBuilder.Embedded;
using Umbraco.ModelsBuilder.Embedded.Building;
using Umbraco.ModelsBuilder.Embedded.Configuration;
namespace Umbraco.Tests.ModelsBuilder
{
[TestFixture]
public class BuilderTests
{
[Test]
public void GenerateSimpleType()
{
// Umbraco returns nice, pascal-cased names
var type1 = new TypeModel
{
Id = 1,
Alias = "type1",
ClrName = "Type1",
ParentId = 0,
BaseType = null,
ItemType = TypeModel.ItemTypes.Content,
};
type1.Properties.Add(new PropertyModel
{
Alias = "prop1",
ClrName = "Prop1",
ModelClrType = typeof(string),
});
var types = new[] { type1 };
var code = new Dictionary<string, string>
{
};
var builder = new TextBuilder(Mock.Of<IModelsBuilderConfig>(), types);
var btypes = builder.TypeModels;
var sb = new StringBuilder();
builder.Generate(sb, builder.GetModelsToGenerate().First());
var gen = sb.ToString();
var version = ApiVersion.Current.Version;
var expected = @"//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Umbraco.ModelsBuilder v" + version + @"
//
// Changes to this file will be lost if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
using Umbraco.ModelsBuilder.Embedded;
namespace Umbraco.Web.PublishedModels
{
[PublishedModel(""type1"")]
public partial class Type1 : PublishedContentModel
{
// helpers
#pragma warning disable 0109 // new is redundant
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
public new const string ModelTypeAlias = ""type1"";
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
public new const PublishedItemType ModelItemType = PublishedItemType.Content;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
public new static IPublishedContentType GetModelContentType()
=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
public static IPublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<Type1, TValue>> selector)
=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);
#pragma warning restore 0109
// ctor
public Type1(IPublishedContent content)
: base(content)
{ }
// properties
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
[ImplementPropertyType(""prop1"")]
public string Prop1 => this.Value<string>(""prop1"");
}
}
";
Console.WriteLine(gen);
Assert.AreEqual(expected.ClearLf(), gen);
}
[Test]
public void GenerateSimpleType_Ambiguous_Issue()
{
// Umbraco returns nice, pascal-cased names
var type1 = new TypeModel
{
Id = 1,
Alias = "type1",
ClrName = "Type1",
ParentId = 0,
BaseType = null,
ItemType = TypeModel.ItemTypes.Content,
};
type1.Properties.Add(new PropertyModel
{
Alias = "foo",
ClrName = "Foo",
ModelClrType = typeof(IEnumerable<>).MakeGenericType(ModelType.For("foo")),
});
var type2 = new TypeModel
{
Id = 2,
Alias = "foo",
ClrName = "Foo",
ParentId = 0,
BaseType = null,
ItemType = TypeModel.ItemTypes.Element,
};
var types = new[] { type1, type2 };
var code = new Dictionary<string, string>
{
{ "code", @"
namespace Umbraco.Web.PublishedModels
{
public partial class Foo
{
}
}
" }
};
var builder = new TextBuilder(Mock.Of<IModelsBuilderConfig>(), types);
var btypes = builder.TypeModels;
builder.ModelsNamespace = "Umbraco.Web.PublishedModels";
var sb1 = new StringBuilder();
builder.Generate(sb1, builder.GetModelsToGenerate().Skip(1).First());
var gen1 = sb1.ToString();
Console.WriteLine(gen1);
var sb = new StringBuilder();
builder.Generate(sb, builder.GetModelsToGenerate().First());
var gen = sb.ToString();
var version = ApiVersion.Current.Version;
var expected = @"//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Umbraco.ModelsBuilder v" + version + @"
//
// Changes to this file will be lost if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
using Umbraco.ModelsBuilder.Embedded;
namespace Umbraco.Web.PublishedModels
{
[PublishedModel(""type1"")]
public partial class Type1 : PublishedContentModel
{
// helpers
#pragma warning disable 0109 // new is redundant
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
public new const string ModelTypeAlias = ""type1"";
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
public new const PublishedItemType ModelItemType = PublishedItemType.Content;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
public new static IPublishedContentType GetModelContentType()
=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
public static IPublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<Type1, TValue>> selector)
=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);
#pragma warning restore 0109
// ctor
public Type1(IPublishedContent content)
: base(content)
{ }
// properties
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")]
[ImplementPropertyType(""foo"")]
public global::System.Collections.Generic.IEnumerable<global::Foo> Foo => this.Value<global::System.Collections.Generic.IEnumerable<global::Foo>>(""foo"");
}
}
";
Console.WriteLine(gen);
Assert.AreEqual(expected.ClearLf(), gen);
}
[Test]
public void GenerateAmbiguous()
{
// NOTE: since
var type1 = new TypeModel
{
Id = 1,
Alias = "type1",
ClrName = "Type1",
ParentId = 0,
BaseType = null,
ItemType = TypeModel.ItemTypes.Content,
IsMixin = true,
};
type1.Properties.Add(new PropertyModel
{
Alias = "prop1",
ClrName = "Prop1",
ModelClrType = typeof(IPublishedContent),
});
type1.Properties.Add(new PropertyModel
{
Alias = "prop2",
ClrName = "Prop2",
ModelClrType = typeof(global::System.Text.StringBuilder),
});
type1.Properties.Add(new PropertyModel
{
Alias = "prop3",
ClrName = "Prop3",
ModelClrType = typeof(global::Umbraco.Core.IO.FileSecurityException),
});
var types = new[] { type1 };
var code = new Dictionary<string, string>
{
};
var builder = new TextBuilder(Mock.Of<IModelsBuilderConfig>(), types);
builder.ModelsNamespace = "Umbraco.ModelsBuilder.Models"; // forces conflict with Umbraco.ModelsBuilder.Umbraco
var btypes = builder.TypeModels;
var sb = new StringBuilder();
foreach (var model in builder.GetModelsToGenerate())
builder.Generate(sb, model);
var gen = sb.ToString();
Console.WriteLine(gen);
Assert.IsTrue(gen.Contains(" global::Umbraco.Core.Models.PublishedContent.IPublishedContent Prop1"));
Assert.IsTrue(gen.Contains(" global::System.Text.StringBuilder Prop2"));
Assert.IsTrue(gen.Contains(" global::Umbraco.Core.IO.FileSecurityException Prop3"));
}
[TestCase("int", typeof(int))]
[TestCase("global::System.Collections.Generic.IEnumerable<int>", typeof(IEnumerable<int>))]
[TestCase("global::Umbraco.Tests.ModelsBuilder.BuilderTestsClass1", typeof(BuilderTestsClass1))]
[TestCase("global::Umbraco.Tests.ModelsBuilder.BuilderTests.Class1", typeof(Class1))]
public void WriteClrType(string expected, Type input)
{
// note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true
// which means global:: syntax will be applied to most things
var builder = new TextBuilder();
builder.ModelsNamespaceForTests = "ModelsNamespace";
var sb = new StringBuilder();
builder.WriteClrType(sb, input);
Assert.AreEqual(expected, sb.ToString());
}
[TestCase("int", typeof(int))]
[TestCase("global::System.Collections.Generic.IEnumerable<int>", typeof(IEnumerable<int>))]
[TestCase("global::Umbraco.Tests.ModelsBuilder.BuilderTestsClass1", typeof(BuilderTestsClass1))]
[TestCase("global::Umbraco.Tests.ModelsBuilder.BuilderTests.Class1", typeof(Class1))]
public void WriteClrTypeUsing(string expected, Type input)
{
// note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true
// which means global:: syntax will be applied to most things
var builder = new TextBuilder();
builder.Using.Add("Umbraco.Tests.ModelsBuilder");
builder.ModelsNamespaceForTests = "ModelsNamespace";
var sb = new StringBuilder();
builder.WriteClrType(sb, input);
Assert.AreEqual(expected, sb.ToString());
}
[Test]
public void WriteClrType_WithUsing()
{
var builder = new TextBuilder();
builder.Using.Add("System.Text");
builder.ModelsNamespaceForTests = "Umbraco.Tests.ModelsBuilder.Models";
var sb = new StringBuilder();
builder.WriteClrType(sb, typeof(StringBuilder));
// note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true
// which means global:: syntax will be applied to most things
Assert.AreEqual("global::System.Text.StringBuilder", sb.ToString());
}
[Test]
public void WriteClrTypeAnother_WithoutUsing()
{
var builder = new TextBuilder();
builder.ModelsNamespaceForTests = "Umbraco.Tests.ModelsBuilder.Models";
var sb = new StringBuilder();
builder.WriteClrType(sb, typeof(StringBuilder));
Assert.AreEqual("global::System.Text.StringBuilder", sb.ToString());
}
[Test]
public void WriteClrType_Ambiguous1()
{
var builder = new TextBuilder();
builder.Using.Add("System.Text");
builder.Using.Add("Umbraco.Tests.ModelsBuilder");
builder.ModelsNamespaceForTests = "SomeRandomNamespace";
var sb = new StringBuilder();
builder.WriteClrType(sb, typeof(global::System.Text.ASCIIEncoding));
// note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true
// which means global:: syntax will be applied to most things
Assert.AreEqual("global::System.Text.ASCIIEncoding", sb.ToString());
}
[Test]
public void WriteClrType_Ambiguous()
{
var builder = new TextBuilder();
builder.Using.Add("System.Text");
builder.Using.Add("Umbraco.Tests.ModelsBuilder");
builder.ModelsNamespaceForTests = "SomeBorkedNamespace";
var sb = new StringBuilder();
builder.WriteClrType(sb, typeof(global::System.Text.ASCIIEncoding));
// note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true
// which means global:: syntax will be applied to most things
Assert.AreEqual("global::System.Text.ASCIIEncoding", sb.ToString());
}
[Test]
public void WriteClrType_Ambiguous2()
{
var builder = new TextBuilder();
builder.Using.Add("System.Text");
builder.Using.Add("Umbraco.Tests.ModelsBuilder");
builder.ModelsNamespaceForTests = "SomeRandomNamespace";
var sb = new StringBuilder();
builder.WriteClrType(sb, typeof(ASCIIEncoding));
// note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true
// which means global:: syntax will be applied to most things
Assert.AreEqual("global::Umbraco.Tests.ModelsBuilder.ASCIIEncoding", sb.ToString());
}
[Test]
public void WriteClrType_AmbiguousNot()
{
var builder = new TextBuilder();
builder.Using.Add("System.Text");
builder.Using.Add("Umbraco.Tests.ModelsBuilder");
builder.ModelsNamespaceForTests = "Umbraco.Tests.ModelsBuilder.Models";
var sb = new StringBuilder();
builder.WriteClrType(sb, typeof(ASCIIEncoding));
// note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true
// which means global:: syntax will be applied to most things
Assert.AreEqual("global::Umbraco.Tests.ModelsBuilder.ASCIIEncoding", sb.ToString());
}
[Test]
public void WriteClrType_AmbiguousWithNested()
{
var builder = new TextBuilder();
builder.Using.Add("System.Text");
builder.Using.Add("Umbraco.Tests.ModelsBuilder");
builder.ModelsNamespaceForTests = "SomeRandomNamespace";
var sb = new StringBuilder();
builder.WriteClrType(sb, typeof(ASCIIEncoding.Nested));
// note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true
// which means global:: syntax will be applied to most things
Assert.AreEqual("global::Umbraco.Tests.ModelsBuilder.ASCIIEncoding.Nested", sb.ToString());
}
public class Class1 { }
}
// make it public to be ambiguous (see above)
public class ASCIIEncoding
{
// can we handle nested types?
public class Nested { }
}
class BuilderTestsClass1 {}
public class System { }
}

View File

@@ -0,0 +1,49 @@
using System.Configuration;
using NUnit.Framework;
using Umbraco.ModelsBuilder.Embedded.Configuration;
namespace Umbraco.Tests.ModelsBuilder
{
[TestFixture]
public class ModelsBuilderConfigTests
{
[Test]
public void Test1()
{
var config = new ModelsBuilderConfig(modelsNamespace: "test1");
Assert.AreEqual("test1", config.ModelsNamespace);
}
[Test]
public void Test2()
{
var config = new ModelsBuilderConfig(modelsNamespace: "test2");
Assert.AreEqual("test2", config.ModelsNamespace);
}
[Test]
public void DefaultModelsNamespace()
{
var config = new ModelsBuilderConfig();
Assert.AreEqual(ModelsBuilderConfig.DefaultModelsNamespace, config.ModelsNamespace);
}
[TestCase("c:/path/to/root", "~/dir/models", false, "c:\\path\\to\\root\\dir\\models")]
[TestCase("c:/path/to/root", "~/../../dir/models", true, "c:\\path\\dir\\models")]
[TestCase("c:/path/to/root", "c:/another/path/to/elsewhere", true, "c:\\another\\path\\to\\elsewhere")]
public void GetModelsDirectoryTests(string root, string config, bool acceptUnsafe, string expected)
{
Assert.AreEqual(expected, ModelsBuilderConfig.GetModelsDirectory(root, config, acceptUnsafe));
}
[TestCase("c:/path/to/root", "~/../../dir/models", false)]
[TestCase("c:/path/to/root", "c:/another/path/to/elsewhere", false)]
public void GetModelsDirectoryThrowsTests(string root, string config, bool acceptUnsafe)
{
Assert.Throws<ConfigurationErrorsException>(() =>
{
var modelsDirectory = ModelsBuilderConfig.GetModelsDirectory(root, config, acceptUnsafe);
});
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Tests.ModelsBuilder
{
public static class StringExtensions
{
public static string ClearLf(this string s)
{
return s.Replace("\r", "");
}
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using Umbraco.ModelsBuilder.Embedded;
using Umbraco.ModelsBuilder.Embedded.Building;
namespace Umbraco.Tests.ModelsBuilder
{
[TestFixture]
public class UmbracoApplicationTests
{
//[Test]
//public void Test()
//{
// // start and terminate
// using (var app = Application.GetApplication(TestOptions.ConnectionString, TestOptions.DatabaseProvider))
// { }
// // start and terminate
// using (var app = Application.GetApplication(TestOptions.ConnectionString, TestOptions.DatabaseProvider))
// { }
// // start, use and terminate
// using (var app = Application.GetApplication(TestOptions.ConnectionString, TestOptions.DatabaseProvider))
// {
// var types = app.GetContentTypes();
// }
//}
[Test]
public void ThrowsOnDuplicateAliases()
{
var typeModels = new List<TypeModel>
{
new TypeModel { ItemType = TypeModel.ItemTypes.Content, Alias = "content1" },
new TypeModel { ItemType = TypeModel.ItemTypes.Content, Alias = "content2" },
new TypeModel { ItemType = TypeModel.ItemTypes.Media, Alias = "media1" },
new TypeModel { ItemType = TypeModel.ItemTypes.Media, Alias = "media2" },
new TypeModel { ItemType = TypeModel.ItemTypes.Member, Alias = "member1" },
new TypeModel { ItemType = TypeModel.ItemTypes.Member, Alias = "member2" },
};
Assert.AreEqual(6, UmbracoServices.EnsureDistinctAliases(typeModels).Count);
typeModels.Add(new TypeModel { ItemType = TypeModel.ItemTypes.Media, Alias = "content1" });
try
{
UmbracoServices.EnsureDistinctAliases(typeModels);
}
catch (NotSupportedException e)
{
Console.WriteLine(e.Message);
return;
}
Assert.Fail("Expected NotSupportedException.");
}
}
}

View File

@@ -134,6 +134,10 @@
<Compile Include="Mapping\MappingTests.cs" />
<Compile Include="Migrations\MigrationPlanTests.cs" />
<Compile Include="Migrations\MigrationTests.cs" />
<Compile Include="ModelsBuilder\BuilderTests.cs" />
<Compile Include="ModelsBuilder\ConfigTests.cs" />
<Compile Include="ModelsBuilder\StringExtensions.cs" />
<Compile Include="ModelsBuilder\UmbracoApplicationTests.cs" />
<Compile Include="Models\ContentScheduleTests.cs" />
<Compile Include="Models\CultureImpactTests.cs" />
<Compile Include="Models\PathValidationTests.cs" />
@@ -549,6 +553,10 @@
<Project>{31785BC3-256C-4613-B2F5-A1B0BDDED8C1}</Project>
<Name>Umbraco.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj">
<Project>{52ac0ba8-a60e-4e36-897b-e8b97a54ed1c}</Project>
<Name>Umbraco.ModelsBuilder.Embedded</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
<Project>{651E1350-91B6-44B7-BD60-7207006D7003}</Project>
<Name>Umbraco.Web</Name>

View File

@@ -138,7 +138,11 @@ angular.module("umbraco.directives")
var unbindModelWatcher = scope.$watch(function() {
return ngModelController.$modelValue;
}, function(newValue) {
update(true);
$timeout(
function() {
update(true);
}
);
});
scope.$on('$destroy', function() {

View File

@@ -16,9 +16,6 @@ angular.module("umbraco.directives")
replace: true,
templateUrl: 'views/components/property/umb-property.html',
link: function (scope) {
scope.propertyActions = [];
userService.getCurrentUser().then(function (u) {
var isAdmin = u.userGroups.indexOf('admin') !== -1;
scope.propertyAlias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.property.alias : null;
@@ -36,6 +33,7 @@ angular.module("umbraco.directives")
$scope.property.propertyErrorMessage = errorMsg;
};
$scope.propertyActions = [];
self.setPropertyActions = function(actions) {
$scope.propertyActions = actions;
};

View File

@@ -15,7 +15,7 @@ function umbPropEditor(umbPropEditorHelper) {
preview: "<"
},
require: "^^form",
require: ["^^form", "?^umbProperty"],
restrict: 'E',
replace: true,
templateUrl: 'views/components/property/umb-property-editor.html',
@@ -24,7 +24,10 @@ function umbPropEditor(umbPropEditorHelper) {
//we need to copy the form controller val to our isolated scope so that
//it get's carried down to the child scopes of this!
//we'll also maintain the current form name.
scope[ctrl.$name] = ctrl;
scope[ctrl[0].$name] = ctrl[0];
// We will capture a reference to umbProperty in this Directive and pass it on to the Scope, so Property-Editor controllers can use it.
scope["umbProperty"] = ctrl[1];
if(!scope.model.alias){
scope.model.alias = Math.random().toString(36).slice(2);

View File

@@ -1,4 +1,4 @@
function modelsBuilderResource($q, $http, umbRequestHelper) {
function modelsBuilderManagementResource($q, $http, umbRequestHelper) {
return {
getModelsOutOfDateStatus: function () {
@@ -20,4 +20,4 @@
}
};
}
angular.module("umbraco.resources").factory("modelsBuilderResource", modelsBuilderResource);
angular.module("umbraco.resources").factory("modelsBuilderManagementResource", modelsBuilderManagementResource);

View File

@@ -27,7 +27,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje
generateModels: function () {
var deferred = $q.defer();
var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null;
var modelsResource = $injector.has("modelsBuilderManagementResource") ? $injector.get("modelsBuilderManagementResource") : null;
var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled;
if (modelsBuilderEnabled && modelsResource) {
modelsResource.buildModels().then(function(result) {
@@ -48,7 +48,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje
checkModelsBuilderStatus: function () {
var deferred = $q.defer();
var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null;
var modelsResource = $injector.has("modelsBuilderManagementResource") ? $injector.get("modelsBuilderManagementResource") : null;
var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true);
if (modelsBuilderEnabled && modelsResource) {

View File

@@ -189,6 +189,17 @@ h5.-black {
.umb-control-group .umb-el-wrap {
padding: 0;
}
.umb-control-group .control-header {
.control-label {
float: left;
}
.control-description {
display: block;
clear: both;
}
}
.form-horizontal .umb-control-group .control-header {
float: left;
width: 160px;
@@ -196,15 +207,12 @@ h5.-black {
text-align: left;
.control-label {
float: left;
width: auto;
padding-top: 0;
text-align: left;
}
.control-description {
display: block;
clear: both;
max-width:480px;// avoiding description becoming too wide when its placed on top of property.
margin-bottom: 10px;
}

View File

@@ -6,12 +6,12 @@
<div class="umb-el-wrap">
<div class="control-header">
<div class="control-header" ng-hide="property.hideLabel === true">
<small ng-if="showInherit" class="db" style="padding-top: 0; margin-bottom: 5px;">
<localize key="contentTypeEditor_inheritedFrom"></localize> {{inheritsFrom}}
</small>
<label class="control-label" ng-hide="property.hideLabel" for="{{property.alias}}" ng-attr-title="{{propertyAlias}}">
<label class="control-label" for="{{property.alias}}" ng-attr-title="{{propertyAlias}}">
{{property.label}}

View File

@@ -1,4 +1,4 @@
function modelsBuilderController($scope, $http, umbRequestHelper, modelsBuilderResource) {
function modelsBuilderManagementController($scope, $http, umbRequestHelper, modelsBuilderManagementResource) {
var vm = this;
@@ -9,8 +9,8 @@
function generate() {
vm.generating = true;
umbRequestHelper.resourcePromise(
$http.post(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "BuildModels")),
'Failed to generate.')
$http.post(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "BuildModels")),
'Failed to generate.')
.then(function (result) {
vm.generating = false;
vm.dashboard = result;
@@ -19,7 +19,7 @@
function reload() {
vm.loading = true;
modelsBuilderResource.getDashboard().then(function (result) {
modelsBuilderManagementResource.getDashboard().then(function (result) {
vm.dashboard = result;
vm.loading = false;
});
@@ -27,7 +27,7 @@
function init() {
vm.loading = true;
modelsBuilderResource.getDashboard().then(function (result) {
modelsBuilderManagementResource.getDashboard().then(function (result) {
vm.dashboard = result;
vm.loading = false;
});
@@ -35,4 +35,4 @@
init();
}
angular.module("umbraco").controller("Umbraco.Dashboard.ModelsBuilderController", modelsBuilderController);
angular.module("umbraco").controller("Umbraco.Dashboard.ModelsBuilderManagementController", modelsBuilderManagementController);

View File

@@ -1,4 +1,4 @@
<div id="modelsBuilder" ng-controller="Umbraco.Dashboard.ModelsBuilderController as vm">
<div id="modelsBuilder" ng-controller="Umbraco.Dashboard.ModelsBuilderManagementController as vm">
<umb-box>
<umb-box-content>

View File

@@ -99,6 +99,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
function sync() {
$scope.model.value = $scope.ids.join();
removeAllEntriesAction.isDisabled = $scope.ids.length === 0;
};
function setDirty() {
@@ -247,6 +248,31 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
return true;
}
function removeAllEntries() {
$scope.mediaItems.length = 0;// AngularJS way to empty the array.
$scope.ids.length = 0;// AngularJS way to empty the array.
sync();
setDirty();
}
var removeAllEntriesAction = {
labelKey: 'clipboard_labelForRemoveAllEntries',
labelTokens: [],
icon: 'trash',
method: removeAllEntries,
isDisabled: true
};
if (multiPicker === true) {
var propertyActions = [
removeAllEntriesAction
];
if ($scope.umbProperty) {
$scope.umbProperty.setPropertyActions(propertyActions);
}
}
$scope.sortableOptions = {
containment: 'parent',
cursor: 'move',

View File

@@ -55,8 +55,8 @@
});
function setCurrentNode(node) {
vm.currentNode = node;
updateModel();
vm.currentNode = node;
}
var copyAllEntries = function() {

View File

@@ -1,18 +0,0 @@
{
// array of files we want to inject into the application
"javascript": [
"~/App_Plugins/ModelsBuilder/modelsbuilder.controller.js",
"~/App_Plugins/ModelsBuilder/modelsbuilder.resource.js"
],
// models builder dashboard
"dashboards": [
{
"alias": "settingsModelsBuilder",
"name": "Models Builder",
"view": "/App_Plugins/ModelsBuilder/modelsbuilder.html",
"sections": [ "settings" ],
"weight": 40
}
]
}

View File

@@ -12,7 +12,7 @@
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
<UseIISExpress>true</UseIISExpress>
<IISExpressSSLPort>44319</IISExpressSSLPort>
<IISExpressSSLPort>44331</IISExpressSSLPort>
<IISExpressAnonymousAuthentication>enabled</IISExpressAnonymousAuthentication>
<IISExpressWindowsAuthentication>disabled</IISExpressWindowsAuthentication>
<IISExpressUseClassicPipelineMode>false</IISExpressUseClassicPipelineMode>
@@ -81,6 +81,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="App_Data\" />
<Folder Include="App_Plugins\" />
<Folder Include="Views\MacroPartials\" />
</ItemGroup>
<ItemGroup>
@@ -110,9 +111,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Umbraco.ModelsBuilder.Ui">
<Version>8.1.0</Version>
</PackageReference>
<PackageReference Include="Umbraco.SqlServerCE" Version="4.0.0.1" />
</ItemGroup>
<ItemGroup>
@@ -124,6 +122,10 @@
<Name>Umbraco.Examine</Name>
<Project>{07FBC26B-2927-4A22-8D96-D644C667FECC}</Project>
</ProjectReference>
<ProjectReference Include="..\Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj">
<Project>{52ac0ba8-a60e-4e36-897b-e8b97a54ed1c}</Project>
<Name>Umbraco.ModelsBuilder.Embedded</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
<Name>Umbraco.Web</Name>
@@ -146,9 +148,6 @@
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
<Content Include="App_Plugins\ModelsBuilder\modelsbuilder.controller.js" />
<Content Include="App_Plugins\ModelsBuilder\modelsbuilder.html" />
<Content Include="App_Plugins\ModelsBuilder\modelsbuilder.resource.js" />
<Content Include="Config\grid.editors.config.js" />
<Content Include="Config\Lang\cs-CZ.user.xml" />
<Content Include="Config\Lang\da-DK.user.xml" />
@@ -174,7 +173,6 @@
<Content Include="Umbraco\Config\Lang\zh_tw.xml" />
<Content Include="Config\Splashes\noNodes.aspx" />
<Content Include="Umbraco\Install\Views\Web.config" />
<Content Include="App_Plugins\ModelsBuilder\package.manifest" />
<None Include="Config\ClientDependency.Release.config">
<DependentUpon>ClientDependency.config</DependentUpon>
<SubType>Designer</SubType>
@@ -349,7 +347,7 @@
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>8600</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:8600/</IISUrl>
<IISUrl>http://localhost:8600</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>False</UseCustomServer>
<CustomServerUrl>
@@ -431,4 +429,4 @@
<Message Text="ConfigFile: $(OriginalFileName) -&gt; $(OutputFileName)" Importance="high" Condition="Exists('$(ModifiedFileName)')" />
<Copy SourceFiles="$(ModifiedFileName)" DestinationFiles="$(OutputFileName)" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" Condition="Exists('$(ModifiedFileName)')" />
</Target>
</Project>
</Project>

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
@@ -139,6 +140,14 @@ namespace Umbraco.Web.Cache
public class JsonPayload
{
[Obsolete("Use the constructor specifying a GUID instead, using this constructor will result in not refreshing all caches")]
public JsonPayload(int id, TreeChangeTypes changeTypes)
{
Id = id;
ChangeTypes = changeTypes;
}
[JsonConstructor]
public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes)
{
Id = id;

View File

@@ -6,7 +6,7 @@ using Umbraco.Core.Composing;
namespace Umbraco.Web.Editors
{
internal class EditorValidatorCollection : BuilderCollectionBase<IEditorValidator>
public class EditorValidatorCollection : BuilderCollectionBase<IEditorValidator>
{
public EditorValidatorCollection(IEnumerable<IEditorValidator> items)
: base(items)

View File

@@ -2,7 +2,7 @@
namespace Umbraco.Web.Editors
{
internal class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase<EditorValidatorCollectionBuilder, EditorValidatorCollection, IEditorValidator>
public class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase<EditorValidatorCollectionBuilder, EditorValidatorCollection, IEditorValidator>
{
protected override EditorValidatorCollectionBuilder This => this;
}

Some files were not shown because too many files have changed in this diff Show More