37 | Last generation failed with the following error:
38 |
{{vm.dashboard.lastError}}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Options/ModelsMode.cs:
--------------------------------------------------------------------------------
1 | namespace Our.ModelsBuilder.Options
2 | {
3 | ///
4 | /// Defines the models generation modes.
5 | ///
6 | public enum ModelsMode
7 | {
8 | ///
9 | /// Do not generate models.
10 | ///
11 | Nothing = 0, // default value
12 |
13 | ///
14 | /// Generate models in memory.
15 | /// When: a content type change occurs.
16 | ///
17 | /// The app does not restart. Models are available in views exclusively.
18 | PureLive,
19 |
20 | ///
21 | /// Generate models in AppData.
22 | /// When: generation is triggered.
23 | ///
24 | /// Generation can be triggered from the dashboard. The app does not restart.
25 | /// Models are not compiled and thus are not available to the project.
26 | AppData,
27 |
28 | ///
29 | /// Generate models in AppData.
30 | /// When: a content type change occurs, or generation is triggered.
31 | ///
32 | /// Generation can be triggered from the dashboard. The app does not restart.
33 | /// Models are not compiled and thus are not available to the project.
34 | LiveAppData,
35 |
36 | ///
37 | /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts).
38 | /// When: generation is triggered.
39 | ///
40 | /// Generation can be triggered from the dashboard. The app does restart. Models
41 | /// are available to the entire project.
42 | Dll,
43 |
44 | ///
45 | /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts).
46 | /// When: a content type change occurs, or generation is triggered.
47 | ///
48 | /// Generation can be triggered from the dashboard. The app does restart. Models
49 | /// are available to the entire project.
50 | LiveDll
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Web/Api/ValidateClientVersionData.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 | using Semver;
4 |
5 | namespace Our.ModelsBuilder.Web.Api
6 | {
7 | [DataContract]
8 | public class ValidateClientVersionData
9 | {
10 | // issues 32, 34... problems when serializing versions
11 | //
12 | // make sure System.Version objects are transfered as strings
13 | // depending on the JSON serializer version, it looks like versions are causing issues
14 | // see
15 | // http://stackoverflow.com/questions/13170386/why-system-version-in-json-string-does-not-deserialize-correctly
16 | //
17 | // if the class is marked with [DataContract] then only properties marked with [DataMember]
18 | // are serialized and the rest is ignored, see
19 | // http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization
20 |
21 | [DataMember]
22 | public string ClientVersionString
23 | {
24 | get => VersionToString(ClientVersion);
25 | set => ClientVersion = ParseVersion(value, false, "client");
26 | }
27 |
28 | [DataMember]
29 | public string MinServerVersionSupportingClientString
30 | {
31 | get => VersionToString(MinServerVersionSupportingClient);
32 | set => MinServerVersionSupportingClient = ParseVersion(value, true, "minServer");
33 | }
34 |
35 | // not serialized
36 | public SemVersion ClientVersion { get; set; }
37 | public SemVersion MinServerVersionSupportingClient { get; set; }
38 |
39 | private static string VersionToString(SemVersion version)
40 | {
41 | return version?.ToString() ?? "0.0.0.0";
42 | }
43 |
44 | private static SemVersion ParseVersion(string value, bool canBeNull, string name)
45 | {
46 | if (string.IsNullOrWhiteSpace(value) && canBeNull)
47 | return null;
48 |
49 | if (SemVersion.TryParse(value, out var version))
50 | return version;
51 |
52 | throw new ArgumentException($"Failed to parse \"{value}\" as {name} version.");
53 | }
54 |
55 | public virtual bool IsValid => ClientVersion != null;
56 | }
57 | }
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Umbraco/ModelsBuilderComposer.cs:
--------------------------------------------------------------------------------
1 | using Our.ModelsBuilder.Building;
2 | using Our.ModelsBuilder.Options;
3 | using Umbraco.Core;
4 | using Umbraco.Core.Composing;
5 | using Umbraco.Core.Models.PublishedContent;
6 | using Umbraco.Web.PublishedCache.NuCache;
7 |
8 | namespace Our.ModelsBuilder.Umbraco
9 | {
10 | [ComposeBefore(typeof(NuCacheComposer))]
11 | [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
12 | public sealed class ModelsBuilderComposer : ComponentComposer, ICoreComposer
13 | {
14 | public override void Compose(Composition composition)
15 | {
16 | base.Compose(composition);
17 |
18 | // compose umbraco & the code factory
19 | composition.Register(Lifetime.Singleton);
20 | composition.RegisterUnique();
21 |
22 | // compose configuration of options
23 | composition.Configs.Add(() => new OptionsConfiguration());
24 | composition.ConfigureOptions(OptionsWebConfigReader.ConfigureOptions);
25 | composition.Register(factory => factory.GetInstance().ModelsBuilderOptions, Lifetime.Singleton);
26 |
27 | // always discover model types in code
28 | // could be used with pure live, to provide some models,
29 | // and then pure live would not generate them
30 | composition.WithCollectionBuilder()
31 | .Add(composition.TypeLoader.GetTypes())
32 | .Add(composition.TypeLoader.GetTypes());
33 |
34 | // create the appropriate factory, depending on options
35 | composition.RegisterUnique(factory =>
36 | {
37 | var options = factory.GetInstance();
38 |
39 | if (options.ModelsMode == ModelsMode.PureLive)
40 | return factory.CreateInstance();
41 |
42 | if (options.EnableFactory)
43 | return new PublishedModelFactory(factory.GetInstance());
44 |
45 | return new NoopPublishedModelFactory();
46 | });
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Options/CodeOptionsBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Our.ModelsBuilder.Options.ContentTypes;
3 |
4 | namespace Our.ModelsBuilder.Options
5 | {
6 | ///
7 | /// Builds the .
8 | ///
9 | public class CodeOptionsBuilder
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | public CodeOptionsBuilder()
15 | {
16 | var contentTypesCodeOptions = new ContentTypesCodeOptions();
17 | CodeOptions = new CodeOptions(contentTypesCodeOptions);
18 | ContentTypes = new ContentTypesCodeOptionsBuilder(contentTypesCodeOptions);
19 | }
20 |
21 | ///
22 | /// Initializes a new instance of the class.
23 | ///
24 | protected CodeOptionsBuilder(CodeOptions codeOptions, ContentTypesCodeOptionsBuilder contentTypesCodeOptionsBuilder)
25 | {
26 | CodeOptions = codeOptions ?? throw new ArgumentNullException(nameof(codeOptions));
27 | ContentTypes = contentTypesCodeOptionsBuilder ?? throw new ArgumentNullException(nameof(contentTypesCodeOptionsBuilder));
28 | }
29 |
30 | ///
31 | /// Gets the options builder for content types.
32 | ///
33 | public virtual ContentTypesCodeOptionsBuilder ContentTypes { get; }
34 |
35 | ///
36 | /// Gets the options.
37 | ///
38 | public virtual CodeOptions CodeOptions { get; }
39 |
40 | ///
41 | /// Sets the models namespace.
42 | ///
43 | /// The models namespace.
44 | public virtual void SetModelsNamespace(string modelsNamespace)
45 | {
46 | CodeOptions.ModelsNamespace = modelsNamespace;
47 | }
48 | ///
49 | /// Sets the assembly name.
50 | ///
51 | /// The models namespace.
52 | public virtual void SetAssemblyName(string assemblyName)
53 | {
54 | CodeOptions.CustomAssemblyName = assemblyName;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | #### Umbraco Models Builder
2 |
3 | Copyright (C) The Umbraco Community 2013-2020
4 | Distributed under the MIT license
5 |
6 | A tool that can generate a complete set of strongly-typed published content models for Umbraco.
7 | Models are available in controllers, views, anywhere.
8 | Runs either from the Umbraco UI, from the command line, or from Visual Studio.
9 |
10 | Requires Umbraco 7.1.4 or later.
11 |
12 | #### Documentation
13 |
14 | More infos, including a (hopefully) **complete documentation**, can be found in the [wiki](https://github.com/OurModelsBuilder/Our.ModelsBuilder/wiki).
15 |
16 | **WARNING** the documentation has not been updated for version 3 (Umbraco.ModelsBuilder) yet.
17 |
18 | #### Building
19 |
20 | Simply building the solution (in Visual Studio) either in Debug or Release does NOT build
21 | any NuGet package. Building in Debug mode does NOT build the VSIX package, but building in
22 | Release mode DOES build the VSIX package.
23 |
24 | **Important** before releasing a new version, ensure that Umbraco.ModelsBuilder.Api.ApiVersion
25 | contains the proper constants for API client/server version check.
26 |
27 | In order to build the NuGet package and the VSIX package,
28 | use the build.ps1 Powershell script:
29 |
30 | To build version 1.2.3.45 (aka release 1.2.3)
31 | build.ps1 1.2.3 45
32 |
33 | To build version 1.2.3.45 beta001 (aka pre-release 1.2.3-beta001)
34 | build.ps1 1.2.3 45 beta001
35 |
36 | The "45" number should be incremented each time we release, so that
37 | version 1.2.3-beta001 has assemblies with version 1.2.3.45
38 | version 1.2.3-beta002 has assemblies with version 1.2.3.46
39 | version 1.2.3 (final) has assemblies with version 1.2.3.47
40 |
41 | This will create directory build/Release/v1.2.3-whatever containing:
42 | - Umbraco.ModelsBuilder.1.2.3-whatever.nuget = main NuGet package
43 | - Umbraco.ModelsBuilder.Api.1.2.3-whatever.nuget = api server NuGet package
44 | - Umbraco.ModelsBuilder.CustomTool-1.2.3-whatever.vsix = Visual Studio Extension
45 |
46 | Note: we are not building an Umbraco package anymore.
47 |
48 | #### Projects
49 |
50 | *Umbraco.ModelsBuilder - the main project, installed on the website
51 | *Umbraco.ModelsBuilder.Api - the api server
52 | *Umbraco.ModelsBuilder.Console - a console tool
53 | *Umbraco.ModelsBuilder.CustomTool - the Visual Studio extension
54 | *Umbraco.ModelsBuilder.Tests - the tests suite
55 |
56 | Both .Console and .CustomTool require that the .Api is installed on the website (not installed by default).
57 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/Write/WriteEdgeCasesTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using NUnit.Framework;
3 | using Our.ModelsBuilder.Building;
4 | using Our.ModelsBuilder.Options;
5 | using Our.ModelsBuilder.Tests.Testing;
6 | using Umbraco.Core.Models.PublishedContent;
7 |
8 | namespace Our.ModelsBuilder.Tests.Write
9 | {
10 | [TestFixture]
11 | public class WriteEdgeCasesTests : TestsBase
12 | {
13 | [Test]
14 | public void WriteAmbiguousTypes()
15 | {
16 | var modelSource = new CodeModelData();
17 |
18 | var type1 = new ContentTypeModel
19 | {
20 | Id = 1,
21 | Alias = "type1",
22 | ParentId = 0,
23 | BaseContentType = null,
24 | Kind = ContentTypeKind.Content,
25 | IsMixin = true,
26 | };
27 | modelSource.ContentTypes.Add(type1);
28 | type1.Properties.Add(new PropertyTypeModel
29 | {
30 | Alias = "prop1",
31 | ContentType = type1,
32 | ValueType = typeof(IPublishedContent),
33 | });
34 | type1.Properties.Add(new PropertyTypeModel
35 | {
36 | Alias = "prop2",
37 | ContentType = type1,
38 | ValueType = typeof(global::System.Text.StringBuilder),
39 | });
40 | type1.Properties.Add(new PropertyTypeModel
41 | {
42 | Alias = "prop3",
43 | ContentType = type1,
44 | ValueType = typeof(global::Umbraco.Core.IO.FileSecurityException),
45 | });
46 |
47 | var codeOptionsBuilder = new CodeOptionsBuilder();
48 |
49 | // forces conflict with Our.ModelsBuilder.Umbraco
50 | codeOptionsBuilder.SetModelsNamespace("Our.ModelsBuilder.Models");
51 |
52 | var modelBuilder = new CodeModelBuilder(new ModelsBuilderOptions(), codeOptionsBuilder.CodeOptions);
53 | var model = modelBuilder.Build(modelSource);
54 |
55 | var writer = new CodeWriter(model).ContentTypesCodeWriter;
56 |
57 | foreach (var typeModel in model.ContentTypes.ContentTypes)
58 | writer.WriteModel(typeModel);
59 | var generated = writer.Code;
60 |
61 | Console.WriteLine(generated);
62 |
63 | Assert.IsTrue(generated.Contains(" IPublishedContent Prop1"));
64 | Assert.IsTrue(generated.Contains(" System.Text.StringBuilder Prop2"));
65 | Assert.IsTrue(generated.Contains(" global::Umbraco.Core.IO.FileSecurityException Prop3"));
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/Compile/CompilerTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using Microsoft.CodeAnalysis.CSharp;
5 | using NUnit.Framework;
6 | using Our.ModelsBuilder.Building;
7 |
8 | namespace Our.ModelsBuilder.Tests.Compile
9 | {
10 | [TestFixture]
11 | public class CompilerTests
12 | {
13 | private string _tempDir;
14 |
15 | [SetUp]
16 | public void Setup()
17 | {
18 | _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
19 | Directory.CreateDirectory(_tempDir);
20 | }
21 |
22 | [TearDown]
23 | public void TearDown()
24 | {
25 | if (_tempDir != null && Directory.Exists(_tempDir))
26 | Directory.Delete(_tempDir, true);
27 | }
28 |
29 | [Test]
30 | public void Compile()
31 | {
32 | const string code1 = @"
33 | using System;
34 | namespace Whatever
35 | {
36 | public class Something
37 | {
38 | public void DoSomething()
39 | {
40 | Console.WriteLine(""Hello!"");
41 | }
42 | }
43 | }
44 | ";
45 |
46 | var filepath = Path.Combine(_tempDir, "Whatever.dll");
47 | if (File.Exists(filepath))
48 | File.Delete(filepath);
49 |
50 | var compiler = new Compiler();
51 | compiler.Compile("Whatever", new Dictionary{{"code", code1}}, _tempDir);
52 | Assert.IsTrue(File.Exists(filepath));
53 |
54 | File.Delete(filepath);
55 | }
56 |
57 | [Test]
58 | public void CompileCSharp6()
59 | {
60 | // see https://roslyn.codeplex.com/wikipage?title=Language%20Feature%20Status&referringTitle=Documentation
61 |
62 | const string code1 = @"
63 | using System;
64 | namespace Whatever
65 | {
66 | public class Something
67 | {
68 | // auto-property initializer
69 | public int Value { get; set; } = 3;
70 |
71 | public void DoSomething()
72 | {
73 | Console.WriteLine(""Hello!"");
74 | }
75 |
76 | public void DoSomething(string[] args)
77 | {
78 | // conditional access
79 | var len = args?.Length ?? 0;
80 | }
81 | }
82 | }
83 | ";
84 |
85 | var filepath = Path.Combine(_tempDir, "Whatever.dll");
86 | if (File.Exists(filepath))
87 | File.Delete(filepath);
88 |
89 | var compiler = new Compiler(LanguageVersion.CSharp6);
90 | compiler.Compile("Whatever", new Dictionary { { "code", code1 } }, _tempDir);
91 | Assert.IsTrue(File.Exists(filepath));
92 |
93 | File.Delete(filepath);
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Console/Our.ModelsBuilder.Console.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | v4.7.2
6 | {F0B757DF-0CF7-4AAF-8DD0-F46EDF25C3A3}
7 | Exe
8 | Our.ModelsBuilder.Console
9 | Our.ModelsBuilder.Console
10 | ..\
11 |
12 |
13 |
14 | AnyCPU
15 | true
16 | full
17 | false
18 | bin\Debug\
19 | DEBUG;TRACE
20 | prompt
21 | 4
22 |
23 |
24 | AnyCPU
25 | pdbonly
26 | true
27 | bin\Release\
28 | TRACE
29 | prompt
30 | 4
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Properties\SolutionInfo.cs
39 |
40 |
41 |
42 |
43 |
44 |
45 | Designer
46 |
47 |
48 |
49 |
50 | {2317be7f-1723-4512-b863-5b6835e583a2}
51 | Our.ModelsBuilder.Web
52 |
53 |
54 | {998fb014-303a-4146-b3e4-b927bab0210f}
55 | Our.ModelsBuilder
56 |
57 |
58 |
59 |
60 | 2.0.5
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/ApiTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using NUnit.Framework;
5 | using Our.ModelsBuilder.Web.Api;
6 |
7 | namespace Our.ModelsBuilder.Tests
8 | {
9 | [TestFixture]
10 | public class ApiTests
11 | {
12 | //[Test]
13 | //[Ignore("That API has been disabled.")]
14 | //public void GetTypeModels()
15 | //{
16 | // // note - works only if the website does not reference types that are not
17 | // // referenced by the current test project!
18 | // var api = new ModelsBuilderApi("http://umbraco.local", "user", "password");
19 | // var res = api.GetTypeModels();
20 | //}
21 |
22 | [Test]
23 | [Ignore("Requires a proper endpoint.")]
24 | public void GetModels()
25 | {
26 | const string text1 = @"
27 | using Our.ModelsBuilder;
28 |
29 | namespace Umbraco.Demo3.Core.Models
30 | {
31 | //[RenamePropertyType(""issued"", ""DateIssued"")]
32 | public partial class NewsItem
33 | {
34 | }
35 | }
36 | ";
37 | const string text2 = @"
38 | using Our.ModelsBuilder;
39 |
40 | //[assembly:IgnoreContentType(""product"")]
41 | ";
42 |
43 | var api = new ApiClient("http://umbraco.local", "user", "password");
44 | var ourFiles = new Dictionary
45 | {
46 | {"file1", text1},
47 | {"file2", text2},
48 | };
49 | var res = api.GetModels(ourFiles, "Our.ModelsBuilder.Tests.Models");
50 |
51 | foreach (var kvp in res)
52 | {
53 | Console.WriteLine("****");
54 | Console.WriteLine(kvp.Key);
55 | Console.WriteLine("----");
56 | Console.WriteLine(kvp.Value);
57 | }
58 | }
59 |
60 | [TestCase("a", "b")]
61 | [TestCase("a:b", "c:d")]
62 | [TestCase("%xx%a%b:c:d:e", "x:y%z%b")]
63 | public void TokenTests(string username, string password)
64 | {
65 | var separator = ":".ToCharArray();
66 |
67 | // ApiClient code
68 | var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(ApiClient.EncodeTokenElement(username) + ':' + ApiClient.EncodeTokenElement(password)));
69 |
70 | // ApiBasicAuthFilter code
71 | var credentials = Encoding.ASCII
72 | .GetString(Convert.FromBase64String(token))
73 | .Split(separator);
74 | if (credentials.Length != 2)
75 | throw new Exception();
76 |
77 | var username2 = ApiClient.DecodeTokenElement(credentials[0]);
78 | var password2 = ApiClient.DecodeTokenElement(credentials[1]);
79 |
80 | Assert.AreEqual(username, username2);
81 | Assert.AreEqual(password, password2);
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Web/WebComponent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Web;
4 | using System.Web.Mvc;
5 | using System.Web.Routing;
6 | using Our.ModelsBuilder.Options;
7 | using Our.ModelsBuilder.Web.Api;
8 | using Our.ModelsBuilder.Web.Umbraco;
9 | using Umbraco.Core.Composing;
10 | using Umbraco.Core.Configuration;
11 | using Umbraco.Web;
12 | using Umbraco.Web.JavaScript;
13 |
14 | namespace Our.ModelsBuilder.Web
15 | {
16 | public class WebComponent : IComponent
17 | {
18 | private readonly IGlobalSettings _globalSettings;
19 | private readonly ModelsBuilderOptions _options;
20 |
21 | public WebComponent(IGlobalSettings globalSettings, ModelsBuilderOptions options)
22 | {
23 | _globalSettings = globalSettings;
24 | _options = options;
25 | }
26 |
27 | public void Initialize()
28 | {
29 | InstallServerVars();
30 |
31 | if (_options.IsApiServer)
32 | {
33 | ModelsBuilderApiController.Route(_globalSettings.GetUmbracoMvcArea());
34 | }
35 | }
36 |
37 | public void Terminate()
38 | { }
39 |
40 | private void InstallServerVars()
41 | {
42 | // register our url - for the back-office api
43 | ServerVariablesParser.Parsing += (sender, serverVars) =>
44 | {
45 | if (!serverVars.ContainsKey("umbracoUrls"))
46 | throw new Exception("Missing umbracoUrls.");
47 | var umbracoUrlsObject = serverVars["umbracoUrls"];
48 | if (umbracoUrlsObject == null)
49 | throw new Exception("Null umbracoUrls");
50 | if (!(umbracoUrlsObject is Dictionary umbracoUrls))
51 | throw new Exception("Invalid umbracoUrls");
52 |
53 | if (!serverVars.ContainsKey("umbracoPlugins"))
54 | throw new Exception("Missing umbracoPlugins.");
55 | if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins))
56 | throw new Exception("Invalid umbracoPlugins");
57 |
58 | if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null");
59 | var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()));
60 |
61 | umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels());
62 | umbracoPlugins["modelsBuilder"] = new Dictionary
63 | {
64 | {"enabled", _options.Enable}
65 | };
66 |
67 | // see modelsbuilder.resource.js
68 | // see Core's contenttypehelper.service.js service
69 | // also register the plugin as 'modelsBuilder' so the Core UI can see it,
70 | // and enhance 'Save' buttons with 'Save and Generate Models'
71 | umbracoPlugins["modelsBuilder"] = umbracoPlugins["modelsBuilder"]; // FIXME uh???
72 | };
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Web/Our.ModelsBuilder.Web.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | v4.7.2
6 | {2317BE7F-1723-4512-B863-5B6835E583A2}
7 | Library
8 | Our.ModelsBuilder.Web
9 | Our.ModelsBuilder.Web
10 |
11 |
12 |
13 | true
14 | full
15 | false
16 | bin\Debug\
17 | DEBUG;TRACE
18 | prompt
19 | 4
20 |
21 |
22 | pdbonly
23 | true
24 | bin\Release\
25 | TRACE
26 | prompt
27 | 4
28 |
29 |
30 |
31 | Properties\SolutionInfo.cs
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {998fb014-303a-4146-b3e4-b927bab0210f}
50 | Our.ModelsBuilder
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 8.3.0-alpha.1
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Building/PropertyTypeModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Umbraco.Core.Models;
4 | using Umbraco.Core.Models.PublishedContent;
5 |
6 | namespace Our.ModelsBuilder.Building
7 | {
8 | ///
9 | /// Represents a model property.
10 | ///
11 | public class PropertyTypeModel
12 | {
13 | #region Things that come from Umbraco
14 |
15 | ///
16 | /// Gets or sets the content type owning the property.
17 | ///
18 | public ContentTypeModel ContentType { get; set; }
19 |
20 | ///
21 | /// Gets or sets the alias of the property.
22 | ///
23 | public string Alias { get; set; }
24 |
25 | ///
26 | /// Gets or sets the name of the property.
27 | ///
28 | public string Name { get; set; }
29 |
30 | ///
31 | /// Gets or sets the description of the property.
32 | ///
33 | public string Description { get; set; }
34 |
35 | ///
36 | /// Gets or sets the content variation of the property.
37 | ///
38 | public ContentVariation Variations { get; set; } = ContentVariation.Nothing;
39 |
40 | ///
41 | /// Gets or sets the model Clr type of the property values (may be a true type, or a ).
42 | ///
43 | ///
44 | /// As indicated by the IPublishedPropertyType, ie by the IPropertyValueConverter
45 | /// if any, else object. May include some that will need to be mapped.
46 | /// The property contains the mapped name of the Clr type of values.
47 | ///
48 | public Type ValueType { get; set; }
49 |
50 | #endregion
51 |
52 | #region Things managed by ModelsBuilder
53 |
54 | ///
55 | /// Gets or sets a value indicating whether this property should be excluded from generation.
56 | ///
57 | public bool IsIgnored { get; set; }
58 |
59 | ///
60 | /// Gets or sets the Clr name of the property.
61 | ///
62 | /// This is the local name eg "Price".
63 | public string ClrName { get; set; }
64 |
65 | ///
66 | /// Gets the Clr type name of the property values.
67 | ///
68 | /// This is the full name eg "System.DateTime".
69 | public string ValueTypeClrFullName;
70 |
71 | ///
72 | /// Gets or sets the list of generation errors for the property.
73 | ///
74 | /// This should be null, unless something prevents the property from being
75 | /// generated, and then the value should explain what. This can be used to generate
76 | /// commented out code eg in PureLive.
77 | public List Errors { get; set; }
78 |
79 | ///
80 | /// Adds an error.
81 | ///
82 | public void AddError(string error)
83 | {
84 | if (Errors == null) Errors = new List();
85 | Errors.Add(error);
86 | }
87 |
88 | #endregion
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/UmbracoExtensions/FallbackValue.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Umbraco.Web;
3 |
4 | // ReSharper disable once CheckNamespace, reason: extensions
5 | namespace Umbraco.Core.Models.PublishedContent
6 | {
7 | public class FallbackValue
8 | where TModel : IPublishedElement
9 | {
10 | private readonly FallbackInfos _fallbackInfos;
11 | private readonly List _fallbacks = new List();
12 | private TValue _defaultValue;
13 |
14 | public FallbackValue(FallbackInfos fallbackInfos)
15 | {
16 | _fallbackInfos = fallbackInfos;
17 | }
18 |
19 | public static implicit operator TValue(FallbackValue fallbackValue)
20 | {
21 | var fallbackInfos = fallbackValue._fallbackInfos;
22 | var property = fallbackInfos.Model.GetProperty(fallbackInfos.PropertyAlias);
23 |
24 | // no need to test for a value on property, if we are running this fallback
25 | // code, we know that it is because there isn't a value - can directly try
26 | // the fallback provider - which can be invoked in two different ways,
27 | // depending on whether we are working on an IPublishedContent or an
28 | // IPublishedElement (which is the reason why there are two Value<>() overloads,
29 | // one for IPublishedContent and one for IPublishedElement)
30 |
31 | var fallback = fallbackInfos.PublishedValueFallback;
32 | var success = fallbackInfos.Model is IPublishedContent publishedContent
33 | ? fallback.TryGetValue(publishedContent, fallbackInfos.PropertyAlias, fallbackInfos.Culture, fallbackInfos.Segment, Fallback.To(fallbackValue._fallbacks.ToArray()), fallbackValue._defaultValue, out var value, out property)
34 | : fallback.TryGetValue(fallbackInfos.Model, fallbackInfos.PropertyAlias, fallbackInfos.Culture, fallbackInfos.Segment, Fallback.To(fallbackValue._fallbacks.ToArray()), fallbackValue._defaultValue, out value);
35 |
36 | if (success)
37 | return value;
38 |
39 | // we *have* to return something
40 | // see PublishedElementExtensions.Value method, there is no "try" here
41 | // so, repeat what that method would do if no fallback method was provided
42 |
43 | // if we have a property, at least let the converter return its own
44 | // vision of 'no value' (could be an empty enumerable) - otherwise, default
45 | return property == null ? default : property.Value(fallbackInfos.Culture, fallbackInfos.Segment);
46 | }
47 |
48 | public FallbackValue To(params int[] values)
49 | {
50 | _fallbacks.AddRange(values);
51 | return this;
52 | }
53 |
54 | public FallbackValue Ancestors()
55 | {
56 | _fallbacks.Add(Fallback.Ancestors);
57 | return this;
58 | }
59 |
60 | public FallbackValue Languages()
61 | {
62 | _fallbacks.Add(Fallback.Language);
63 | return this;
64 | }
65 |
66 | public FallbackValue Default(TValue defaultValue)
67 | {
68 | _defaultValue = defaultValue;
69 | _fallbacks.Add(Fallback.DefaultValue);
70 | return this;
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Validation/ContentTypeModelValidatorBase.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.ComponentModel.DataAnnotations;
3 | using System.Linq;
4 | using Our.ModelsBuilder.Options;
5 | using Umbraco.Core;
6 | using Umbraco.Core.Models.PublishedContent;
7 | using Umbraco.Web.Editors;
8 | using Umbraco.Web.Models.ContentEditing;
9 |
10 | namespace Our.ModelsBuilder.Validation
11 | {
12 | public abstract class ContentTypeModelValidatorBase : EditorValidator
13 | where TModel: ContentTypeSave
14 | where TProperty: PropertyTypeBasic
15 | {
16 | private readonly ModelsBuilderOptions _options;
17 |
18 | protected ContentTypeModelValidatorBase(ModelsBuilderOptions options)
19 | {
20 | _options = options;
21 | }
22 |
23 | protected override IEnumerable Validate(TModel model)
24 | {
25 | //don't do anything if we're not enabled
26 | if (_options.Enable)
27 | {
28 | var properties = model.Groups.SelectMany(x => x.Properties)
29 | .Where(x => x.Inherited == false)
30 | .ToArray();
31 |
32 | foreach (var prop in properties)
33 | {
34 | var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop));
35 |
36 | if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant())
37 | 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[]
38 | {
39 | $"Groups[{model.Groups.IndexOf(propertyGroup)}].Properties[{propertyGroup.Properties.IndexOf(prop)}].Alias"
40 | });
41 |
42 | //we need to return the field name with an index so it's wired up correctly
43 | var groupIndex = model.Groups.IndexOf(propertyGroup);
44 | var propertyIndex = propertyGroup.Properties.IndexOf(prop);
45 |
46 | var validationResult = ValidateProperty(prop, groupIndex, propertyIndex);
47 | if (validationResult != null)
48 | {
49 | yield return validationResult;
50 | }
51 | }
52 | }
53 | }
54 |
55 | private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex)
56 | {
57 | //don't let them match any properties or methods in IPublishedContent
58 | //TODO: There are probably more!
59 | var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray();
60 | var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray();
61 |
62 | var alias = property.Alias;
63 |
64 | if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias))
65 | {
66 | return new ValidationResult(
67 | $"The alias {alias} is a reserved term and cannot be used", new[]
68 | {
69 | $"Groups[{groupIndex}].Properties[{propertyIndex}].Alias"
70 | });
71 | }
72 |
73 | return null;
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/DotNet/ExpressionTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using System.Reflection;
4 | using NUnit.Framework;
5 |
6 | namespace Our.ModelsBuilder.Tests.DotNet
7 | {
8 | [TestFixture]
9 | public class ExpressionTests
10 | {
11 | [Test]
12 | public void Test()
13 | {
14 | var o = new ModelClass();
15 | var mi = SelectProperty(o, x => x.ValueInt);
16 | }
17 |
18 | private MemberInfo SelectProperty(TModel model, Expression> property)
19 | {
20 | if (property.NodeType != ExpressionType.Lambda)
21 | throw new Exception("not a lambda: " + property.NodeType);
22 |
23 | var lambda = (LambdaExpression) property;
24 | var lambdaBody = lambda.Body;
25 |
26 | if (lambdaBody.NodeType != ExpressionType.MemberAccess)
27 | throw new Exception("not a member access: " + lambdaBody.NodeType);
28 |
29 | var member = (MemberExpression) lambdaBody;
30 | if (member.Expression.NodeType != ExpressionType.Parameter)
31 | throw new Exception("not a parameter: " + member.Expression.NodeType);
32 |
33 | return member.Member;
34 | }
35 |
36 | public static MemberInfo FindProperty(LambdaExpression lambda)
37 | {
38 | void Throw()
39 | {
40 | throw new ArgumentException($"Expression '{lambda}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.", nameof(lambda));
41 | }
42 |
43 | Expression expr = lambda;
44 | var loop = true;
45 | while (loop)
46 | {
47 | switch (expr.NodeType)
48 | {
49 | case ExpressionType.Convert:
50 | expr = ((UnaryExpression) expr).Operand;
51 | break;
52 | case ExpressionType.Lambda:
53 | expr = ((LambdaExpression) expr).Body;
54 | break;
55 | //case ExpressionType.Call:
56 | // var callExpr = (MethodCallExpression) expr;
57 | // var method = callExpr.Method;
58 | // if (method.DeclaringType != typeof(NPocoSqlExtensions.Statics) || method.Name != "Alias" || !(callExpr.Arguments[1] is ConstantExpression aliasExpr))
59 | // Throw();
60 | // expr = callExpr.Arguments[0];
61 | // alias = aliasExpr.Value.ToString();
62 | // break;
63 | case ExpressionType.MemberAccess:
64 | var memberExpr = (MemberExpression) expr;
65 | if (memberExpr.Expression.NodeType != ExpressionType.Parameter && memberExpr.Expression.NodeType != ExpressionType.Convert)
66 | Throw();
67 | return memberExpr.Member;
68 | default:
69 | loop = false;
70 | break;
71 | }
72 | }
73 |
74 | throw new Exception("Configuration for members is only supported for top-level individual members on a type.");
75 | }
76 | private class ModelClass
77 | {
78 | public int ValueInt { get; set; }
79 | public string ValueString { get; set; }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Web/Api/ApiBasicAuthFilter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Net;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Web.Http.Controllers;
7 | using System.Web.Security;
8 | using Umbraco.Core;
9 | using Umbraco.Core.Composing;
10 | using Umbraco.Core.Models.Membership;
11 |
12 | namespace Our.ModelsBuilder.Web.Api
13 | {
14 |
15 | //TODO: This needs to be changed:
16 | // * Authentication cannot happen in a filter, only Authorization
17 | // * The filter must be an AuthorizationFilter, not an ActionFilter
18 | // * Authorization must be done using the Umbraco logic - it is very specific for claim checking for ASP.Net Identity
19 | // * Theoretically this shouldn't be required whatsoever because when we authenticate a request that has Basic Auth (i.e. for
20 | // VS to work, it will add the correct Claims to the Identity and it will automatically be authorized.
21 | //
22 | // we *do* have POC supporting ASP.NET identity, however they require some config on the server
23 | // we'll keep using this quick-and-dirty method for the time being
24 |
25 | public class ApiBasicAuthFilter : System.Web.Http.Filters.ActionFilterAttribute // use the http one, not mvc, with api controllers!
26 | {
27 | private static readonly char[] Separator = ":".ToCharArray();
28 | private readonly string _section;
29 |
30 | public ApiBasicAuthFilter(string section)
31 | {
32 | _section = section;
33 | }
34 |
35 | public override void OnActionExecuting(HttpActionContext actionContext)
36 | {
37 | try
38 | {
39 | var user = Authenticate(actionContext.Request);
40 | if (user == null || !user.AllowedSections.Contains(_section))
41 | {
42 | actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
43 | }
44 | //else
45 | //{
46 | // // note - would that be a proper way to pass data to the controller?
47 | // // see http://stevescodingblog.co.uk/basic-authentication-with-asp-net-webapi/
48 | // actionContext.ControllerContext.RouteData.Values["umbraco-user"] = user;
49 | //}
50 |
51 | base.OnActionExecuting(actionContext);
52 | }
53 | catch
54 | {
55 | actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
56 | }
57 | }
58 |
59 | private static IUser Authenticate(HttpRequestMessage request)
60 | {
61 | var ah = request.Headers.Authorization;
62 | if (ah == null || ah.Scheme != "Basic")
63 | return null;
64 |
65 | var token = ah.Parameter;
66 | var credentials = Encoding.ASCII
67 | .GetString(Convert.FromBase64String(token))
68 | .Split(Separator);
69 | if (credentials.Length != 2)
70 | return null;
71 |
72 | var username = ApiClient.DecodeTokenElement(credentials[0]);
73 | var password = ApiClient.DecodeTokenElement(credentials[1]);
74 |
75 | var provider = Membership.Providers[Constants.Security.UserMembershipProviderName];
76 | if (provider == null || !provider.ValidateUser(username, password))
77 | return null;
78 | var user = Current.Services.UserService.GetByUsername(username);
79 | if (!user.IsApproved || user.IsLockedOut)
80 | return null;
81 | return user;
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Extension/GeneratorWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Text;
4 | using System.Windows;
5 | using System.Windows.Controls;
6 | using System.Windows.Threading;
7 | using EnvDTE;
8 | using Microsoft.VisualStudio.PlatformUI;
9 | using Microsoft.VisualStudio.Shell;
10 | using Our.ModelsBuilder.Api;
11 |
12 | namespace Our.ModelsBuilder.Extension
13 | {
14 | ///
15 | /// Interaction logic for GeneratorWindow.xaml
16 | ///
17 | public partial class GeneratorWindow : DialogWindow
18 | {
19 | private readonly AsyncPackage _package;
20 | private readonly ProjectItem _sourceItem;
21 |
22 | private readonly StringBuilder _text = new StringBuilder();
23 | private int _progress;
24 |
25 | public GeneratorWindow(AsyncPackage package, ProjectItem sourceItem)
26 | {
27 | _package = package;
28 | _sourceItem = sourceItem;
29 |
30 | InitializeComponent();
31 |
32 | ButtonClose.IsEnabled = false;
33 |
34 | ProgressBar.Minimum = 0;
35 | ProgressBar.Maximum = 100;
36 |
37 | Text.IsReadOnly = true;
38 | Text.TextWrapping = TextWrapping.Wrap;
39 | Text.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
40 | Text.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
41 |
42 | WriteLine("Models Builder " + ApiVersion.Current.Version);
43 | WriteLine();
44 | }
45 |
46 | protected override void OnClosing(CancelEventArgs e)
47 | {
48 | base.OnClosing(e);
49 |
50 | if (_progress < 100)
51 | e.Cancel = true;
52 | }
53 |
54 | protected override void OnContentRendered(EventArgs e)
55 | {
56 | base.OnContentRendered(e);
57 |
58 | //var time = Stopwatch.StartNew();
59 | var generator = new Generator(_package, _sourceItem);
60 | generator.Progressed += (message, percent) => // invoked on main thread
61 | {
62 | //Write($"[{time.ElapsedMilliseconds:000000}] ");
63 | WriteLine(message);
64 | Progress(percent);
65 | };
66 |
67 | // not too proud of that async hackish code
68 | ThreadHelper.JoinableTaskFactory.Run(async () =>
69 | {
70 | await generator.GenerateAsync();
71 | });
72 | }
73 |
74 | public void Write(string text)
75 | {
76 | _text.Append(text);
77 | Text.Text = _text.ToString();
78 | Render(Text);
79 | }
80 |
81 | public void WriteLine(string text = null)
82 | {
83 | if (!string.IsNullOrWhiteSpace(text))
84 | _text.Append(text);
85 | _text.AppendLine();
86 | Text.Text = _text.ToString();
87 | Text.ScrollToEnd();
88 | Render(Text);
89 | }
90 |
91 | public void Progress(int progress)
92 | {
93 | if (progress <= _progress)
94 | return;
95 |
96 | ProgressBar.Value = progress;
97 | Render(ProgressBar);
98 | _progress = progress;
99 |
100 | if (progress == 100)
101 | ButtonClose.IsEnabled = true;
102 | }
103 |
104 | private void Close_OnClick(object sender, RoutedEventArgs e)
105 | {
106 | Close();
107 | }
108 |
109 | // not too proud of that one, but how do you refresh the UI?
110 | // https://www.meziantou.net/refresh-a-wpf-control.htm
111 | // https://www.c-sharpcorner.com/article/update-ui-with-wpf-dispatcher-and-tpl/
112 | private static void Render(DispatcherObject dispatcherObject)
113 | {
114 | #pragma warning disable VSTHRD001 // bah
115 | dispatcherObject.Dispatcher.Invoke(() => { }, DispatcherPriority.Render);
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Web/Plugin/DashboardUtilities.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Our.ModelsBuilder.Api;
3 | using Our.ModelsBuilder.Options;
4 | using Our.ModelsBuilder.Umbraco;
5 | using Umbraco.Core;
6 | using Umbraco.Core.Composing;
7 | using Umbraco.Core.Models.PublishedContent;
8 |
9 | namespace Our.ModelsBuilder.Web.Plugin
10 | {
11 | internal class DashboardUtilities
12 | {
13 | private readonly ModelsBuilderOptions _options;
14 |
15 | public DashboardUtilities(ModelsBuilderOptions options)
16 | {
17 | _options = options;
18 | }
19 |
20 | public bool CanGenerate()
21 | {
22 | return _options.ModelsMode.SupportsExplicitGeneration();
23 | }
24 |
25 | public bool GenerateCausesRestart()
26 | {
27 | return _options.ModelsMode.IsAnyDll();
28 | }
29 |
30 | public bool AreModelsOutOfDate()
31 | {
32 | return OutOfDateModelsStatus.IsOutOfDate;
33 | }
34 |
35 | public string LastError()
36 | {
37 | return ModelsGenerationError.GetLastError();
38 | }
39 |
40 | public string Text()
41 | {
42 | if (!_options.Enable)
43 | return "Version: " + ApiVersion.Current.Version + "
ModelsBuilder is disabled (the .Enable appSetting is missing, or its value is not 'true').";
44 |
45 | var sb = new StringBuilder();
46 |
47 | sb.Append("Version: ");
48 | sb.Append(ApiVersion.Current.Version);
49 | sb.Append("
");
50 |
51 | sb.Append("ModelsBuilder is enabled, with the following configuration:");
52 |
53 | sb.Append("
");
54 |
55 | sb.Append("
The models factory is ");
56 | sb.Append(_options.EnableFactory || _options.ModelsMode == ModelsMode.PureLive
57 | ? "enabled"
58 | : "not enabled. Umbraco will not use models");
59 | if (_options.EnableFactory || _options.ModelsMode == ModelsMode.PureLive)
60 | {
61 | sb.Append(", of type ");
62 | sb.Append(Current.Factory.GetInstance().GetType().FullName);
63 | sb.Append("");
64 | }
65 | sb.Append(".
No models mode is specified: models will not be generated.
");
70 |
71 | sb.Append($"
Models namespace is {_options.ModelsNamespace} but may be overriden by attribute.
");
72 |
73 | sb.Append("
Tracking of out-of-date models is ");
74 | sb.Append(_options.FlagOutOfDateModels ? "enabled" : "not enabled");
75 | sb.Append(".
");
76 |
77 | sb.Append("
The API is ");
78 | if (_options.EnableApi)
79 | {
80 | sb.Append("enabled");
81 | if (!_options.IsDebug) sb.Append(". However, the API runs only with debug compilation mode");
82 | }
83 | else sb.Append("not enabled");
84 | sb.Append(". ");
85 |
86 | if (!_options.IsApiServer)
87 | sb.Append("External tools such as Visual Studio cannot use the API");
88 | else
89 | sb.Append("The API endpoint is open on this server");
90 | sb.Append(".
");
91 |
92 | sb.Append("
BackOffice integrations are ");
93 | sb.Append(_options.EnableBackOffice ? "enabled (else, you would not see this dashboard)" : "disabled");
94 | sb.Append(".
");
95 |
96 | sb.Append("
");
97 |
98 | return sb.ToString();
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/ApiVersionTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using NUnit.Framework;
3 | using Our.ModelsBuilder.Api;
4 | using Semver;
5 |
6 | namespace Our.ModelsBuilder.Tests
7 | {
8 | [TestFixture]
9 | public class ApiVersionTests
10 | {
11 | [Test]
12 | public void IsCompatibleTest()
13 | {
14 | // executing version 3.0.0, accepting connections from 2.0.0
15 | var av = new ApiVersion(new SemVersion(3, 0, 0), new SemVersion(2, 0, 0));
16 |
17 | // server version 1.0.0 is not compatible (too old)
18 | Assert.IsFalse(av.IsCompatibleWith(new SemVersion(1, 0, 0)));
19 |
20 | // server version 2.0.0 or 3.0.0 is compatible
21 | Assert.IsTrue(av.IsCompatibleWith(new SemVersion(2, 0, 0)));
22 | Assert.IsTrue(av.IsCompatibleWith(new SemVersion(3, 0, 0)));
23 |
24 | // server version 4.0.0 is not compatible (too recent)
25 | Assert.IsFalse(av.IsCompatibleWith(new SemVersion(4, 0, 0)));
26 |
27 | // but can declare it is, indeed, compatible with version 2.0.0 or 3.0.0
28 | Assert.IsTrue(av.IsCompatibleWith(new SemVersion(4, 0, 0), new SemVersion(2, 0, 0)));
29 | Assert.IsTrue(av.IsCompatibleWith(new SemVersion(4, 0, 0), new SemVersion(3, 0, 0)));
30 |
31 | // but...
32 | Assert.IsFalse(av.IsCompatibleWith(new SemVersion(4, 0, 0), new SemVersion(3, 0, 1)));
33 | }
34 |
35 | [Test]
36 | public void CurrentIsCompatibleTest()
37 | {
38 | var av = ApiVersion.Current;
39 |
40 | // client version < MinClientVersionSupportedByServer are not supported
41 | Assert.IsFalse(av.IsCompatibleWith(GetPreviousVersion(av.MinClientVersionSupportedByServer)));
42 |
43 | // client version MinClientVersionSupportedByServer-Version are supported
44 | Assert.IsTrue(av.IsCompatibleWith(av.MinClientVersionSupportedByServer));
45 |
46 | // client version > Version are not supported
47 | Assert.IsFalse(av.IsCompatibleWith(GetNextVersion(av.Version)));
48 |
49 | // unless client says so
50 | Assert.IsTrue(av.IsCompatibleWith(GetNextVersion(av.Version), av.Version));
51 | }
52 |
53 | [Test]
54 | public void NextVersionTest()
55 | {
56 | Assert.AreEqual(new SemVersion(0, 0, 1), GetNextVersion(new SemVersion(0, 0, 0)));
57 | Assert.AreEqual(new SemVersion(1, 0, 1), GetNextVersion(new SemVersion(1, 0, 0)));
58 | Assert.AreEqual(new SemVersion(1, 0, 8), GetNextVersion(new SemVersion(1, 0, 7)));
59 | }
60 |
61 | [Test]
62 | public void PreviousVersionTest()
63 | {
64 | Assert.AreEqual(new SemVersion(0, 0, 0), GetPreviousVersion(new SemVersion(0, 0, 1)));
65 | Assert.AreEqual(new SemVersion(0, 0, 1), GetPreviousVersion(new SemVersion(0, 0, 2)));
66 | Assert.AreEqual(new SemVersion(0, 0, 999), GetPreviousVersion(new SemVersion(0, 1, 0)));
67 | Assert.AreEqual(new SemVersion(0, 0, 999), GetPreviousVersion(new SemVersion(0, 1, 0)));
68 | Assert.AreEqual(new SemVersion(0, 999, 999), GetPreviousVersion(new SemVersion(1, 0, 0)));
69 | Assert.AreEqual(new SemVersion(1, 999, 999), GetPreviousVersion(new SemVersion(2, 0, 0)));
70 | }
71 |
72 | private static SemVersion GetNextVersion(SemVersion version)
73 | {
74 | return new SemVersion(version.Major, version.Minor, version.Patch + 1);
75 | }
76 |
77 | private static SemVersion GetPreviousVersion(SemVersion version)
78 | {
79 | if (version.Prerelease != "")
80 | {
81 | var p = version.Prerelease.Split('.');
82 | return new SemVersion(version.Major, version.Minor, version.Patch, p[0] + "." + (int.Parse(p[1]) - 1));
83 | }
84 | if (version.Patch > 0)
85 | return new SemVersion(version.Major, version.Minor, version.Patch - 1);
86 | if (version.Minor > 0)
87 | return new SemVersion(version.Major, version.Minor - 1, 999);
88 | if (version.Major > 0)
89 | return new SemVersion(version.Major - 1, 999, 999);
90 |
91 | throw new ArgumentOutOfRangeException(nameof(version));
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Building/CodeWriter.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace Our.ModelsBuilder.Building
4 | {
5 | ///
6 | /// Provides the default code writer.
7 | ///
8 | public class CodeWriter : ModelsCodeWriter, ICodeWriter
9 | {
10 | private ContentTypesCodeWriter _contentTypesContentTypesCodeWriter;
11 | private InfosCodeWriter _infosCodeWriter;
12 |
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | public CodeWriter(CodeModel model, StringBuilder text = null)
17 | : base(model, text)
18 | { }
19 |
20 | ///
21 | public virtual ContentTypesCodeWriter ContentTypesCodeWriter
22 | => _contentTypesContentTypesCodeWriter ??= new ContentTypesCodeWriter(this);
23 |
24 | ///
25 | public virtual InfosCodeWriter InfosCodeWriter
26 | => _infosCodeWriter ??= new InfosCodeWriter(this);
27 |
28 | #region Write Complete Files
29 |
30 | ///
31 | /// Writes a using statement if it is not already defined by the code model.
32 | ///
33 | protected virtual void WriteUsing(string ns)
34 | {
35 | if (!CodeModel.Using.Contains(ns))
36 | WriteIndentLine($"using {ns};");
37 | }
38 |
39 | ///
40 | /// Writes the using statements defined by the code model.
41 | ///
42 | public virtual void WriteUsing()
43 | {
44 | foreach (var t in CodeModel.Using)
45 | WriteIndentLine($"using {t};");
46 | }
47 |
48 | ///
49 | public virtual void WriteModelFile(ContentTypeModel model)
50 | {
51 | WriteFileHeader();
52 | WriteLine();
53 |
54 | WriteUsing();
55 | WriteUsing("System.CodeDom.Compiler");
56 | WriteLine();
57 |
58 | WriteBlockStart($"namespace {CodeModel.ModelsNamespace}");
59 | ContentTypesCodeWriter.WriteModel(model);
60 | WriteBlockEnd();
61 | }
62 |
63 | ///
64 | public virtual void WriteSingleFile()
65 | {
66 | WriteFileHeader();
67 | WriteLine();
68 |
69 | WriteUsing();
70 | WriteUsing("System");
71 | WriteUsing("System.Linq");
72 | WriteUsing("System");
73 | WriteUsing("System.Linq");
74 | WriteUsing("System.Collections.Generic");
75 | WriteUsing("System.CodeDom.Compiler");
76 | WriteUsing("Umbraco.Core.Models.PublishedContent");
77 | WriteUsing("Our.ModelsBuilder");
78 | WriteUsing("Our.ModelsBuilder.Umbraco");
79 | WriteLine();
80 |
81 | // assembly attributes marker
82 | WriteIndentLine("//ASSATTR");
83 | WriteLine();
84 |
85 | WriteBlockStart($"namespace {CodeModel.ModelsNamespace}");
86 | ContentTypesCodeWriter.WriteModels(CodeModel.ContentTypes.ContentTypes);
87 | WriteBlockEnd();
88 |
89 | WriteLine();
90 |
91 | WriteBlockStart($"namespace {CodeModel.ModelInfosClassNamespace}");
92 | InfosCodeWriter.WriteInfosClass(CodeModel);
93 | WriteBlockEnd();
94 | }
95 |
96 | ///
97 | public virtual void WriteModelInfosFile()
98 | {
99 | WriteFileHeader();
100 | WriteLine();
101 |
102 | WriteUsing();
103 | WriteUsing("System");
104 | WriteUsing("System.Linq");
105 | WriteUsing("System.Collections.Generic");
106 | WriteUsing("System.CodeDom.Compiler");
107 | WriteUsing("Umbraco.Core.Models.PublishedContent");
108 | WriteUsing("Our.ModelsBuilder");
109 | WriteUsing("Our.ModelsBuilder.Umbraco");
110 | WriteLine();
111 |
112 | WriteBlockStart($"namespace {CodeModel.ModelInfosClassNamespace}");
113 | InfosCodeWriter.WriteInfosClass(CodeModel);
114 | WriteBlockEnd();
115 | }
116 |
117 | #endregion
118 | }
119 | }
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Extension/ExtensionPackage.vsct:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
28 |
29 |
36 |
37 |
40 |
41 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
58 |
59 |
60 |
61 |
66 |
67 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/DotNet/AppDomainTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using NUnit.Framework;
7 |
8 | namespace Our.ModelsBuilder.Tests.DotNet
9 | {
10 | [TestFixture]
11 | public class AppDomainTests
12 | {
13 | [Test]
14 | [Ignore("no idea what we're testing here?")]
15 | public void Test()
16 | {
17 | // read http://msdn.microsoft.com/en-us/library/ms173139%28v=vs.90%29.aspx
18 |
19 | // test executes in project/bin/Debug or /Release
20 | Console.WriteLine("FriendlyName " + AppDomain.CurrentDomain.FriendlyName);
21 | Console.WriteLine("BaseDirectory " + AppDomain.CurrentDomain.BaseDirectory); // project's bin
22 | Console.WriteLine("SearchPath " + AppDomain.CurrentDomain.RelativeSearchPath);
23 | Console.WriteLine("CodeBase " + Assembly.GetExecutingAssembly().CodeBase);
24 | Console.WriteLine("CurrentDirectory " + Directory.GetCurrentDirectory());
25 |
26 | var domainSetup = new AppDomainSetup();
27 |
28 | var bzzt = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bzzt");
29 | Console.WriteLine("Bzzt " + bzzt);
30 | if (Directory.Exists(bzzt))
31 | Directory.Delete(bzzt, true);
32 | Directory.CreateDirectory(bzzt);
33 | var load = Path.Combine(bzzt, "Our.ModelsBuilder.Tests.dll"); // we want to load the copy!
34 | File.Copy("Our.ModelsBuilder.Tests.dll", load);
35 |
36 | // fixme - why do we want copies? why cant we load stuff from where we are?
37 | // because - we want Umbraco plugin whatever to discover those things properly!
38 | File.Copy("Our.ModelsBuilder.dll", Path.Combine(bzzt, "Our.ModelsBuilder.dll")); // REQUIRED if we load copies
39 |
40 | // fixme - notes
41 | // because we set a root dir to the app which is in appdata
42 | // then IOHelper.GetRootDirectorySafe returns that directory ie AppData/Zbu
43 | // and IOHelper.GetRootDirectoryBinFolder looks into ~/bin/debug, ~/bin/release, ~/bin then ~/
44 | // and this is where TypeFinder.GetAllAssemblies will be looking into
45 |
46 | // app domain defaults to resharper's bin - FIXME how can it load our dll?
47 | // this makes sure it uses the right app base
48 | // with: directly look for the assembly in there
49 | // without: look for the assembly in resharper's bin first... then in there, why? 'cos it's the calling assembly!
50 | domainSetup.ApplicationBase = bzzt;
51 | // /bin rejected, outside appbase ... but anything else seems to be ignored?
52 | //domainSetup.PrivateBinPath = "bzzt"; // absolutely no idea what it does
53 | domainSetup.ApplicationName = "Test Application";
54 |
55 | // fixme - then we MUST copy a bunch of binaries out there!
56 | var domain = AppDomain.CreateDomain("Test Domain", null, domainSetup);
57 | // the dll here is relative to the local domain path, not the remote one?!
58 | var remote = domain.CreateInstanceFromAndUnwrap(load, "Our.ModelsBuilder.Tests.RemoteObject") as RemoteObject;
59 | var sho = remote.GetSharedObjects();
60 | Console.WriteLine(remote.GetAppDomainDetails());
61 | //Console.WriteLine(domain.);
62 | AppDomain.Unload(domain);
63 |
64 | Assert.Throws(() => remote.GetAppDomainDetails());
65 |
66 | var asho = sho.ToArray();
67 | Assert.AreEqual(1, asho.Length);
68 | Assert.AreEqual("hello", asho[0].Value);
69 | }
70 | }
71 |
72 | // read
73 | // http://blogs.microsoft.co.il/sasha/2010/05/06/assembly-private-bin-path-pitfall/
74 | // run fuslogvw.exe from an elevated Visual Studio command prompt
75 |
76 | [Serializable]
77 | public class SharedObject
78 | {
79 | public string Value { get; set; }
80 | }
81 |
82 | public class RemoteObject : MarshalByRefObject
83 | {
84 | public IEnumerable GetSharedObjects()
85 | {
86 | // in order for this to work... where should we look into?
87 | Assembly.Load("Our.ModelsBuilder");
88 |
89 | return new[] { new SharedObject { Value = "hello" } };
90 | }
91 |
92 | public string GetAppDomainDetails()
93 | {
94 | return AppDomain.CurrentDomain.FriendlyName
95 | + Environment.NewLine + AppDomain.CurrentDomain.BaseDirectory
96 | + Environment.NewLine + AppDomain.CurrentDomain.RelativeSearchPath
97 | + Environment.NewLine + Assembly.GetExecutingAssembly().CodeBase;
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Umbraco/LiveModelsProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Web.Hosting;
4 | using Our.ModelsBuilder.Building;
5 | using Our.ModelsBuilder.Options;
6 | using Umbraco.Core.Composing;
7 | using Umbraco.Core.Logging;
8 | using Umbraco.Web.Cache;
9 |
10 | namespace Our.ModelsBuilder.Umbraco
11 | {
12 | // supports LiveDll and LiveAppData - but not PureLive
13 | //public class LiveModelsComponent
14 | public sealed class LiveModelsProvider // FIXME this should just be a component?
15 | {
16 | private static ICodeFactory _codeFactory;
17 | private static ModelsBuilderOptions _options;
18 | private static Mutex _mutex;
19 | private static int _req;
20 |
21 | // we do not manage pure live here
22 | internal static bool IsEnabled => _options.ModelsMode.IsLiveNotPure();
23 |
24 | internal static void Install(ICodeFactory factory, ModelsBuilderOptions options)
25 | {
26 | // just be sure
27 | if (!IsEnabled)
28 | return;
29 |
30 | _codeFactory = factory;
31 | _options = options;
32 |
33 | // initialize mutex
34 | // ApplicationId will look like "/LM/W3SVC/1/Root/AppName"
35 | // name is system-wide and must be less than 260 chars
36 | var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider";
37 | _mutex = new Mutex(false, name);
38 |
39 | // anything changes, and we want to re-generate models.
40 | ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
41 | DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
42 |
43 | // at the end of a request since we're restarting the pool
44 | // NOTE - this does NOT trigger - see module below
45 | //umbracoApplication.EndRequest += GenerateModelsIfRequested;
46 | }
47 |
48 | // NOTE
49 | // Using HttpContext Items fails because CacheUpdated triggers within
50 | // some asynchronous backend task where we seem to have no HttpContext.
51 |
52 | // So we use a static (non request-bound) var to register that models
53 | // need to be generated. Could be by another request. Anyway. We could
54 | // have collisions but... you know the risk.
55 |
56 | private static void RequestModelsGeneration(object sender, EventArgs args)
57 | {
58 | //HttpContext.Current.Items[this] = true;
59 | Current.Logger.Debug("Requested to generate models.");
60 | Interlocked.Exchange(ref _req, 1);
61 | }
62 |
63 | public static void GenerateModelsIfRequested(object sender, EventArgs args)
64 | {
65 | //if (HttpContext.Current.Items[this] == null) return;
66 | if (Interlocked.Exchange(ref _req, 0) == 0) return;
67 |
68 | // cannot use a simple lock here because we don't want another AppDomain
69 | // to generate while we do... and there could be 2 AppDomains if the app restarts.
70 |
71 | try
72 | {
73 | Current.Logger.Debug("Generate models...");
74 | const int timeout = 2*60*1000; // 2 mins
75 | _mutex.WaitOne(timeout); // wait until it is safe, and acquire
76 | Current.Logger.Info("Generate models now.");
77 | GenerateModels();
78 | ModelsGenerationError.Clear();
79 | Current.Logger.Info("Generated.");
80 | }
81 | catch (TimeoutException)
82 | {
83 | Current.Logger.Warn("Timeout, models were NOT generated.");
84 | }
85 | catch (Exception e)
86 | {
87 | ModelsGenerationError.Report("Failed to build Live models.", e);
88 | Current.Logger.Error("Failed to generate models.", e);
89 | }
90 | finally
91 | {
92 | _mutex.ReleaseMutex(); // release
93 | }
94 | }
95 |
96 | private static void GenerateModels()
97 | {
98 | var modelsDirectory = _options.ModelsDirectory;
99 | var modelsNamespace = _options.ModelsNamespace;
100 |
101 | var bin = HostingEnvironment.MapPath("~/bin");
102 | if (bin == null)
103 | throw new Exception("Panic: bin is null.");
104 |
105 | // EnableDllModels will recycle the app domain - but this request will end properly
106 | var generator = new Generator(_codeFactory, _options);
107 | generator.GenerateModels(modelsDirectory, _options.ModelsMode.IsAnyDll() ? bin : null, modelsNamespace);
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29006.145
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{C7535949-9EEE-4711-8351-0CA8EB32DBE6}"
7 | ProjectSection(SolutionItems) = preProject
8 | ..\license.txt = ..\license.txt
9 | ..\readme.md = ..\readme.md
10 | SolutionInfo.cs = SolutionInfo.cs
11 | EndProjectSection
12 | EndProject
13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{42C3FC1A-26BA-4FAB-9585-AC8C25449B8F}"
14 | ProjectSection(SolutionItems) = preProject
15 | ..\build\build-bootstrap.ps1 = ..\build\build-bootstrap.ps1
16 | ..\build\build.ps1 = ..\build\build.ps1
17 | EndProjectSection
18 | EndProject
19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ModelsBuilder", "ModelsBuilder", "{1FFB7043-4B35-4A92-80FC-A2306511C8D5}"
20 | ProjectSection(SolutionItems) = preProject
21 | ..\build\Nuspecs\ModelsBuilder\web.config.install.xdt = ..\build\Nuspecs\ModelsBuilder\web.config.install.xdt
22 | ..\build\Nuspecs\ModelsBuilder\web.config.uninstall.xdt = ..\build\Nuspecs\ModelsBuilder\web.config.uninstall.xdt
23 | EndProjectSection
24 | EndProject
25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Nuspecs", "Nuspecs", "{30368E85-95E0-4A26-AC5A-D8570DB16FDA}"
26 | ProjectSection(SolutionItems) = preProject
27 | ..\build\Nuspecs\logo.png = ..\build\Nuspecs\logo.png
28 | ..\build\Nuspecs\Our.ModelsBuilder.nuspec = ..\build\Nuspecs\Our.ModelsBuilder.nuspec
29 | ..\build\Nuspecs\Our.ModelsBuilder.Web.nuspec = ..\build\Nuspecs\Our.ModelsBuilder.Web.nuspec
30 | EndProjectSection
31 | EndProject
32 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.ModelsBuilder", "Our.ModelsBuilder\Our.ModelsBuilder.csproj", "{998FB014-303A-4146-B3E4-B927BAB0210F}"
33 | EndProject
34 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.ModelsBuilder.Tests", "Our.ModelsBuilder.Tests\Our.ModelsBuilder.Tests.csproj", "{CF597D44-04EA-4FD7-89C8-E4849D1355C8}"
35 | EndProject
36 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.ModelsBuilder.Console", "Our.ModelsBuilder.Console\Our.ModelsBuilder.Console.csproj", "{F0B757DF-0CF7-4AAF-8DD0-F46EDF25C3A3}"
37 | EndProject
38 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.ModelsBuilder.Web", "Our.ModelsBuilder.Web\Our.ModelsBuilder.Web.csproj", "{2317BE7F-1723-4512-B863-5B6835E583A2}"
39 | EndProject
40 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.ModelsBuilder.Extension", "Our.ModelsBuilder.Extension\Our.ModelsBuilder.Extension.csproj", "{0419A43D-78C2-46C9-95A9-2470F224D60F}"
41 | EndProject
42 | Global
43 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
44 | Debug|Any CPU = Debug|Any CPU
45 | Release|Any CPU = Release|Any CPU
46 | EndGlobalSection
47 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
48 | {998FB014-303A-4146-B3E4-B927BAB0210F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {998FB014-303A-4146-B3E4-B927BAB0210F}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {998FB014-303A-4146-B3E4-B927BAB0210F}.Release|Any CPU.ActiveCfg = Release|Any CPU
51 | {998FB014-303A-4146-B3E4-B927BAB0210F}.Release|Any CPU.Build.0 = Release|Any CPU
52 | {CF597D44-04EA-4FD7-89C8-E4849D1355C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53 | {CF597D44-04EA-4FD7-89C8-E4849D1355C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
54 | {CF597D44-04EA-4FD7-89C8-E4849D1355C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
55 | {CF597D44-04EA-4FD7-89C8-E4849D1355C8}.Release|Any CPU.Build.0 = Release|Any CPU
56 | {F0B757DF-0CF7-4AAF-8DD0-F46EDF25C3A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57 | {F0B757DF-0CF7-4AAF-8DD0-F46EDF25C3A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
58 | {F0B757DF-0CF7-4AAF-8DD0-F46EDF25C3A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
59 | {2317BE7F-1723-4512-B863-5B6835E583A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60 | {2317BE7F-1723-4512-B863-5B6835E583A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
61 | {2317BE7F-1723-4512-B863-5B6835E583A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
62 | {2317BE7F-1723-4512-B863-5B6835E583A2}.Release|Any CPU.Build.0 = Release|Any CPU
63 | {0419A43D-78C2-46C9-95A9-2470F224D60F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
64 | {0419A43D-78C2-46C9-95A9-2470F224D60F}.Debug|Any CPU.Build.0 = Debug|Any CPU
65 | {0419A43D-78C2-46C9-95A9-2470F224D60F}.Release|Any CPU.ActiveCfg = Release|Any CPU
66 | {0419A43D-78C2-46C9-95A9-2470F224D60F}.Release|Any CPU.Build.0 = Release|Any CPU
67 | EndGlobalSection
68 | GlobalSection(SolutionProperties) = preSolution
69 | HideSolutionNode = FALSE
70 | EndGlobalSection
71 | GlobalSection(NestedProjects) = preSolution
72 | {1FFB7043-4B35-4A92-80FC-A2306511C8D5} = {30368E85-95E0-4A26-AC5A-D8570DB16FDA}
73 | {30368E85-95E0-4A26-AC5A-D8570DB16FDA} = {42C3FC1A-26BA-4FAB-9585-AC8C25449B8F}
74 | EndGlobalSection
75 | GlobalSection(ExtensibilityGlobals) = postSolution
76 | SolutionGuid = {C58B1916-804E-47EC-BD71-E1FC6A3C9924}
77 | EndGlobalSection
78 | EndGlobal
79 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Building/CodeModelBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Our.ModelsBuilder.Options;
3 |
4 | namespace Our.ModelsBuilder.Building
5 | {
6 | ///
7 | /// Implements the default .
8 | ///
9 | public class CodeModelBuilder : ICodeModelBuilder
10 | {
11 | // FIXME but we still need ... to 'build' options vs to 'query' options
12 | // ICodeFactory / CodeFactory
13 | // provides the key services to generate models
14 | // CodeModelSourceProvider
15 | // provides a CodeModelSource
16 | // CodeModelSource
17 | // represents a source of models, i.e. raw from Umbraco
18 | // CodeOptionsBuilder
19 | // is passed to CodeParser
20 | // builds a CodeOptions
21 | // ICodeParser / CodeParser
22 | // parses existing code and updates the CodeOptionsBuilder
23 | // CodeOptions
24 | // provides options for building a code model
25 | // ICodeModelBuilder / CodeModelBuilder
26 | // + ContentTypesCodeModelBuilder
27 | // builds a code model, from a CodeModelSource, CodeOptions and a Configuration
28 | // CodeModel
29 | // provides a complete description of the code to be written
30 | // ICodeWriter / CodeWriter
31 | // + ContentTypesCodeWriter
32 | // writes code defined in a code model
33 |
34 | ///
35 | /// Initializes a new instance of the class.
36 | ///
37 | public CodeModelBuilder(ModelsBuilderOptions options, CodeOptions codeOptions)
38 | : this(options, codeOptions, new ContentTypesCodeModelBuilder(options, codeOptions))
39 | { }
40 |
41 | ///
42 | /// Initializes a new instance of the class.
43 | ///
44 | protected CodeModelBuilder(ModelsBuilderOptions options, CodeOptions codeOptions, ContentTypesCodeModelBuilder contentTypes)
45 | {
46 | Options = options;
47 | CodeOptions = codeOptions;
48 | ContentTypes = contentTypes;
49 | }
50 |
51 | ///
52 | /// Gets the content types code model builder.
53 | ///
54 | protected ContentTypesCodeModelBuilder ContentTypes { get; }
55 |
56 | protected ModelsBuilderOptions Options { get; }
57 |
58 | protected CodeOptions CodeOptions { get; }
59 |
60 | ///
61 | public virtual CodeModel Build(CodeModelData data)
62 | {
63 | var model = new CodeModel(data, Options.LanguageVersion)
64 | {
65 | ModelsNamespace = GetModelsNamespace(),
66 | CustomAssemblyName = GetAssemblyName(),
67 | Using = GetUsing()
68 | };
69 |
70 | // TODO: refactor using, have transform.Use("namespace")
71 |
72 | ContentTypes.Build(model);
73 |
74 | return model;
75 | }
76 | public virtual string GetAssemblyName()
77 | {
78 | // use namespace from code options... or from options
79 | var modelsNamespace = CodeOptions.HasCustomAssemblyName
80 | ? CodeOptions.CustomAssemblyName
81 | : Options.AssemblyName;
82 |
83 | // otherwise, use const
84 | if (string.IsNullOrWhiteSpace(modelsNamespace))
85 | modelsNamespace = GetDefaultAssemblyName();
86 |
87 | return modelsNamespace;
88 | }
89 | public virtual string GetDefaultAssemblyName() => null;
90 |
91 | protected virtual string GetDefaultModelsNamespace() => "Umbraco.Web.PublishedModels";
92 |
93 | protected virtual string GetModelsNamespace()
94 | {
95 | // use namespace from code options... or from options
96 | var modelsNamespace = CodeOptions.HasModelsNamespace
97 | ? CodeOptions.ModelsNamespace
98 | : Options.ModelsNamespace;
99 |
100 | // otherwise, use const
101 | if (string.IsNullOrWhiteSpace(modelsNamespace))
102 | modelsNamespace = GetDefaultModelsNamespace();
103 |
104 | return modelsNamespace;
105 | }
106 |
107 | ///
108 | /// Gets the namespace to use in 'using' section.
109 | ///
110 | protected virtual ISet GetUsing() => new HashSet
111 | {
112 | // initialize with default values
113 | "System",
114 | "System.Collections.Generic",
115 | "System.Linq.Expressions",
116 | "System.Web",
117 | "Umbraco.Core.Models",
118 | "Umbraco.Core.Models.PublishedContent",
119 | "Umbraco.Web",
120 | "Our.ModelsBuilder",
121 | "Our.ModelsBuilder.Umbraco",
122 | };
123 |
124 | }
125 | }
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/Our.ModelsBuilder.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | v4.7.2
6 | {CF597D44-04EA-4FD7-89C8-E4849D1355C8}
7 | Library
8 | Our.ModelsBuilder.Tests
9 | Our.ModelsBuilder.Tests
10 | ..\
11 |
12 |
13 |
14 | true
15 | full
16 | false
17 | bin\Debug\
18 | DEBUG;TRACE
19 | prompt
20 | 4
21 | latest
22 |
23 |
24 | pdbonly
25 | true
26 | bin\Release\
27 | TRACE
28 | prompt
29 | 4
30 | latest
31 |
32 |
33 |
34 | 4.13.1
35 |
36 |
37 | 3.12.0
38 |
39 |
40 | 3.15.1
41 |
42 |
43 | 8.3.1
44 |
45 |
46 |
47 |
48 | Properties\SolutionInfo.cs
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | Designer
87 |
88 |
89 |
90 |
91 | {2317be7f-1723-4512-b863-5b6835e583a2}
92 | Our.ModelsBuilder.Web
93 |
94 |
95 | {998fb014-303a-4146-b3e4-b927bab0210f}
96 | Our.ModelsBuilder
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Api/ApiVersion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using Newtonsoft.Json;
4 | using Semver;
5 |
6 | namespace Our.ModelsBuilder.Api
7 | {
8 | ///
9 | /// Manages API version handshake between client and server.
10 | ///
11 | [JsonObject(MemberSerialization.OptIn)]
12 | public class ApiVersion
13 | {
14 | #region Configure
15 |
16 | // indicate the minimum version of the client API that is supported by this server's API.
17 | // eg our (server) Version = 4.8 but we support connections from (client) VSIX down to version 3.2
18 | // => as a server, we accept connections from client down to version ...
19 | private static readonly SemVersion MinClientVersionSupportedByServerConst = SemVersion.Parse("4.0.0-alpha.0");
20 |
21 | // indicate the minimum version of the server that can support the client API
22 | // eg our (client) Version = 4.8 and we know we're compatible with website server down to version 3.2
23 | // => as a client, we tell the server down to version ... that it should accept us
24 | private static readonly SemVersion MinServerVersionSupportingClientConst = SemVersion.Parse("4.0.0-alpha.0");
25 |
26 | #endregion
27 |
28 | ///
29 | /// Initializes a new instance of the class.
30 | ///
31 | /// The currently executing version.
32 | /// The min client version supported by the server.
33 | /// An opt min server version supporting the client.
34 | ///
35 | internal ApiVersion(SemVersion executingVersion, SemVersion minClientVersionSupportedByServer, SemVersion minServerVersionSupportingClient = null)
36 | {
37 | Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion));
38 | MinClientVersionSupportedByServer = minClientVersionSupportedByServer ?? throw new ArgumentNullException(nameof(minClientVersionSupportedByServer));
39 | MinServerVersionSupportingClient = minServerVersionSupportingClient;
40 | }
41 |
42 | private static SemVersion CurrentAssemblyVersion
43 | => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion);
44 |
45 | ///
46 | /// Gets the currently executing API version.
47 | ///
48 | public static ApiVersion Current { get; }
49 | = new ApiVersion(CurrentAssemblyVersion, MinClientVersionSupportedByServerConst, MinServerVersionSupportingClientConst);
50 |
51 | ///
52 | /// Gets the executing version of the API.
53 | ///
54 | [JsonProperty("version")]
55 | public SemVersion Version { get; }
56 |
57 | ///
58 | /// Gets the min client version supported by the server.
59 | ///
60 | [JsonProperty("minClientVersionSupportedByServer")]
61 | public SemVersion MinClientVersionSupportedByServer { get; }
62 |
63 | ///
64 | /// Gets the min server version supporting the client.
65 | ///
66 | [JsonProperty("minServerVersionSupportingClient")]
67 | public SemVersion MinServerVersionSupportingClient { get; }
68 |
69 | ///
70 | /// Gets a value indicating whether the API server is compatible with a client.
71 | ///
72 | /// The client version.
73 | /// An opt min server version supporting the client.
74 | ///
75 | /// A client is compatible with a server if
76 | /// * the client version is greater-or-equal MinClientVersionSupportedByServer
77 | /// * the client version is lower-or-equal the server version
78 | /// unless MinServerVersionSupportingClient indicates that the server should support a more recent client
79 | ///
80 | ///
81 | public bool IsCompatibleWith(SemVersion clientVersion, SemVersion minServerVersionSupportingClient = null)
82 | {
83 | // client cannot be older than server's min supported version
84 | if (clientVersion < MinClientVersionSupportedByServer)
85 | return false;
86 |
87 | // if we know about this client (client is older than server), it is supported
88 | if (clientVersion <= Version) // if we know about this client (client older than server)
89 | return true;
90 |
91 | // if we don't know about this client (client is newer than server),
92 | // give server a chance to tell client it is, indeed, ok to support it
93 | return minServerVersionSupportingClient != null && minServerVersionSupportingClient <= Version;
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Web/WebComposer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using Our.ModelsBuilder.Options;
4 | using Our.ModelsBuilder.Umbraco;
5 | using Our.ModelsBuilder.Validation;
6 | using Our.ModelsBuilder.Web.Api;
7 | using Umbraco.Core;
8 | using Umbraco.Core.Composing;
9 | using Umbraco.Web;
10 | using Umbraco.Web.Editors;
11 | using Umbraco.Web.WebApi;
12 | using Embedded = Umbraco.ModelsBuilder.Embedded;
13 |
14 | namespace Our.ModelsBuilder.Web
15 | {
16 | [Disable]
17 | public class NoopComposer : IComposer
18 | {
19 | public void Compose(Composition composition)
20 | { }
21 | }
22 |
23 | public class DisableUmbracoModelsBuilderAttribute : DisableAttribute
24 | {
25 | public DisableUmbracoModelsBuilderAttribute()
26 | {
27 | var field = typeof(DisableAttribute).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance);
28 | if (field == null) throw new Exception("panic: cannot get DisableAttribute.DisableType backing field.");
29 |
30 | var type = Type.GetType("Umbraco.ModelsBuilder.Umbraco.ModelsBuilderComposer,Umbraco.ModelsBuilder", false);
31 | field.SetValue(this, type ?? typeof(NoopComposer));
32 | }
33 | }
34 |
35 | // disable the original MB that shipped with the CMS and ppl may still have
36 | // do not reference Umbraco.ModelsBuilder - the type will be replaced at runtime
37 | // (see DisableUmbracoModelsBuilderAttribute class above)
38 | //[Disable(typeof(global::Umbraco.ModelsBuilder.Umbraco.ModelsBuilderComposer))]
39 | [DisableUmbracoModelsBuilder]
40 |
41 | // disable the embedded MB that ships with the CMS
42 | [Disable(typeof(Embedded.Compose.ModelsBuilderComposer))]
43 |
44 | // after our own Our ModelsBuilderComposer and options composers
45 | [ComposeAfter(typeof(ModelsBuilderComposer))]
46 | [ComposeAfter(typeof(IOptionsComposer))]
47 |
48 | [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
49 | public class WebComposer : ComponentComposer, IUserComposer
50 | {
51 | public override void Compose(Composition composition)
52 | {
53 | base.Compose(composition);
54 |
55 | // remove the embedded dashboard
56 | composition.Dashboards().Remove();
57 |
58 | // remove the embedded controller
59 | // (embedded code uses features via a component - convoluted
60 | composition.WithCollectionBuilder()
61 | .Remove();
62 |
63 | // add our manifest, depending on configuration
64 | composition.ManifestFilters().Append();
65 |
66 | // replaces the model validators
67 | // Core (in WebInitialComposer) registers with:
68 | //
69 | // composition.WithCollectionBuilder()
70 | // .Add(() => composition.TypeLoader.GetTypes());
71 | //
72 | // so ours are already in there, but better be safe: clear the collection,
73 | // and then add exactly those that we want.
74 |
75 | composition.WithCollectionBuilder()
76 | .Clear();
77 |
78 | // get the options
79 | // NOTE: after that point, the options are frozen
80 | var options = composition.Configs.GetConfig().ModelsBuilderOptions;
81 |
82 | if (options.EnableBackOffice)
83 | {
84 | composition.WithCollectionBuilder()
85 | .Add()
86 | .Add()
87 | .Add();
88 | }
89 |
90 | // setup the API if enabled (and in debug mode)
91 |
92 | // the controller is hidden from type finder (to prevent it from being always and
93 | // automatically registered), which means that Umbraco.Web.Composing.CompositionExtensions
94 | // Controllers has *not* registered it into the container, and that it is not part of
95 | // UmbracoApiControllerTypeCollection (and won't get routed etc)
96 |
97 | // so...
98 | // register it in the container
99 | // do NOT add it to the collection - we will route it in the component, our way
100 | // fixme - explain why?
101 |
102 | if (options.IsApiServer)
103 | {
104 | // add the controller to the list of known controllers
105 | //composition.WithCollectionBuilder()
106 | // .Add();
107 |
108 | // register the controller into the container
109 | composition.Register(typeof(ModelsBuilderApiController), Lifetime.Request);
110 | }
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/Write/WriteClrTypeTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using NUnit.Framework;
5 | using Our.ModelsBuilder.Building;
6 | using Our.ModelsBuilder.Options;
7 | using Our.ModelsBuilder.Options.ContentTypes;
8 | using Our.ModelsBuilder.Tests.Testing;
9 |
10 | namespace Our.ModelsBuilder.Tests.Write
11 | {
12 | [TestFixture]
13 | public class WriteClrTypeTests : TestsBase
14 | {
15 | [TestCase("int", typeof(int))]
16 | [TestCase("IEnumerable", typeof(IEnumerable))]
17 | [TestCase("Our.ModelsBuilder.Tests.BuilderTestsClass1", typeof(BuilderTestsClass1))]
18 | [TestCase("Our.ModelsBuilder.Tests.Write.WriteClrTypeTests.Class1", typeof(Class1))]
19 | public void WriteClrType(string expected, Type input)
20 | {
21 | var codeModelBuilder = new CodeModelBuilder(new ModelsBuilderOptions(), new CodeOptions(new ContentTypesCodeOptions()));
22 | var codeModel = codeModelBuilder.Build(new CodeModelData());
23 | codeModel.ModelsNamespace = "ModelsNamespace";
24 |
25 | var writer = new CodeWriter(codeModel);
26 |
27 | writer.WriteClrType(input);
28 | Assert.AreEqual(expected, writer.Code);
29 | }
30 |
31 | [TestCase("int", typeof(int))]
32 | [TestCase("IEnumerable", typeof(IEnumerable))]
33 | [TestCase("BuilderTestsClass1", typeof(BuilderTestsClass1))]
34 | [TestCase("WriteClrTypeTests.Class1", typeof(Class1))]
35 | public void WriteClrTypeUsing(string expected, Type input)
36 | {
37 | var codeModelBuilder = new CodeModelBuilder(new ModelsBuilderOptions(), new CodeOptions(new ContentTypesCodeOptions()));
38 | var codeModel = codeModelBuilder.Build(new CodeModelData());
39 | codeModel.ModelsNamespace = "ModelsNamespace";
40 | codeModel.Using.Add("Our.ModelsBuilder.Tests"); // BuilderTestsClass1
41 | codeModel.Using.Add("Our.ModelsBuilder.Tests.Write"); // WriteClrTypeTests.Class1
42 |
43 | var writer = new CodeWriter(codeModel);
44 |
45 | writer.WriteClrType(input);
46 | Assert.AreEqual(expected, writer.Code);
47 | }
48 |
49 | [TestCase(true, true, "Borked", typeof(global::System.Text.ASCIIEncoding), "System.Text.ASCIIEncoding")]
50 | [TestCase(true, false, "Borked", typeof(global::System.Text.ASCIIEncoding), "ASCIIEncoding")]
51 | [TestCase(false, true, "Borked", typeof(global::System.Text.ASCIIEncoding), "System.Text.ASCIIEncoding")]
52 | [TestCase(false, false, "Borked", typeof(global::System.Text.ASCIIEncoding), "System.Text.ASCIIEncoding")]
53 |
54 | [TestCase(true, true, "Our.ModelsBuilder.Tests", typeof(global::System.Text.ASCIIEncoding), "global::System.Text.ASCIIEncoding")]
55 | [TestCase(true, false, "Our.ModelsBuilder.Tests", typeof(global::System.Text.ASCIIEncoding), "global::System.Text.ASCIIEncoding")]
56 | [TestCase(false, true, "Our.ModelsBuilder.Tests", typeof(global::System.Text.ASCIIEncoding), "global::System.Text.ASCIIEncoding")]
57 | [TestCase(false, false, "Our.ModelsBuilder.Tests", typeof(global::System.Text.ASCIIEncoding), "global::System.Text.ASCIIEncoding")]
58 |
59 | [TestCase(true, true, "Borked", typeof(StringBuilder), "StringBuilder")]
60 | [TestCase(true, false, "Borked", typeof(StringBuilder), "StringBuilder")]
61 | [TestCase(false, true, "Borked", typeof(StringBuilder), "System.Text.StringBuilder")] // magic? using = not ambiguous
62 | [TestCase(false, false, "Borked", typeof(StringBuilder), "System.Text.StringBuilder")]
63 |
64 | [TestCase(true, true, "Our.ModelsBuilder.Tests", typeof(StringBuilder), "StringBuilder")]
65 | [TestCase(true, false, "Our.ModelsBuilder.Tests", typeof(StringBuilder), "StringBuilder")]
66 | [TestCase(false, true, "Our.ModelsBuilder.Tests", typeof(StringBuilder), "global::System.Text.StringBuilder")] // magic? in ns = ambiguous
67 | [TestCase(false, false, "Our.ModelsBuilder.Tests", typeof(StringBuilder), "global::System.Text.StringBuilder")]
68 | public void WriteClrType_Ambiguous_Ns(bool usingSystem, bool usingZb, string ns, Type type, string expected)
69 | {
70 | var codeModel = new CodeModel(new CodeModelData()) { ModelsNamespace = ns };
71 | if (usingSystem) codeModel.Using.Add("System.Text");
72 | if (usingZb) codeModel.Using.Add("Our.ModelsBuilder.Tests");
73 |
74 | var writer = new CodeWriter(codeModel);
75 |
76 | writer.WriteClrType(type);
77 |
78 | Assert.AreEqual(expected, writer.Code);
79 | }
80 |
81 | [Test]
82 | public void WriteClrType_AmbiguousWithNested()
83 | {
84 | var codeModel = new CodeModel(new CodeModelData()) { ModelsNamespace = "SomeRandomNamespace" };
85 | codeModel.Using.Add("System.Text");
86 | codeModel.Using.Add("Our.ModelsBuilder.Tests");
87 |
88 | var writer = new CodeWriter(codeModel);
89 |
90 | writer.WriteClrType(typeof(ASCIIEncoding.Nested));
91 |
92 | // full type name is needed but not global::
93 | Assert.AreEqual("Our.ModelsBuilder.Tests.ASCIIEncoding.Nested", writer.Code);
94 | }
95 |
96 | public class Class1 { }
97 | }
98 | }
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/UmbracoExtensions/PublishedElementExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using System.Reflection;
4 | using Our.ModelsBuilder;
5 | using Umbraco.Core.Models.PublishedContent;
6 |
7 | // ReSharper disable once CheckNamespace, reason: extension methods
8 | namespace Umbraco.Web
9 | {
10 | ///
11 | /// Provides extension methods to models.
12 | ///
13 | public static class OurModelsBuilderPublishedElementExtensions // ensure name does not conflicts with Core's class
14 | {
15 | ///
16 | /// Gets the value of a property.
17 | ///
18 | /// The type of the content model.
19 | /// The type of the property value.
20 | /// The content model.
21 | /// The alias of the property.
22 | /// An optional culture.
23 | /// An optional segment.
24 | /// A fallback method.
25 | /// A value for the property.
26 | public static TValue Value(this TModel model, string alias, string culture = null, string segment = null, Func, TValue> fallback = null)
27 | where TModel : IPublishedElement
28 | {
29 | var property = model.GetProperty(alias);
30 |
31 | // if we have a property, and it has a value, return that value
32 | if (property != null && property.HasValue(culture, segment))
33 | return property.Value(culture, segment);
34 |
35 | // else use the fallback method, if any
36 | if (fallback != default)
37 | return fallback(new FallbackInfos(model, alias, culture, segment));
38 |
39 | // else... if we have a property, at least let the converter return its own
40 | // vision of 'no value' (could be an empty enumerable) - otherwise, default
41 | return property == null ? default : property.Value(culture, segment);
42 | }
43 |
44 | // note: the method below used to be provided by Core, but then when they embedded MB, they ran into
45 | // collision, and their "fix" consisted in renaming the method "ValueFor" - so we can provide it here.
46 | // see: https://github.com/umbraco/Umbraco-CMS/issues/7469
47 |
48 | ///
49 | /// Gets the value of a property.
50 | ///
51 | /// The type of the model.
52 | /// The type of the property.
53 | /// The model.
54 | /// An expression selecting the property.
55 | /// An optional culture.
56 | /// An optional segment.
57 | /// An optional fallback.
58 | /// /// An optional default value.
59 | /// The value of the property.
60 | public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default)
61 | where TModel : IPublishedElement
62 | {
63 | var alias = GetAlias(model, property);
64 | return model.Value(alias, culture, segment, fallback, defaultValue);
65 | }
66 |
67 | ///
68 | /// Gets the alias of a property.
69 | ///
70 | /// The type of the model.
71 | /// The type of the property.
72 | /// The model.
73 | /// An expression selecting the property.
74 | /// The alias of the property.
75 | private static string GetAlias(TModel model, Expression> property)
76 | {
77 | if (property.NodeType != ExpressionType.Lambda)
78 | throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property));
79 |
80 | var lambda = (LambdaExpression) property;
81 | var lambdaBody = lambda.Body;
82 |
83 | if (lambdaBody.NodeType != ExpressionType.MemberAccess)
84 | throw new ArgumentException("Not a proper lambda expression (body).", nameof(property));
85 |
86 | var memberExpression = (MemberExpression)lambdaBody;
87 | if (memberExpression.Expression.NodeType != ExpressionType.Parameter)
88 | throw new ArgumentException("Not a proper lambda expression (member).", nameof(property));
89 |
90 | var member = memberExpression.Member;
91 |
92 | var attribute = member.GetCustomAttribute();
93 | if (attribute == null)
94 | throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute.");
95 |
96 | return attribute.PropertyTypeAlias;
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder.Tests/Custom/CustomCodeFactory.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Our.ModelsBuilder.Building;
3 | using Our.ModelsBuilder.Options;
4 | using Our.ModelsBuilder.Options.ContentTypes;
5 |
6 | namespace Our.ModelsBuilder.Tests.Custom
7 | {
8 | public class CustomCodeFactory : ICodeFactory
9 | {
10 | public ICodeModelDataSource CreateCodeModelDataSource()
11 | => new CustomCodeModelDataSource();
12 |
13 | public CodeOptionsBuilder CreateCodeOptionsBuilder()
14 | => new CustomCodeOptionsBuilder();
15 |
16 | public ICodeParser CreateCodeParser()
17 | => new CustomCodeParser();
18 |
19 | public ICodeModelBuilder CreateCodeModelBuilder(ModelsBuilderOptions options, CodeOptions codeOptions)
20 | => new CustomCodeModelBuilderX(options, codeOptions);
21 |
22 | public ICodeWriter CreateCodeWriter(CodeModel model, StringBuilder text = null)
23 | => new CustomCodeWriter(model, text);
24 | }
25 |
26 | public class CustomCodeModelDataSource : ICodeModelDataSource
27 | {
28 | public CodeModelData GetCodeModelData()
29 | => new CodeModelData();
30 | }
31 |
32 | // custom: support more options
33 | //
34 | public class CustomCodeOptionsBuilder : CodeOptionsBuilder
35 | {
36 | public CustomCodeOptionsBuilder()
37 | : this (new CustomCodeOptions())
38 | { }
39 |
40 | private CustomCodeOptionsBuilder(CustomCodeOptions codeOptions)
41 | : this(codeOptions, new CustomContentTypesCodeOptionsBuilder(codeOptions.CustomContentTypes))
42 | { }
43 |
44 | private CustomCodeOptionsBuilder(CustomCodeOptions codeOptions, CustomContentTypesCodeOptionsBuilder contentTypesCodeOptionsBuilder)
45 | : base(codeOptions, contentTypesCodeOptionsBuilder)
46 | {
47 | CustomOptions = codeOptions;
48 | CustomContentTypes = contentTypesCodeOptionsBuilder;
49 | }
50 |
51 | public CustomCodeOptions CustomOptions { get; }
52 |
53 | public CustomContentTypesCodeOptionsBuilder CustomContentTypes { get; }
54 |
55 | // nothing to override
56 | }
57 |
58 | // custom: support more options
59 | //
60 | public class CustomContentTypesCodeOptionsBuilder : ContentTypesCodeOptionsBuilder
61 | {
62 | public CustomContentTypesCodeOptionsBuilder(CustomContentTypesCodeOptions options)
63 | : base(options)
64 | {
65 | CustomOptions = options;
66 | }
67 |
68 | public CustomContentTypesCodeOptions CustomOptions { get; }
69 |
70 | // override!
71 | }
72 |
73 | // custom: support more options
74 | //
75 | public class CustomCodeOptions : CodeOptions
76 | {
77 | public CustomCodeOptions()
78 | : this(new CustomContentTypesCodeOptions())
79 | { }
80 |
81 | private CustomCodeOptions(CustomContentTypesCodeOptions contentTypesCodeOptions)
82 | : base(contentTypesCodeOptions)
83 | {
84 | CustomContentTypes = contentTypesCodeOptions;
85 | }
86 |
87 | public CustomContentTypesCodeOptions CustomContentTypes { get; }
88 |
89 | // nothing to override
90 | }
91 |
92 | // custom: support more options
93 | //
94 | public class CustomContentTypesCodeOptions : ContentTypesCodeOptions
95 | {
96 | // nothing to override
97 | }
98 |
99 | public class CustomCodeModelBuilderX : CodeModelBuilder // FIXME name?
100 | {
101 | public CustomCodeModelBuilderX(ModelsBuilderOptions options, CodeOptions codeOptions)
102 | : base(options, codeOptions, new CustomContentTypesCodeModelBuilder(options, codeOptions))
103 | { }
104 |
105 | // building the code model - the options we are getting may be custom options
106 | // override Build
107 | }
108 |
109 | public class CustomContentTypesCodeModelBuilder : ContentTypesCodeModelBuilder
110 | {
111 | public CustomContentTypesCodeModelBuilder(ModelsBuilderOptions options, CodeOptions codeOptions)
112 | : base(options, codeOptions)
113 | { }
114 |
115 | // tons of things we can override when building the code model
116 |
117 | // determines the Clr name of a content type
118 | protected override string GetClrName(ContentTypeModel contentTypeModel)
119 | {
120 | return "PREFIX_" + base.GetClrName(contentTypeModel) + "_SUFFIX";
121 | }
122 |
123 | // determines the Clr name of a property type
124 | protected override string GetClrName(PropertyTypeModel propertyModel)
125 | {
126 | return "PREFIX_" + base.GetClrName(propertyModel) + "_SUFFIX";
127 | }
128 |
129 | // the rest is more internal stuff - do we want to override it?
130 | }
131 |
132 | public class CustomCodeParser : CodeParser
133 | { }
134 |
135 | public class CustomCodeWriter : CodeWriter
136 | {
137 | // FIXME the custom types & infos writers should be in ctors too!
138 |
139 | public CustomCodeWriter(CodeModel model, StringBuilder text = null)
140 | : base(model, text)
141 | { }
142 |
143 | // override...
144 | }
145 |
146 | public class CustomContentTypesCodeWriter : ContentTypesCodeWriter
147 | {
148 | public CustomContentTypesCodeWriter(ModelsCodeWriter origin)
149 | : base(origin)
150 | { }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Options/ModelsBuilderOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Configuration;
2 | using System.Web.Configuration;
3 | using Microsoft.CodeAnalysis.CSharp;
4 |
5 | namespace Our.ModelsBuilder.Options
6 | {
7 | public class ModelsBuilderOptions
8 | {
9 | public class Defaults
10 | {
11 | public const bool Enable = false;
12 | public const bool EnableApi = false;
13 | public const bool EnableBackOffice = true;
14 | public const bool EnableFactory = true;
15 | public const ModelsMode ModelsMode = Options.ModelsMode.Nothing;
16 | public const bool AcceptUnsafeModelsDirectory = false;
17 | public const bool FlagOutOfDateModels = true;
18 | public const int DebugLevel = 0;
19 | public const LanguageVersion LanguageVersion = Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp7_3;
20 | public const string ModelsNamespace = "Umbraco.Web.PublishedModels";
21 | public const string ModelsDirectory = "~/App_Data/Models";
22 | }
23 |
24 | ///
25 | /// Gets a value indicating whether the whole models experience is enabled.
26 | ///
27 | ///
28 | /// If this is false then absolutely nothing happens.
29 | /// Default value is false which means that unless we have this setting, nothing happens.
30 | ///
31 | public bool Enable { get; set; } = Defaults.Enable;
32 |
33 | ///
34 | /// Gets a value indicating whether to enable the API.
35 | ///
36 | ///
37 | /// Default value is true.
38 | /// The API is used by the Visual Studio extension and the console tool to talk to Umbraco
39 | /// and retrieve the content types. It needs to be enabled so the extension & tool can work.
40 | ///
41 | public bool EnableApi { get; set; } = Defaults.EnableApi;
42 |
43 | ///
44 | /// Gets a value indicating whether back-office integration is enabled.
45 | ///
46 | public bool EnableBackOffice { get; set; } = Defaults.EnableBackOffice;
47 |
48 | ///
49 | /// Gets a value indicating whether we should enable the models factory.
50 | ///
51 | /// Default value is true because no factory is enabled by default in Umbraco.
52 | public bool EnableFactory { get; set; } = Defaults.EnableFactory;
53 |
54 | ///
55 | /// Gets the models mode.
56 | ///
57 | public ModelsMode ModelsMode { get; set; } = Defaults.ModelsMode;
58 |
59 | ///
60 | /// Gets the models namespace.
61 | ///
62 | /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied.
63 | public string ModelsNamespace { get; set; } = Defaults.ModelsNamespace;
64 | public string AssemblyName { get; set; } = Defaults.ModelsNamespace;
65 | ///
66 | /// Gets the Roslyn parser language version.
67 | ///
68 | public LanguageVersion LanguageVersion { get; set; } = Defaults.LanguageVersion;
69 |
70 | ///
71 | /// Gets a value indicating whether we should flag out-of-date models.
72 | ///
73 | /// Models become out-of-date when data types or content types are updated. When this
74 | /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are
75 | /// generated through the dashboard, the files is cleared. Default value is false.
76 | public bool FlagOutOfDateModels { get; set; } = Defaults.FlagOutOfDateModels;
77 |
78 | ///
79 | /// Gets the models directory.
80 | ///
81 | /// Default is ~/App_Data/Models but that can be changed.
82 | public string ModelsDirectory { get; set; } = Defaults.ModelsDirectory;
83 |
84 | ///
85 | /// Gets a value indicating whether to accept an unsafe value for ModelsDirectory.
86 | ///
87 | /// An unsafe value is an absolute path, or a relative path pointing outside
88 | /// of the website root.
89 | public bool AcceptUnsafeModelsDirectory { get; set; } = Defaults.AcceptUnsafeModelsDirectory;
90 |
91 | ///
92 | /// Gets a value indicating the debug log level.
93 | ///
94 | /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe).
95 | public int DebugLevel { get; set; } = Defaults.DebugLevel;
96 |
97 | ///
98 | /// Gets a value indicating whether to serve the API.
99 | ///
100 | public bool IsApiServer => EnableApi && IsDebug;
101 |
102 | ///
103 | /// Gets a value indicating whether system.web/compilation/@debug is true.
104 | ///
105 | public bool IsDebug
106 | {
107 | get
108 | {
109 | var section = (CompilationSection) ConfigurationManager.GetSection("system.web/compilation");
110 | return section != null && section.Debug;
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/Our.ModelsBuilder/Building/Generator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using Our.ModelsBuilder.Options;
6 | using Our.ModelsBuilder.Umbraco;
7 |
8 | namespace Our.ModelsBuilder.Building
9 | {
10 | public class Generator
11 | {
12 | private readonly ICodeFactory _codeFactory;
13 | private readonly ModelsBuilderOptions _options;
14 |
15 | public Generator(ICodeFactory codeFactory, ModelsBuilderOptions options)
16 | {
17 | _codeFactory = codeFactory;
18 | _options = options;
19 | }
20 |
21 | public void GenerateModels(string modelsDirectory, string modelsNamespace, string bin)
22 | {
23 | if (!Directory.Exists(modelsDirectory))
24 | Directory.CreateDirectory(modelsDirectory);
25 |
26 | // delete all existing generated files
27 | foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
28 | File.Delete(file);
29 |
30 | // get our (non-generated) files
31 | var files = Directory.GetFiles(modelsDirectory, "*.cs").ToDictionary(x => x, File.ReadAllText);
32 |
33 | var codeModel = CreateModels(modelsNamespace, files, (name, code) =>
34 | {
35 | var filename = Path.Combine(modelsDirectory, name + ".generated.cs");
36 | File.WriteAllText(filename, code);
37 | });
38 |
39 | // the idea was to calculate the current hash and to add it as an extra file to the compilation,
40 | // in order to be able to detect whether a DLL is consistent with an environment - however the
41 | // environment *might not* contain the local partial files, and thus it could be impossible to
42 | // calculate the hash. So... maybe that's not a good idea after all?
43 | /*
44 | var currentHash = HashHelper.Hash(ourFiles, typeModels);
45 | ourFiles["models.hash.cs"] = $@"using Our.ModelsBuilder;
46 | [assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")]
47 | ";
48 | */
49 |
50 | if (bin != null)
51 | {
52 | // build
53 | foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
54 | files[file] = File.ReadAllText(file);
55 | var compiler = new Compiler(_options.LanguageVersion);
56 | // FIXME what is the name of the DLL as soon as we accept several namespaces = an option?
57 | compiler.Compile(codeModel.AssemblyName, files, bin);
58 | }
59 |
60 | OutOfDateModelsStatus.Clear();
61 | }
62 |
63 | public Dictionary GetModels(string modelsNamespace, IDictionary files)
64 | {
65 | var generated = new Dictionary();
66 | CreateModels(modelsNamespace, files, (name, code) => generated[name] = code);
67 | return generated;
68 | }
69 |
70 | private CodeModel CreateModels(string modelsNamespace, IDictionary sources, Action acceptModel)
71 | {
72 | // get model data from Umbraco, and create a code model (via all the steps)
73 | var modelData = _codeFactory.CreateCodeModelDataSource().GetCodeModelData();
74 | var codeModel = CreateCodeModel(_codeFactory, sources, modelData, _options, modelsNamespace);
75 |
76 | // create a code writer
77 | var codeWriter = _codeFactory.CreateCodeWriter(codeModel);
78 |
79 | // write each model file
80 | foreach (var contentTypeModel in codeModel.ContentTypes.ContentTypes)
81 | {
82 | codeWriter.Reset();
83 | codeWriter.WriteModelFile(contentTypeModel);
84 |
85 | // detect name collision
86 | if (contentTypeModel.ClrName == codeModel.ModelInfosClassName)
87 | throw new InvalidOperationException($"Collision, cannot use {codeModel.ModelInfosClassName} for both a content type and the infos class.");
88 |
89 | acceptModel(contentTypeModel.ClrName, codeWriter.Code);
90 | }
91 |
92 | // write the info files
93 | codeWriter.Reset();
94 | codeWriter.WriteModelInfosFile();
95 | acceptModel(codeModel.ModelInfosClassName, codeWriter.Code);
96 |
97 | return codeModel;
98 | }
99 |
100 | public static CodeModel CreateCodeModel(ICodeFactory codeFactory, IDictionary sources, CodeModelData modelData, ModelsBuilderOptions options, string modelsNamespace = null)
101 | {
102 | // create an option builder
103 | var optionsBuilder = codeFactory.CreateCodeOptionsBuilder();
104 |
105 | // create a parser, and parse the (non-generated) files, updating the options builder
106 | var parser = codeFactory.CreateCodeParser();
107 | parser.Parse(sources, optionsBuilder, ReferencedAssemblies.References);
108 |
109 | // apply namespace - may come from e.g. the Visual Studio extension - FIXME no?
110 | if (!string.IsNullOrWhiteSpace(modelsNamespace))
111 | optionsBuilder.SetModelsNamespace(modelsNamespace);
112 |
113 | // create a code model builder, and build the code model
114 | var codeModelBuilder = codeFactory.CreateCodeModelBuilder(options, optionsBuilder.CodeOptions);
115 | return codeModelBuilder.Build(modelData);
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------