Compare commits

...

5 Commits

Author SHA1 Message Date
Stephan b3867d9d55 Post-merge fixes 2019-06-27 12:47:40 +02:00
Stephan 834aeee107 Fix slimmed down ModelsBuilder 2019-06-27 12:47:40 +02:00
Stephan f3643747b3 Publicize IEditorValidator 2019-06-27 12:47:20 +02:00
Stephan ee83c02424 Import slimmed down ModelsBuilder 2019-06-27 12:47:19 +02:00
Stephan 355a5e0eb4 Remove ModelsBuilder NuGet dependency, add project 2019-06-27 12:47:19 +02:00
54 changed files with 633 additions and 2279 deletions
+3
View File
@@ -51,14 +51,17 @@
<file src="$BuildTmp$\WebApp\bin\Umbraco.Web.dll" target="lib\net472\Umbraco.Web.dll" /> <file src="$BuildTmp$\WebApp\bin\Umbraco.Web.dll" target="lib\net472\Umbraco.Web.dll" />
<file src="$BuildTmp$\WebApp\bin\Umbraco.Web.UI.dll" target="lib\net472\Umbraco.Web.UI.dll" /> <file src="$BuildTmp$\WebApp\bin\Umbraco.Web.UI.dll" target="lib\net472\Umbraco.Web.UI.dll" />
<file src="$BuildTmp$\WebApp\bin\Umbraco.Examine.dll" target="lib\net472\Umbraco.Examine.dll" /> <file src="$BuildTmp$\WebApp\bin\Umbraco.Examine.dll" target="lib\net472\Umbraco.Examine.dll" />
<file src="$BuildTmp$\WebApp\bin\Umbraco.ModelsBuilder.dll" target="lib\net472\Umbraco.ModelsBuilder.dll" />
<!-- docs --> <!-- docs -->
<file src="$BuildTmp$\WebApp\bin\Umbraco.Web.xml" target="lib\net472\Umbraco.Web.xml" /> <file src="$BuildTmp$\WebApp\bin\Umbraco.Web.xml" target="lib\net472\Umbraco.Web.xml" />
<file src="$BuildTmp$\WebApp\bin\Umbraco.Web.UI.xml" target="lib\net472\Umbraco.Web.UI.xml" /> <file src="$BuildTmp$\WebApp\bin\Umbraco.Web.UI.xml" target="lib\net472\Umbraco.Web.UI.xml" />
<file src="$BuildTmp$\WebApp\bin\Umbraco.Examine.xml" target="lib\net472\Umbraco.Examine.xml" /> <file src="$BuildTmp$\WebApp\bin\Umbraco.Examine.xml" target="lib\net472\Umbraco.Examine.xml" />
<file src="$BuildTmp$\WebApp\bin\Umbraco.ModelsBuilder.xml" target="lib\net472\Umbraco.ModelsBuilder.xml" />
<!-- symbols --> <!-- symbols -->
<file src="$BuildTmp$\bin\Umbraco.Web.pdb" target="lib" /> <file src="$BuildTmp$\bin\Umbraco.Web.pdb" target="lib" />
<file src="$BuildTmp$\bin\Umbraco.Examine.pdb" target="lib" /> <file src="$BuildTmp$\bin\Umbraco.Examine.pdb" target="lib" />
<file src="$BuildTmp$\bin\Umbraco.ModelsBuilder.pdb" target="lib" />
</files> </files>
</package> </package>
-1
View File
@@ -25,7 +25,6 @@
not want this to happen as the alpha of the next major is, really, the next major already. not want this to happen as the alpha of the next major is, really, the next major already.
--> -->
<dependency id="Microsoft.AspNet.SignalR.Core" version="[2.4.0, 2.999999)" /> <dependency id="Microsoft.AspNet.SignalR.Core" version="[2.4.0, 2.999999)" />
<dependency id="Umbraco.ModelsBuilder.Ui" version="[8.1.0]" />
<dependency id="ImageProcessor.Web" version="[4.10.0.100,4.999999)" /> <dependency id="ImageProcessor.Web" version="[4.10.0.100,4.999999)" />
<dependency id="ImageProcessor.Web.Config" version="[2.5.0.100,2.999999)" /> <dependency id="ImageProcessor.Web.Config" version="[2.5.0.100,2.999999)" />
<dependency id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="[2.0.1,2.999999)" /> <dependency id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="[2.0.1,2.999999)" />
@@ -1,86 +0,0 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http.Controllers;
using System.Web.Security;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Membership;
namespace Umbraco.ModelsBuilder.Api
{
//TODO: This needs to be changed:
// * Authentication cannot happen in a filter, only Authorization
// * The filter must be an AuthorizationFilter, not an ActionFilter
// * Authorization must be done using the Umbraco logic - it is very specific for claim checking for ASP.Net Identity
// * Theoretically this shouldn't be required whatsoever because when we authenticate a request that has Basic Auth (i.e. for
// VS to work, it will add the correct Claims to the Identity and it will automatically be authorized.
//
// we *do* have POC supporting ASP.NET identity, however they require some config on the server
// we'll keep using this quick-and-dirty method for the time being
public class ApiBasicAuthFilter : System.Web.Http.Filters.ActionFilterAttribute // use the http one, not mvc, with api controllers!
{
private static readonly char[] Separator = ":".ToCharArray();
private readonly string _section;
public ApiBasicAuthFilter(string section)
{
_section = section;
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var user = Authenticate(actionContext.Request);
if (user == null || !user.AllowedSections.Contains(_section))
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
//else
//{
// // note - would that be a proper way to pass data to the controller?
// // see http://stevescodingblog.co.uk/basic-authentication-with-asp-net-webapi/
// actionContext.ControllerContext.RouteData.Values["umbraco-user"] = user;
//}
base.OnActionExecuting(actionContext);
}
catch
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
}
private static IUser Authenticate(HttpRequestMessage request)
{
var ah = request.Headers.Authorization;
if (ah == null || ah.Scheme != "Basic")
return null;
var token = ah.Parameter;
var credentials = Encoding.ASCII
.GetString(Convert.FromBase64String(token))
.Split(Separator);
if (credentials.Length != 2)
return null;
var username = ApiClient.DecodeTokenElement(credentials[0]);
var password = ApiClient.DecodeTokenElement(credentials[1]);
var providerKey = UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider;
var provider = Membership.Providers[providerKey];
if (provider == null || !provider.ValidateUser(username, password))
return null;
var user = Current.Services.UserService.GetByUsername(username);
if (!user.IsApproved || user.IsLockedOut)
return null;
return user;
}
}
}
-159
View File
@@ -1,159 +0,0 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Text;
namespace Umbraco.ModelsBuilder.Api
{
public class ApiClient
{
private readonly string _url;
private readonly string _user;
private readonly string _password;
private readonly JsonMediaTypeFormatter _formatter;
private readonly MediaTypeFormatter[] _formatters;
// fixme hardcoded?
// could be options - but we cannot "discover" them as the API client runs outside of the web app
// in addition, anything that references the controller forces API clients to reference Umbraco.Core
private const string ApiControllerUrl = "/Umbraco/BackOffice/ModelsBuilder/ModelsBuilderApi/";
public ApiClient(string url, string user, string password)
{
_url = url.TrimEnd('/');
_user = user;
_password = password;
_formatter = new JsonMediaTypeFormatter();
_formatters = new MediaTypeFormatter[] { _formatter };
}
private void SetBaseAddress(HttpClient client, string url)
{
try
{
client.BaseAddress = new Uri(url);
}
catch
{
throw new UriFormatException($"Invalid URI: the format of the URI \"{url}\" could not be determined.");
}
}
public void ValidateClientVersion()
{
// FIXME - add proxys support
var hch = new HttpClientHandler();
using (var client = new HttpClient(hch))
{
SetBaseAddress(client, _url);
Authorize(client);
var data = new ValidateClientVersionData
{
ClientVersion = ApiVersion.Current.Version,
MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient,
};
var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.ValidateClientVersion),
data, _formatter).Result;
// this is not providing enough details in case of an error - do our own reporting
//result.EnsureSuccessStatusCode();
EnsureSuccess(result);
}
}
public IDictionary<string, string> GetModels(Dictionary<string, string> ourFiles, string modelsNamespace)
{
// FIXME - add proxys support
var hch = new HttpClientHandler();
//hch.Proxy = new WebProxy("path.to.proxy", 8888);
//hch.UseProxy = true;
using (var client = new HttpClient(hch))
{
SetBaseAddress(client, _url);
Authorize(client);
var data = new GetModelsData
{
Namespace = modelsNamespace,
ClientVersion = ApiVersion.Current.Version,
MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient,
Files = ourFiles
};
var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.GetModels),
data, _formatter).Result;
// this is not providing enough details in case of an error - do our own reporting
//result.EnsureSuccessStatusCode();
EnsureSuccess(result);
var genFiles = result.Content.ReadAsAsync<IDictionary<string, string>>(_formatters).Result;
return genFiles;
}
}
private static void EnsureSuccess(HttpResponseMessage result)
{
if (result.IsSuccessStatusCode) return;
var text = result.Content.ReadAsStringAsync().Result;
throw new Exception($"Response status code does not indicate success ({result.StatusCode})\n{text}");
}
private void Authorize(HttpClient client)
{
AuthorizeBasic(client);
}
// fixme - for the time being, we don't cache the token and we auth on each API call
// not used at the moment
/*
private void AuthorizeIdentity(HttpClient client)
{
var formData = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("userName", _user),
new KeyValuePair<string, string>("password", _password),
});
var result = client.PostAsync(_url + UmbracoOAuthTokenUrl, formData).Result;
EnsureSuccess(result);
var token = result.Content.ReadAsAsync<TokenData>(_formatters).Result;
if (token.TokenType != "bearer")
throw new Exception($"Received invalid token type \"{token.TokenType}\", expected \"bearer\".");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
}
*/
private void AuthorizeBasic(HttpClient client)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes(EncodeTokenElement(_user) + ':' + EncodeTokenElement(_password))));
}
public static string EncodeTokenElement(string s)
{
return s.Replace("%", "%a").Replace(":", "%b");
}
public static string DecodeTokenElement(string s)
{
return s.Replace("%b", ":").Replace("%a", "%");
}
}
}
@@ -1,27 +0,0 @@
using System.Collections.Generic;
using System.Text;
using Umbraco.ModelsBuilder.Building;
using Umbraco.ModelsBuilder.Umbraco;
namespace Umbraco.ModelsBuilder.Api
{
internal static class ApiHelper
{
public static Dictionary<string, string> GetModels(UmbracoServices umbracoServices, string modelsNamespace, IDictionary<string, string> files)
{
var typeModels = umbracoServices.GetAllTypes();
var parseResult = new CodeParser().ParseWithReferencedAssemblies(files);
var builder = new TextBuilder(typeModels, parseResult, modelsNamespace);
var models = new Dictionary<string, string>();
foreach (var typeModel in builder.GetModelsToGenerate())
{
var sb = new StringBuilder();
builder.Generate(sb, typeModel);
models[typeModel.ClrName] = sb.ToString();
}
return models;
}
}
}
+9 -61
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Reflection; using System.Reflection;
using Semver;
namespace Umbraco.ModelsBuilder.Api namespace Umbraco.ModelsBuilder.Api
{ {
@@ -8,81 +9,28 @@ namespace Umbraco.ModelsBuilder.Api
/// </summary> /// </summary>
public class ApiVersion public class ApiVersion
{ {
#region Configure
// indicate the minimum version of the client API that is supported by this server's API.
// eg our Version = 4.8 but we support connections from VSIX down to version 3.2
// => as a server, we accept connections from client down to version ...
private static readonly Version MinClientVersionSupportedByServerConst = new Version(3, 0, 0, 0);
// indicate the minimum version of the server that can support the client API
// eg our Version = 4.8 and we know we're compatible with website server down to version 3.2
// => as a client, we tell the server down to version ... that it should accept us
private static readonly Version MinServerVersionSupportingClientConst = new Version(3, 0, 0, 0);
#endregion
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApiVersion"/> class. /// Initializes a new instance of the <see cref="ApiVersion"/> class.
/// </summary> /// </summary>
/// <param name="executingVersion">The currently executing version.</param> /// <param name="executingVersion">The currently executing version.</param>
/// <param name="minClientVersionSupportedByServer">The min client version supported by the server.</param> /// <exception cref="ArgumentNullException"></exception>
/// <param name="minServerVersionSupportingClient">An opt min server version supporting the client.</param> internal ApiVersion(SemVersion executingVersion)
internal ApiVersion(Version executingVersion, Version minClientVersionSupportedByServer, Version minServerVersionSupportingClient = null)
{ {
if (executingVersion == null) throw new ArgumentNullException(nameof(executingVersion)); Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion));
if (minClientVersionSupportedByServer == null) throw new ArgumentNullException(nameof(minClientVersionSupportedByServer));
Version = executingVersion;
MinClientVersionSupportedByServer = minClientVersionSupportedByServer;
MinServerVersionSupportingClient = minServerVersionSupportingClient;
} }
private static SemVersion CurrentAssemblyVersion
=> SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion);
/// <summary> /// <summary>
/// Gets the currently executing API version. /// Gets the currently executing API version.
/// </summary> /// </summary>
public static ApiVersion Current { get; } public static ApiVersion Current { get; }
= new ApiVersion(Assembly.GetExecutingAssembly().GetName().Version, = new ApiVersion(CurrentAssemblyVersion);
MinClientVersionSupportedByServerConst, MinServerVersionSupportingClientConst);
/// <summary> /// <summary>
/// Gets the executing version of the API. /// Gets the executing version of the API.
/// </summary> /// </summary>
public Version Version { get; } public SemVersion Version { get; }
/// <summary>
/// Gets the min client version supported by the server.
/// </summary>
public Version MinClientVersionSupportedByServer { get; }
/// <summary>
/// Gets the min server version supporting the client.
/// </summary>
public Version MinServerVersionSupportingClient { get; }
/// <summary>
/// Gets a value indicating whether the API server is compatible with a client.
/// </summary>
/// <param name="clientVersion">The client version.</param>
/// <param name="minServerVersionSupportingClient">An opt min server version supporting the client.</param>
/// <remarks>
/// <para>A client is compatible with a server if the client version is greater-or-equal _minClientVersionSupportedByServer
/// (ie client can be older than server, up to a point) AND the client version is lower-or-equal the server version
/// (ie client cannot be more recent than server) UNLESS the server .</para>
/// </remarks>
public bool IsCompatibleWith(Version clientVersion, Version minServerVersionSupportingClient = null)
{
// client cannot be older than server's min supported version
if (clientVersion < MinClientVersionSupportedByServer)
return false;
// if we know about this client (client is older than server), it is supported
if (clientVersion <= Version) // if we know about this client (client older than server)
return true;
// if we don't know about this client (client is newer than server),
// give server a chance to tell client it is, indeed, ok to support it
return minServerVersionSupportingClient != null && minServerVersionSupportingClient <= Version;
}
} }
} }
@@ -1,17 +0,0 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Umbraco.ModelsBuilder.Api
{
[DataContract]
public class GetModelsData : ValidateClientVersionData
{
[DataMember]
public string Namespace { get; set; }
[DataMember]
public IDictionary<string, string> Files { get; set; }
public override bool IsValid => base.IsValid && Files != null;
}
}
@@ -1,85 +0,0 @@
using System;
using System.Net;
using System.Net.Http;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Umbraco;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
namespace Umbraco.ModelsBuilder.Api
{
// read http://umbraco.com/follow-us/blog-archive/2014/1/17/heads-up,-breaking-change-coming-in-702-and-62.aspx
// read http://our.umbraco.org/forum/developers/api-questions/43025-Web-API-authentication
// UmbracoAuthorizedApiController :: /Umbraco/BackOffice/Zbu/ModelsBuilderApi/GetTypeModels
// UmbracoApiController :: /Umbraco/Zbu/ModelsBuilderApi/GetTypeModels ?? UNLESS marked with isbackoffice
//
// BEWARE! the controller url is hard-coded in ModelsBuilderApi and needs to be in sync!
[PluginController(ControllerArea)]
[IsBackOffice]
//[UmbracoApplicationAuthorize(Constants.Applications.Developer)] // see ApiBasicAuthFilter - that one would be for ASP.NET identity
public class ModelsBuilderApiController : UmbracoApiController // UmbracoAuthorizedApiController - for ASP.NET identity
{
public const string ControllerArea = "ModelsBuilder";
private readonly UmbracoServices _umbracoServices;
public ModelsBuilderApiController(UmbracoServices umbracoServices)
{
_umbracoServices = umbracoServices;
}
// invoked by the API
[System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers!
[ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth
public HttpResponseMessage ValidateClientVersion(ValidateClientVersionData data)
{
if (!UmbracoConfig.For.ModelsBuilder().ApiServer)
return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you.");
if (!ModelState.IsValid || data == null || !data.IsValid)
return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data.");
var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient);
return (checkResult.Success
? Request.CreateResponse(HttpStatusCode.OK, "OK", Configuration.Formatters.JsonFormatter)
: checkResult.Result);
}
// invoked by the API
[System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers!
[ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth
public HttpResponseMessage GetModels(GetModelsData data)
{
if (!UmbracoConfig.For.ModelsBuilder().ApiServer)
return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you.");
if (!ModelState.IsValid || data == null || !data.IsValid)
return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data.");
var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient);
if (!checkResult.Success)
return checkResult.Result;
var models = ApiHelper.GetModels(_umbracoServices, data.Namespace, data.Files);
return Request.CreateResponse(HttpStatusCode.OK, models, Configuration.Formatters.JsonFormatter);
}
private Attempt<HttpResponseMessage> CheckVersion(Version clientVersion, Version minServerVersionSupportingClient)
{
if (clientVersion == null)
return Attempt<HttpResponseMessage>.Fail(Request.CreateResponse(HttpStatusCode.Forbidden,
$"API version conflict: client version (<null>) is not compatible with server version({ApiVersion.Current.Version})."));
// minServerVersionSupportingClient can be null
var isOk = ApiVersion.Current.IsCompatibleWith(clientVersion, minServerVersionSupportingClient);
var response = isOk ? null : Request.CreateResponse(HttpStatusCode.Forbidden,
$"API version conflict: client version ({clientVersion}) is not compatible with server version({ApiVersion.Current.Version}).");
return Attempt<HttpResponseMessage>.If(isOk, response);
}
}
}
@@ -1,17 +0,0 @@
using System.Runtime.Serialization;
namespace Umbraco.ModelsBuilder.Api
{
[DataContract]
class TokenData
{
[DataMember(Name = "access_token")]
public string AccessToken { get; set; }
[DataMember(Name = "token_type")]
public string TokenType { get; set; }
[DataMember(Name = "expires_in")]
public int ExpiresIn { get; set; }
}
}
@@ -1,57 +0,0 @@
using System;
using System.Runtime.Serialization;
namespace Umbraco.ModelsBuilder.Api
{
[DataContract]
public class ValidateClientVersionData
{
// issues 32, 34... problems when serializing versions
//
// make sure System.Version objects are transfered as strings
// depending on the JSON serializer version, it looks like versions are causing issues
// see
// http://stackoverflow.com/questions/13170386/why-system-version-in-json-string-does-not-deserialize-correctly
//
// if the class is marked with [DataContract] then only properties marked with [DataMember]
// are serialized and the rest is ignored, see
// http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization
[DataMember]
public string ClientVersionString
{
get { return VersionToString(ClientVersion); }
set { ClientVersion = ParseVersion(value, false, "client"); }
}
[DataMember]
public string MinServerVersionSupportingClientString
{
get { return VersionToString(MinServerVersionSupportingClient); }
set { MinServerVersionSupportingClient = ParseVersion(value, true, "minServer"); }
}
// not serialized
public Version ClientVersion { get; set; }
public Version MinServerVersionSupportingClient { get; set; }
private static string VersionToString(Version version)
{
return version?.ToString() ?? "0.0.0.0";
}
private static Version ParseVersion(string value, bool canBeNull, string name)
{
if (string.IsNullOrWhiteSpace(value) && canBeNull)
return null;
Version version;
if (Version.TryParse(value, out version))
return version;
throw new ArgumentException($"Failed to parse \"{value}\" as {name} version.");
}
public virtual bool IsValid => ClientVersion != null;
}
}
+34 -120
View File
@@ -5,6 +5,7 @@ using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration; using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
@@ -25,7 +26,8 @@ namespace Umbraco.ModelsBuilder.Building
private readonly IList<TypeModel> _typeModels; private readonly IList<TypeModel> _typeModels;
protected Dictionary<string, string> ModelsMap { get; } = new Dictionary<string, string>(); protected Dictionary<string, string> ModelsMap { get; } = new Dictionary<string, string>();
protected ParseResult ParseResult { get; }
private static Config Config => Current.Configs.ModelsBuilder();
// the list of assemblies that will be 'using' by default // the list of assemblies that will be 'using' by default
protected readonly IList<string> TypesUsing = new List<string> protected readonly IList<string> TypesUsing = new List<string>
@@ -58,7 +60,7 @@ namespace Umbraco.ModelsBuilder.Building
/// <returns>The models to generate, ie those that are not ignored.</returns> /// <returns>The models to generate, ie those that are not ignored.</returns>
public IEnumerable<TypeModel> GetModelsToGenerate() public IEnumerable<TypeModel> GetModelsToGenerate()
{ {
return _typeModels.Where(x => !x.IsContentIgnored); return _typeModels;
} }
/// <summary> /// <summary>
@@ -72,27 +74,25 @@ namespace Umbraco.ModelsBuilder.Building
/// and the result of code parsing. /// and the result of code parsing.
/// </summary> /// </summary>
/// <param name="typeModels">The list of models to generate.</param> /// <param name="typeModels">The list of models to generate.</param>
/// <param name="parseResult">The result of code parsing.</param> protected Builder(IList<TypeModel> typeModels)
protected Builder(IList<TypeModel> typeModels, ParseResult parseResult) : this(typeModels, null)
{ { }
_typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels));
ParseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult));
Prepare();
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Builder"/> class with a list of models to generate, /// Initializes a new instance of the <see cref="Builder"/> class with a list of models to generate,
/// the result of code parsing, and a models namespace. /// the result of code parsing, and a models namespace.
/// </summary> /// </summary>
/// <param name="typeModels">The list of models to generate.</param> /// <param name="typeModels">The list of models to generate.</param>
/// <param name="parseResult">The result of code parsing.</param>
/// <param name="modelsNamespace">The models namespace.</param> /// <param name="modelsNamespace">The models namespace.</param>
protected Builder(IList<TypeModel> typeModels, ParseResult parseResult, string modelsNamespace) protected Builder(IList<TypeModel> typeModels, string modelsNamespace)
: this(typeModels, parseResult)
{ {
_typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels));
// can be null or empty, we'll manage // can be null or empty, we'll manage
ModelsNamespace = modelsNamespace; ModelsNamespace = modelsNamespace;
// but we want it to prepare
Prepare();
} }
// for unit tests only // for unit tests only
@@ -108,49 +108,9 @@ namespace Umbraco.ModelsBuilder.Building
/// </remarks> /// </remarks>
private void Prepare() private void Prepare()
{ {
var pureLive = UmbracoConfig.For.ModelsBuilder().ModelsMode == ModelsMode.PureLive; TypeModel.MapModelTypes(_typeModels, ModelsNamespace);
// mark IsContentIgnored models that we discovered should be ignored var pureLive = Config.ModelsMode == ModelsMode.PureLive;
// then propagate / ignore children of ignored contents
// ignore content = don't generate a class for it, don't generate children
foreach (var typeModel in _typeModels.Where(x => ParseResult.IsIgnored(x.Alias)))
typeModel.IsContentIgnored = true;
foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored && x.EnumerateBaseTypes().Any(xx => xx.IsContentIgnored)))
typeModel.IsContentIgnored = true;
// handle model renames
foreach (var typeModel in _typeModels.Where(x => ParseResult.IsContentRenamed(x.Alias)))
{
typeModel.ClrName = ParseResult.ContentClrName(typeModel.Alias);
typeModel.IsRenamed = true;
ModelsMap[typeModel.Alias] = typeModel.ClrName;
}
// handle implement
foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentImplement(x.Alias)))
{
typeModel.HasImplement = true;
}
// mark OmitBase models that we discovered already have a base class
foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentBase(ParseResult.ContentClrName(x.Alias) ?? x.ClrName)))
typeModel.HasBase = true;
foreach (var typeModel in _typeModels)
{
// mark IsRemoved properties that we discovered should be ignored
// ie is marked as ignored on type, or on any parent type
var tm = typeModel;
foreach (var property in typeModel.Properties
.Where(property => tm.EnumerateBaseTypes(true).Any(x => ParseResult.IsPropertyIgnored(ParseResult.ContentClrName(x.Alias) ?? x.ClrName, property.Alias))))
{
property.IsIgnored = true;
}
// handle property renames
foreach (var property in typeModel.Properties)
property.ClrName = ParseResult.PropertyClrName(ParseResult.ContentClrName(typeModel.Alias) ?? typeModel.ClrName, property.Alias) ?? property.ClrName;
}
// for the first two of these two tests, // for the first two of these two tests,
// always throw, even in purelive: cannot happen unless ppl start fidling with attributes to rename // always throw, even in purelive: cannot happen unless ppl start fidling with attributes to rename
@@ -158,22 +118,22 @@ namespace Umbraco.ModelsBuilder.Building
// for the last one, don't throw in purelive, see comment // for the last one, don't throw in purelive, see comment
// ensure we have no duplicates type names // ensure we have no duplicates type names
foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) foreach (var xx in _typeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1))
throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" throw new InvalidOperationException($"Type name \"{xx.Key}\" is used"
+ $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique."
+ " Consider using an attribute to assign different names to conflicting types."); + " Consider using an attribute to assign different names to conflicting types.");
// ensure we have no duplicates property names // ensure we have no duplicates property names
foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored)) foreach (var typeModel in _typeModels)
foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) foreach (var xx in typeModel.Properties.GroupBy(x => x.ClrName).Where(x => x.Count() > 1))
throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\""
+ $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique."
+ " Consider using an attribute to assign different names to conflicting properties."); + " Consider using an attribute to assign different names to conflicting properties.");
// ensure content & property type don't have identical name (csharp hates it) // ensure content & property type don't have identical name (csharp hates it)
foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored)) foreach (var typeModel in _typeModels)
{ {
foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored && x.ClrName == typeModel.ClrName)) foreach (var xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName))
{ {
if (!pureLive) if (!pureLive)
throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"."
@@ -204,7 +164,7 @@ namespace Umbraco.ModelsBuilder.Building
// collect all the (non-removed) types implemented at parent level // collect all the (non-removed) types implemented at parent level
// ie the parent content types and the mixins content types, recursively // ie the parent content types and the mixins content types, recursively
var parentImplems = new List<TypeModel>(); var parentImplems = new List<TypeModel>();
if (typeModel.BaseType != null && !typeModel.BaseType.IsContentIgnored) if (typeModel.BaseType != null)
TypeModel.CollectImplems(parentImplems, typeModel.BaseType); TypeModel.CollectImplems(parentImplems, typeModel.BaseType);
// interfaces we must declare we implement (initially empty) // interfaces we must declare we implement (initially empty)
@@ -212,7 +172,6 @@ namespace Umbraco.ModelsBuilder.Building
// and except those that are already declared at the parent level // and except those that are already declared at the parent level
// in other words, DeclaringInterfaces is "local mixins" // in other words, DeclaringInterfaces is "local mixins"
var declaring = typeModel.MixinTypes var declaring = typeModel.MixinTypes
.Where(x => !x.IsContentIgnored)
.Except(parentImplems); .Except(parentImplems);
typeModel.DeclaringInterfaces.AddRange(declaring); typeModel.DeclaringInterfaces.AddRange(declaring);
@@ -227,43 +186,16 @@ namespace Umbraco.ModelsBuilder.Building
typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems));
} }
// register using types // ensure elements don't inherit from non-elements
foreach (var usingNamespace in ParseResult.UsingNamespaces) foreach (var typeModel in _typeModels.Where(x => x.IsElement))
{ {
if (!TypesUsing.Contains(usingNamespace)) if (typeModel.BaseType != null && !typeModel.BaseType.IsElement)
TypesUsing.Add(usingNamespace); throw new Exception($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not.");
var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList();
if (errs.Count > 0)
throw new Exception($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not.");
} }
// discover static mixin methods
foreach (var typeModel in _typeModels)
typeModel.StaticMixinMethods.AddRange(ParseResult.StaticMixinMethods(typeModel.ClrName));
// handle ctor
foreach (var typeModel in _typeModels.Where(x => ParseResult.HasCtor(x.ClrName)))
typeModel.HasCtor = true;
}
private SemanticModel _ambiguousSymbolsModel;
private int _ambiguousSymbolsPos;
// internal for tests
internal void PrepareAmbiguousSymbols()
{
var codeBuilder = new StringBuilder();
foreach (var t in TypesUsing)
codeBuilder.AppendFormat("using {0};\n", t);
codeBuilder.AppendFormat("namespace {0}\n{{ }}\n", GetModelsNamespace());
var compiler = new Compiler();
SyntaxTree[] trees;
var compilation = compiler.GetCompilation("MyCompilation", new Dictionary<string, string> { { "code", codeBuilder.ToString() } }, out trees);
var tree = trees[0];
_ambiguousSymbolsModel = compilation.GetSemanticModel(tree);
var namespaceSyntax = tree.GetRoot().DescendantNodes().OfType<NamespaceDeclarationSyntax>().First();
//var namespaceSymbol = model.GetDeclaredSymbol(namespaceSyntax);
_ambiguousSymbolsPos = namespaceSyntax.OpenBraceToken.SpanStart;
} }
// looking for a simple symbol eg 'Umbraco' or 'String' // looking for a simple symbol eg 'Umbraco' or 'String'
@@ -273,20 +205,10 @@ namespace Umbraco.ModelsBuilder.Building
// - 1 symbol is found BUT not matching (implicitely ambiguous) // - 1 symbol is found BUT not matching (implicitely ambiguous)
protected bool IsAmbiguousSymbol(string symbol, string match) protected bool IsAmbiguousSymbol(string symbol, string match)
{ {
if (_ambiguousSymbolsModel == null) // cannot figure out is a symbol is ambiguous without Roslyn
PrepareAmbiguousSymbols(); // so... let's say everything is ambiguous - code won't be
if (_ambiguousSymbolsModel == null) // pretty but it'll work
throw new Exception("Could not prepare ambiguous symbols."); return true;
var symbols = _ambiguousSymbolsModel.LookupNamespacesAndTypes(_ambiguousSymbolsPos, null, symbol);
if (symbols.Length > 1) return true;
if (symbols.Length == 0) return false; // what else?
// only 1 - ensure it matches
var found = symbols[0].ToDisplayString();
var pos = found.IndexOf('<'); // generic?
if (pos > 0) found = found.Substring(0, pos); // strip
return found != match; // and compare
} }
internal string ModelsNamespaceForTests; internal string ModelsNamespaceForTests;
@@ -296,25 +218,17 @@ namespace Umbraco.ModelsBuilder.Building
if (ModelsNamespaceForTests != null) if (ModelsNamespaceForTests != null)
return ModelsNamespaceForTests; return ModelsNamespaceForTests;
// code attribute overrides everything
if (ParseResult.HasModelsNamespace)
return ParseResult.ModelsNamespace;
// if builder was initialized with a namespace, use that one // if builder was initialized with a namespace, use that one
if (!string.IsNullOrWhiteSpace(ModelsNamespace)) if (!string.IsNullOrWhiteSpace(ModelsNamespace))
return ModelsNamespace; return ModelsNamespace;
// default // default
// fixme - should NOT reference config here, should make ModelsNamespace mandatory // fixme - should NOT reference config here, should make ModelsNamespace mandatory
return UmbracoConfig.For.ModelsBuilder().ModelsNamespace; return Config.ModelsNamespace;
} }
protected string GetModelsBaseClassName(TypeModel type) protected string GetModelsBaseClassName(TypeModel type)
{ {
// code attribute overrides everything
if (ParseResult.HasModelsBaseClassName)
return ParseResult.ModelsBaseClassName;
// default // default
return type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; return type.IsElement ? "PublishedElementModel" : "PublishedContentModel";
} }
@@ -1,113 +0,0 @@
using System.CodeDom;
using System.Collections.Generic;
namespace Umbraco.ModelsBuilder.Building
{
// NOTE
// See nodes in Builder.cs class - that one does not work, is not complete,
// and was just some sort of experiment...
/// <summary>
/// Implements a builder that works by using CodeDom
/// </summary>
internal class CodeDomBuilder : Builder
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeDomBuilder"/> class with a list of models to generate.
/// </summary>
/// <param name="typeModels">The list of models to generate.</param>
public CodeDomBuilder(IList<TypeModel> typeModels)
: base(typeModels, null)
{ }
/// <summary>
/// Outputs a generated model to a code namespace.
/// </summary>
/// <param name="ns">The code namespace.</param>
/// <param name="typeModel">The model to generate.</param>
public void Generate(CodeNamespace ns, TypeModel typeModel)
{
// what about USING?
// what about references?
if (typeModel.IsMixin)
{
var i = new CodeTypeDeclaration("I" + typeModel.ClrName)
{
IsInterface = true,
IsPartial = true,
Attributes = MemberAttributes.Public
};
i.BaseTypes.Add(typeModel.BaseType == null ? "IPublishedContent" : "I" + typeModel.BaseType.ClrName);
foreach (var mixinType in typeModel.DeclaringInterfaces)
i.BaseTypes.Add(mixinType.ClrName);
i.Comments.Add(new CodeCommentStatement($"Mixin content Type {typeModel.Id} with alias \"{typeModel.Alias}\""));
foreach (var propertyModel in typeModel.Properties)
{
var p = new CodeMemberProperty
{
Name = propertyModel.ClrName,
Type = new CodeTypeReference(propertyModel.ModelClrType),
Attributes = MemberAttributes.Public,
HasGet = true,
HasSet = false
};
i.Members.Add(p);
}
}
var c = new CodeTypeDeclaration(typeModel.ClrName)
{
IsClass = true,
IsPartial = true,
Attributes = MemberAttributes.Public
};
c.BaseTypes.Add(typeModel.BaseType == null ? "PublishedContentModel" : typeModel.BaseType.ClrName);
// if it's a missing it implements its own interface
if (typeModel.IsMixin)
c.BaseTypes.Add("I" + typeModel.ClrName);
// write the mixins, if any, as interfaces
// only if not a mixin because otherwise the interface already has them
if (typeModel.IsMixin == false)
foreach (var mixinType in typeModel.DeclaringInterfaces)
c.BaseTypes.Add("I" + mixinType.ClrName);
foreach (var mixin in typeModel.MixinTypes)
c.BaseTypes.Add("I" + mixin.ClrName);
c.Comments.Add(new CodeCommentStatement($"Content Type {typeModel.Id} with alias \"{typeModel.Alias}\""));
foreach (var propertyModel in typeModel.Properties)
{
var p = new CodeMemberProperty
{
Name = propertyModel.ClrName,
Type = new CodeTypeReference(propertyModel.ModelClrType),
Attributes = MemberAttributes.Public,
HasGet = true,
HasSet = false
};
p.GetStatements.Add(new CodeMethodReturnStatement( // return
new CodeMethodInvokeExpression(
new CodeMethodReferenceExpression(
new CodeThisReferenceExpression(), // this
"Value", // .Value
new[] // <T>
{
new CodeTypeReference(propertyModel.ModelClrType)
}),
new CodeExpression[] // ("alias")
{
new CodePrimitiveExpression(propertyModel.Alias)
})));
c.Members.Add(p);
}
}
}
}
@@ -1,238 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.ModelsBuilder.Building
{
/// <summary>
/// Implements code parsing.
/// </summary>
/// <remarks>Parses user's code and look for generator's instructions.</remarks>
internal class CodeParser
{
/// <summary>
/// Parses a set of file.
/// </summary>
/// <param name="files">A set of (filename,content) representing content to parse.</param>
/// <returns>The result of the code parsing.</returns>
/// <remarks>The set of files is a dictionary of name, content.</remarks>
public ParseResult Parse(IDictionary<string, string> files)
{
return Parse(files, Enumerable.Empty<PortableExecutableReference>());
}
/// <summary>
/// Parses a set of file.
/// </summary>
/// <param name="files">A set of (filename,content) representing content to parse.</param>
/// <param name="references">Assemblies to reference in compilations.</param>
/// <returns>The result of the code parsing.</returns>
/// <remarks>The set of files is a dictionary of name, content.</remarks>
public ParseResult Parse(IDictionary<string, string> files, IEnumerable<PortableExecutableReference> references)
{
SyntaxTree[] trees;
var compiler = new Compiler { References = references };
var compilation = compiler.GetCompilation("Umbraco.ModelsBuilder.Generated", files, out trees);
var disco = new ParseResult();
foreach (var tree in trees)
Parse(disco, compilation, tree);
return disco;
}
public ParseResult ParseWithReferencedAssemblies(IDictionary<string, string> files)
{
return Parse(files, ReferencedAssemblies.References);
}
private static void Parse(ParseResult disco, CSharpCompilation compilation, SyntaxTree tree)
{
var model = compilation.GetSemanticModel(tree);
//we quite probably have errors but that is normal
//var diags = model.GetDiagnostics();
var classDecls = tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>();
foreach (var classSymbol in classDecls.Select(x => model.GetDeclaredSymbol(x)))
{
ParseClassSymbols(disco, classSymbol);
var baseClassSymbol = classSymbol.BaseType;
if (baseClassSymbol != null)
//disco.SetContentBaseClass(SymbolDisplay.ToDisplayString(classSymbol), SymbolDisplay.ToDisplayString(baseClassSymbol));
disco.SetContentBaseClass(classSymbol.Name, baseClassSymbol.Name);
var interfaceSymbols = classSymbol.Interfaces;
disco.SetContentInterfaces(classSymbol.Name, //SymbolDisplay.ToDisplayString(classSymbol),
interfaceSymbols.Select(x => x.Name)); //SymbolDisplay.ToDisplayString(x)));
var hasCtor = classSymbol.Constructors
.Any(x =>
{
if (x.IsStatic) return false;
if (x.Parameters.Length != 1) return false;
var type1 = x.Parameters[0].Type;
var type2 = typeof (IPublishedContent);
return type1.ToDisplayString() == type2.FullName;
});
if (hasCtor)
disco.SetHasCtor(classSymbol.Name);
foreach (var propertySymbol in classSymbol.GetMembers().Where(x => x is IPropertySymbol))
ParsePropertySymbols(disco, classSymbol, propertySymbol);
foreach (var staticMethodSymbol in classSymbol.GetMembers().Where(x => x is IMethodSymbol))
ParseMethodSymbol(disco, classSymbol, staticMethodSymbol);
}
var interfaceDecls = tree.GetRoot().DescendantNodes().OfType<InterfaceDeclarationSyntax>();
foreach (var interfaceSymbol in interfaceDecls.Select(x => model.GetDeclaredSymbol(x)))
{
ParseClassSymbols(disco, interfaceSymbol);
var interfaceSymbols = interfaceSymbol.Interfaces;
disco.SetContentInterfaces(interfaceSymbol.Name, //SymbolDisplay.ToDisplayString(interfaceSymbol),
interfaceSymbols.Select(x => x.Name)); // SymbolDisplay.ToDisplayString(x)));
}
ParseAssemblySymbols(disco, compilation.Assembly);
}
private static void ParseClassSymbols(ParseResult disco, ISymbol symbol)
{
foreach (var attrData in symbol.GetAttributes())
{
var attrClassSymbol = attrData.AttributeClass;
// handle errors
if (attrClassSymbol is IErrorTypeSymbol) continue;
if (attrData.AttributeConstructor == null) continue;
var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol);
switch (attrClassName)
{
case "Umbraco.ModelsBuilder.IgnorePropertyTypeAttribute":
var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value;
disco.SetIgnoredProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToIgnore);
break;
case "Umbraco.ModelsBuilder.RenamePropertyTypeAttribute":
var propertyAliasToRename = (string)attrData.ConstructorArguments[0].Value;
var propertyRenamed = (string)attrData.ConstructorArguments[1].Value;
disco.SetRenamedProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToRename, propertyRenamed);
break;
// that one causes all sorts of issues with references to Umbraco.Core in Roslyn
//case "Umbraco.Core.Models.PublishedContent.PublishedContentModelAttribute":
// var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value;
// disco.SetRenamedContent(contentAliasToRename, symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/);
// break;
case "Umbraco.ModelsBuilder.ImplementContentTypeAttribute":
var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value;
disco.SetRenamedContent(contentAliasToRename, symbol.Name, true /*SymbolDisplay.ToDisplayString(symbol)*/);
break;
}
}
}
private static void ParsePropertySymbols(ParseResult disco, ISymbol classSymbol, ISymbol symbol)
{
foreach (var attrData in symbol.GetAttributes())
{
var attrClassSymbol = attrData.AttributeClass;
// handle errors
if (attrClassSymbol is IErrorTypeSymbol) continue;
if (attrData.AttributeConstructor == null) continue;
var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol);
// ReSharper disable once SwitchStatementMissingSomeCases
switch (attrClassName)
{
case "Umbraco.ModelsBuilder.ImplementPropertyTypeAttribute":
var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value;
disco.SetIgnoredProperty(classSymbol.Name /*SymbolDisplay.ToDisplayString(classSymbol)*/, propertyAliasToIgnore);
break;
}
}
}
private static void ParseAssemblySymbols(ParseResult disco, ISymbol symbol)
{
foreach (var attrData in symbol.GetAttributes())
{
var attrClassSymbol = attrData.AttributeClass;
// handle errors
if (attrClassSymbol is IErrorTypeSymbol) continue;
if (attrData.AttributeConstructor == null) continue;
var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol);
switch (attrClassName)
{
case "Umbraco.ModelsBuilder.IgnoreContentTypeAttribute":
var contentAliasToIgnore = (string)attrData.ConstructorArguments[0].Value;
// see notes in IgnoreContentTypeAttribute
//var ignoreContent = (bool)attrData.ConstructorArguments[1].Value;
//var ignoreMixin = (bool)attrData.ConstructorArguments[1].Value;
//var ignoreMixinProperties = (bool)attrData.ConstructorArguments[1].Value;
disco.SetIgnoredContent(contentAliasToIgnore /*, ignoreContent, ignoreMixin, ignoreMixinProperties*/);
break;
case "Umbraco.ModelsBuilder.RenameContentTypeAttribute":
var contentAliasToRename = (string) attrData.ConstructorArguments[0].Value;
var contentRenamed = (string)attrData.ConstructorArguments[1].Value;
disco.SetRenamedContent(contentAliasToRename, contentRenamed, false);
break;
case "Umbraco.ModelsBuilder.ModelsBaseClassAttribute":
var modelsBaseClass = (INamedTypeSymbol) attrData.ConstructorArguments[0].Value;
if (modelsBaseClass is IErrorTypeSymbol)
throw new Exception($"Invalid base class type \"{modelsBaseClass.Name}\".");
disco.SetModelsBaseClassName(SymbolDisplay.ToDisplayString(modelsBaseClass));
break;
case "Umbraco.ModelsBuilder.ModelsNamespaceAttribute":
var modelsNamespace= (string) attrData.ConstructorArguments[0].Value;
disco.SetModelsNamespace(modelsNamespace);
break;
case "Umbraco.ModelsBuilder.ModelsUsingAttribute":
var usingNamespace = (string)attrData.ConstructorArguments[0].Value;
disco.SetUsingNamespace(usingNamespace);
break;
}
}
}
private static void ParseMethodSymbol(ParseResult disco, ISymbol classSymbol, ISymbol symbol)
{
var methodSymbol = symbol as IMethodSymbol;
if (methodSymbol == null
|| !methodSymbol.IsStatic
|| methodSymbol.IsGenericMethod
|| methodSymbol.ReturnsVoid
|| methodSymbol.IsExtensionMethod
|| methodSymbol.Parameters.Length != 1)
return;
var returnType = methodSymbol.ReturnType;
var paramSymbol = methodSymbol.Parameters[0];
var paramType = paramSymbol.Type;
// cannot do this because maybe the param type is ISomething and we don't have
// that type yet - will be generated - so cannot put any condition on it really
//const string iPublishedContent = "Umbraco.Core.Models.IPublishedContent";
//var implements = paramType.AllInterfaces.Any(x => x.ToDisplayString() == iPublishedContent);
//if (!implements)
// return;
disco.SetStaticMixinMethod(classSymbol.Name, methodSymbol.Name, returnType.Name, paramType.Name);
}
}
}
@@ -1,171 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
namespace Umbraco.ModelsBuilder.Building
{
// main Roslyn compiler
internal class Compiler
{
private readonly LanguageVersion _languageVersion;
public Compiler()
: this(UmbracoConfig.For.ModelsBuilder().LanguageVersion)
{ }
public Compiler(LanguageVersion languageVersion)
{
_languageVersion = languageVersion;
References = ReferencedAssemblies.References;
Debug = HttpContext.Current != null && HttpContext.Current.IsDebuggingEnabled;
}
// gets or sets the references
public IEnumerable<PortableExecutableReference> References { get; set; }
public bool Debug { get; set; }
// gets a compilation
public CSharpCompilation GetCompilation(string assemblyName, IDictionary<string, string> files)
{
SyntaxTree[] trees;
return GetCompilation(assemblyName, files, out trees);
}
// gets a compilation
// used by CodeParser to get a "compilation" of the existing files
public CSharpCompilation GetCompilation(string assemblyName, IDictionary<string, string> files, out SyntaxTree[] trees)
{
var options = new CSharpParseOptions(_languageVersion);
trees = files.Select(x =>
{
var text = x.Value;
var tree = CSharpSyntaxTree.ParseText(text, /*options:*/ options);
var diagnostic = tree.GetDiagnostics().FirstOrDefault(y => y.Severity == DiagnosticSeverity.Error);
if (diagnostic != null)
ThrowExceptionFromDiagnostic(x.Key, x.Value, diagnostic);
return tree;
}).ToArray();
var refs = References;
var compilationOptions = new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default,
optimizationLevel: Debug ? OptimizationLevel.Debug : OptimizationLevel.Release
);
var compilation = CSharpCompilation.Create(
assemblyName,
/*syntaxTrees:*/ trees,
/*references:*/ refs,
compilationOptions);
return compilation;
}
// compile files into a Dll
// used by ModelsBuilderBackOfficeController in [Live]Dll mode, to compile the models to disk
public void Compile(string assemblyName, IDictionary<string, string> files, string binPath)
{
var assemblyPath = Path.Combine(binPath, assemblyName + ".dll");
using (var stream = new FileStream(assemblyPath, FileMode.Create))
{
Compile(assemblyName, files, stream);
}
// this is how we'd create the pdb:
/*
var pdbPath = Path.Combine(binPath, assemblyName + ".pdb");
// create the compilation
var compilation = GetCompilation(assemblyName, files);
// check diagnostics for errors (not warnings)
foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error))
ThrowExceptionFromDiagnostic(files, diag);
// emit
var result = compilation.Emit(assemblyPath, pdbPath);
if (result.Success) return;
// deal with errors
var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error);
ThrowExceptionFromDiagnostic(files, diagnostic);
*/
}
// compile files into an assembly
public Assembly Compile(string assemblyName, IDictionary<string, string> files)
{
using (var stream = new MemoryStream())
{
Compile(assemblyName, files, stream);
return Assembly.Load(stream.GetBuffer());
}
}
// compile one file into an assembly
public Assembly Compile(string assemblyName, string path, string code)
{
using (var stream = new MemoryStream())
{
Compile(assemblyName, new Dictionary<string, string> { { path, code } }, stream);
return Assembly.Load(stream.GetBuffer());
}
}
// compiles files into a stream
public void Compile(string assemblyName, IDictionary<string, string> files, Stream stream)
{
// create the compilation
var compilation = GetCompilation(assemblyName, files);
// check diagnostics for errors (not warnings)
foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error))
ThrowExceptionFromDiagnostic(files, diag);
// emit
var result = compilation.Emit(stream);
if (result.Success) return;
// deal with errors
var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error);
ThrowExceptionFromDiagnostic(files, diagnostic);
}
// compiles one file into a stream
public void Compile(string assemblyName, string path, string code, Stream stream)
{
Compile(assemblyName, new Dictionary<string, string> { { path, code } }, stream);
}
private static void ThrowExceptionFromDiagnostic(IDictionary<string, string> files, Diagnostic diagnostic)
{
var message = diagnostic.GetMessage();
if (diagnostic.Location == Location.None)
throw new CompilerException(message);
var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1;
var path = diagnostic.Location.SourceTree.FilePath;
var code = files.ContainsKey(path) ? files[path] : string.Empty;
throw new CompilerException(message, path, code, position);
}
private static void ThrowExceptionFromDiagnostic(string path, string code, Diagnostic diagnostic)
{
var message = diagnostic.GetMessage();
if (diagnostic.Location == Location.None)
throw new CompilerException(message);
var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1;
throw new CompilerException(message, path, code, position);
}
}
}
@@ -1,25 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder.Building
{
public class CompilerException : Exception
{
public CompilerException(string message)
: base(message)
{ }
public CompilerException(string message, string path, string sourceCode, int line)
: base(message)
{
Path = path;
SourceCode = sourceCode;
Line = line;
}
public string Path { get; } = string.Empty;
public string SourceCode { get; } = string.Empty;
public int Line { get; } = -1;
}
}
@@ -1,275 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.ModelsBuilder.Building
{
/// <summary>
/// Contains the result of a code parsing.
/// </summary>
internal class ParseResult
{
// "alias" is the umbraco alias
// content "name" is the complete name eg Foo.Bar.Name
// property "name" is just the local name
// see notes in IgnoreContentTypeAttribute
private readonly HashSet<string> _ignoredContent
= new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
//private readonly HashSet<string> _ignoredMixin
// = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
//private readonly HashSet<string> _ignoredMixinProperties
// = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
private readonly Dictionary<string, string> _renamedContent
= new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
private readonly HashSet<string> _withImplementContent
= new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
private readonly Dictionary<string, HashSet<string>> _ignoredProperty
= new Dictionary<string, HashSet<string>>();
private readonly Dictionary<string, Dictionary<string, string>> _renamedProperty
= new Dictionary<string, Dictionary<string, string>>();
private readonly Dictionary<string, string> _contentBase
= new Dictionary<string, string>();
private readonly Dictionary<string, string[]> _contentInterfaces
= new Dictionary<string, string[]>();
private readonly List<string> _usingNamespaces
= new List<string>();
private readonly Dictionary<string, List<StaticMixinMethodInfo>> _staticMixins
= new Dictionary<string, List<StaticMixinMethodInfo>>();
private readonly HashSet<string> _withCtor
= new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
public static readonly ParseResult Empty = new ParseResult();
private class StaticMixinMethodInfo
{
public StaticMixinMethodInfo(string contentName, string methodName, string returnType, string paramType)
{
ContentName = contentName;
MethodName = methodName;
//ReturnType = returnType;
//ParamType = paramType;
}
// short name eg Type1
public string ContentName { get; private set; }
// short name eg GetProp1
public string MethodName { get; private set; }
// those types cannot be FQ because when parsing, some of them
// might not exist since we're generating them... and so prob.
// that info is worthless - not using it anyway at the moment...
//public string ReturnType { get; private set; }
//public string ParamType { get; private set; }
}
#region Declare
// content with that alias should not be generated
// alias can end with a * (wildcard)
public void SetIgnoredContent(string contentAlias /*, bool ignoreContent, bool ignoreMixin, bool ignoreMixinProperties*/)
{
//if (ignoreContent)
_ignoredContent.Add(contentAlias);
//if (ignoreMixin)
// _ignoredMixin.Add(contentAlias);
//if (ignoreMixinProperties)
// _ignoredMixinProperties.Add(contentAlias);
}
// content with that alias should be generated with a different name
public void SetRenamedContent(string contentAlias, string contentName, bool withImplement)
{
_renamedContent[contentAlias] = contentName;
if (withImplement)
_withImplementContent.Add(contentAlias);
}
// property with that alias should not be generated
// applies to content name and any content that implements it
// here, contentName may be an interface
// alias can end with a * (wildcard)
public void SetIgnoredProperty(string contentName, string propertyAlias)
{
HashSet<string> ignores;
if (!_ignoredProperty.TryGetValue(contentName, out ignores))
ignores = _ignoredProperty[contentName] = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
ignores.Add(propertyAlias);
}
// property with that alias should be generated with a different name
// applies to content name and any content that implements it
// here, contentName may be an interface
public void SetRenamedProperty(string contentName, string propertyAlias, string propertyName)
{
Dictionary<string, string> renames;
if (!_renamedProperty.TryGetValue(contentName, out renames))
renames = _renamedProperty[contentName] = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
renames[propertyAlias] = propertyName;
}
// content with that name has a base class so no need to generate one
public void SetContentBaseClass(string contentName, string baseName)
{
if (baseName.ToLowerInvariant() != "object")
_contentBase[contentName] = baseName;
}
// content with that name implements the interfaces
public void SetContentInterfaces(string contentName, IEnumerable<string> interfaceNames)
{
_contentInterfaces[contentName] = interfaceNames.ToArray();
}
public void SetModelsBaseClassName(string modelsBaseClassName)
{
ModelsBaseClassName = modelsBaseClassName;
}
public void SetModelsNamespace(string modelsNamespace)
{
ModelsNamespace = modelsNamespace;
}
public void SetUsingNamespace(string usingNamespace)
{
_usingNamespaces.Add(usingNamespace);
}
public void SetStaticMixinMethod(string contentName, string methodName, string returnType, string paramType)
{
if (!_staticMixins.ContainsKey(contentName))
_staticMixins[contentName] = new List<StaticMixinMethodInfo>();
_staticMixins[contentName].Add(new StaticMixinMethodInfo(contentName, methodName, returnType, paramType));
}
public void SetHasCtor(string contentName)
{
_withCtor.Add(contentName);
}
#endregion
#region Query
public bool IsIgnored(string contentAlias)
{
return IsContentOrMixinIgnored(contentAlias, _ignoredContent);
}
//public bool IsMixinIgnored(string contentAlias)
//{
// return IsContentOrMixinIgnored(contentAlias, _ignoredMixin);
//}
//public bool IsMixinPropertiesIgnored(string contentAlias)
//{
// return IsContentOrMixinIgnored(contentAlias, _ignoredMixinProperties);
//}
private static bool IsContentOrMixinIgnored(string contentAlias, HashSet<string> ignored)
{
if (ignored.Contains(contentAlias)) return true;
return ignored
.Where(x => x.EndsWith("*"))
.Select(x => x.Substring(0, x.Length - 1))
.Any(x => contentAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase));
}
public bool HasContentBase(string contentName)
{
return _contentBase.ContainsKey(contentName);
}
public bool IsContentRenamed(string contentAlias)
{
return _renamedContent.ContainsKey(contentAlias);
}
public bool HasContentImplement(string contentAlias)
{
return _withImplementContent.Contains(contentAlias);
}
public string ContentClrName(string contentAlias)
{
string name;
return (_renamedContent.TryGetValue(contentAlias, out name)) ? name : null;
}
public bool IsPropertyIgnored(string contentName, string propertyAlias)
{
HashSet<string> ignores;
if (_ignoredProperty.TryGetValue(contentName, out ignores))
{
if (ignores.Contains(propertyAlias)) return true;
if (ignores
.Where(x => x.EndsWith("*"))
.Select(x => x.Substring(0, x.Length - 1))
.Any(x => propertyAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase)))
return true;
}
string baseName;
if (_contentBase.TryGetValue(contentName, out baseName)
&& IsPropertyIgnored(baseName, propertyAlias)) return true;
string[] interfaceNames;
if (_contentInterfaces.TryGetValue(contentName, out interfaceNames)
&& interfaceNames.Any(interfaceName => IsPropertyIgnored(interfaceName, propertyAlias))) return true;
return false;
}
public string PropertyClrName(string contentName, string propertyAlias)
{
Dictionary<string, string> renames;
string name;
if (_renamedProperty.TryGetValue(contentName, out renames)
&& renames.TryGetValue(propertyAlias, out name)) return name;
string baseName;
if (_contentBase.TryGetValue(contentName, out baseName)
&& null != (name = PropertyClrName(baseName, propertyAlias))) return name;
string[] interfaceNames;
if (_contentInterfaces.TryGetValue(contentName, out interfaceNames)
&& null != (name = interfaceNames
.Select(interfaceName => PropertyClrName(interfaceName, propertyAlias))
.FirstOrDefault(x => x != null))) return name;
return null;
}
public bool HasModelsBaseClassName
{
get { return !string.IsNullOrWhiteSpace(ModelsBaseClassName); }
}
public string ModelsBaseClassName { get; private set; }
public bool HasModelsNamespace
{
get { return !string.IsNullOrWhiteSpace(ModelsNamespace); }
}
public string ModelsNamespace { get; private set; }
public IEnumerable<string> UsingNamespaces
{
get { return _usingNamespaces; }
}
public IEnumerable<string> StaticMixinMethods(string contentName)
{
return _staticMixins.ContainsKey(contentName)
? _staticMixins[contentName].Select(x => x.MethodName)
: Enumerable.Empty<string>() ;
}
public bool HasCtor(string contentName)
{
return _withCtor.Contains(contentName);
}
#endregion
}
}
@@ -41,11 +41,6 @@ namespace Umbraco.ModelsBuilder.Building
/// </summary> /// </summary>
public string ClrTypeName; public string ClrTypeName;
/// <summary>
/// Gets a value indicating whether this property should be excluded from generation.
/// </summary>
public bool IsIgnored;
/// <summary> /// <summary>
/// Gets the generation errors for the property. /// Gets the generation errors for the property.
/// </summary> /// </summary>
@@ -3,8 +3,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration; using Umbraco.Core.Configuration;
using Umbraco.Core.Models.PublishedContent; using Umbraco.ModelsBuilder.Api;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
namespace Umbraco.ModelsBuilder.Building namespace Umbraco.ModelsBuilder.Building
@@ -19,9 +20,8 @@ namespace Umbraco.ModelsBuilder.Building
/// and the result of code parsing. /// and the result of code parsing.
/// </summary> /// </summary>
/// <param name="typeModels">The list of models to generate.</param> /// <param name="typeModels">The list of models to generate.</param>
/// <param name="parseResult">The result of code parsing.</param> public TextBuilder(IList<TypeModel> typeModels)
public TextBuilder(IList<TypeModel> typeModels, ParseResult parseResult) : base(typeModels)
: base(typeModels, parseResult)
{ } { }
/// <summary> /// <summary>
@@ -29,21 +29,22 @@ namespace Umbraco.ModelsBuilder.Building
/// the result of code parsing, and a models namespace. /// the result of code parsing, and a models namespace.
/// </summary> /// </summary>
/// <param name="typeModels">The list of models to generate.</param> /// <param name="typeModels">The list of models to generate.</param>
/// <param name="parseResult">The result of code parsing.</param>
/// <param name="modelsNamespace">The models namespace.</param> /// <param name="modelsNamespace">The models namespace.</param>
public TextBuilder(IList<TypeModel> typeModels, ParseResult parseResult, string modelsNamespace) public TextBuilder(IList<TypeModel> typeModels, string modelsNamespace)
: base(typeModels, parseResult, modelsNamespace) : base(typeModels, modelsNamespace)
{ } { }
// internal for unit tests only // internal for unit tests only
internal TextBuilder() internal TextBuilder()
{ } { }
private static Config Config => Current.Configs.ModelsBuilder();
/// <summary> /// <summary>
/// Outputs a generated model to a string builder. /// Outputs a generated model to a string builder.
/// </summary> /// </summary>
/// <param name="sb">The string builder.</param> /// <param name="sb">The string builder.</param>
/// <param name="typeModel">The model to generate.</param> /// <param name="typeModel">The model to generate.</param>
public void Generate(StringBuilder sb, TypeModel typeModel) public void Generate(StringBuilder sb, TypeModel typeModel)
{ {
WriteHeader(sb); WriteHeader(sb);
@@ -97,6 +98,20 @@ namespace Umbraco.ModelsBuilder.Building
TextHeaderWriter.WriteHeader(sb); TextHeaderWriter.WriteHeader(sb);
} }
// writes an attribute that identifies code generated by a tool
// (helps reduce warnings, tools such as FxCop use it)
// see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107
// see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute
// see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/
//
// note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
// and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
//
private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs)
{
sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder\", \"{1}\")]\n", tabs, ApiVersion.Current.Version);
}
private void WriteContentType(StringBuilder sb, TypeModel type) private void WriteContentType(StringBuilder sb, TypeModel type)
{ {
string sep; string sep;
@@ -104,11 +119,11 @@ namespace Umbraco.ModelsBuilder.Building
if (type.IsMixin) if (type.IsMixin)
{ {
// write the interface declaration // write the interface declaration
sb.AppendFormat("\t// Mixin content Type {0} with alias \"{1}\"\n", type.Id, type.Alias); sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias);
if (!string.IsNullOrWhiteSpace(type.Name)) if (!string.IsNullOrWhiteSpace(type.Name))
sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name)); sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name));
sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName);
var implements = type.BaseType == null || type.BaseType.IsContentIgnored var implements = type.BaseType == null
? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent"))
: type.BaseType.ClrName; : type.BaseType.ClrName;
if (implements != null) if (implements != null)
@@ -126,7 +141,7 @@ namespace Umbraco.ModelsBuilder.Building
// write the properties - only the local (non-ignored) ones, we're an interface // write the properties - only the local (non-ignored) ones, we're an interface
var more = false; var more = false;
foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) foreach (var prop in type.Properties.OrderBy(x => x.ClrName))
{ {
if (more) sb.Append("\n"); if (more) sb.Append("\n");
more = true; more = true;
@@ -137,8 +152,6 @@ namespace Umbraco.ModelsBuilder.Building
} }
// write the class declaration // write the class declaration
if (type.IsRenamed)
sb.AppendFormat("\t// Content Type {0} with alias \"{1}\"\n", type.Id, type.Alias);
if (!string.IsNullOrWhiteSpace(type.Name)) if (!string.IsNullOrWhiteSpace(type.Name))
sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name)); sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name));
// cannot do it now. see note in ImplementContentTypeAttribute // cannot do it now. see note in ImplementContentTypeAttribute
@@ -148,7 +161,7 @@ namespace Umbraco.ModelsBuilder.Building
sb.AppendFormat("\tpublic partial class {0}", type.ClrName); sb.AppendFormat("\tpublic partial class {0}", type.ClrName);
var inherits = type.HasBase var inherits = type.HasBase
? null // has its own base already ? null // has its own base already
: (type.BaseType == null || type.BaseType.IsContentIgnored : (type.BaseType == null
? GetModelsBaseClassName(type) ? GetModelsBaseClassName(type)
: type.BaseType.ClrName); : type.BaseType.ClrName);
if (inherits != null) if (inherits != null)
@@ -178,22 +191,25 @@ namespace Umbraco.ModelsBuilder.Building
// as 'new' since parent has its own - or maybe not - disable warning // as 'new' since parent has its own - or maybe not - disable warning
sb.Append("\t\t// helpers\n"); sb.Append("\t\t// helpers\n");
sb.Append("#pragma warning disable 0109 // new is redundant\n"); sb.Append("#pragma warning disable 0109 // new is redundant\n");
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n",
type.Alias); type.Alias);
var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n",
itemType); itemType);
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.Append("\t\tpublic new static PublishedContentType GetModelContentType()\n"); sb.Append("\t\tpublic new static PublishedContentType GetModelContentType()\n");
sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n"); sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n");
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\tpublic static PublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<{0}, TValue>> selector)\n", sb.AppendFormat("\t\tpublic static PublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<{0}, TValue>> selector)\n",
type.ClrName); type.ClrName);
sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n"); sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n");
sb.Append("#pragma warning restore 0109\n\n"); sb.Append("#pragma warning restore 0109\n\n");
// write the ctor // write the ctor
if (!type.HasCtor) sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n",
sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", type.ClrName, type.IsElement ? "Element" : "Content");
type.ClrName, type.IsElement ? "Element" : "Content");
// write the properties // write the properties
sb.Append("\t\t// properties\n"); sb.Append("\t\t// properties\n");
@@ -205,10 +221,10 @@ namespace Umbraco.ModelsBuilder.Building
private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) private void WriteContentTypeProperties(StringBuilder sb, TypeModel type)
{ {
var staticMixinGetters = UmbracoConfig.For.ModelsBuilder().StaticMixinGetters; var staticMixinGetters = true;
// write the properties // write the properties
foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) foreach (var prop in type.Properties.OrderBy(x => x.ClrName))
WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null);
// no need to write the parent properties since we inherit from the parent // no need to write the parent properties since we inherit from the parent
@@ -217,7 +233,7 @@ namespace Umbraco.ModelsBuilder.Building
// write the mixins properties // write the mixins properties
foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName))
foreach (var prop in mixinType.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName))
if (staticMixinGetters) if (staticMixinGetters)
WriteMixinProperty(sb, prop, mixinType.ClrName); WriteMixinProperty(sb, prop, mixinType.ClrName);
else else
@@ -242,6 +258,7 @@ namespace Umbraco.ModelsBuilder.Building
sb.Append("\t\t///</summary>\n"); sb.Append("\t\t///</summary>\n");
} }
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);
sb.Append("\t\tpublic "); sb.Append("\t\tpublic ");
@@ -256,7 +273,7 @@ namespace Umbraco.ModelsBuilder.Building
private static string MixinStaticGetterName(string clrName) private static string MixinStaticGetterName(string clrName)
{ {
return string.Format(UmbracoConfig.For.ModelsBuilder().StaticMixinGetterPattern, clrName); return string.Format("Get{0}", clrName);
} }
private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null) private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null)
@@ -300,6 +317,7 @@ namespace Umbraco.ModelsBuilder.Building
sb.Append("\t\t///</summary>\n"); sb.Append("\t\t///</summary>\n");
} }
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);
if (mixinStatic) if (mixinStatic)
@@ -343,6 +361,7 @@ namespace Umbraco.ModelsBuilder.Building
if (!string.IsNullOrWhiteSpace(property.Name)) if (!string.IsNullOrWhiteSpace(property.Name))
sb.AppendFormat("\t\t/// <summary>Static getter for {0}</summary>\n", XmlCommentString(property.Name)); sb.AppendFormat("\t\t/// <summary>Static getter for {0}</summary>\n", XmlCommentString(property.Name));
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.Append("\t\tpublic static "); sb.Append("\t\tpublic static ");
WriteClrType(sb, property.ClrTypeName); WriteClrType(sb, property.ClrTypeName);
sb.AppendFormat(" {0}(I{1} that) => that.Value", sb.AppendFormat(" {0}(I{1} that) => that.Value",
@@ -397,6 +416,7 @@ namespace Umbraco.ModelsBuilder.Building
if (!string.IsNullOrWhiteSpace(property.Name)) if (!string.IsNullOrWhiteSpace(property.Name))
sb.AppendFormat("\t\t/// <summary>{0}</summary>\n", XmlCommentString(property.Name)); sb.AppendFormat("\t\t/// <summary>{0}</summary>\n", XmlCommentString(property.Name));
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.Append("\t\t"); sb.Append("\t\t");
WriteClrType(sb, property.ClrTypeName); WriteClrType(sb, property.ClrTypeName);
sb.AppendFormat(" {0} {{ get; }}\n", sb.AppendFormat(" {0} {{ get; }}\n",
@@ -461,7 +481,7 @@ namespace Umbraco.ModelsBuilder.Building
s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]);
// takes care eg of "System.Int32" vs. "int" // takes care eg of "System.Int32" vs. "int"
if (TypesMap.TryGetValue(s.ToLowerInvariant(), out string typeName)) if (TypesMap.TryGetValue(s, out string typeName))
{ {
sb.Append(typeName); sb.Append(typeName);
return; return;
@@ -481,6 +501,11 @@ namespace Umbraco.ModelsBuilder.Building
typeName = typeName.Substring(p + 1); typeName = typeName.Substring(p + 1);
typeUsing = x; typeUsing = x;
} }
else if (x == ModelsNamespace) // that one is used by default
{
typeName = typeName.Substring(p + 1);
typeUsing = ModelsNamespace;
}
} }
// nested types *after* using // nested types *after* using
@@ -531,24 +556,24 @@ namespace Umbraco.ModelsBuilder.Building
return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' ');
} }
private static readonly IDictionary<string, string> TypesMap = new Dictionary<string, string> private static readonly IDictionary<string, string> TypesMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{ {
{ "system.int16", "short" }, { "System.Int16", "short" },
{ "system.int32", "int" }, { "System.Int32", "int" },
{ "system.int64", "long" }, { "System.Int64", "long" },
{ "system.string", "string" }, { "System.String", "string" },
{ "system.object", "object" }, { "System.Object", "object" },
{ "system.boolean", "bool" }, { "System.Boolean", "bool" },
{ "system.void", "void" }, { "System.Void", "void" },
{ "system.char", "char" }, { "System.Char", "char" },
{ "system.byte", "byte" }, { "System.Byte", "byte" },
{ "system.uint16", "ushort" }, { "System.UInt16", "ushort" },
{ "system.uint32", "uint" }, { "System.UInt32", "uint" },
{ "system.uint64", "ulong" }, { "System.UInt64", "ulong" },
{ "system.sbyte", "sbyte" }, { "System.SByte", "sbyte" },
{ "system.single", "float" }, { "System.Single", "float" },
{ "system.double", "double" }, { "System.Double", "double" },
{ "system.decimal", "decimal" } { "System.Decimal", "decimal" }
}; };
} }
} }
+20 -23
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.ModelsBuilder.Building namespace Umbraco.ModelsBuilder.Building
{ {
@@ -88,16 +89,6 @@ namespace Umbraco.ModelsBuilder.Building
/// or because the existing user's code declares a base class for this model.</remarks> /// or because the existing user's code declares a base class for this model.</remarks>
public bool HasBase; public bool HasBase;
/// <summary>
/// Gets a value indicating whether this model has been renamed.
/// </summary>
public bool IsRenamed;
/// <summary>
/// Gets a value indicating whether this model has [ImplementContentType] already.
/// </summary>
public bool HasImplement;
/// <summary> /// <summary>
/// Gets a value indicating whether this model is used as a mixin by another model. /// Gets a value indicating whether this model is used as a mixin by another model.
/// </summary> /// </summary>
@@ -108,16 +99,6 @@ namespace Umbraco.ModelsBuilder.Building
/// </summary> /// </summary>
public bool IsParent; public bool IsParent;
/// <summary>
/// Gets a value indicating whether this model should be excluded from generation.
/// </summary>
public bool IsContentIgnored;
/// <summary>
/// Gets a value indicating whether the ctor is already defined in a partial.
/// </summary>
public bool HasCtor;
/// <summary> /// <summary>
/// Gets a value indicating whether the type is an element. /// Gets a value indicating whether the type is an element.
/// </summary> /// </summary>
@@ -181,11 +162,11 @@ namespace Umbraco.ModelsBuilder.Building
/// <remarks>Includes the specified type.</remarks> /// <remarks>Includes the specified type.</remarks>
internal static void CollectImplems(ICollection<TypeModel> types, TypeModel type) internal static void CollectImplems(ICollection<TypeModel> types, TypeModel type)
{ {
if (!type.IsContentIgnored && types.Contains(type) == false) if (types.Contains(type) == false)
types.Add(type); types.Add(type);
if (type.BaseType != null && !type.BaseType.IsContentIgnored) if (type.BaseType != null)
CollectImplems(types, type.BaseType); CollectImplems(types, type.BaseType);
foreach (var mixin in type.MixinTypes.Where(x => !x.IsContentIgnored)) foreach (var mixin in type.MixinTypes)
CollectImplems(types, mixin); CollectImplems(types, mixin);
} }
@@ -204,5 +185,21 @@ namespace Umbraco.ModelsBuilder.Building
typeModel = typeModel.BaseType; typeModel = typeModel.BaseType;
} }
} }
/// <summary>
/// Maps ModelType.
/// </summary>
public static void MapModelTypes(IList<TypeModel> typeModels, string ns)
{
var hasNs = !string.IsNullOrWhiteSpace(ns);
var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? (ns + "." + x.ClrName) : x.ClrName);
foreach (var typeModel in typeModels)
{
foreach (var propertyModel in typeModel.Properties)
{
propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map);
}
}
}
} }
} }
@@ -0,0 +1,20 @@
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Provides extension methods for the <see cref="UmbracoConfig"/> class.
/// </summary>
public static class ConfigsExtensions
{
/// <summary>
/// Gets the models builder configuration.
/// </summary>
/// <remarks>Getting the models builder configuration freezes its state,
/// and any attempt at modifying the configuration using the Setup method
/// will be ignored.</remarks>
public static Config ModelsBuilder(this Configs configs)
=> configs.GetConfig<Config>();
}
}
@@ -1,10 +1,8 @@
using System; using System;
using System.Configuration; using System.Configuration;
using System.IO; using System.IO;
using System.Reflection;
using System.Web.Configuration; using System.Web.Configuration;
using System.Web.Hosting; using System.Web.Hosting;
using Microsoft.CodeAnalysis.CSharp;
using Umbraco.Core; using Umbraco.Core;
namespace Umbraco.ModelsBuilder.Configuration namespace Umbraco.ModelsBuilder.Configuration
@@ -14,40 +12,13 @@ namespace Umbraco.ModelsBuilder.Configuration
/// </summary> /// </summary>
public class Config public class Config
{ {
private static Config _value;
/// <summary>
/// Gets the configuration - internal so that the UmbracoConfig extension
/// can get the value to initialize its own value. Either a value has
/// been provided via the Setup method, or a new instance is created, which
/// will load settings from the config file.
/// </summary>
internal static Config Value => _value ?? new Config();
/// <summary>
/// Sets the configuration programmatically.
/// </summary>
/// <param name="config">The configuration.</param>
/// <remarks>
/// <para>Once the configuration has been accessed via the UmbracoConfig extension,
/// it cannot be changed anymore, and using this method will achieve nothing.</para>
/// <para>For tests, see UmbracoConfigExtensions.ResetConfig().</para>
/// </remarks>
public static void Setup(Config config)
{
_value = config;
}
internal const string DefaultStaticMixinGetterPattern = "Get{0}";
internal const LanguageVersion DefaultLanguageVersion = LanguageVersion.CSharp6;
internal const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; internal const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels";
internal const ClrNameSource DefaultClrNameSource = ClrNameSource.Alias; // for legacy reasons
internal const string DefaultModelsDirectory = "~/App_Data/Models"; internal const string DefaultModelsDirectory = "~/App_Data/Models";
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Config"/> class. /// Initializes a new instance of the <see cref="Config"/> class.
/// </summary> /// </summary>
private Config() public Config()
{ {
const string prefix = "Umbraco.ModelsBuilder."; const string prefix = "Umbraco.ModelsBuilder.";
@@ -56,10 +27,7 @@ namespace Umbraco.ModelsBuilder.Configuration
Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true"; Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true";
// ensure defaults are initialized for tests // ensure defaults are initialized for tests
StaticMixinGetterPattern = DefaultStaticMixinGetterPattern;
LanguageVersion = DefaultLanguageVersion;
ModelsNamespace = DefaultModelsNamespace; ModelsNamespace = DefaultModelsNamespace;
ClrNameSource = DefaultClrNameSource;
ModelsDirectory = HostingEnvironment.IsHosted ModelsDirectory = HostingEnvironment.IsHosted
? HostingEnvironment.MapPath(DefaultModelsDirectory) ? HostingEnvironment.MapPath(DefaultModelsDirectory)
: DefaultModelsDirectory.TrimStart("~/"); : DefaultModelsDirectory.TrimStart("~/");
@@ -80,12 +48,6 @@ namespace Umbraco.ModelsBuilder.Configuration
case nameof(ModelsMode.PureLive): case nameof(ModelsMode.PureLive):
ModelsMode = ModelsMode.PureLive; ModelsMode = ModelsMode.PureLive;
break; break;
case nameof(ModelsMode.Dll):
ModelsMode = ModelsMode.Dll;
break;
case nameof(ModelsMode.LiveDll):
ModelsMode = ModelsMode.LiveDll;
break;
case nameof(ModelsMode.AppData): case nameof(ModelsMode.AppData):
ModelsMode = ModelsMode.AppData; ModelsMode = ModelsMode.AppData;
break; break;
@@ -94,17 +56,15 @@ namespace Umbraco.ModelsBuilder.Configuration
break; break;
default: default:
throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode."
+ " Note that modes are case-sensitive."); + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode))));
} }
} }
// default: false // default: false
EnableApi = ConfigurationManager.AppSettings[prefix + "EnableApi"].InvariantEquals("true");
AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true");
// default: true // default: true
EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false"); EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false");
StaticMixinGetters = !ConfigurationManager.AppSettings[prefix + "StaticMixinGetters"].InvariantEquals("false");
FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false");
// default: initialized above with DefaultModelsNamespace const // default: initialized above with DefaultModelsNamespace const
@@ -112,45 +72,6 @@ namespace Umbraco.ModelsBuilder.Configuration
if (!string.IsNullOrWhiteSpace(value)) if (!string.IsNullOrWhiteSpace(value))
ModelsNamespace = value; ModelsNamespace = value;
// default: initialized above with DefaultStaticMixinGetterPattern const
value = ConfigurationManager.AppSettings[prefix + "StaticMixinGetterPattern"];
if (!string.IsNullOrWhiteSpace(value))
StaticMixinGetterPattern = value;
// default: initialized above with DefaultLanguageVersion const
value = ConfigurationManager.AppSettings[prefix + "LanguageVersion"];
if (!string.IsNullOrWhiteSpace(value))
{
LanguageVersion lv;
if (!Enum.TryParse(value, true, out lv))
throw new ConfigurationErrorsException($"Invalid language version \"{value}\".");
LanguageVersion = lv;
}
// default: initialized above with DefaultClrNameSource const
value = ConfigurationManager.AppSettings[prefix + "ClrNameSource"];
if (!string.IsNullOrWhiteSpace(value))
{
switch (value)
{
case nameof(ClrNameSource.Nothing):
ClrNameSource = ClrNameSource.Nothing;
break;
case nameof(ClrNameSource.Alias):
ClrNameSource = ClrNameSource.Alias;
break;
case nameof(ClrNameSource.RawAlias):
ClrNameSource = ClrNameSource.RawAlias;
break;
case nameof(ClrNameSource.Name):
ClrNameSource = ClrNameSource.Name;
break;
default:
throw new ConfigurationErrorsException($"ClrNameSource \"{value}\" is not a valid source."
+ " Note that sources are case-sensitive.");
}
}
// default: initialized above with DefaultModelsDirectory const // default: initialized above with DefaultModelsDirectory const
value = ConfigurationManager.AppSettings[prefix + "ModelsDirectory"]; value = ConfigurationManager.AppSettings[prefix + "ModelsDirectory"];
if (!string.IsNullOrWhiteSpace(value)) if (!string.IsNullOrWhiteSpace(value))
@@ -186,14 +107,9 @@ namespace Umbraco.ModelsBuilder.Configuration
public Config( public Config(
bool enable = false, bool enable = false,
ModelsMode modelsMode = ModelsMode.Nothing, ModelsMode modelsMode = ModelsMode.Nothing,
bool enableApi = true,
string modelsNamespace = null, string modelsNamespace = null,
bool enableFactory = true, bool enableFactory = true,
LanguageVersion languageVersion = DefaultLanguageVersion,
bool staticMixinGetters = true,
string staticMixinGetterPattern = null,
bool flagOutOfDateModels = true, bool flagOutOfDateModels = true,
ClrNameSource clrNameSource = DefaultClrNameSource,
string modelsDirectory = null, string modelsDirectory = null,
bool acceptUnsafeModelsDirectory = false, bool acceptUnsafeModelsDirectory = false,
int debugLevel = 0) int debugLevel = 0)
@@ -201,14 +117,9 @@ namespace Umbraco.ModelsBuilder.Configuration
Enable = enable; Enable = enable;
ModelsMode = modelsMode; ModelsMode = modelsMode;
EnableApi = enableApi;
ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace; ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace;
EnableFactory = enableFactory; EnableFactory = enableFactory;
LanguageVersion = languageVersion;
StaticMixinGetters = staticMixinGetters;
StaticMixinGetterPattern = string.IsNullOrWhiteSpace(staticMixinGetterPattern) ? DefaultStaticMixinGetterPattern : staticMixinGetterPattern;
FlagOutOfDateModels = flagOutOfDateModels; FlagOutOfDateModels = flagOutOfDateModels;
ClrNameSource = clrNameSource;
ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory; ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory;
AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory; AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory;
DebugLevel = debugLevel; DebugLevel = debugLevel;
@@ -259,27 +170,6 @@ namespace Umbraco.ModelsBuilder.Configuration
/// </summary> /// </summary>
public ModelsMode ModelsMode { get; } public ModelsMode ModelsMode { get; }
/// <summary>
/// Gets a value indicating whether to serve the API.
/// </summary>
public bool ApiServer => EnableApi && ApiInstalled && IsDebug;
/// <summary>
/// Gets a value indicating whether to enable the API.
/// </summary>
/// <remarks>
/// <para>Default value is <c>true</c>.</para>
/// <para>The API is used by the Visual Studio extension and the console tool to talk to Umbraco
/// and retrieve the content types. It needs to be enabled so the extension & tool can work.</para>
/// </remarks>
public bool EnableApi { get; }
/// <summary>
/// Gets a value indicating whether the API is installed.
/// </summary>
// fixme - this is now always true as the API is part of Core
public bool ApiInstalled => true;
/// <summary> /// <summary>
/// Gets a value indicating whether system.web/compilation/@debug is true. /// Gets a value indicating whether system.web/compilation/@debug is true.
/// </summary> /// </summary>
@@ -304,24 +194,6 @@ namespace Umbraco.ModelsBuilder.Configuration
/// <remarks>Default value is <c>true</c> because no factory is enabled by default in Umbraco.</remarks> /// <remarks>Default value is <c>true</c> because no factory is enabled by default in Umbraco.</remarks>
public bool EnableFactory { get; } public bool EnableFactory { get; }
/// <summary>
/// Gets the Roslyn parser language version.
/// </summary>
/// <remarks>Default value is <c>CSharp6</c>.</remarks>
public LanguageVersion LanguageVersion { get; }
/// <summary>
/// Gets a value indicating whether to generate static mixin getters.
/// </summary>
/// <remarks>Default value is <c>false</c> for backward compat reaons.</remarks>
public bool StaticMixinGetters { get; }
/// <summary>
/// Gets the string pattern for mixin properties static getter name.
/// </summary>
/// <remarks>Default value is "GetXxx". Standard string format.</remarks>
public string StaticMixinGetterPattern { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether we should flag out-of-date models. /// Gets a value indicating whether we should flag out-of-date models.
/// </summary> /// </summary>
@@ -330,11 +202,6 @@ namespace Umbraco.ModelsBuilder.Configuration
/// generated through the dashboard, the files is cleared. Default value is <c>false</c>.</remarks> /// generated through the dashboard, the files is cleared. Default value is <c>false</c>.</remarks>
public bool FlagOutOfDateModels { get; } public bool FlagOutOfDateModels { get; }
/// <summary>
/// Gets the CLR name source.
/// </summary>
public ClrNameSource ClrNameSource { get; }
/// <summary> /// <summary>
/// Gets the models directory. /// Gets the models directory.
/// </summary> /// </summary>
@@ -31,22 +31,6 @@
/// </summary> /// </summary>
/// <remarks>Generation can be triggered from the dashboard. The app does not restart. /// <remarks>Generation can be triggered from the dashboard. The app does not restart.
/// Models are not compiled and thus are not available to the project.</remarks> /// Models are not compiled and thus are not available to the project.</remarks>
LiveAppData, LiveAppData
/// <summary>
/// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts).
/// When: generation is triggered.
/// </summary>
/// <remarks>Generation can be triggered from the dashboard. The app does restart. Models
/// are available to the entire project.</remarks>
Dll,
/// <summary>
/// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts).
/// When: a content type change occurs, or generation is triggered.
/// </summary>
/// <remarks>Generation can be triggered from the dashboard. The app does restart. Models
/// are available to the entire project.</remarks>
LiveDll
} }
} }
@@ -12,7 +12,6 @@
{ {
return return
modelsMode == ModelsMode.PureLive modelsMode == ModelsMode.PureLive
|| modelsMode == ModelsMode.LiveDll
|| modelsMode == ModelsMode.LiveAppData; || modelsMode == ModelsMode.LiveAppData;
} }
@@ -22,18 +21,7 @@
public static bool IsLiveNotPure(this ModelsMode modelsMode) public static bool IsLiveNotPure(this ModelsMode modelsMode)
{ {
return return
modelsMode == ModelsMode.LiveDll modelsMode == ModelsMode.LiveAppData;
|| modelsMode == ModelsMode.LiveAppData;
}
/// <summary>
/// Gets a value indicating whether the mode is [Live]Dll.
/// </summary>
public static bool IsAnyDll(this ModelsMode modelsMode)
{
return
modelsMode == ModelsMode.Dll
|| modelsMode == ModelsMode.LiveDll;
} }
/// <summary> /// <summary>
@@ -42,10 +30,8 @@
public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) public static bool SupportsExplicitGeneration(this ModelsMode modelsMode)
{ {
return return
modelsMode == ModelsMode.Dll modelsMode == ModelsMode.AppData
|| modelsMode == ModelsMode.LiveDll
|| modelsMode == ModelsMode.AppData
|| modelsMode == ModelsMode.LiveAppData; || modelsMode == ModelsMode.LiveAppData;
} }
} }
} }
@@ -1,34 +0,0 @@
using System.Threading;
using Umbraco.Core.Configuration;
namespace Umbraco.ModelsBuilder.Configuration
{
/// <summary>
/// Provides extension methods for the <see cref="UmbracoConfig"/> class.
/// </summary>
public static class UmbracoConfigExtensions
{
private static Config _config;
/// <summary>
/// Gets the models builder configuration.
/// </summary>
/// <param name="umbracoConfig">The umbraco configuration.</param>
/// <returns>The models builder configuration.</returns>
/// <remarks>Getting the models builder configuration freezes its state,
/// and any attempt at modifying the configuration using the Setup method
/// will be ignored.</remarks>
public static Config ModelsBuilder(this UmbracoConfig umbracoConfig)
{
// capture the current Config2.Default value, cannot change anymore
LazyInitializer.EnsureInitialized(ref _config, () => Config.Value);
return _config;
}
// internal for tests
internal static void ResetConfig()
{
_config = null;
}
}
}
@@ -1,6 +1,5 @@
using System; using System.Text;
using System.Text; using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Umbraco; using Umbraco.ModelsBuilder.Umbraco;
@@ -8,14 +7,11 @@ namespace Umbraco.ModelsBuilder.Dashboard
{ {
internal static class BuilderDashboardHelper internal static class BuilderDashboardHelper
{ {
private static Config Config => Current.Configs.ModelsBuilder();
public static bool CanGenerate() public static bool CanGenerate()
{ {
return UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration(); return Config.ModelsMode.SupportsExplicitGeneration();
}
public static bool GenerateCausesRestart()
{
return UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll();
} }
public static bool AreModelsOutOfDate() public static bool AreModelsOutOfDate()
@@ -30,12 +26,17 @@ namespace Umbraco.ModelsBuilder.Dashboard
public static string Text() public static string Text()
{ {
var config = UmbracoConfig.For.ModelsBuilder(); var config = Config;
if (!config.Enable) if (!config.Enable)
return "ModelsBuilder is disabled<br />(the .Enable key is missing, or its value is not 'true')."; return "Version: " + Api.ApiVersion.Current.Version + "<br />&nbsp;<br />ModelsBuilder is disabled<br />(the .Enable key is missing, or its value is not 'true').";
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append("Version: ");
sb.Append(Api.ApiVersion.Current.Version);
sb.Append("<br />&nbsp;<br />");
sb.Append("ModelsBuilder is enabled, with the following configuration:"); sb.Append("ModelsBuilder is enabled, with the following configuration:");
sb.Append("<ul>"); sb.Append("<ul>");
@@ -46,39 +47,12 @@ namespace Umbraco.ModelsBuilder.Dashboard
: "not enabled. Umbraco will <em>not</em> use models"); : "not enabled. Umbraco will <em>not</em> use models");
sb.Append(".</li>"); sb.Append(".</li>");
sb.Append("<li>The <strong>API</strong> is ");
if (config.ApiInstalled && config.EnableApi)
{
sb.Append("installed and enabled");
if (!config.IsDebug) sb.Append(".<br />However, the API runs only with <em>debug</em> compilation mode");
}
else if (config.ApiInstalled || config.EnableApi)
sb.Append(config.ApiInstalled ? "installed but not enabled" : "enabled but not installed");
else sb.Append("neither installed nor enabled");
sb.Append(".<br />");
if (!config.ApiServer)
sb.Append("External tools such as Visual Studio <em>cannot</em> use the API");
else
sb.Append("<span style=\"color:orange;font-weight:bold;\">The API endpoint is open on this server</span>");
sb.Append(".</li>");
sb.Append(config.ModelsMode != ModelsMode.Nothing sb.Append(config.ModelsMode != ModelsMode.Nothing
? $"<li><strong>{config.ModelsMode} models</strong> are enabled.</li>" ? $"<li><strong>{config.ModelsMode} models</strong> are enabled.</li>"
: "<li>No models mode is specified: models will <em>not</em> be generated.</li>"); : "<li>No models mode is specified: models will <em>not</em> be generated.</li>");
sb.Append($"<li>Models namespace is {config.ModelsNamespace}.</li>"); sb.Append($"<li>Models namespace is {config.ModelsNamespace}.</li>");
sb.Append("<li>Static mixin getters are ");
sb.Append(config.StaticMixinGetters ? "enabled" : "disabled");
if (config.StaticMixinGetters)
{
sb.Append(". The pattern for getters is ");
sb.Append(string.IsNullOrWhiteSpace(config.StaticMixinGetterPattern)
? "not configured (will use default)"
: $"\"{config.StaticMixinGetterPattern}\"");
}
sb.Append(".</li>");
sb.Append("<li>Tracking of <strong>out-of-date models</strong> is "); sb.Append("<li>Tracking of <strong>out-of-date models</strong> is ");
sb.Append(config.FlagOutOfDateModels ? "enabled" : "not enabled"); sb.Append(config.FlagOutOfDateModels ? "enabled" : "not enabled");
sb.Append(".</li>"); sb.Append(".</li>");
@@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using Umbraco.ModelsBuilder;
namespace Umbraco.ModelsBuilder
{
// for the time being it's all-or-nothing
// when the content type is ignored then
// - no class is generated for that content type
// - no class is generated for any child of that class
// - no interface is generated for that content type as a mixin
// - and it is ignored as a mixin ie its properties are not generated
// in the future we may way to do
// [assembly:IgnoreContentType("foo", ContentTypeIgnorable.ContentType|ContentTypeIgnorable.Mixin|ContentTypeIgnorable.MixinProperties)]
// so that we can
// - generate a class for that content type, or not
// - if not generated, generate children or not
// - if generate children, include properties or not
// - generate an interface for that content type as a mixin
// - if not generated, still generate properties in content types implementing the mixin or not
// but... I'm not even sure it makes sense
// if we don't want it... we don't want it.
// about ignoring
// - content (don't generate the content, use as mixin)
// - mixin (don't generate the interface, use the properties)
// - mixin properties (generate the interface, not the properties)
// - mixin: local only or children too...
/// <summary>
/// Indicates that no model should be generated for a specified content type alias.
/// </summary>
/// <remarks>When a content type is ignored, its descendants are also ignored.</remarks>
/// <remarks>Supports trailing wildcard eg "foo*".</remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public sealed class IgnoreContentTypeAttribute : Attribute
{
public IgnoreContentTypeAttribute(string alias /*, bool ignoreContent = true, bool ignoreMixin = true, bool ignoreMixinProperties = true*/)
{}
}
}
@@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates that no model should be generated for a specified property type alias.
/// </summary>
/// <remarks>Supports trailing wildcard eg "foo*".</remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class IgnorePropertyTypeAttribute : Attribute
{
public IgnorePropertyTypeAttribute(string alias)
{}
}
}
@@ -1,20 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder
{
// NOTE
// that attribute should inherit from PublishedModelAttribute
// so we do not have different syntaxes
// but... is sealed at the moment.
/// <summary>
/// Indicates that a (partial) class defines the model type for a specific alias.
/// </summary>
/// <remarks>Though a model will be generated - so that is the way to register a rename.</remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ImplementContentTypeAttribute : Attribute
{
public ImplementContentTypeAttribute(string alias)
{ }
}
}
@@ -1,8 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.ModelsBuilder namespace Umbraco.ModelsBuilder
{ {
@@ -1,16 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates the default base class for models.
/// </summary>
/// <remarks>Otherwise it is PublishedContentModel. Would make sense to inherit from PublishedContentModel.</remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
public sealed class ModelsBaseClassAttribute : Attribute
{
public ModelsBaseClassAttribute(Type type)
{}
}
}
@@ -1,16 +0,0 @@
using System;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates the models namespace.
/// </summary>
/// <remarks>Will override anything else that might come from settings.</remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
public sealed class ModelsNamespaceAttribute : Attribute
{
public ModelsNamespaceAttribute(string modelsNamespace)
{}
}
}
@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using Umbraco.ModelsBuilder;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates namespaces that should be used in generated models (in using clauses).
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public sealed class ModelsUsingAttribute : Attribute
{
public ModelsUsingAttribute(string usingNamespace)
{}
}
}
@@ -7,8 +7,4 @@ using System.Runtime.InteropServices;
[assembly: AssemblyProduct("Umbraco CMS")] [assembly: AssemblyProduct("Umbraco CMS")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("7020a059-c0d1-43a0-8efd-23591a0c9af6")] [assembly: Guid("52ac0ba8-a60e-4e36-897b-e8b97a54ed1c")]
// code analysis
// IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "~_~")]
@@ -2,9 +2,11 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web; using Umbraco.ModelsBuilder;
namespace Umbraco.ModelsBuilder // same namespace as original Umbraco.Web PublishedElementExtensions
// ReSharper disable once CheckNamespace
namespace Umbraco.Web
{ {
/// <summary> /// <summary>
/// Provides extension methods to models. /// Provides extension methods to models.
@@ -14,13 +16,14 @@ namespace Umbraco.ModelsBuilder
/// <summary> /// <summary>
/// Gets the value of a property. /// Gets the value of a property.
/// </summary> /// </summary>
public static TValue Value<TModel, TValue>(this TModel model, Expression<Func<TModel, TValue>> property, string culture = ".", string segment = ".") public static TValue Value<TModel, TValue>(this TModel model, Expression<Func<TModel, TValue>> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default)
where TModel : IPublishedElement where TModel : IPublishedElement
{ {
var alias = GetAlias(model, property); var alias = GetAlias(model, property);
return model.Value<TValue>(alias, culture, segment); return model.Value<TValue>(alias, culture, segment, fallback, defaultValue);
} }
// fixme that one should be public so ppl can use it
private static string GetAlias<TModel, TValue>(TModel model, Expression<Func<TModel, TValue>> property) private static string GetAlias<TModel, TValue>(TModel model, Expression<Func<TModel, TValue>> property)
{ {
if (property.NodeType != ExpressionType.Lambda) if (property.NodeType != ExpressionType.Lambda)
@@ -1,20 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.ModelsBuilder
{
public static class PublishedPropertyTypeExtensions
{
// fixme - need to rewrite that one - we don't have prevalues anymore
//public static KeyValuePair<int, string>[] PreValues(this PublishedPropertyType propertyType)
//{
// return ApplicationContext.Current.Services.DataTypeService
// .GetPreValuesCollectionByDataTypeId(propertyType.DataType.Id)
// .PreValuesAsArray
// .Select(x => new KeyValuePair<int, string>(x.Id, x.Value))
// .ToArray();
//}
}
}
@@ -4,7 +4,6 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Web.Compilation; using System.Web.Compilation;
using System.Web.Hosting; using System.Web.Hosting;
using Microsoft.CodeAnalysis;
using Umbraco.Core; using Umbraco.Core;
namespace Umbraco.ModelsBuilder namespace Umbraco.ModelsBuilder
@@ -12,17 +11,12 @@ namespace Umbraco.ModelsBuilder
internal static class ReferencedAssemblies internal static class ReferencedAssemblies
{ {
private static readonly Lazy<IEnumerable<string>> LazyLocations; private static readonly Lazy<IEnumerable<string>> LazyLocations;
private static readonly Lazy<IEnumerable<PortableExecutableReference>> LazyReferences;
static ReferencedAssemblies() static ReferencedAssemblies()
{ {
LazyLocations = new Lazy<IEnumerable<string>>(() => HostingEnvironment.IsHosted LazyLocations = new Lazy<IEnumerable<string>>(() => HostingEnvironment.IsHosted
? GetAllReferencedAssembliesLocationFromBuildManager() ? GetAllReferencedAssembliesLocationFromBuildManager()
: GetAllReferencedAssembliesFromDomain()); : GetAllReferencedAssembliesFromDomain());
LazyReferences = new Lazy<IEnumerable<PortableExecutableReference>>(() => Locations
.Select(x => MetadataReference.CreateFromFile(x))
.ToArray());
} }
/// <summary> /// <summary>
@@ -31,19 +25,68 @@ namespace Umbraco.ModelsBuilder
/// </summary> /// </summary>
public static IEnumerable<string> Locations => LazyLocations.Value; public static IEnumerable<string> Locations => LazyLocations.Value;
/// <summary> public static Assembly GetNetStandardAssembly(List<Assembly> assemblies)
/// Gets the metadata reference of all the referenced assemblies. {
/// </summary> if (assemblies == null)
public static IEnumerable<PortableExecutableReference> References => LazyReferences.Value; assemblies = BuildManager.GetReferencedAssemblies().Cast<Assembly>().ToList();
// hosted, get referenced assemblies from the BuildManader and filter // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of
// the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out
try
{
// so, resorting to an ugly trick
// we should have System.Reflection.Metadata around, and it should reference netstandard
var someAssembly = assemblies.First(x => x.FullName.StartsWith("System.Reflection.Metadata,"));
var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,"));
var netStandard = Assembly.Load(netStandardAssemblyName.FullName);
return netStandard;
}
catch { /* never mind */ }
return null;
}
public static Assembly GetNetStandardAssembly()
{
// in PreApplicationStartMethod we cannot get BuildManager.Referenced assemblies, do it differently
try
{
var someAssembly = Assembly.Load("System.Reflection.Metadata");
var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,"));
var netStandard = Assembly.Load(netStandardAssemblyName.FullName);
return netStandard;
}
catch { /* never mind */ }
return null;
}
// hosted, get referenced assemblies from the BuildManager and filter
private static IEnumerable<string> GetAllReferencedAssembliesLocationFromBuildManager() private static IEnumerable<string> GetAllReferencedAssembliesLocationFromBuildManager()
{ {
return BuildManager.GetReferencedAssemblies() var assemblies = BuildManager.GetReferencedAssemblies().Cast<Assembly>().ToList();
.Cast<Assembly>()
assemblies.Add(typeof(ReferencedAssemblies).Assembly); // always include ourselves
// see https://github.com/aspnet/RoslynCodeDomProvider/blob/master/src/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/CSharpCompiler.cs:
// mentions "Bug 913691: Explicitly add System.Runtime as a reference."
// and explicitly adds System.Runtime to references before invoking csc.exe
// so, doing the same here
try
{
var systemRuntime = Assembly.Load("System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
assemblies.Add(systemRuntime);
}
catch { /* never mind */ }
// for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of
// the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out
var netStandard = GetNetStandardAssembly(assemblies);
if (netStandard != null) assemblies.Add(netStandard);
return assemblies
.Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace()) .Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace())
.Select(x => x.Location) .Select(x => x.Location)
.And(typeof(ReferencedAssemblies).Assembly.Location) // always include ourselves
.Distinct() .Distinct()
.ToList(); .ToList();
} }
@@ -132,6 +175,5 @@ namespace Umbraco.ModelsBuilder
return null; return null;
} }
} }
} }
} }
@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates a model name for a specified content alias.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public sealed class RenameContentTypeAttribute : Attribute
{
public RenameContentTypeAttribute(string alias, string name)
{}
}
}
@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.ModelsBuilder
{
/// <summary>
/// Indicates a model name for a specified property alias.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class RenamePropertyTypeAttribute : Attribute
{
public RenamePropertyTypeAttribute(string alias, string name)
{}
}
}
@@ -4,13 +4,14 @@
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7020A059-C0D1-43A0-8EFD-23591A0C9AF6}</ProjectGuid> <ProjectGuid>{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}</ProjectGuid>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Umbraco.ModelsBuilder</RootNamespace> <RootNamespace>Umbraco.ModelsBuilder</RootNamespace>
<AssemblyName>Umbraco.ModelsBuilder</AssemblyName> <AssemblyName>Umbraco.ModelsBuilder</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@@ -32,9 +33,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Web" /> <Reference Include="System.Web" />
<Reference Include="System.Web.ApplicationServices" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
@@ -46,67 +48,43 @@
<Compile Include="..\SolutionInfo.cs"> <Compile Include="..\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link> <Link>Properties\SolutionInfo.cs</Link>
</Compile> </Compile>
<Compile Include="Api\ApiBasicAuthFilter.cs" />
<Compile Include="Api\ApiClient.cs" />
<Compile Include="Api\ApiHelper.cs" />
<Compile Include="Api\ApiVersion.cs" /> <Compile Include="Api\ApiVersion.cs" />
<Compile Include="Api\GetModelsData.cs" />
<Compile Include="Api\ModelsBuilderApiController.cs" />
<Compile Include="Api\TokenData.cs" />
<Compile Include="Api\ValidateClientVersionData.cs" />
<Compile Include="Building\Builder.cs" /> <Compile Include="Building\Builder.cs" />
<Compile Include="Building\CodeDomBuilder.cs" />
<Compile Include="Building\CodeParser.cs" />
<Compile Include="Building\Compiler.cs" />
<Compile Include="Building\CompilerException.cs" />
<Compile Include="Building\ParseResult.cs" />
<Compile Include="Building\PropertyModel.cs" /> <Compile Include="Building\PropertyModel.cs" />
<Compile Include="Building\TextBuilder.cs" /> <Compile Include="Building\TextBuilder.cs" />
<Compile Include="Building\TextHeaderWriter.cs" /> <Compile Include="Building\TextHeaderWriter.cs" />
<Compile Include="Building\TypeModel.cs" /> <Compile Include="Building\TypeModel.cs" />
<Compile Include="ConfigsExtensions.cs" />
<Compile Include="Configuration\ClrNameSource.cs" /> <Compile Include="Configuration\ClrNameSource.cs" />
<Compile Include="Configuration\Config.cs" /> <Compile Include="Configuration\Config.cs" />
<Compile Include="Configuration\ModelsMode.cs" /> <Compile Include="Configuration\ModelsMode.cs" />
<Compile Include="Configuration\ModelsModeExtensions.cs" /> <Compile Include="Configuration\ModelsModeExtensions.cs" />
<Compile Include="Configuration\UmbracoConfigExtensions.cs" />
<Compile Include="Dashboard\BuilderDashboardHelper.cs" /> <Compile Include="Dashboard\BuilderDashboardHelper.cs" />
<Compile Include="EnumerableExtensions.cs" /> <Compile Include="EnumerableExtensions.cs" />
<Compile Include="IgnoreContentTypeAttribute.cs" />
<Compile Include="IgnorePropertyTypeAttribute.cs" />
<Compile Include="ImplementContentTypeAttribute.cs" />
<Compile Include="ImplementPropertyTypeAttribute.cs" /> <Compile Include="ImplementPropertyTypeAttribute.cs" />
<Compile Include="ModelsBaseClassAttribute.cs" />
<Compile Include="ModelsBuilderAssemblyAttribute.cs" /> <Compile Include="ModelsBuilderAssemblyAttribute.cs" />
<Compile Include="ModelsNamespaceAttribute.cs" />
<Compile Include="ModelsUsingAttribute.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PublishedElementExtensions.cs" /> <Compile Include="PublishedElementExtensions.cs" />
<Compile Include="PublishedPropertyTypeExtensions.cs" />
<Compile Include="PureLiveAssemblyAttribute.cs" /> <Compile Include="PureLiveAssemblyAttribute.cs" />
<Compile Include="ReferencedAssemblies.cs" /> <Compile Include="ReferencedAssemblies.cs" />
<Compile Include="RenameContentTypeAttribute.cs" />
<Compile Include="RenamePropertyTypeAttribute.cs" />
<Compile Include="TypeExtensions.cs" /> <Compile Include="TypeExtensions.cs" />
<Compile Include="Umbraco\UmbracoServices.cs" />
<Compile Include="Umbraco\HashCombiner.cs" /> <Compile Include="Umbraco\HashCombiner.cs" />
<Compile Include="Umbraco\HashHelper.cs" /> <Compile Include="Umbraco\HashHelper.cs" />
<Compile Include="Umbraco\LiveModelsProvider.cs" /> <Compile Include="Umbraco\LiveModelsProvider.cs" />
<Compile Include="Umbraco\ModelsBuilderBackOfficeController.cs" /> <Compile Include="Umbraco\ModelsBuilderBackOfficeController.cs" />
<Compile Include="Umbraco\ModelsBuilderComponent.cs" /> <Compile Include="Umbraco\ModelsBuilderComponent.cs" />
<Compile Include="Umbraco\ModelsBuilderComposer.cs" />
<Compile Include="Umbraco\ModelsBuilderInitializer.cs" />
<Compile Include="Umbraco\ModelsGenerationError.cs" /> <Compile Include="Umbraco\ModelsGenerationError.cs" />
<Compile Include="Umbraco\OutOfDateModelsStatus.cs" /> <Compile Include="Umbraco\OutOfDateModelsStatus.cs" />
<Compile Include="Umbraco\PublishedModelUtility.cs" /> <Compile Include="Umbraco\PublishedModelUtility.cs" />
<Compile Include="Umbraco\PureLiveModelFactory.cs" /> <Compile Include="Umbraco\PureLiveModelFactory.cs" />
<Compile Include="Umbraco\UmbracoServices.cs" />
<Compile Include="Validation\ContentTypeModelValidator.cs" /> <Compile Include="Validation\ContentTypeModelValidator.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp">
<Version>2.8.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj"> <ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj">
<Project>{31785bc3-256c-4613-b2f5-a1b0bdded8c1}</Project> <Project>{31785BC3-256C-4613-B2F5-A1B0BDDED8C1}</Project>
<Name>Umbraco.Core</Name> <Name>Umbraco.Core</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj"> <ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
@@ -114,5 +92,10 @@
<Name>Umbraco.Web</Name> <Name>Umbraco.Web</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.Mvc">
<Version>5.2.7</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>
@@ -6,13 +6,10 @@ namespace Umbraco.ModelsBuilder.Umbraco
{ {
class HashHelper class HashHelper
{ {
public static string Hash(IDictionary<string, string> ourFiles, IEnumerable<TypeModel> typeModels) public static string Hash(IEnumerable<TypeModel> typeModels)
{ {
var hash = new HashCombiner(); var hash = new HashCombiner();
foreach (var kvp in ourFiles)
hash.Add(kvp.Key + "::" + kvp.Value);
// see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash
// ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference
@@ -3,7 +3,6 @@ using System.Threading;
using System.Web; using System.Web;
using System.Web.Hosting; using System.Web.Hosting;
using Umbraco.Core.Composing; using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging; using Umbraco.Core.Logging;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Umbraco; using Umbraco.ModelsBuilder.Umbraco;
@@ -21,15 +20,10 @@ namespace Umbraco.ModelsBuilder.Umbraco
private static Mutex _mutex; private static Mutex _mutex;
private static int _req; private static int _req;
internal static bool IsEnabled private static Config Config => Current.Configs.ModelsBuilder();
{
get // we do not manage pure live here
{ internal static bool IsEnabled => Config.ModelsMode.IsLiveNotPure();
var config = UmbracoConfig.For.ModelsBuilder();
return config.ModelsMode.IsLiveNotPure();
// we do not manage pure live here
}
}
internal static void Install(UmbracoServices umbracoServices) internal static void Install(UmbracoServices umbracoServices)
{ {
@@ -104,16 +98,15 @@ namespace Umbraco.ModelsBuilder.Umbraco
private static void GenerateModels() private static void GenerateModels()
{ {
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; var modelsDirectory = Config.ModelsDirectory;
var modelsNamespace = Config.ModelsNamespace;
var bin = HostingEnvironment.MapPath("~/bin"); var bin = HostingEnvironment.MapPath("~/bin");
if (bin == null) if (bin == null)
throw new Exception("Panic: bin is null."); throw new Exception("Panic: bin is null.");
var config = UmbracoConfig.For.ModelsBuilder();
// EnableDllModels will recycle the app domain - but this request will end properly // EnableDllModels will recycle the app domain - but this request will end properly
ModelsBuilderBackOfficeController.GenerateModels(_umbracoServices, modelsDirectory, config.ModelsMode.IsAnyDll() ? bin : null); ModelsBuilderBackOfficeController.GenerateModels(_umbracoServices, modelsDirectory, modelsNamespace);
} }
} }
@@ -133,9 +126,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
public static void Install() public static void Install()
{ {
if (!LiveModelsProvider.IsEnabled) // always - don't read config in PreApplicationStartMethod
return;
HttpApplication.RegisterModule(typeof(LiveModelsProviderModule)); HttpApplication.RegisterModule(typeof(LiveModelsProviderModule));
} }
} }
@@ -6,7 +6,6 @@ using System.Net.Http;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text; using System.Text;
using System.Web.Hosting; using System.Web.Hosting;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Building; using Umbraco.ModelsBuilder.Building;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
using Umbraco.ModelsBuilder.Dashboard; using Umbraco.ModelsBuilder.Dashboard;
@@ -23,38 +22,42 @@ namespace Umbraco.ModelsBuilder.Umbraco
/// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to /// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to
/// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats. /// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats.
/// </remarks> /// </remarks>
[UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)] [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)]
public class ModelsBuilderBackOfficeController : UmbracoAuthorizedJsonController public class ModelsBuilderBackOfficeController : UmbracoAuthorizedJsonController
{ {
private readonly UmbracoServices _umbracoServices; private readonly UmbracoServices _umbracoServices;
private readonly Config _config;
public ModelsBuilderBackOfficeController(UmbracoServices umbracoServices) public ModelsBuilderBackOfficeController(UmbracoServices umbracoServices, Config config)
{ {
_umbracoServices = umbracoServices; _umbracoServices = umbracoServices;
_config = config;
} }
// invoked by the dashboard // invoked by the dashboard
// requires that the user is logged into the backoffice and has access to the developer section // requires that the user is logged into the backoffice and has access to the settings section
// beware! the name of the method appears in modelsbuilder.controller.js // beware! the name of the method appears in modelsbuilder.controller.js
[System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers!
public HttpResponseMessage BuildModels() public HttpResponseMessage BuildModels()
{ {
try try
{ {
if (!UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration()) var config = _config;
if (!config.ModelsMode.SupportsExplicitGeneration())
{ {
var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." };
return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter); return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter);
} }
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; var modelsDirectory = config.ModelsDirectory;
var bin = HostingEnvironment.MapPath("~/bin"); var bin = HostingEnvironment.MapPath("~/bin");
if (bin == null) if (bin == null)
throw new Exception("Panic: bin is null."); throw new Exception("Panic: bin is null.");
// EnableDllModels will recycle the app domain - but this request will end properly // EnableDllModels will recycle the app domain - but this request will end properly
GenerateModels(modelsDirectory, UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll() ? bin : null); GenerateModels(modelsDirectory);
ModelsGenerationError.Clear(); ModelsGenerationError.Clear();
} }
@@ -67,7 +70,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
} }
// invoked by the back-office // invoked by the back-office
// requires that the user is logged into the backoffice and has access to the developer section // requires that the user is logged into the backoffice and has access to the settings section
[System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers!
public HttpResponseMessage GetModelsOutOfDateStatus() public HttpResponseMessage GetModelsOutOfDateStatus()
{ {
@@ -81,7 +84,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
} }
// invoked by the back-office // invoked by the back-office
// requires that the user is logged into the backoffice and has access to the developer section // requires that the user is logged into the backoffice and has access to the settings section
// beware! the name of the method appears in modelsbuilder.controller.js // beware! the name of the method appears in modelsbuilder.controller.js
[System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers!
public HttpResponseMessage GetDashboard() public HttpResponseMessage GetDashboard()
@@ -93,21 +96,20 @@ namespace Umbraco.ModelsBuilder.Umbraco
{ {
return new Dashboard return new Dashboard
{ {
Enable = UmbracoConfig.For.ModelsBuilder().Enable, Enable = _config.Enable,
Text = BuilderDashboardHelper.Text(), Text = BuilderDashboardHelper.Text(),
CanGenerate = BuilderDashboardHelper.CanGenerate(), CanGenerate = BuilderDashboardHelper.CanGenerate(),
GenerateCausesRestart = BuilderDashboardHelper.GenerateCausesRestart(),
OutOfDateModels = BuilderDashboardHelper.AreModelsOutOfDate(), OutOfDateModels = BuilderDashboardHelper.AreModelsOutOfDate(),
LastError = BuilderDashboardHelper.LastError(), LastError = BuilderDashboardHelper.LastError(),
}; };
} }
private void GenerateModels(string modelsDirectory, string bin) private void GenerateModels(string modelsDirectory)
{ {
GenerateModels(_umbracoServices, modelsDirectory, bin); GenerateModels(_umbracoServices, modelsDirectory, _config.ModelsNamespace);
} }
internal static void GenerateModels(UmbracoServices umbracoServices, string modelsDirectory, string bin) internal static void GenerateModels(UmbracoServices umbracoServices, string modelsDirectory, string modelsNamespace)
{ {
if (!Directory.Exists(modelsDirectory)) if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory); Directory.CreateDirectory(modelsDirectory);
@@ -117,9 +119,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
var typeModels = umbracoServices.GetAllTypes(); var typeModels = umbracoServices.GetAllTypes();
var ourFiles = Directory.GetFiles(modelsDirectory, "*.cs").ToDictionary(x => x, File.ReadAllText); var builder = new TextBuilder(typeModels, modelsNamespace);
var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles);
var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace);
foreach (var typeModel in builder.GetModelsToGenerate()) foreach (var typeModel in builder.GetModelsToGenerate())
{ {
@@ -140,14 +140,6 @@ namespace Umbraco.ModelsBuilder.Umbraco
"; ";
*/ */
if (bin != null)
{
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
ourFiles[file] = File.ReadAllText(file);
var compiler = new Compiler();
compiler.Compile(builder.GetModelsNamespace(), ourFiles, bin);
}
OutOfDateModelsStatus.Clear(); OutOfDateModelsStatus.Clear();
} }
@@ -169,8 +161,6 @@ namespace Umbraco.ModelsBuilder.Umbraco
public string Text; public string Text;
[DataMember(Name = "canGenerate")] [DataMember(Name = "canGenerate")]
public bool CanGenerate; public bool CanGenerate;
[DataMember(Name = "generateCausesRestart")]
public bool GenerateCausesRestart;
[DataMember(Name = "outOfDateModels")] [DataMember(Name = "outOfDateModels")]
public bool OutOfDateModels; public bool OutOfDateModels;
[DataMember(Name = "lastError")] [DataMember(Name = "lastError")]
@@ -1,95 +1,56 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using System.Web; using System.Web;
using System.Web.Mvc; using System.Web.Mvc;
using System.Web.Routing; using System.Web.Routing;
using LightInject;
using Umbraco.Core;
using Umbraco.Core.Components;
using Umbraco.Core.Composing; using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO; using Umbraco.Core.IO;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services; using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement; using Umbraco.Core.Services.Implement;
using Umbraco.ModelsBuilder.Api;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
using Umbraco.Web; using Umbraco.Web;
using Umbraco.Web.PublishedCache.NuCache; using Umbraco.Web.JavaScript;
using Umbraco.Web.UI.JavaScript; using Umbraco.Web.Mvc;
namespace Umbraco.ModelsBuilder.Umbraco namespace Umbraco.ModelsBuilder.Umbraco
{ {
[RequiredComponent(typeof(NuCacheComponent))] public class ModelsBuilderComponent : IComponent
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class ModelsBuilderComponent : UmbracoComponentBase, IUmbracoCoreComponent
{ {
public override void Compose(Composition composition) private readonly UmbracoServices _umbracoServices;
private readonly Config _config;
public ModelsBuilderComponent(UmbracoServices umbracoServices, Config config)
{ {
base.Compose(composition); _umbracoServices = umbracoServices;
_config = config;
composition.Container.Register<UmbracoServices>(new PerContainerLifetime());
var config = UmbracoConfig.For.ModelsBuilder();
if (config.ModelsMode == ModelsMode.PureLive)
ComposeForLiveModels(composition.Container);
else if (config.EnableFactory)
ComposeForDefaultModelsFactory(composition.Container);
// always setup the dashboard
InstallServerVars(composition.Container.GetInstance<IRuntimeState>().Level);
composition.Container.Register(typeof(ModelsBuilderBackOfficeController), new PerRequestLifeTime());
// setup the API if enabled (and in debug mode)
if (config.ApiServer)
composition.Container.Register(typeof(ModelsBuilderApiController), new PerRequestLifeTime());
} }
public void Initialize(UmbracoServices umbracoServices) public void Initialize()
{ {
var config = UmbracoConfig.For.ModelsBuilder(); // always setup the dashboard
// note: UmbracoApiController instances are automatically registered
InstallServerVars();
if (config.Enable) ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException;
if (_config.Enable)
FileService.SavingTemplate += FileService_SavingTemplate; FileService.SavingTemplate += FileService_SavingTemplate;
// fixme LiveModelsProvider should not be static // fixme LiveModelsProvider should not be static
if (config.ModelsMode.IsLiveNotPure()) if (_config.ModelsMode.IsLiveNotPure())
LiveModelsProvider.Install(umbracoServices); LiveModelsProvider.Install(_umbracoServices);
// fixme OutOfDateModelsStatus should not be static // fixme OutOfDateModelsStatus should not be static
if (config.FlagOutOfDateModels) if (_config.FlagOutOfDateModels)
OutOfDateModelsStatus.Install(); OutOfDateModelsStatus.Install();
} }
private void ComposeForDefaultModelsFactory(IServiceContainer container) public void Terminate()
{ { }
container.RegisterSingleton<IPublishedModelFactory>(factory
=> new PublishedModelFactory(factory.GetInstance<TypeLoader>().GetTypes<PublishedContentModel>()));
}
private void ComposeForLiveModels(IServiceContainer container) private void InstallServerVars()
{
container.RegisterSingleton<IPublishedModelFactory, PureLiveModelFactory>();
// the following would add @using statement in every view so user's don't
// have to do it - however, then noone understands where the @using statement
// comes from, and it cannot be avoided / removed --- DISABLED
//
/*
// no need for @using in views
// note:
// we are NOT using the in-code attribute here, config is required
// because that would require parsing the code... and what if it changes?
// we can AddGlobalImport not sure we can remove one anyways
var modelsNamespace = Configuration.Config.ModelsNamespace;
if (string.IsNullOrWhiteSpace(modelsNamespace))
modelsNamespace = Configuration.Config.DefaultModelsNamespace;
System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace);
*/
}
private void InstallServerVars(RuntimeLevel level)
{ {
// register our url - for the backoffice api // register our url - for the backoffice api
ServerVariablesParser.Parsing += (sender, serverVars) => ServerVariablesParser.Parsing += (sender, serverVars) =>
@@ -111,18 +72,15 @@ namespace Umbraco.ModelsBuilder.Umbraco
var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()));
umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl<ModelsBuilderBackOfficeController>(controller => controller.BuildModels()); umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl<ModelsBuilderBackOfficeController>(controller => controller.BuildModels());
umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(level); umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings();
}; };
} }
private Dictionary<string, object> GetModelsBuilderSettings(RuntimeLevel level) private Dictionary<string, object> GetModelsBuilderSettings()
{ {
if (level != RuntimeLevel.Run)
return null;
var settings = new Dictionary<string, object> var settings = new Dictionary<string, object>
{ {
{"enabled", UmbracoConfig.For.ModelsBuilder().Enable} {"enabled", _config.Enable}
}; };
return settings; return settings;
@@ -138,7 +96,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
{ {
// don't do anything if the factory is not enabled // don't do anything if the factory is not enabled
// because, no factory = no models (even if generation is enabled) // because, no factory = no models (even if generation is enabled)
if (!UmbracoConfig.For.ModelsBuilder().EnableFactory) return; if (!_config.EnableFactory) return;
// don't do anything if this special key is not found // don't do anything if this special key is not found
if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return;
@@ -159,7 +117,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
var name = template.Name; // will be the name of the content type since we are creating var name = template.Name; // will be the name of the content type since we are creating
var className = UmbracoServices.GetClrName(name, alias); var className = UmbracoServices.GetClrName(name, alias);
var modelNamespace = UmbracoConfig.For.ModelsBuilder().ModelsNamespace; var modelNamespace = _config.ModelsNamespace;
// we do not support configuring this at the moment, so just let Umbraco use its default value // we do not support configuring this at the moment, so just let Umbraco use its default value
//var modelNamespaceAlias = ...; //var modelNamespaceAlias = ...;
@@ -174,5 +132,55 @@ namespace Umbraco.ModelsBuilder.Umbraco
} }
} }
} }
private void ContentModelBinder_ModelBindingException(object sender, ContentModelBinder.ModelBindingArgs args)
{
var sourceAttr = args.SourceType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
var modelAttr = args.ModelType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
// if source or model is not a ModelsBuider type...
if (sourceAttr == null || modelAttr == null)
{
// if neither are ModelsBuilder types, give up entirely
if (sourceAttr == null && modelAttr == null)
return;
// else report, but better not restart (loops?)
args.Message.Append(" The ");
args.Message.Append(sourceAttr == null ? "view model" : "source");
args.Message.Append(" is a ModelsBuilder type, but the ");
args.Message.Append(sourceAttr != null ? "view model" : "source");
args.Message.Append(" is not. The application is in an unstable state and should be restarted.");
return;
}
// both are ModelsBuilder types
var pureSource = sourceAttr.PureLive;
var pureModel = modelAttr.PureLive;
if (sourceAttr.PureLive || modelAttr.PureLive)
{
if (pureSource == false || pureModel == false)
{
// only one is pure - report, but better not restart (loops?)
args.Message.Append(pureSource
? " The content model is PureLive, but the view model is not."
: " The view model is PureLive, but the content model is not.");
args.Message.Append(" The application is in an unstable state and should be restarted.");
}
else
{
// both are pure - report, and if different versions, restart
// if same version... makes no sense... and better not restart (loops?)
var sourceVersion = args.SourceType.Assembly.GetName().Version;
var modelVersion = args.ModelType.Assembly.GetName().Version;
args.Message.Append(" Both view and content models are PureLive, with ");
args.Message.Append(sourceVersion == modelVersion
? "same version. The application is in an unstable state and should be restarted."
: "different versions. The application is in an unstable state and is going to be restarted.");
args.Restart = sourceVersion != modelVersion;
}
}
}
} }
} }
@@ -0,0 +1,60 @@
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.ModelsBuilder.Configuration;
using Umbraco.Web.PublishedCache.NuCache;
namespace Umbraco.ModelsBuilder.Umbraco
{
[ComposeBefore(typeof(NuCacheComposer))]
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public sealed class ModelsBuilderComposer : ComponentComposer<ModelsBuilderComponent>, ICoreComposer
{
public override void Compose(Composition composition)
{
base.Compose(composition);
composition.Register<UmbracoServices>(Lifetime.Singleton);
composition.Configs.Add(() => new Config());
if (composition.Configs.ModelsBuilder().ModelsMode == ModelsMode.PureLive)
ComposeForLiveModels(composition);
else if (composition.Configs.ModelsBuilder().EnableFactory)
ComposeForDefaultModelsFactory(composition);
}
private void ComposeForDefaultModelsFactory(Composition composition)
{
composition.RegisterUnique<IPublishedModelFactory>(factory =>
{
var typeLoader = factory.GetInstance<TypeLoader>();
var types = typeLoader
.GetTypes<PublishedElementModel>() // element models
.Concat(typeLoader.GetTypes<PublishedContentModel>()); // content models
return new PublishedModelFactory(types);
});
}
private void ComposeForLiveModels(Composition composition)
{
composition.RegisterUnique<IPublishedModelFactory, PureLiveModelFactory>();
// the following would add @using statement in every view so user's don't
// have to do it - however, then noone understands where the @using statement
// comes from, and it cannot be avoided / removed --- DISABLED
//
/*
// no need for @using in views
// note:
// we are NOT using the in-code attribute here, config is required
// because that would require parsing the code... and what if it changes?
// we can AddGlobalImport not sure we can remove one anyways
var modelsNamespace = Configuration.Config.ModelsNamespace;
if (string.IsNullOrWhiteSpace(modelsNamespace))
modelsNamespace = Configuration.Config.DefaultModelsNamespace;
System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace);
*/
}
}
}
@@ -0,0 +1,28 @@
using System.Web;
using System.Web.Compilation;
using Umbraco.ModelsBuilder.Umbraco;
[assembly: PreApplicationStartMethod(typeof(ModelsBuilderInitializer), "Initialize")]
namespace Umbraco.ModelsBuilder.Umbraco
{
public static class ModelsBuilderInitializer
{
public static void Initialize()
{
// for some reason, netstandard is missing from BuildManager.ReferencedAssemblies and yet, is part of
// the references that CSharpCompiler receives - in some cases eg when building views - but not when
// using BuildManager to build the PureLive models - where is it coming from? cannot figure it out
// so... cheating here
// this is equivalent to adding
// <add assembly="netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51" />
// to web.config system.web/compilation/assemblies
var netStandard = ReferencedAssemblies.GetNetStandardAssembly();
if (netStandard != null)
BuildManager.AddReferencedAssembly(netStandard);
}
}
}
@@ -1,13 +1,15 @@
using System; using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Umbraco.Core.Configuration; using Umbraco.Core.Composing;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
namespace Umbraco.ModelsBuilder.Umbraco namespace Umbraco.ModelsBuilder.Umbraco
{ {
internal static class ModelsGenerationError internal static class ModelsGenerationError
{ {
private static Config Config => Current.Configs.ModelsBuilder();
public static void Clear() public static void Clear()
{ {
var errFile = GetErrFile(); var errFile = GetErrFile();
@@ -50,7 +52,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
private static string GetErrFile() private static string GetErrFile()
{ {
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; var modelsDirectory = Config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory)) if (!Directory.Exists(modelsDirectory))
return null; return null;
@@ -1,8 +1,5 @@
using System; using System.IO;
using System.IO; using Umbraco.Core.Composing;
using System.Web.Hosting;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
using Umbraco.Web.Cache; using Umbraco.Web.Cache;
@@ -10,10 +7,12 @@ namespace Umbraco.ModelsBuilder.Umbraco
{ {
public sealed class OutOfDateModelsStatus public sealed class OutOfDateModelsStatus
{ {
private static Config Config => Current.Configs.ModelsBuilder();
internal static void Install() internal static void Install()
{ {
// just be sure // just be sure
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) if (Config.FlagOutOfDateModels == false)
return; return;
ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write();
@@ -22,7 +21,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
private static string GetFlagPath() private static string GetFlagPath()
{ {
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; var modelsDirectory = Config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory)) if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory); Directory.CreateDirectory(modelsDirectory);
return Path.Combine(modelsDirectory, "ood.flag"); return Path.Combine(modelsDirectory, "ood.flag");
@@ -37,19 +36,19 @@ namespace Umbraco.ModelsBuilder.Umbraco
public static void Clear() public static void Clear()
{ {
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return; if (Config.FlagOutOfDateModels == false) return;
var path = GetFlagPath(); var path = GetFlagPath();
if (path == null || !File.Exists(path)) return; if (path == null || !File.Exists(path)) return;
File.Delete(path); File.Delete(path);
} }
public static bool IsEnabled => UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels; public static bool IsEnabled => Config.FlagOutOfDateModels;
public static bool IsOutOfDate public static bool IsOutOfDate
{ {
get get
{ {
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return false; if (Config.FlagOutOfDateModels == false) return false;
var path = GetFlagPath(); var path = GetFlagPath();
return path != null && File.Exists(path); return path != null && File.Exists(path);
} }
@@ -24,7 +24,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
// // etc... // // etc...
//} //}
public static PublishedContentType GetModelContentType(PublishedItemType itemType, string alias) public static IPublishedContentType GetModelContentType(PublishedItemType itemType, string alias)
{ {
var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject! var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject!
switch (itemType) switch (itemType)
@@ -40,7 +40,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
} }
} }
public static PublishedPropertyType GetModelPropertyType<TModel, TValue>(PublishedContentType contentType, Expression<Func<TModel, TValue>> selector) public static IPublishedPropertyType GetModelPropertyType<TModel, TValue>(PublishedContentType contentType, Expression<Func<TModel, TValue>> selector)
//where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel
{ {
// fixme therefore, missing a check on TModel here // fixme therefore, missing a check on TModel here
@@ -3,19 +3,18 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Reflection.Emit; using System.Reflection.Emit;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Web;
using System.Web.Compilation; using System.Web.Compilation;
using System.Web.Hosting; using System.Web.Hosting;
using System.Web.WebPages.Razor; using System.Web.WebPages.Razor;
using Umbraco.Core; using Umbraco.Core;
using Umbraco.Core.Configuration; using Umbraco.Core.Composing;
using Umbraco.Core.Logging; using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Cache; using Umbraco.Web.Cache;
using Umbraco.ModelsBuilder.Building; using Umbraco.ModelsBuilder.Building;
@@ -24,14 +23,14 @@ using File = System.IO.File;
namespace Umbraco.ModelsBuilder.Umbraco namespace Umbraco.ModelsBuilder.Umbraco
{ {
internal class PureLiveModelFactory : IPublishedModelFactory, IRegisteredObject internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject
{ {
private Assembly _modelsAssembly; private Assembly _modelsAssembly;
private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() }; private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
private volatile bool _hasModels; // volatile 'cos reading outside lock private volatile bool _hasModels; // volatile 'cos reading outside lock
private bool _pendingRebuild; private bool _pendingRebuild;
private readonly ProfilingLogger _logger; private readonly IProfilingLogger _logger;
private readonly FileSystemWatcher _watcher; private readonly FileSystemWatcher _watcher;
private int _ver, _skipver; private int _ver, _skipver;
private readonly int _debugLevel; private readonly int _debugLevel;
@@ -43,19 +42,21 @@ namespace Umbraco.ModelsBuilder.Umbraco
private const string ProjVirt = "~/App_Data/Models/all.generated.cs"; private const string ProjVirt = "~/App_Data/Models/all.generated.cs";
private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" }; private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" };
public PureLiveModelFactory(Lazy<UmbracoServices> umbracoServices, ProfilingLogger logger) private readonly Config _config;
public PureLiveModelFactory(Lazy<UmbracoServices> umbracoServices, IProfilingLogger logger, Config config)
{ {
_umbracoServices = umbracoServices; _umbracoServices = umbracoServices;
_logger = logger; _logger = logger;
_config = config;
_ver = 1; // zero is for when we had no version _ver = 1; // zero is for when we had no version
_skipver = -1; // nothing to skip _skipver = -1; // nothing to skip
ContentTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels();
DataTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels();
RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted; RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted;
if (!HostingEnvironment.IsHosted) return; if (!HostingEnvironment.IsHosted) return;
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory)) if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory); Directory.CreateDirectory(modelsDirectory);
@@ -68,9 +69,23 @@ namespace Umbraco.ModelsBuilder.Umbraco
_watcher.EnableRaisingEvents = true; _watcher.EnableRaisingEvents = true;
// get it here, this need to be fast // get it here, this need to be fast
_debugLevel = UmbracoConfig.For.ModelsBuilder().DebugLevel; _debugLevel = _config.DebugLevel;
} }
#region ILivePublishedModelFactory
/// <inheritdoc />
public object SyncRoot { get; } = new object();
/// <inheritdoc />
public void Refresh()
{
ResetModels();
EnsureModels();
}
#endregion
#region IPublishedModelFactory #region IPublishedModelFactory
public IPublishedElement CreateModel(IPublishedElement element) public IPublishedElement CreateModel(IPublishedElement element)
@@ -115,7 +130,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (ctor != null) return ctor(); if (ctor != null) return ctor();
var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType);
ctor = modelInfo.ListCtor = ReflectionUtilities.EmitCtor<Func<IList>>(declaring: listType); ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor<Func<IList>>(declaring: listType);
return ctor(); return ctor();
} }
@@ -163,7 +178,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (_modelsAssembly == null) return; if (_modelsAssembly == null) return;
if (_debugLevel > 0) if (_debugLevel > 0)
_logger.Logger.Debug<PureLiveModelFactory>("RazorBuildProvider.CodeGenerationStarted"); _logger.Debug<PureLiveModelFactory>("RazorBuildProvider.CodeGenerationStarted");
if (!(sender is RazorBuildProvider provider)) return; if (!(sender is RazorBuildProvider provider)) return;
// add the assembly, and add a dependency to a text file that will change on each // add the assembly, and add a dependency to a text file that will change on each
@@ -182,7 +197,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
// tells the factory that it should build a new generation of models // tells the factory that it should build a new generation of models
private void ResetModels() private void ResetModels()
{ {
_logger.Logger.Debug<PureLiveModelFactory>("Resetting models."); _logger.Debug<PureLiveModelFactory>("Resetting models.");
try try
{ {
@@ -190,6 +205,19 @@ namespace Umbraco.ModelsBuilder.Umbraco
_hasModels = false; _hasModels = false;
_pendingRebuild = true; _pendingRebuild = true;
var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory);
// clear stuff
var modelsHashFile = Path.Combine(modelsDirectory, "models.hash");
//var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs");
//var projFile = Path.Combine(modelsDirectory, "all.generated.cs");
var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path");
if (File.Exists(dllPathFile)) File.Delete(dllPathFile);
if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile);
} }
finally finally
{ {
@@ -216,7 +244,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
internal Infos EnsureModels() internal Infos EnsureModels()
{ {
if (_debugLevel > 0) if (_debugLevel > 0)
_logger.Logger.Debug<PureLiveModelFactory>("Ensuring models."); _logger.Debug<PureLiveModelFactory>("Ensuring models.");
// don't use an upgradeable lock here because only 1 thread at a time could enter it // don't use an upgradeable lock here because only 1 thread at a time could enter it
try try
@@ -270,8 +298,8 @@ namespace Umbraco.ModelsBuilder.Umbraco
{ {
try try
{ {
_logger.Logger.Error<PureLiveModelFactory>("Failed to build models.", e); _logger.Error<PureLiveModelFactory>("Failed to build models.", e);
_logger.Logger.Warn<PureLiveModelFactory>("Running without models."); // be explicit _logger.Warn<PureLiveModelFactory>("Running without models."); // be explicit
ModelsGenerationError.Report("Failed to build PureLive models.", e); ModelsGenerationError.Report("Failed to build PureLive models.", e);
} }
finally finally
@@ -300,19 +328,12 @@ namespace Umbraco.ModelsBuilder.Umbraco
private Assembly GetModelsAssembly(bool forceRebuild) private Assembly GetModelsAssembly(bool forceRebuild)
{ {
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory)) if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory); Directory.CreateDirectory(modelsDirectory);
// must filter out *.generated.cs because we haven't deleted them yet!
var ourFiles = Directory.Exists(modelsDirectory)
? Directory.GetFiles(modelsDirectory, "*.cs")
.Where(x => !x.EndsWith(".generated.cs"))
.ToDictionary(x => x, File.ReadAllText)
: new Dictionary<string, string>();
var typeModels = UmbracoServices.GetAllTypes(); var typeModels = UmbracoServices.GetAllTypes();
var currentHash = HashHelper.Hash(ourFiles, typeModels); var currentHash = HashHelper.Hash(typeModels);
var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); var modelsHashFile = Path.Combine(modelsDirectory, "models.hash");
var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs");
var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); var projFile = Path.Combine(modelsDirectory, "all.generated.cs");
@@ -323,31 +344,48 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (!forceRebuild) if (!forceRebuild)
{ {
_logger.Logger.Debug<PureLiveModelFactory>("Looking for cached models."); _logger.Debug<PureLiveModelFactory>("Looking for cached models.");
if (File.Exists(modelsHashFile) && File.Exists(projFile)) if (File.Exists(modelsHashFile) && File.Exists(projFile))
{ {
var cachedHash = File.ReadAllText(modelsHashFile); var cachedHash = File.ReadAllText(modelsHashFile);
if (currentHash != cachedHash) if (currentHash != cachedHash)
{ {
_logger.Logger.Debug<PureLiveModelFactory>("Found obsolete cached models."); _logger.Debug<PureLiveModelFactory>("Found obsolete cached models.");
forceRebuild = true; forceRebuild = true;
} }
// else cachedHash matches currentHash, we can try to load an existing dll
} }
else else
{ {
_logger.Logger.Debug<PureLiveModelFactory>("Could not find cached models."); _logger.Debug<PureLiveModelFactory>("Could not find cached models.");
forceRebuild = true; forceRebuild = true;
} }
} }
Assembly assembly; Assembly assembly;
if (forceRebuild == false) if (!forceRebuild)
{ {
// try to load the dll directly (avoid rebuilding) // try to load the dll directly (avoid rebuilding)
//
// ensure that the .dll file does not have a corresponding .dll.delete file
// as that would mean the the .dll file is going to be deleted and should not
// be re-used - that should not happen in theory, but better be safe
//
// ensure that the .dll file is in the current codegen directory - when IIS
// or Express does a full restart, it can switch to an entirely new codegen
// directory, and then we end up referencing a dll which is *not* in that
// directory, and BuildManager fails to instantiate views ("the view found
// at ... was not created").
//
if (File.Exists(dllPathFile)) if (File.Exists(dllPathFile))
{ {
var dllPath = File.ReadAllText(dllPathFile); var dllPath = File.ReadAllText(dllPathFile);
if (File.Exists(dllPath)) var codegen = HttpRuntime.CodegenDir;
_logger.Debug<PureLiveModelFactory>($"Cached models dll at {dllPath}.");
if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete") && dllPath.StartsWith(codegen))
{ {
assembly = Assembly.LoadFile(dllPath); assembly = Assembly.LoadFile(dllPath);
var attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>(); var attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
@@ -359,13 +397,23 @@ namespace Umbraco.ModelsBuilder.Umbraco
// with the "same but different" version of the assembly in memory // with the "same but different" version of the assembly in memory
_skipver = assembly.GetName().Version.Revision; _skipver = assembly.GetName().Version.Revision;
_logger.Logger.Debug<PureLiveModelFactory>("Loading cached models (dll)."); _logger.Debug<PureLiveModelFactory>("Loading cached models (dll).");
return assembly; return assembly;
} }
_logger.Debug<PureLiveModelFactory>("Cached models dll cannot be loaded (invalid assembly).");
} }
else if (!File.Exists(dllPath))
_logger.Debug<PureLiveModelFactory>("Cached models dll does not exist.");
else if (File.Exists(dllPath + ".delete"))
_logger.Debug<PureLiveModelFactory>("Cached models dll is marked for deletion.");
else if (!dllPath.StartsWith(codegen))
_logger.Debug<PureLiveModelFactory>("Cached models dll is in a different codegen directory.");
else
_logger.Debug<PureLiveModelFactory>("Cached models dll cannot be loaded (why?).");
} }
// mmust reset the version in the file else it would keep growing // must reset the version in the file else it would keep growing
// loading cached modules only happens when the app restarts // loading cached modules only happens when the app restarts
var text = File.ReadAllText(projFile); var text = File.ReadAllText(projFile);
var match = AssemblyVersionRegex.Match(text); var match = AssemblyVersionRegex.Match(text);
@@ -381,18 +429,26 @@ namespace Umbraco.ModelsBuilder.Umbraco
//File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver); //File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver);
_ver++; _ver++;
assembly = BuildManager.GetCompiledAssembly(ProjVirt); try
File.WriteAllText(dllPathFile, assembly.Location); {
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
File.WriteAllText(dllPathFile, assembly.Location);
}
catch
{
ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile);
throw;
}
_logger.Logger.Debug<PureLiveModelFactory>("Loading cached models (source)."); _logger.Debug<PureLiveModelFactory>("Loading cached models (source).");
return assembly; return assembly;
} }
// need to rebuild // need to rebuild
_logger.Logger.Debug<PureLiveModelFactory>("Rebuilding models."); _logger.Debug<PureLiveModelFactory>("Rebuilding models.");
// generate code, save // generate code, save
var code = GenerateModelsCode(ourFiles, typeModels); var code = GenerateModelsCode(typeModels);
// add extra attributes, // add extra attributes,
// PureLiveAssembly helps identifying Assemblies that contain PureLive models // PureLiveAssembly helps identifying Assemblies that contain PureLive models
// AssemblyVersion is so that we have a different version for each rebuild // AssemblyVersion is so that we have a different version for each rebuild
@@ -404,21 +460,47 @@ namespace Umbraco.ModelsBuilder.Umbraco
File.WriteAllText(modelsSrcFile, code); File.WriteAllText(modelsSrcFile, code);
// generate proj, save // generate proj, save
ourFiles["models.generated.cs"] = code; var projFiles = new Dictionary<string, string>
var proj = GenerateModelsProj(ourFiles); {
{ "models.generated.cs", code }
};
var proj = GenerateModelsProj(projFiles);
File.WriteAllText(projFile, proj); File.WriteAllText(projFile, proj);
// compile and register // compile and register
assembly = BuildManager.GetCompiledAssembly(ProjVirt); try
File.WriteAllText(dllPathFile, assembly.Location); {
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
File.WriteAllText(dllPathFile, assembly.Location);
File.WriteAllText(modelsHashFile, currentHash);
}
catch
{
ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile);
throw;
}
// assuming we can write and it's not going to cause exceptions... _logger.Debug<PureLiveModelFactory>("Done rebuilding.");
File.WriteAllText(modelsHashFile, currentHash);
_logger.Logger.Debug<PureLiveModelFactory>("Done rebuilding.");
return assembly; return assembly;
} }
private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile)
{
_logger.Debug<PureLiveModelFactory>("Failed to compile.");
// the dll file reference still points to the previous dll, which is obsolete
// now and will be deleted by ASP.NET eventually, so better clear that reference.
// also touch the proj file to force views to recompile - don't delete as it's
// useful to have the source around for debugging.
try
{
if (File.Exists(dllPathFile)) File.Delete(dllPathFile);
if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile);
if (File.Exists(projFile)) File.SetLastWriteTime(projFile, DateTime.Now);
}
catch { /* enough */ }
}
private static Infos RegisterModels(IEnumerable<Type> types) private static Infos RegisterModels(IEnumerable<Type> types)
{ {
var ctorArgTypes = new[] { typeof (IPublishedElement) }; var ctorArgTypes = new[] { typeof (IPublishedElement) };
@@ -466,26 +548,16 @@ namespace Umbraco.ModelsBuilder.Umbraco
return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map }; return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map };
} }
private static string GenerateModelsCode(IDictionary<string, string> ourFiles, IList<TypeModel> typeModels) private string GenerateModelsCode(IList<TypeModel> typeModels)
{ {
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; var modelsDirectory = _config.ModelsDirectory;
if (!Directory.Exists(modelsDirectory)) if (!Directory.Exists(modelsDirectory))
Directory.CreateDirectory(modelsDirectory); Directory.CreateDirectory(modelsDirectory);
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
File.Delete(file); File.Delete(file);
var map = typeModels.ToDictionary(x => x.Alias, x => x.ClrName); var builder = new TextBuilder(typeModels, _config.ModelsNamespace);
foreach (var typeModel in typeModels)
{
foreach (var propertyModel in typeModel.Properties)
{
propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map);
}
}
var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles);
var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace);
var codeBuilder = new StringBuilder(); var codeBuilder = new StringBuilder();
builder.Generate(codeBuilder, builder.GetModelsToGenerate()); builder.Generate(codeBuilder, builder.GetModelsToGenerate());
@@ -577,7 +649,7 @@ namespace Umbraco.ModelsBuilder.Umbraco
//if (_building && OurFiles.Contains(changed)) //if (_building && OurFiles.Contains(changed))
//{ //{
// //_logger.Logger.Info<PureLiveModelFactory>("Ignoring files self-changes."); // //_logger.Info<PureLiveModelFactory>("Ignoring files self-changes.");
// return; // return;
//} //}
@@ -585,9 +657,12 @@ namespace Umbraco.ModelsBuilder.Umbraco
if (OurFiles.Contains(changed)) if (OurFiles.Contains(changed))
return; return;
_logger.Logger.Info<PureLiveModelFactory>("Detected files changes."); _logger.Info<PureLiveModelFactory>("Detected files changes.");
ResetModels(); lock (SyncRoot) // don't reset while being locked
{
ResetModels();
}
} }
public void Stop(bool immediate) public void Stop(bool immediate)
@@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Umbraco.Core; using Umbraco.Core;
using Umbraco.Core.Configuration; using Umbraco.Core.Composing;
using Umbraco.Core.Models; using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services; using Umbraco.Core.Services;
@@ -27,6 +27,8 @@ namespace Umbraco.ModelsBuilder.Umbraco
_publishedContentTypeFactory = publishedContentTypeFactory; _publishedContentTypeFactory = publishedContentTypeFactory;
} }
private static Config Config => Current.Configs.ModelsBuilder();
#region Services #region Services
public IList<TypeModel> GetAllTypes() public IList<TypeModel> GetAllTypes()
@@ -60,38 +62,8 @@ namespace Umbraco.ModelsBuilder.Umbraco
public static string GetClrName(string name, string alias) public static string GetClrName(string name, string alias)
{ {
// ideally we should just be able to re-use Umbraco's alias, // ModelsBuilder's legacy - but not ideal
// just upper-casing the first letter, however in v7 for backward return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase);
// compatibility reasons aliases derive from names via ToSafeAlias which is
// PreFilter = ApplyUrlReplaceCharacters,
// IsTerm = (c, leading) => leading
// ? char.IsLetter(c) // only letters
// : (char.IsLetterOrDigit(c) || c == '_'), // letter, digit or underscore
// StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase,
// BreakTermsOnUpper = false
//
// but that is not ideal with acronyms and casing
// however we CANNOT change Umbraco
// so, adding a way to "do it right" deriving from name, here
switch (UmbracoConfig.For.ModelsBuilder().ClrNameSource)
{
case ClrNameSource.RawAlias:
// use Umbraco's alias
return alias;
case ClrNameSource.Alias:
// ModelsBuilder's legacy - but not ideal
return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase);
case ClrNameSource.Name:
// derive from name
var source = name.TrimStart('_'); // because CleanStringType.ConvertCase accepts them
return source.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase | CleanStringType.Ascii);
default:
throw new Exception("Invalid ClrNameSource.");
}
} }
private IList<TypeModel> GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) private IList<TypeModel> GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes)
@@ -119,33 +91,23 @@ namespace Umbraco.ModelsBuilder.Umbraco
throw new Exception($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); throw new Exception($"Panic: duplicate type ClrName \"{typeModel.ClrName}\".");
uniqueTypes.Add(typeModel.ClrName); uniqueTypes.Add(typeModel.ClrName);
// fixme - we need a better way at figuring out what's an element type!
// and then we should not do the alias filtering below
bool IsElement(PublishedContentType x)
{
return x.Alias.InvariantEndsWith("Element");
}
var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType);
switch (itemType) switch (itemType)
{ {
case PublishedItemType.Content: case PublishedItemType.Content:
if (IsElement(publishedContentType)) typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element
{ ? TypeModel.ItemTypes.Element
typeModel.ItemType = TypeModel.ItemTypes.Element; : TypeModel.ItemTypes.Content;
if (typeModel.ClrName.InvariantEndsWith("Element"))
typeModel.ClrName = typeModel.ClrName.Substring(0, typeModel.ClrName.Length - "Element".Length);
}
else
{
typeModel.ItemType = TypeModel.ItemTypes.Content;
}
break; break;
case PublishedItemType.Media: case PublishedItemType.Media:
typeModel.ItemType = TypeModel.ItemTypes.Media; typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element
? TypeModel.ItemTypes.Element
: TypeModel.ItemTypes.Media;
break; break;
case PublishedItemType.Member: case PublishedItemType.Member:
typeModel.ItemType = TypeModel.ItemTypes.Member; typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element
? TypeModel.ItemTypes.Element
: TypeModel.ItemTypes.Member;
break; break;
default: default:
throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType));
@@ -2,8 +2,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Umbraco.Core; using Umbraco.Core;
using Umbraco.Core.Configuration; using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Models.PublishedContent;
using Umbraco.ModelsBuilder.Configuration; using Umbraco.ModelsBuilder.Configuration;
using Umbraco.Web.Editors; using Umbraco.Web.Editors;
@@ -12,37 +11,36 @@ using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.ModelsBuilder.Validation namespace Umbraco.ModelsBuilder.Validation
{ {
/// <summary> /// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that /// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used /// no illegal aliases are used
/// </summary> /// </summary>
internal class ContentTypeModelValidator : ContentTypeModelValidatorBase<DocumentTypeSave, PropertyTypeBasic> internal class ContentTypeModelValidator : ContentTypeModelValidatorBase<DocumentTypeSave, PropertyTypeBasic>
{ { }
}
/// <summary> /// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that /// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used /// no illegal aliases are used
/// </summary> /// </summary>
internal class MediaTypeModelValidator : ContentTypeModelValidatorBase<MediaTypeSave, PropertyTypeBasic> internal class MediaTypeModelValidator : ContentTypeModelValidatorBase<MediaTypeSave, PropertyTypeBasic>
{ { }
}
/// <summary> /// <summary>
/// Used to validate the aliases for the content type when MB is enabled to ensure that /// Used to validate the aliases for the content type when MB is enabled to ensure that
/// no illegal aliases are used /// no illegal aliases are used
/// </summary> /// </summary>
internal class MemberTypeModelValidator : ContentTypeModelValidatorBase<MemberTypeSave, MemberPropertyTypeBasic> internal class MemberTypeModelValidator : ContentTypeModelValidatorBase<MemberTypeSave, MemberPropertyTypeBasic>
{ { }
}
internal abstract class ContentTypeModelValidatorBase<TModel, TProperty> : EditorValidator<TModel> internal abstract class ContentTypeModelValidatorBase<TModel, TProperty> : EditorValidator<TModel>
where TModel: ContentTypeSave<TProperty> where TModel: ContentTypeSave<TProperty>
where TProperty: PropertyTypeBasic where TProperty: PropertyTypeBasic
{ {
private static Config Config => Current.Configs.ModelsBuilder();
protected override IEnumerable<ValidationResult> Validate(TModel model) protected override IEnumerable<ValidationResult> Validate(TModel model)
{ {
//don't do anything if we're not enabled //don't do anything if we're not enabled
if (UmbracoConfig.For.ModelsBuilder().Enable) if (Config.Enable)
{ {
var properties = model.Groups.SelectMany(x => x.Properties) var properties = model.Groups.SelectMany(x => x.Properties)
.Where(x => x.Inherited == false) .Where(x => x.Inherited == false)
@@ -51,7 +49,7 @@ namespace Umbraco.ModelsBuilder.Validation
foreach (var prop in properties) foreach (var prop in properties)
{ {
var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop));
if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant()) if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant())
yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[] 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[]
{ {
+4
View File
@@ -117,6 +117,10 @@
<Name>Umbraco.Examine</Name> <Name>Umbraco.Examine</Name>
<Project>{07FBC26B-2927-4A22-8D96-D644C667FECC}</Project> <Project>{07FBC26B-2927-4A22-8D96-D644C667FECC}</Project>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\Umbraco.ModelsBuilder\Umbraco.ModelsBuilder.csproj">
<Project>{52ac0ba8-a60e-4e36-897b-e8b97a54ed1c}</Project>
<Name>Umbraco.ModelsBuilder</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj"> <ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project> <Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
<Name>Umbraco.Web</Name> <Name>Umbraco.Web</Name>
@@ -8,7 +8,7 @@ namespace Umbraco.Web.Editors
/// Provides a base class for <see cref="IEditorValidator"/> implementations. /// Provides a base class for <see cref="IEditorValidator"/> implementations.
/// </summary> /// </summary>
/// <typeparam name="T">The validated object type.</typeparam> /// <typeparam name="T">The validated object type.</typeparam>
internal abstract class EditorValidator<T> : IEditorValidator public abstract class EditorValidator<T> : IEditorValidator
{ {
public Type ModelType => typeof (T); public Type ModelType => typeof (T);
@@ -16,4 +16,4 @@ namespace Umbraco.Web.Editors
protected abstract IEnumerable<ValidationResult> Validate(T model); protected abstract IEnumerable<ValidationResult> Validate(T model);
} }
} }
+8 -2
View File
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio Version 16
VisualStudioVersion = 15.0.27004.2005 VisualStudioVersion = 16.0.29009.5
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}"
EndProject EndProject
@@ -103,6 +103,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp
..\.github\ISSUE_TEMPLATE\5_Security_issue.md = ..\.github\ISSUE_TEMPLATE\5_Security_issue.md ..\.github\ISSUE_TEMPLATE\5_Security_issue.md = ..\.github\ISSUE_TEMPLATE\5_Security_issue.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder", "Umbraco.ModelsBuilder\Umbraco.ModelsBuilder.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -135,6 +137,10 @@ Global
{3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU
{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE