diff --git a/.gitattributes b/.gitattributes
index a664be3a85..c8987ade67 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -13,7 +13,7 @@
*.png binary
*.gif binary
-*.cs text=auto diff=csharp
+*.cs text=auto diff=csharp
*.vb text=auto
*.c text=auto
*.cpp text=auto
@@ -41,9 +41,13 @@
*.fs text=auto
*.fsx text=auto
*.hs text=auto
+*.json text=auto
+*.xml text=auto
-*.csproj text=auto merge=union
-*.vbproj text=auto merge=union
-*.fsproj text=auto merge=union
-*.dbproj text=auto merge=union
-*.sln text=auto eol=crlf merge=union
+*.csproj text=auto merge=union
+*.vbproj text=auto merge=union
+*.fsproj text=auto merge=union
+*.dbproj text=auto merge=union
+*.sln text=auto eol=crlf merge=union
+
+*.gitattributes text=auto
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index e009ee2294..cea5859486 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -60,10 +60,10 @@ Great question! The short version goes like this:

- * **Switch to the correct branch** - switch to the v8-dev branch
+ * **Switch to the correct branch** - switch to the `v8/contrib` branch
* **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md)
* **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions)
- * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/dev`, create a new branch first.
+ * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first.
* **Push** - great, now you can push the changes up to your fork on GitHub
* **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here] (https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
@@ -158,7 +158,7 @@ To find the general areas for something you're looking to fix or improve, have a
### Which branch should I target for my contributions?
-We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
+We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/contrib`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise.
@@ -184,10 +184,10 @@ Then when you want to get the changes from the main repository:
```
git fetch upstream
-git rebase upstream/v8/dev
+git rebase upstream/v8/contrib
```
-In this command we're syncing with the `v8/dev` branch, but you can of course choose another one if needed.
+In this command we're syncing with the `v8/contrib` branch, but you can of course choose another one if needed.
(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated))
diff --git a/.github/README.md b/.github/README.md
index d6d978c3d6..467ca6e5e6 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -1,4 +1,4 @@
-# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://twitter.com/intent/follow?screen_name=umbraco)
+# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://twitter.com/intent/follow?screen_name=umbraco)
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
diff --git a/.github/img/defaultbranch.png b/.github/img/defaultbranch.png
index f3a5b9efbc..3550b5c34c 100644
Binary files a/.github/img/defaultbranch.png and b/.github/img/defaultbranch.png differ
diff --git a/.gitignore b/.gitignore
index a0ff4d5b27..12ad3299ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,6 +55,7 @@ App_Data/TEMP/*
src/Umbraco.Web.UI/[Cc]ss/*
src/Umbraco.Web.UI/App_Code/*
src/Umbraco.Web.UI/App_Data/*
+src/Umbraco.Web.UI/data/*
src/Umbraco.Tests/App_Data/*
src/Umbraco.Web.UI/[Mm]edia/*
src/Umbraco.Web.UI/[Mm]aster[Pp]ages/*
diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec
index c8374bc2f7..347bde139e 100644
--- a/build/NuSpecs/UmbracoCms.Web.nuspec
+++ b/build/NuSpecs/UmbracoCms.Web.nuspec
@@ -28,7 +28,7 @@
-
+
@@ -52,14 +52,17 @@
+
+
+
diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec
index 97e9ef3df2..a6b06d9964 100644
--- a/build/NuSpecs/UmbracoCms.nuspec
+++ b/build/NuSpecs/UmbracoCms.nuspec
@@ -26,7 +26,6 @@
not want this to happen as the alpha of the next major is, really, the next major already.
-->
-
diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs
index 9b5319dc41..62b240f10f 100644
--- a/src/Umbraco.Core/Composing/ComponentCollection.cs
+++ b/src/Umbraco.Core/Composing/ComponentCollection.cs
@@ -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($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
- component.Terminate();
- component.DisposeIfDisposable();
+ try
+ {
+ component.Terminate();
+ component.DisposeIfDisposable();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Error while terminating component {ComponentType}.", componentType.FullName);
+ }
}
}
}
diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs
index 5b157307ab..fe5a82047a 100644
--- a/src/Umbraco.Core/ContentVariationExtensions.cs
+++ b/src/Umbraco.Core/ContentVariationExtensions.cs
@@ -19,6 +19,11 @@ namespace Umbraco.Core
///
public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture();
+ ///
+ /// Determines whether the content type varies by segment.
+ ///
+ public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment();
+
///
/// Determines whether the content type is invariant.
///
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
index 6164f828f0..922fdceff8 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
@@ -186,6 +186,7 @@ namespace Umbraco.Core.Migrations.Upgrade
// to 8.6.0
To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}");
To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}");
+ To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
//FINAL
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs
new file mode 100644
index 0000000000..75de01dd7f
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs
@@ -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();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/MediaTypeExtensions.cs b/src/Umbraco.Core/Models/MediaTypeExtensions.cs
new file mode 100644
index 0000000000..4e2ae5822a
--- /dev/null
+++ b/src/Umbraco.Core/Models/MediaTypeExtensions.cs
@@ -0,0 +1,10 @@
+namespace Umbraco.Core.Models
+{
+ internal static class MediaTypeExtensions
+ {
+ internal static bool IsSystemMediaType(this IMediaType mediaType) =>
+ mediaType.Alias == Constants.Conventions.MediaTypes.File
+ || mediaType.Alias == Constants.Conventions.MediaTypes.Folder
+ || mediaType.Alias == Constants.Conventions.MediaTypes.Image;
+ }
+}
diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
index 3c2c3deda4..4b203c128f 100644
--- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
@@ -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; }
diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
index 69b0698a96..254e04d2d5 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
@@ -26,5 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories
///
///
bool HasContainerInPath(string contentPath);
+
+ ///
+ /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
+ ///
+ bool HasContentNodes(int id);
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index 6385482686..6f714ff187 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -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",
///
/// 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.
///
/// Entity to correct properties for
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();
var nodeId = -1;
var propertyTypeId = -1;
@@ -1324,6 +1323,17 @@ WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentT
return Database.ExecuteScalar(sql) > 0;
}
+ ///
+ /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
+ ///
+ public bool HasContentNodes(int id)
+ {
+ var sql = new Sql(
+ $"SELECT CASE WHEN EXISTS (SELECT * FROM {Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END",
+ new { id });
+ return Database.ExecuteScalar(sql) == 1;
+ }
+
protected override IEnumerable GetDeleteClauses()
{
// in theory, services should have ensured that content items of the given content type
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
index dd9c7c93e5..afe1af7eb4 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -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;
@@ -229,6 +228,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return MapDtosToContent(Database.Fetch(sql), true);
}
+ // TODO: This method needs to return a readonly version of IContent! The content returned
+ // from this method does not contain all of the data required to re-persist it and if that
+ // is attempted some odd things will occur.
+ // Either we create an IContentReadOnly (which ultimately we should for vNext so we can
+ // differentiate between methods that return entities that can be re-persisted or not), or
+ // in the meantime to not break API compatibility, we can add a property to IContentBase
+ // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw
+ // an exception if that entity is passed to a Save method.
+ // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService.
+ // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly
+ // which can return IContentReadOnly.
+ // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a
+ // content item. Ideally for paged data that populates list views, these would be ultra slim
+ // content items, there's no reason to populate those with really anything apart from property data,
+ // but until we do something like the above, we can't do that since it would be breaking and unclear.
public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take)
{
var sql = GetBaseQuery(QueryType.Many, false)
@@ -236,7 +250,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.OrderByDescending(x => x.Current)
.AndByDescending(x => x.VersionDate);
- return MapDtosToContent(Database.Fetch(sql), true, true).Skip(skip).Take(take);
+ return MapDtosToContent(Database.Fetch(sql), true,
+ // load bare minimum, need variants though since this is used to rollback with variants
+ false, false, false, true).Skip(skip).Take(take);
}
public override IContent GetVersion(int versionId)
@@ -1056,7 +1072,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return base.ApplySystemOrdering(ref sql, ordering);
}
- private IEnumerable MapDtosToContent(List dtos, bool withCache = false, bool slim = false)
+ private IEnumerable MapDtosToContent(List dtos,
+ bool withCache = false,
+ bool loadProperties = true,
+ bool loadTemplates = true,
+ bool loadSchedule = true,
+ bool loadVariants = true)
{
var temps = new List>();
var contentTypes = new Dictionary();
@@ -1089,7 +1110,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 +1135,71 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
temps.Add(temp);
}
- if (!slim)
+ Dictionary 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 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;
diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs
index afd602cfc9..87e0732d47 100644
--- a/src/Umbraco.Core/Properties/AssemblyInfo.cs
+++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs
@@ -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")]
diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
index 51e5d756eb..6ed3c85e91 100644
--- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
+++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
@@ -39,6 +39,11 @@ namespace Umbraco.Core.Services
int Count();
+ ///
+ /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
+ ///
+ bool HasContentNodes(int id);
+
IEnumerable GetAll(params int[] ids);
IEnumerable GetAll(IEnumerable ids);
diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
index 7ae330f8f1..da532e2765 100644
--- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
+++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
@@ -372,6 +372,15 @@ namespace Umbraco.Core.Services.Implement
}
}
+ public bool HasContentNodes(int id)
+ {
+ using (var scope = ScopeProvider.CreateScope(autoComplete: true))
+ {
+ scope.ReadLock(ReadLockIds);
+ return Repository.HasContentNodes(id);
+ }
+ }
+
#endregion
#region Save
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 1600d3f7fc..3b6554642f 100755
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -132,6 +132,7 @@
+
@@ -280,6 +281,7 @@
+
diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj
index 7eff1bddc2..0e0ee62139 100644
--- a/src/Umbraco.Examine/Umbraco.Examine.csproj
+++ b/src/Umbraco.Examine/Umbraco.Examine.csproj
@@ -49,7 +49,7 @@
-
+ 1.0.0-beta2-19324-01runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -112,4 +112,4 @@
-
+
\ No newline at end of file
diff --git a/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs b/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs
new file mode 100644
index 0000000000..22347edd60
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Reflection;
+using Semver;
+
+namespace Umbraco.ModelsBuilder.Embedded
+{
+ ///
+ /// Manages API version handshake between client and server.
+ ///
+ public class ApiVersion
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The currently executing version.
+ ///
+ internal ApiVersion(SemVersion executingVersion)
+ {
+ Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion));
+ }
+
+ private static SemVersion CurrentAssemblyVersion
+ => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion);
+
+ ///
+ /// Gets the currently executing API version.
+ ///
+ public static ApiVersion Current { get; }
+ = new ApiVersion(CurrentAssemblyVersion);
+
+ ///
+ /// Gets the executing version of the API.
+ ///
+ public SemVersion Version { get; }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs
new file mode 100644
index 0000000000..1fdb64c62a
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs
@@ -0,0 +1,17 @@
+using Umbraco.ModelsBuilder.Embedded.Configuration;
+using Umbraco.Web.Models.ContentEditing;
+
+namespace Umbraco.ModelsBuilder.Embedded.BackOffice
+{
+ ///
+ /// Used to validate the aliases for the content type when MB is enabled to ensure that
+ /// no illegal aliases are used
+ ///
+ // ReSharper disable once UnusedMember.Global - This is typed scanned
+ public class ContentTypeModelValidator : ContentTypeModelValidatorBase
+ {
+ public ContentTypeModelValidator(IModelsBuilderConfig config) : base(config)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs
new file mode 100644
index 0000000000..15ca2cca24
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs
@@ -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 : EditorValidator
+ where TModel : ContentTypeSave
+ where TProperty : PropertyTypeBasic
+ {
+ private readonly IModelsBuilderConfig _config;
+
+ public ContentTypeModelValidatorBase(IModelsBuilderConfig config)
+ {
+ _config = config;
+ }
+
+ protected override IEnumerable 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;
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs
new file mode 100644
index 0000000000..25ddc838e8
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs
@@ -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 + "
ModelsBuilder is disabled (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("
");
+
+ sb.Append("ModelsBuilder is enabled, with the following configuration:");
+
+ sb.Append("
");
+
+ sb.Append("
The models factory is ");
+ sb.Append(_config.EnableFactory || _config.ModelsMode == ModelsMode.PureLive
+ ? "enabled"
+ : "not enabled. Umbraco will not use models");
+ sb.Append(".
No models mode is specified: models will not be generated.
");
+
+ sb.Append($"
Models namespace is {_config.ModelsNamespace}.
");
+
+ sb.Append("
Tracking of out-of-date models is ");
+ sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled");
+ sb.Append(".
");
+
+ sb.Append("
");
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs
new file mode 100644
index 0000000000..9dc1ea6c20
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs
@@ -0,0 +1,17 @@
+using Umbraco.ModelsBuilder.Embedded.Configuration;
+using Umbraco.Web.Models.ContentEditing;
+
+namespace Umbraco.ModelsBuilder.Embedded.BackOffice
+{
+ ///
+ /// Used to validate the aliases for the content type when MB is enabled to ensure that
+ /// no illegal aliases are used
+ ///
+ // ReSharper disable once UnusedMember.Global - This is typed scanned
+ public class MediaTypeModelValidator : ContentTypeModelValidatorBase
+ {
+ public MediaTypeModelValidator(IModelsBuilderConfig config) : base(config)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs
new file mode 100644
index 0000000000..8d0a98eeab
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs
@@ -0,0 +1,17 @@
+using Umbraco.ModelsBuilder.Embedded.Configuration;
+using Umbraco.Web.Models.ContentEditing;
+
+namespace Umbraco.ModelsBuilder.Embedded.BackOffice
+{
+ ///
+ /// Used to validate the aliases for the content type when MB is enabled to ensure that
+ /// no illegal aliases are used
+ ///
+ // ReSharper disable once UnusedMember.Global - This is typed scanned
+ public class MemberTypeModelValidator : ContentTypeModelValidatorBase
+ {
+ public MemberTypeModelValidator(IModelsBuilderConfig config) : base(config)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs
new file mode 100644
index 0000000000..1d9de265e9
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs
@@ -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
+{
+ ///
+ /// API controller for use in the Umbraco back office with Angular resources
+ ///
+ ///
+ /// 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.
+ ///
+ [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; }
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder/Building/Builder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs
similarity index 54%
rename from src/Umbraco.ModelsBuilder/Building/Builder.cs
rename to src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs
index acfa402355..ffd56d4312 100644
--- a/src/Umbraco.ModelsBuilder/Building/Builder.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs
@@ -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
///
internal abstract class Builder
{
+
private readonly IList _typeModels;
protected Dictionary ModelsMap { get; } = new Dictionary();
- protected ParseResult ParseResult { get; }
// the list of assemblies that will be 'using' by default
protected readonly IList TypesUsing = new List
@@ -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"
};
///
@@ -55,10 +49,10 @@ namespace Umbraco.ModelsBuilder.Building
///
/// Gets the list of models to generate.
///
- /// The models to generate, ie those that are not ignored.
+ /// The models to generate
public IEnumerable GetModelsToGenerate()
{
- return _typeModels.Where(x => !x.IsContentIgnored);
+ return _typeModels;
}
///
@@ -67,90 +61,39 @@ namespace Umbraco.ModelsBuilder.Building
/// Includes those that are ignored.
internal IList TypeModels => _typeModels;
- ///
- /// Initializes a new instance of the class with a list of models to generate
- /// and the result of code parsing.
- ///
- /// The list of models to generate.
- /// The result of code parsing.
- protected Builder(IList typeModels, ParseResult parseResult)
- {
- _typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels));
- ParseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult));
-
- Prepare();
- }
-
///
/// Initializes a new instance of the class with a list of models to generate,
/// the result of code parsing, and a models namespace.
///
/// The list of models to generate.
- /// The result of code parsing.
/// The models namespace.
- protected Builder(IList typeModels, ParseResult parseResult, string modelsNamespace)
- : this(typeModels, parseResult)
+ protected Builder(IModelsBuilderConfig config, IList 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; }
+
///
/// Prepares generation by processing the result of code parsing.
///
- ///
- /// 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.
- ///
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();
- 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 { { "code", codeBuilder.ToString() } }, out trees);
- var tree = trees[0];
- _ambiguousSymbolsModel = compilation.GetSemanticModel(tree);
-
- var namespaceSyntax = tree.GetRoot().DescendantNodes().OfType().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";
}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs
new file mode 100644
index 0000000000..8a3bc5a5b5
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs
@@ -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();
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs
similarity index 89%
rename from src/Umbraco.ModelsBuilder/Building/PropertyModel.cs
rename to src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs
index 1595b3f888..af5445b175 100644
--- a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
-namespace Umbraco.ModelsBuilder.Building
+namespace Umbraco.ModelsBuilder.Embedded.Building
{
///
/// Represents a model property.
@@ -41,11 +41,6 @@ namespace Umbraco.ModelsBuilder.Building
///
public string ClrTypeName;
- ///
- /// Gets a value indicating whether this property should be excluded from generation.
- ///
- public bool IsIgnored;
-
///
/// Gets the generation errors for the property.
///
diff --git a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs
similarity index 83%
rename from src/Umbraco.ModelsBuilder/Building/TextBuilder.cs
rename to src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs
index 85ccd541b7..d1190a0374 100644
--- a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs
@@ -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
{
///
/// Implements a builder that works by writing text.
@@ -19,20 +17,8 @@ namespace Umbraco.ModelsBuilder.Building
/// and the result of code parsing.
///
/// The list of models to generate.
- /// The result of code parsing.
- public TextBuilder(IList typeModels, ParseResult parseResult)
- : base(typeModels, parseResult)
- { }
-
- ///
- /// Initializes a new instance of the class with a list of models to generate,
- /// the result of code parsing, and a models namespace.
- ///
- /// The list of models to generate.
- /// The result of code parsing.
- /// The models namespace.
- public TextBuilder(IList typeModels, ParseResult parseResult, string modelsNamespace)
- : base(typeModels, parseResult, modelsNamespace)
+ public TextBuilder(IModelsBuilderConfig config, IList 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/// {0}\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/// {0}\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(Expression> selector)\n",
+ WriteGeneratedCodeAttribute(sb, "\t\t");
+ sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(Expression> 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///\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///\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/// Static getter for {0}\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/// {0}\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 TypesMap = new Dictionary
+ private static readonly IDictionary TypesMap = new Dictionary(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" }
};
}
}
diff --git a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs
similarity index 89%
rename from src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs
rename to src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs
index d165f03907..a93df97806 100644
--- a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs
@@ -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
{
///
/// Outputs an "auto-generated" header to a string builder.
diff --git a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs
similarity index 85%
rename from src/Umbraco.ModelsBuilder/Building/TypeModel.cs
rename to src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs
index 5ada8e881c..95356cf3ff 100644
--- a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs
@@ -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
{
///
/// Represents a model.
@@ -76,10 +77,10 @@ namespace Umbraco.ModelsBuilder.Building
///
public readonly List ImplementingInterfaces = new List();
- ///
- /// Gets the list of existing static mixin method candidates.
- ///
- public readonly List StaticMixinMethods = new List();
+ /////
+ ///// Gets the list of existing static mixin method candidates.
+ /////
+ //public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used
///
/// 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.
public bool HasBase;
- ///
- /// Gets a value indicating whether this model has been renamed.
- ///
- public bool IsRenamed;
-
- ///
- /// Gets a value indicating whether this model has [ImplementContentType] already.
- ///
- public bool HasImplement;
-
///
/// Gets a value indicating whether this model is used as a mixin by another model.
///
@@ -108,16 +99,6 @@ namespace Umbraco.ModelsBuilder.Building
///
public bool IsParent;
- ///
- /// Gets a value indicating whether this model should be excluded from generation.
- ///
- public bool IsContentIgnored;
-
- ///
- /// Gets a value indicating whether the ctor is already defined in a partial.
- ///
- public bool HasCtor;
-
///
/// Gets a value indicating whether the type is an element.
///
@@ -181,11 +162,11 @@ namespace Umbraco.ModelsBuilder.Building
/// Includes the specified type.
internal static void CollectImplems(ICollection 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;
}
}
+
+ ///
+ /// Maps ModelType.
+ ///
+ public static void MapModelTypes(IList 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);
+ }
+ }
+ }
}
}
diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs
similarity index 81%
rename from src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs
rename to src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs
index c530cbbd6b..2f14bec875 100644
--- a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs
@@ -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 ourFiles, IEnumerable typeModels)
+ public static string Hash(IEnumerable 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();
}
}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs
new file mode 100644
index 0000000000..c599785711
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs
@@ -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
+{
+ ///
+ /// Special component used for when MB is disabled with the legacy MB is detected
+ ///
+ 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();
+ }
+
+ public void Terminate()
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs
new file mode 100644
index 0000000000..0e41c9ac62
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs
@@ -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 umbracoUrls))
+ throw new ArgumentException("Invalid umbracoUrls");
+
+ if (!serverVars.ContainsKey("umbracoPlugins"))
+ throw new ArgumentException("Missing umbracoPlugins.");
+ if (!(serverVars["umbracoPlugins"] is Dictionary 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(controller => controller.BuildModels());
+ umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings();
+ };
+ }
+
+ private Dictionary GetModelsBuilderSettings()
+ {
+ var settings = new Dictionary
+ {
+ {"enabled", _config.Enable}
+ };
+
+ return settings;
+ }
+
+ ///
+ /// 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
+ ///
+ ///
+ ///
+ private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs 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();
+ var modelAttr = args.ModelType.Assembly.GetCustomAttribute();
+
+ // 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;
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs
new file mode 100644
index 0000000000..45c4de5d2a
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs
@@ -0,0 +1,103 @@
+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();
+
+
+ composition.Configs.Add(() => new ModelsBuilderConfig());
+
+ if (isLegacyModelsBuilderInstalled)
+ {
+ ComposeForLegacyModelsBuilder(composition);
+ return;
+ }
+
+ composition.Components().Append();
+ composition.Register(Lifetime.Singleton);
+
+ composition.RegisterUnique();
+ composition.RegisterUnique();
+ composition.RegisterUnique();
+ composition.RegisterUnique();
+
+ 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("ModelsBuilder.Embedded is disabled, the external ModelsBuilder was detected.");
+ composition.Components().Append();
+ composition.Dashboards().Remove();
+ }
+
+ private void ComposeForDefaultModelsFactory(Composition composition)
+ {
+ composition.RegisterUnique(factory =>
+ {
+ var typeLoader = factory.GetInstance();
+ var types = typeLoader
+ .GetTypes() // element models
+ .Concat(typeLoader.GetTypes()); // content models
+ return new PublishedModelFactory(types);
+ });
+ }
+
+ private void ComposeForLiveModels(Composition composition)
+ {
+ composition.RegisterUnique();
+
+ // 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);
+ */
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs
new file mode 100644
index 0000000000..a86669b135
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs
@@ -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
+ //
+ // to web.config system.web/compilation/assemblies
+
+ var netStandard = ReferencedAssemblies.GetNetStandardAssembly();
+ if (netStandard != null)
+ BuildManager.AddReferencedAssembly(netStandard);
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs
new file mode 100644
index 0000000000..d634547a49
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs
@@ -0,0 +1,20 @@
+using Umbraco.Core.Configuration;
+using Umbraco.ModelsBuilder.Embedded.Configuration;
+
+namespace Umbraco.ModelsBuilder.Embedded
+{
+ ///
+ /// Provides extension methods for the class.
+ ///
+ public static class ConfigsExtensions
+ {
+ ///
+ /// Gets the models builder configuration.
+ ///
+ /// Getting the models builder configuration freezes its state,
+ /// and any attempt at modifying the configuration using the Setup method
+ /// will be ignored.
+ public static IModelsBuilderConfig ModelsBuilder(this Configs configs)
+ => configs.GetConfig();
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs
new file mode 100644
index 0000000000..7e96aec60e
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs
@@ -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; }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs
new file mode 100644
index 0000000000..179fcecfcb
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs
@@ -0,0 +1,226 @@
+using System;
+using System.Configuration;
+using System.IO;
+using System.Threading;
+using System.Web.Configuration;
+using Umbraco.Core;
+using Umbraco.Core.IO;
+
+namespace Umbraco.ModelsBuilder.Embedded.Configuration
+{
+ ///
+ /// Represents the models builder configuration.
+ ///
+ public class ModelsBuilderConfig : IModelsBuilderConfig
+ {
+ private const string Prefix = "Umbraco.ModelsBuilder.";
+ private object _modelsModelLock;
+ private bool _modelsModelConfigured;
+ private ModelsMode _modelsMode;
+ private object _flagOutOfDateModelsLock;
+ private bool _flagOutOfDateModelsConfigured;
+ private bool _flagOutOfDateModels;
+ public const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels";
+ public const string DefaultModelsDirectory = "~/App_Data/Models";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ModelsBuilderConfig()
+ {
+ // giant kill switch, default: false
+ // must be explicitely set to true for anything else to happen
+ Enable = ConfigurationManager.AppSettings[Prefix + "Enable"] == "true";
+
+ // ensure defaults are initialized for tests
+ ModelsNamespace = DefaultModelsNamespace;
+ ModelsDirectory = IOHelper.MapPath(DefaultModelsDirectory);
+ DebugLevel = 0;
+
+ // stop here, everything is false
+ if (!Enable) return;
+
+ // default: false
+ AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[Prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true");
+
+ // default: true
+ EnableFactory = !ConfigurationManager.AppSettings[Prefix + "EnableFactory"].InvariantEquals("false");
+
+ // default: initialized above with DefaultModelsNamespace const
+ var value = ConfigurationManager.AppSettings[Prefix + "ModelsNamespace"];
+ if (!string.IsNullOrWhiteSpace(value))
+ ModelsNamespace = value;
+
+ // default: initialized above with DefaultModelsDirectory const
+ value = ConfigurationManager.AppSettings[Prefix + "ModelsDirectory"];
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ var root = IOHelper.MapPath("~/");
+ if (root == null)
+ throw new ConfigurationErrorsException("Could not determine root directory.");
+
+ // GetModelsDirectory will ensure that the path is safe
+ ModelsDirectory = GetModelsDirectory(root, value, AcceptUnsafeModelsDirectory);
+ }
+
+ // default: 0
+ value = ConfigurationManager.AppSettings[Prefix + "DebugLevel"];
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ if (!int.TryParse(value, out var debugLevel))
+ throw new ConfigurationErrorsException($"Invalid debug level \"{value}\".");
+ DebugLevel = debugLevel;
+ }
+
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ModelsBuilderConfig(
+ bool enable = false,
+ ModelsMode modelsMode = ModelsMode.Nothing,
+ string modelsNamespace = null,
+ bool enableFactory = true,
+ bool flagOutOfDateModels = true,
+ string modelsDirectory = null,
+ bool acceptUnsafeModelsDirectory = false,
+ int debugLevel = 0)
+ {
+ Enable = enable;
+ _modelsMode = modelsMode;
+
+ ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace;
+ EnableFactory = enableFactory;
+ _flagOutOfDateModels = flagOutOfDateModels;
+ ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory;
+ AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory;
+ DebugLevel = debugLevel;
+ }
+
+ // internal for tests
+ internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe)
+ {
+ // making sure it is safe, ie under the website root,
+ // unless AcceptUnsafeModelsDirectory and then everything is OK.
+
+ if (!Path.IsPathRooted(root))
+ throw new ConfigurationErrorsException($"Root is not rooted \"{root}\".");
+
+ if (config.StartsWith("~/"))
+ {
+ var dir = Path.Combine(root, config.TrimStart("~/"));
+
+ // sanitize - GetFullPath will take care of any relative
+ // segments in path, eg '../../foo.tmp' - it may throw a SecurityException
+ // if the combined path reaches illegal parts of the filesystem
+ dir = Path.GetFullPath(dir);
+ root = Path.GetFullPath(root);
+
+ if (!dir.StartsWith(root) && !acceptUnsafe)
+ throw new ConfigurationErrorsException($"Invalid models directory \"{config}\".");
+
+ return dir;
+ }
+
+ if (acceptUnsafe)
+ return Path.GetFullPath(config);
+
+ throw new ConfigurationErrorsException($"Invalid models directory \"{config}\".");
+ }
+
+ ///
+ /// Gets a value indicating whether the whole models experience is enabled.
+ ///
+ ///
+ /// If this is false then absolutely nothing happens.
+ /// Default value is false which means that unless we have this setting, nothing happens.
+ ///
+ public bool Enable { get; }
+
+ ///
+ /// Gets the models mode.
+ ///
+ public ModelsMode ModelsMode =>
+ LazyInitializer.EnsureInitialized(ref _modelsMode, ref _modelsModelConfigured, ref _modelsModelLock, () =>
+ {
+ // mode
+ var modelsMode = ConfigurationManager.AppSettings[Prefix + "ModelsMode"];
+ if (string.IsNullOrWhiteSpace(modelsMode)) return ModelsMode.Nothing; //default
+ switch (modelsMode)
+ {
+ case nameof(ModelsMode.Nothing):
+ return ModelsMode.Nothing;
+ case nameof(ModelsMode.PureLive):
+ return ModelsMode.PureLive;
+ case nameof(ModelsMode.AppData):
+ return ModelsMode.AppData;
+ case nameof(ModelsMode.LiveAppData):
+ return ModelsMode.LiveAppData;
+ default:
+ throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode))));
+ }
+ });
+
+ ///
+ /// Gets a value indicating whether system.web/compilation/@debug is true.
+ ///
+ public bool IsDebug
+ {
+ get
+ {
+ var section = (CompilationSection)ConfigurationManager.GetSection("system.web/compilation");
+ return section != null && section.Debug;
+ }
+ }
+
+ ///
+ /// Gets the models namespace.
+ ///
+ /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied.
+ public string ModelsNamespace { get; }
+
+ ///
+ /// Gets a value indicating whether we should enable the models factory.
+ ///
+ /// Default value is true because no factory is enabled by default in Umbraco.
+ public bool EnableFactory { get; }
+
+ ///
+ /// Gets a value indicating whether we should flag out-of-date models.
+ ///
+ /// Models become out-of-date when data types or content types are updated. When this
+ /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are
+ /// generated through the dashboard, the files is cleared. Default value is false.
+ public bool FlagOutOfDateModels
+ => LazyInitializer.EnsureInitialized(ref _flagOutOfDateModels, ref _flagOutOfDateModelsConfigured, ref _flagOutOfDateModelsLock, () =>
+ {
+ var flagOutOfDateModels = !ConfigurationManager.AppSettings[Prefix + "FlagOutOfDateModels"].InvariantEquals("false");
+ if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive())
+ {
+ flagOutOfDateModels = false;
+ }
+
+ return flagOutOfDateModels;
+ });
+
+ ///
+ /// Gets the models directory.
+ ///
+ /// Default is ~/App_Data/Models but that can be changed.
+ public string ModelsDirectory { get; }
+
+ ///
+ /// Gets a value indicating whether to accept an unsafe value for ModelsDirectory.
+ ///
+ /// An unsafe value is an absolute path, or a relative path pointing outside
+ /// of the website root.
+ public bool AcceptUnsafeModelsDirectory { get; }
+
+ ///
+ /// Gets a value indicating the debug log level.
+ ///
+ /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe).
+ public int DebugLevel { get; }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs
similarity index 56%
rename from src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs
rename to src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs
index e04c4dee90..e0286fdab1 100644
--- a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs
@@ -1,4 +1,4 @@
-namespace Umbraco.ModelsBuilder.Configuration
+namespace Umbraco.ModelsBuilder.Embedded.Configuration
{
///
/// Defines the models generation modes.
@@ -8,7 +8,7 @@
///
/// Do not generate models.
///
- Nothing = 0, // default value
+ Nothing = 0, // default value
///
/// Generate models in memory.
@@ -31,22 +31,6 @@
///
/// Generation can be triggered from the dashboard. The app does not restart.
/// Models are not compiled and thus are not available to the project.
- LiveAppData,
-
- ///
- /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts).
- /// When: generation is triggered.
- ///
- /// Generation can be triggered from the dashboard. The app does restart. Models
- /// are available to the entire project.
- Dll,
-
- ///
- /// 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.
- ///
- /// Generation can be triggered from the dashboard. The app does restart. Models
- /// are available to the entire project.
- LiveDll
+ LiveAppData
}
}
diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs
similarity index 61%
rename from src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs
rename to src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs
index be609c0548..be638729ea 100644
--- a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs
@@ -1,4 +1,4 @@
-namespace Umbraco.ModelsBuilder.Configuration
+namespace Umbraco.ModelsBuilder.Embedded.Configuration
{
///
/// Provides extensions for the 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;
- }
-
- ///
- /// Gets a value indicating whether the mode is [Live]Dll.
- ///
- public static bool IsAnyDll(this ModelsMode modelsMode)
- {
- return
- modelsMode == ModelsMode.Dll
- || modelsMode == ModelsMode.LiveDll;
+ modelsMode == ModelsMode.LiveAppData;
}
///
@@ -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;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs b/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs
similarity index 76%
rename from src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs
rename to src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs
index e11662eb24..1c1fca6f73 100644
--- a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs
@@ -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()
diff --git a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs
similarity index 79%
rename from src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs
rename to src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs
index c5d8f8cad4..0359c49654 100644
--- a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs
@@ -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
{
///
/// Indicates that a property implements a given property alias.
diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs
new file mode 100644
index 0000000000..333181f27c
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs
@@ -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("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("Generate models...");
+ const int timeout = 2 * 60 * 1000; // 2 mins
+ _mutex.WaitOne(timeout); // wait until it is safe, and acquire
+ _logger.Info("Generate models now.");
+ GenerateModels();
+ _mbErrors.Clear();
+ _logger.Info("Generated.");
+ }
+ catch (TimeoutException)
+ {
+ _logger.Warn("Timeout, models were NOT generated.");
+ }
+ catch (Exception e)
+ {
+ _mbErrors.Report("Failed to build Live models.", e);
+ _logger.Error("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();
+ }
+
+
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs
new file mode 100644
index 0000000000..678ff241b0
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs
@@ -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(); // 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));
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs
similarity index 95%
rename from src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs
rename to src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs
index ed956852f8..7570c0b5b2 100644
--- a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs
@@ -1,6 +1,6 @@
using System;
-namespace Umbraco.ModelsBuilder
+namespace Umbraco.ModelsBuilder.Embedded
{
///
/// Indicates that an Assembly is a Models Builder assembly.
diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs
new file mode 100644
index 0000000000..b8b1945f32
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs
@@ -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();
+ }
+
+}
diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs
similarity index 68%
rename from src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs
rename to src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs
index 7102190b5e..a692f633a5 100644
--- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs
@@ -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;
diff --git a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs
similarity index 55%
rename from src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs
rename to src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs
index a047f21edb..5425c31c77 100644
--- a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs
@@ -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);
}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs b/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..68c149adde
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs
@@ -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")]
diff --git a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs
similarity index 80%
rename from src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs
rename to src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs
index f3320b5dfb..29429ba74f 100644
--- a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs
@@ -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
{
///
/// Provides extension methods to models.
@@ -14,13 +17,14 @@ namespace Umbraco.ModelsBuilder
///
/// Gets the value of a property.
///
- public static TValue Value(this TModel model, Expression> property, string culture = ".", string segment = ".")
+ public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default)
where TModel : IPublishedElement
{
var alias = GetAlias(model, property);
- return model.Value(alias, culture, segment);
+ return model.Value(alias, culture, segment, fallback, defaultValue);
}
+ // fixme that one should be public so ppl can use it
private static string GetAlias(TModel model, Expression> property)
{
if (property.NodeType != ExpressionType.Lambda)
diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs
similarity index 77%
rename from src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs
rename to src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs
index c70e8a3b65..8a6ed83ce9 100644
--- a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs
@@ -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
{
+ ///
+ /// This is called from within the generated model classes
+ ///
+ ///
+ /// DO NOT REMOVE - although there are not code references this is used directly by the generated models.
+ ///
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(PublishedContentType contentType, Expression> selector)
- //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel
+ public static IPublishedPropertyType GetModelPropertyType(IPublishedContentType contentType, Expression> 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()
.SingleOrDefault();
diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs
similarity index 72%
rename from src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs
rename to src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs
index 9558c0140e..8e8a19c729 100644
--- a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs
@@ -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() };
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;
+ private readonly Lazy _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, ProfilingLogger logger)
+ private readonly IModelsBuilderConfig _config;
+ private readonly ModelsGenerationError _errors;
+
+ public PureLiveModelFactory(Lazy 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
+
+ ///
+ public object SyncRoot { get; } = new object();
+
+ ///
+ 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>(declaring: listType);
+ ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType);
return ctor();
}
@@ -163,7 +178,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (_modelsAssembly == null) return;
if (_debugLevel > 0)
- _logger.Logger.Debug("RazorBuildProvider.CodeGenerationStarted");
+ _logger.Debug("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("Resetting models.");
+ _logger.Debug("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("Ensuring models.");
+ _logger.Debug("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() || x.Inherits());
_infos = RegisterModels(types);
- ModelsGenerationError.Clear();
+ _errors.Clear();
}
catch (Exception e)
{
try
{
- _logger.Logger.Error("Failed to build models.", e);
- _logger.Logger.Warn("Running without models."); // be explicit
- ModelsGenerationError.Report("Failed to build PureLive models.", e);
+ _logger.Error("Failed to build models.", e);
+ _logger.Warn("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();
-
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("Looking for cached models.");
+ _logger.Debug("Looking for cached models.");
if (File.Exists(modelsHashFile) && File.Exists(projFile))
{
var cachedHash = File.ReadAllText(modelsHashFile);
if (currentHash != cachedHash)
{
- _logger.Logger.Debug("Found obsolete cached models.");
+ _logger.Debug("Found obsolete cached models.");
forceRebuild = true;
}
+
+ // else cachedHash matches currentHash, we can try to load an existing dll
}
else
{
- _logger.Logger.Debug("Could not find cached models.");
+ _logger.Debug("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($"Cached models dll at {dllPath}.");
+
+ if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete") && dllPath.StartsWith(codegen))
{
assembly = Assembly.LoadFile(dllPath);
var attr = assembly.GetCustomAttribute();
@@ -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("Loading cached models (dll).");
+ _logger.Debug("Loading cached models (dll).");
return assembly;
}
+
+ _logger.Debug("Cached models dll cannot be loaded (invalid assembly).");
}
+ else if (!File.Exists(dllPath))
+ _logger.Debug("Cached models dll does not exist.");
+ else if (File.Exists(dllPath + ".delete"))
+ _logger.Debug("Cached models dll is marked for deletion.");
+ else if (!dllPath.StartsWith(codegen))
+ _logger.Debug("Cached models dll is in a different codegen directory.");
+ else
+ _logger.Debug("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("Loading cached models (source).");
+ _logger.Debug("Loading cached models (source).");
return assembly;
}
// need to rebuild
- _logger.Logger.Debug("Rebuilding models.");
+ _logger.Debug("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
+ {
+ { "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("Done rebuilding.");
+ _logger.Debug("Done rebuilding.");
return assembly;
}
+ private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile)
+ {
+ _logger.Debug("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 types)
{
- var ctorArgTypes = new[] { typeof (IPublishedElement) };
+ var ctorArgTypes = new[] { typeof(IPublishedElement) };
var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
var map = new Dictionary();
@@ -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(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) meth.CreateDelegate(typeof (Func));
+ var func = (Func)meth.CreateDelegate(typeof(Func));
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 ourFiles, IList typeModels)
+ private string GenerateModelsCode(IList 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("Ignoring files self-changes.");
+ // //_logger.Info("Ignoring files self-changes.");
// return;
//}
@@ -585,9 +657,10 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (OurFiles.Contains(changed))
return;
- _logger.Logger.Info("Detected files changes.");
+ _logger.Info("Detected files changes.");
- ResetModels();
+ lock (SyncRoot) // don't reset while being locked
+ ResetModels();
}
public void Stop(bool immediate)
diff --git a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs b/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs
similarity index 54%
rename from src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs
rename to src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs
index 42e8b3b9c9..8886afa1c8 100644
--- a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs
@@ -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> LazyLocations;
- private static readonly Lazy> LazyReferences;
static ReferencedAssemblies()
{
LazyLocations = new Lazy>(() => HostingEnvironment.IsHosted
? GetAllReferencedAssembliesLocationFromBuildManager()
: GetAllReferencedAssembliesFromDomain());
-
- LazyReferences = new Lazy>(() => Locations
- .Select(x => MetadataReference.CreateFromFile(x))
- .ToArray());
}
///
@@ -31,19 +25,68 @@ namespace Umbraco.ModelsBuilder
///
public static IEnumerable Locations => LazyLocations.Value;
- ///
- /// Gets the metadata reference of all the referenced assemblies.
- ///
- public static IEnumerable References => LazyReferences.Value;
+ public static Assembly GetNetStandardAssembly(List assemblies)
+ {
+ if (assemblies == null)
+ assemblies = BuildManager.GetReferencedAssemblies().Cast().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 GetAllReferencedAssembliesLocationFromBuildManager()
{
- return BuildManager.GetReferencedAssemblies()
- .Cast()
+ var assemblies = BuildManager.GetReferencedAssemblies().Cast().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 GetDeepReferencedAssemblies(Assembly assembly)
- {
- var visiting = new Stack();
- var visited = new HashSet();
-
- 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;
}
}
-
}
}
diff --git a/src/Umbraco.ModelsBuilder/TypeExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs
similarity index 96%
rename from src/Umbraco.ModelsBuilder/TypeExtensions.cs
rename to src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs
index d3b3ff6b4e..1f270a80a6 100644
--- a/src/Umbraco.ModelsBuilder/TypeExtensions.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs
@@ -1,6 +1,6 @@
using System;
-namespace Umbraco.ModelsBuilder
+namespace Umbraco.ModelsBuilder.Embedded
{
internal static class TypeExtensions
{
diff --git a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj
similarity index 57%
rename from src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj
rename to src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj
index 60ef944a8c..75121a635d 100644
--- a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj
+++ b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj
@@ -4,13 +4,15 @@
DebugAnyCPU
- {7020A059-C0D1-43A0-8EFD-23591A0C9AF6}
+ {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}LibraryProperties
- Umbraco.ModelsBuilder
- Umbraco.ModelsBuilder
+ Umbraco.ModelsBuilder.Embedded
+ Umbraco.ModelsBuilder.Embeddedv4.7.2512
+ true
+ 7.3true
@@ -28,13 +30,14 @@
TRACEprompt4
- bin\Release\Umbraco.ModelsBuilder.xml
+ bin\Release\Umbraco.ModelsBuilder.Embedded.xml
+
+
-
@@ -46,62 +49,48 @@
Properties\SolutionInfo.cs
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- 2.8.0
+ 2.10.01.0.0-beta2-19324-01
@@ -111,7 +100,7 @@
- {31785bc3-256c-4613-b2f5-a1b0bdded8c1}
+ {31785BC3-256C-4613-B2F5-A1B0BDDED8C1}Umbraco.Core
@@ -119,5 +108,11 @@
Umbraco.Web
+
+
+ 5.2.7
+
+
+
\ No newline at end of file
diff --git a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs b/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs
similarity index 68%
rename from src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs
rename to src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs
index f0347d9194..5ede5f45e9 100644
--- a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs
@@ -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();
+ // 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().ToArray()));
types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray()));
types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().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 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 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;
}
diff --git a/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs b/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs
deleted file mode 100644
index cc862ff207..0000000000
--- a/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Umbraco.ModelsBuilder/Api/ApiClient.cs b/src/Umbraco.ModelsBuilder/Api/ApiClient.cs
deleted file mode 100644
index dde3641b97..0000000000
--- a/src/Umbraco.ModelsBuilder/Api/ApiClient.cs
+++ /dev/null
@@ -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 GetModels(Dictionary 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>(_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("grant_type", "password"),
- new KeyValuePair("userName", _user),
- new KeyValuePair("password", _password),
- });
-
- var result = client.PostAsync(_url + UmbracoOAuthTokenUrl, formData).Result;
-
- EnsureSuccess(result);
-
- var token = result.Content.ReadAsAsync(_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", "%");
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs b/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs
deleted file mode 100644
index fa6492fe3f..0000000000
--- a/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs
+++ /dev/null
@@ -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 GetModels(UmbracoServices umbracoServices, string modelsNamespace, IDictionary files)
- {
- var typeModels = umbracoServices.GetAllTypes();
-
- var parseResult = new CodeParser().ParseWithReferencedAssemblies(files);
- var builder = new TextBuilder(typeModels, parseResult, modelsNamespace);
-
- var models = new Dictionary();
- foreach (var typeModel in builder.GetModelsToGenerate())
- {
- var sb = new StringBuilder();
- builder.Generate(sb, typeModel);
- models[typeModel.ClrName] = sb.ToString();
- }
- return models;
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs b/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs
deleted file mode 100644
index 2ee64b8c54..0000000000
--- a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using System;
-using System.Reflection;
-
-namespace Umbraco.ModelsBuilder.Api
-{
- ///
- /// Manages API version handshake between client and server.
- ///
- 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
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The currently executing version.
- /// The min client version supported by the server.
- /// An opt min server version supporting the client.
- 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;
- }
-
- ///
- /// Gets the currently executing API version.
- ///
- public static ApiVersion Current { get; }
- = new ApiVersion(Assembly.GetExecutingAssembly().GetName().Version,
- MinClientVersionSupportedByServerConst, MinServerVersionSupportingClientConst);
-
- ///
- /// Gets the executing version of the API.
- ///
- public Version Version { get; }
-
- ///
- /// Gets the min client version supported by the server.
- ///
- public Version MinClientVersionSupportedByServer { get; }
-
- ///
- /// Gets the min server version supporting the client.
- ///
- public Version MinServerVersionSupportingClient { get; }
-
- ///
- /// Gets a value indicating whether the API server is compatible with a client.
- ///
- /// The client version.
- /// An opt min server version supporting the client.
- ///
- /// 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 .
- ///
- 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;
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs b/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs
deleted file mode 100644
index 9a5c55afc2..0000000000
--- a/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs
+++ /dev/null
@@ -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 Files { get; set; }
-
- public override bool IsValid => base.IsValid && Files != null;
- }
-}
\ No newline at end of file
diff --git a/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs b/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs
deleted file mode 100644
index 444910b069..0000000000
--- a/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs
+++ /dev/null
@@ -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 CheckVersion(Version clientVersion, Version minServerVersionSupportingClient)
- {
- if (clientVersion == null)
- return Attempt.Fail(Request.CreateResponse(HttpStatusCode.Forbidden,
- $"API version conflict: client version () 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.If(isOk, response);
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Api/TokenData.cs b/src/Umbraco.ModelsBuilder/Api/TokenData.cs
deleted file mode 100644
index c34a6c75c5..0000000000
--- a/src/Umbraco.ModelsBuilder/Api/TokenData.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs b/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs
deleted file mode 100644
index 39ef08d816..0000000000
--- a/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs
+++ /dev/null
@@ -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;
- }
-}
\ No newline at end of file
diff --git a/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs b/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs
deleted file mode 100644
index 925337bd1e..0000000000
--- a/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs
+++ /dev/null
@@ -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...
-
- ///
- /// Implements a builder that works by using CodeDom
- ///
- internal class CodeDomBuilder : Builder
- {
- ///
- /// Initializes a new instance of the class with a list of models to generate.
- ///
- /// The list of models to generate.
- public CodeDomBuilder(IList typeModels)
- : base(typeModels, null)
- { }
-
- ///
- /// Outputs a generated model to a code namespace.
- ///
- /// The code namespace.
- /// The model to generate.
- 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[] //
- {
- new CodeTypeReference(propertyModel.ModelClrType)
- }),
- new CodeExpression[] // ("alias")
- {
- new CodePrimitiveExpression(propertyModel.Alias)
- })));
- c.Members.Add(p);
- }
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Building/CodeParser.cs b/src/Umbraco.ModelsBuilder/Building/CodeParser.cs
deleted file mode 100644
index 30fcbf1f91..0000000000
--- a/src/Umbraco.ModelsBuilder/Building/CodeParser.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Implements code parsing.
- ///
- /// Parses user's code and look for generator's instructions.
- internal class CodeParser
- {
- ///
- /// Parses a set of file.
- ///
- /// A set of (filename,content) representing content to parse.
- /// The result of the code parsing.
- /// The set of files is a dictionary of name, content.
- public ParseResult Parse(IDictionary files)
- {
- return Parse(files, Enumerable.Empty());
- }
-
- ///
- /// Parses a set of file.
- ///
- /// A set of (filename,content) representing content to parse.
- /// Assemblies to reference in compilations.
- /// The result of the code parsing.
- /// The set of files is a dictionary of name, content.
- public ParseResult Parse(IDictionary files, IEnumerable 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 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();
- 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();
- 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);
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Building/Compiler.cs b/src/Umbraco.ModelsBuilder/Building/Compiler.cs
deleted file mode 100644
index 66064bef0b..0000000000
--- a/src/Umbraco.ModelsBuilder/Building/Compiler.cs
+++ /dev/null
@@ -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 References { get; set; }
-
- public bool Debug { get; set; }
-
- // gets a compilation
- public CSharpCompilation GetCompilation(string assemblyName, IDictionary 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 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 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 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 { { path, code } }, stream);
- return Assembly.Load(stream.GetBuffer());
- }
- }
-
- // compiles files into a stream
- public void Compile(string assemblyName, IDictionary 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 { { path, code } }, stream);
- }
-
- private static void ThrowExceptionFromDiagnostic(IDictionary 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);
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Building/CompilerException.cs b/src/Umbraco.ModelsBuilder/Building/CompilerException.cs
deleted file mode 100644
index e978f67ae5..0000000000
--- a/src/Umbraco.ModelsBuilder/Building/CompilerException.cs
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/Umbraco.ModelsBuilder/Building/ParseResult.cs b/src/Umbraco.ModelsBuilder/Building/ParseResult.cs
deleted file mode 100644
index d1f61363ff..0000000000
--- a/src/Umbraco.ModelsBuilder/Building/ParseResult.cs
+++ /dev/null
@@ -1,275 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Umbraco.ModelsBuilder.Building
-{
- ///
- /// Contains the result of a code parsing.
- ///
- 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 _ignoredContent
- = new HashSet(StringComparer.InvariantCultureIgnoreCase);
- //private readonly HashSet _ignoredMixin
- // = new HashSet(StringComparer.InvariantCultureIgnoreCase);
- //private readonly HashSet _ignoredMixinProperties
- // = new HashSet(StringComparer.InvariantCultureIgnoreCase);
- private readonly Dictionary _renamedContent
- = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
- private readonly HashSet _withImplementContent
- = new HashSet(StringComparer.InvariantCultureIgnoreCase);
- private readonly Dictionary> _ignoredProperty
- = new Dictionary>();
- private readonly Dictionary> _renamedProperty
- = new Dictionary>();
- private readonly Dictionary _contentBase
- = new Dictionary();
- private readonly Dictionary _contentInterfaces
- = new Dictionary();
- private readonly List _usingNamespaces
- = new List();
- private readonly Dictionary> _staticMixins
- = new Dictionary>();
- private readonly HashSet _withCtor
- = new HashSet(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 ignores;
- if (!_ignoredProperty.TryGetValue(contentName, out ignores))
- ignores = _ignoredProperty[contentName] = new HashSet(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 renames;
- if (!_renamedProperty.TryGetValue(contentName, out renames))
- renames = _renamedProperty[contentName] = new Dictionary(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 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();
-
- _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 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 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 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 UsingNamespaces
- {
- get { return _usingNamespaces; }
- }
-
- public IEnumerable StaticMixinMethods(string contentName)
- {
- return _staticMixins.ContainsKey(contentName)
- ? _staticMixins[contentName].Select(x => x.MethodName)
- : Enumerable.Empty() ;
- }
-
- public bool HasCtor(string contentName)
- {
- return _withCtor.Contains(contentName);
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs b/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs
deleted file mode 100644
index d195846411..0000000000
--- a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace Umbraco.ModelsBuilder.Configuration
-{
- ///