diff --git a/src/Umbraco.Core/Auditing/Audit.cs b/src/Umbraco.Core/Auditing/Audit.cs
index 92ea763461..fe1ec955b8 100644
--- a/src/Umbraco.Core/Auditing/Audit.cs
+++ b/src/Umbraco.Core/Auditing/Audit.cs
@@ -1,7 +1,10 @@
namespace Umbraco.Core.Auditing
{
- public class Audit
+ public static class Audit
{
- public IAuditWriteProvider WriteProvider { get; set; }
+ public static void Add(AuditTypes type, string comment, int userId, int objectId)
+ {
+ AuditTrail.Current.AddEntry(type, comment, userId, objectId);
+ }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Auditing/AuditTrail.cs b/src/Umbraco.Core/Auditing/AuditTrail.cs
new file mode 100644
index 0000000000..321ecb40aa
--- /dev/null
+++ b/src/Umbraco.Core/Auditing/AuditTrail.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Umbraco.Core.Auditing
+{
+ ///
+ /// Represents the Audit implementation
+ ///
+ public class AuditTrail
+ {
+ #region Singleton
+
+ private static readonly Lazy lazy = new Lazy(() => new AuditTrail());
+
+ public static AuditTrail Current { get { return lazy.Value; } }
+
+ private AuditTrail()
+ {
+ WriteProvider = new DataAuditWriteProvider();
+ }
+
+ #endregion
+
+ private IAuditWriteProvider WriteProvider { get; set; }
+
+ public void AddEntry(AuditTypes type, string comment, int userId, int objectId)
+ {
+ WriteProvider.WriteEntry(objectId, userId, DateTime.UtcNow, type.ToString(), comment);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Auditing/AuditTypes.cs b/src/Umbraco.Core/Auditing/AuditTypes.cs
new file mode 100644
index 0000000000..4407c698b3
--- /dev/null
+++ b/src/Umbraco.Core/Auditing/AuditTypes.cs
@@ -0,0 +1,85 @@
+namespace Umbraco.Core.Auditing
+{
+ ///
+ /// Enums for vailable types of auditing
+ ///
+ public enum AuditTypes
+ {
+ ///
+ /// Used when new nodes are added
+ ///
+ New,
+ ///
+ /// Used when nodes are saved
+ ///
+ Save,
+ ///
+ /// Used when nodes are opened
+ ///
+ Open,
+ ///
+ /// Used when nodes are deleted
+ ///
+ Delete,
+ ///
+ /// Used when nodes are published
+ ///
+ Publish,
+ ///
+ /// Used when nodes are send to publishing
+ ///
+ SendToPublish,
+ ///
+ /// Used when nodes are unpublished
+ ///
+ UnPublish,
+ ///
+ /// Used when nodes are moved
+ ///
+ Move,
+ ///
+ /// Used when nodes are copied
+ ///
+ Copy,
+ ///
+ /// Used when nodes are assígned a domain
+ ///
+ AssignDomain,
+ ///
+ /// Used when public access are changed for a node
+ ///
+ PublicAccess,
+ ///
+ /// Used when nodes are sorted
+ ///
+ Sort,
+ ///
+ /// Used when a notification are send to a user
+ ///
+ Notify,
+ ///
+ /// General system notification
+ ///
+ System,
+ ///
+ /// Used when a node's content is rolled back to a previous version
+ ///
+ RollBack,
+ ///
+ /// Used when a package is installed
+ ///
+ PackagerInstall,
+ ///
+ /// Used when a package is uninstalled
+ ///
+ PackagerUninstall,
+ ///
+ /// Used when a node is send to translation
+ ///
+ SendToTranslate,
+ ///
+ /// Use this log action for custom log messages that should be shown in the audit trail
+ ///
+ Custom
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Auditing/DataAuditWriteProvider.cs b/src/Umbraco.Core/Auditing/DataAuditWriteProvider.cs
new file mode 100644
index 0000000000..e29f6e93b8
--- /dev/null
+++ b/src/Umbraco.Core/Auditing/DataAuditWriteProvider.cs
@@ -0,0 +1,29 @@
+using System;
+using Umbraco.Core.Models.Rdbms;
+using Umbraco.Core.Persistence;
+
+namespace Umbraco.Core.Auditing
+{
+ public class DataAuditWriteProvider : IAuditWriteProvider
+ {
+ ///
+ /// Writes an audit entry to the underlaying datastore.
+ ///
+ /// Id of the object (Content, ContentType, Media, etc.)
+ /// Id of the user
+ /// Datestamp
+ /// Audit header
+ /// Audit comment
+ public void WriteEntry(int objectId, int userId, DateTime date, string header, string comment)
+ {
+ DatabaseFactory.Current.Database.Insert(new LogDto
+ {
+ Comment = comment,
+ Datestamp = date,
+ Header = header,
+ NodeId = objectId,
+ UserId = userId
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Auditing/IAuditWriteProvider.cs b/src/Umbraco.Core/Auditing/IAuditWriteProvider.cs
index 84150ad66f..ab4abceb7f 100644
--- a/src/Umbraco.Core/Auditing/IAuditWriteProvider.cs
+++ b/src/Umbraco.Core/Auditing/IAuditWriteProvider.cs
@@ -1,7 +1,17 @@
-namespace Umbraco.Core.Auditing
+using System;
+
+namespace Umbraco.Core.Auditing
{
public interface IAuditWriteProvider
{
-
+ ///
+ /// Writes an audit entry to the underlaying datastore.
+ ///
+ /// Id of the object (Content, ContentType, Media, etc.)
+ /// Id of the user
+ /// Datestamp
+ /// Audit header
+ /// Audit comment
+ void WriteEntry(int objectId, int userId, DateTime date, string header, string comment);
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs
index 118cd5f538..308c803f73 100644
--- a/src/Umbraco.Core/Models/ContentBase.cs
+++ b/src/Umbraco.Core/Models/ContentBase.cs
@@ -6,7 +6,6 @@ using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Umbraco.Core.Models.EntityBase;
-using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Models
{
@@ -16,7 +15,7 @@ namespace Umbraco.Core.Models
public abstract class ContentBase : Entity, IContentBase
{
protected IContentTypeComposition ContentTypeBase;
- private int _parentId;
+ private Lazy _parentId;
private string _name;
private int _sortOrder;
private int _level;
@@ -28,11 +27,13 @@ namespace Umbraco.Core.Models
protected ContentBase(int parentId, IContentTypeComposition contentType, PropertyCollection properties)
{
- Mandate.ParameterCondition(parentId != 0, "parentId");
+ //Mandate.ParameterCondition(parentId != 0, "parentId");
Mandate.ParameterNotNull(contentType, "contentType");
Mandate.ParameterNotNull(properties, "properties");
- _parentId = parentId;
+ //_parentId = parentId;
+ _parentId = new Lazy(() => parentId);
+
_contentTypeId = int.Parse(contentType.Id.ToString(CultureInfo.InvariantCulture));
ContentTypeBase = contentType;
_properties = properties;
@@ -62,10 +63,10 @@ namespace Umbraco.Core.Models
[DataMember]
public virtual int ParentId
{
- get { return _parentId; }
+ get { return _parentId.Value; }
set
{
- _parentId = value;
+ _parentId = new Lazy(() => value);
OnPropertyChanged(ParentIdSelector);
}
}
diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs
index 17d320a9d6..f492518661 100644
--- a/src/Umbraco.Core/Models/EntityBase/Entity.cs
+++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs
@@ -16,7 +16,7 @@ namespace Umbraco.Core.Models.EntityBase
{
private bool _hasIdentity;
private int? _hash;
- private int _id;
+ private Lazy _id;
private Guid _key;
///
@@ -27,15 +27,15 @@ namespace Umbraco.Core.Models.EntityBase
{
get
{
- return _id;
+ return _id == null ? default(int) : _id.Value;
}
set
{
- _id = value;
+ _id = new Lazy(() => value);
HasIdentity = true;
}
}
-
+
///
/// Guid based Id
///
@@ -47,7 +47,7 @@ namespace Umbraco.Core.Models.EntityBase
get
{
if (_key == Guid.Empty)
- return Id.ToGuid();
+ return _id.Value.ToGuid();
return _key;
}
@@ -88,7 +88,7 @@ namespace Umbraco.Core.Models.EntityBase
protected void ResetIdentity()
{
_hasIdentity = false;
- _id = 0;
+ _id = new Lazy(() => default(int));
}
///
diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs
index 384aa325d8..48fe6019fb 100644
--- a/src/Umbraco.Core/Models/IContentBase.cs
+++ b/src/Umbraco.Core/Models/IContentBase.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Models.EntityBase;
-using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Models
{
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index a5d1b48bac..ca8fa099fe 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -429,19 +429,57 @@ namespace Umbraco.Core.Services
}
///
- /// Saves a collection of objects
+ /// Saves a collection of objects.
///
+ ///
+ /// If the collection of content contains new objects that references eachother by Id or ParentId,
+ /// then use the overload Save method with a collection of Lazy .
+ ///
/// Collection of to save
/// Optional Id of the User saving the Content
public void Save(IEnumerable contents, int userId = -1)
+ {
+ var repository = RepositoryResolver.ResolveByType(_unitOfWork);
+ var containsNew = contents.Any(x => x.HasIdentity == false);
+
+ if (containsNew)
+ {
+ foreach (var content in contents)
+ {
+ SetWriter(content, userId);
+ repository.AddOrUpdate(content);
+ _unitOfWork.Commit();
+ }
+ }
+ else
+ {
+ foreach (var content in contents)
+ {
+ SetWriter(content, userId);
+ repository.AddOrUpdate(content);
+ }
+ _unitOfWork.Commit();
+ }
+ }
+
+ ///
+ /// Saves a collection of lazy loaded objects.
+ ///
+ ///
+ /// This method ensures that Content is saved lazily, so a new graph of
+ /// objects can be saved in bulk. But not that objects are saved one at a time to ensure Ids.
+ ///
+ /// Collection of Lazy to save
+ /// Optional Id of the User saving the Content
+ public void Save(IEnumerable> contents, int userId = -1)
{
var repository = RepositoryResolver.ResolveByType(_unitOfWork);
foreach (var content in contents)
{
- SetWriter(content, userId);
- repository.AddOrUpdate(content);
+ SetWriter(content.Value, userId);
+ repository.AddOrUpdate(content.Value);
+ _unitOfWork.Commit();
}
- _unitOfWork.Commit();
}
///
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index 9e64851fa8..9549bde49b 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -130,12 +130,27 @@ namespace Umbraco.Core.Services
void Save(IContent content, int userId = -1);
///
- /// Saves a collection of objects
+ /// Saves a collection of objects.
///
+ ///
+ /// If the collection of content contains new objects that references eachother by Id or ParentId,
+ /// then use the overload Save method with a collection of Lazy .
+ ///
/// Collection of to save
/// Optional Id of the User saving the Content
void Save(IEnumerable contents, int userId = -1);
+ ///
+ /// Saves a collection of lazy loaded objects.
+ ///
+ ///
+ /// This method ensures that Content is saved lazily, so a new graph of
+ /// objects can be saved in bulk. But not that objects are saved one at a time to ensure Ids.
+ ///
+ /// Collection of Lazy to save
+ /// Optional Id of the User saving the Content
+ void Save(IEnumerable> contents, int userId = -1);
+
///
/// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin.
///
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index e0d39c12d6..5f4bde8b72 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -60,7 +60,10 @@
+
+
+
diff --git a/src/Umbraco.Tests/Auditing/AuditTests.cs b/src/Umbraco.Tests/Auditing/AuditTests.cs
new file mode 100644
index 0000000000..ff5e6bb164
--- /dev/null
+++ b/src/Umbraco.Tests/Auditing/AuditTests.cs
@@ -0,0 +1,35 @@
+using System.Linq;
+using NUnit.Framework;
+using Umbraco.Core.Auditing;
+using Umbraco.Core.Models.Rdbms;
+using Umbraco.Tests.TestHelpers;
+
+namespace Umbraco.Tests.Auditing
+{
+ [TestFixture]
+ public class AuditTests : BaseDatabaseFactoryTest
+ {
+ [SetUp]
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [Test]
+ public void Can_Add_Audit_Entry()
+ {
+ Audit.Add(AuditTypes.System, "This is a System audit trail", 0, -1);
+
+ var dtos = DatabaseContext.Database.Fetch("WHERE id > -1");
+
+ Assert.That(dtos.Any(), Is.True);
+ Assert.That(dtos.First().Comment, Is.EqualTo("This is a System audit trail"));
+ }
+
+ [TearDown]
+ public override void TearDown()
+ {
+ base.TearDown();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs
index 502b65da7f..48d6630488 100644
--- a/src/Umbraco.Tests/Services/ContentServiceTests.cs
+++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs
@@ -4,6 +4,9 @@ using System.Linq;
using NUnit.Framework;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Rdbms;
+using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.Repositories;
+using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
@@ -466,6 +469,20 @@ namespace Umbraco.Tests.Services
Assert.That(list.Any(x => !x.HasIdentity), Is.False);
}
+ [Test]
+ public void Can_Bulk_Save_New_Hierarchy_Content()
+ {
+ // Arrange
+ var contentService = ServiceContext.ContentService;
+ var hierarchy = CreateContentHierarchy();
+
+ // Act
+ contentService.Save(hierarchy, 0);
+
+ Assert.That(hierarchy.Any(), Is.True);
+ Assert.That(hierarchy.Any(x => x.Value.HasIdentity == false), Is.False);
+ }
+
[Test]
public void Can_Delete_Content_Of_Specific_ContentType()
{
@@ -561,6 +578,7 @@ namespace Umbraco.Tests.Services
Assert.AreNotEqual(content.Name, copy.Name);
}
+ [Test, NUnit.Framework.Ignore]
public void Can_Send_To_Publication()
{ }
@@ -585,6 +603,34 @@ namespace Umbraco.Tests.Services
Assert.AreEqual(subpage2.Name, rollback.Name);
}
+ [Test]
+ public void Can_Save_Lazy_Content()
+ {
+ var unitOfWork = new PetaPocoUnitOfWork();
+ var contentType = ServiceContext.ContentTypeService.GetContentType("umbTextpage");
+ var root = ServiceContext.ContentService.GetById(1046);
+
+ var c = new Lazy(() => MockedContent.CreateSimpleContent(contentType, "Hierarchy Simple Text Page", root.Id));
+ var c2 = new Lazy(() => MockedContent.CreateSimpleContent(contentType, "Hierarchy Simple Text Subpage", c.Value.Id));
+ var list = new List> {c, c2};
+
+ var repository = RepositoryResolver.ResolveByType(unitOfWork);
+ foreach (var content in list)
+ {
+ repository.AddOrUpdate(content.Value);
+ unitOfWork.Commit();
+ }
+
+ Assert.That(c.Value.HasIdentity, Is.True);
+ Assert.That(c2.Value.HasIdentity, Is.True);
+
+ Assert.That(c.Value.Id > 0, Is.True);
+ Assert.That(c2.Value.Id > 0, Is.True);
+
+ Assert.That(c.Value.ParentId > 0, Is.True);
+ Assert.That(c2.Value.ParentId > 0, Is.True);
+ }
+
[TearDown]
public override void TearDown()
{
@@ -620,5 +666,38 @@ namespace Umbraco.Tests.Services
trashed.Trashed = true;
ServiceContext.ContentService.Save(trashed, 0);
}
+
+ private IEnumerable> CreateContentHierarchy()
+ {
+ var contentType = ServiceContext.ContentTypeService.GetContentType("umbTextpage");
+ var root = ServiceContext.ContentService.GetById(1046);
+
+ var list = new List>();
+
+ for (int i = 0; i < 10; i++)
+ {
+ var content = new Lazy(
+ () => MockedContent.CreateSimpleContent(contentType, "Hierarchy Simple Text Page " + i, root.Id));
+ list.Add(content);
+ list.AddRange(CreateChildrenOf(contentType, content, 4));
+
+ Console.WriteLine("Created: 'Hierarchy Simple Text Page {0}'", i);
+ }
+
+ return list;
+ }
+
+ private IEnumerable> CreateChildrenOf(IContentType contentType, Lazy content, int depth)
+ {
+ var list = new List>();
+ for (int i = 0; i < depth; i++)
+ {
+ var c = new Lazy(() => MockedContent.CreateSimpleContent(contentType, "Hierarchy Simple Text Subpage " + i, content.Value.Id));
+ list.Add(c);
+
+ Console.WriteLine("Created: 'Hierarchy Simple Text Subpage {0}' - Depth: {1}", i, depth);
+ }
+ return list;
+ }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs
index 7ced5269a3..79ce23d63b 100644
--- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs
+++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs
@@ -1,6 +1,6 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Umbraco.Core.Models;
-using Umbraco.Core.Models.Membership;
namespace Umbraco.Tests.TestHelpers.Entities
{
@@ -8,7 +8,7 @@ namespace Umbraco.Tests.TestHelpers.Entities
{
public static Content CreateSimpleContent(IContentType contentType)
{
- var content = new Content(-1, contentType) { Name = "Home", Language = "en-US", Level = 1, ParentId = -1, SortOrder = 1, CreatorId = 0, WriterId = 0 };
+ var content = new Content(-1, contentType) { Name = "Home", Language = "en-US", Level = 1, SortOrder = 1, CreatorId = 0, WriterId = 0 };
object obj =
new
{
@@ -24,7 +24,7 @@ namespace Umbraco.Tests.TestHelpers.Entities
public static Content CreateSimpleContent(IContentType contentType, string name, int parentId)
{
- var content = new Content(parentId, contentType) { Name = name, Language = "en-US", ParentId = parentId, CreatorId = 0, WriterId = 0 };
+ var content = new Content(parentId, contentType) { Name = name, Language = "en-US", CreatorId = 0, WriterId = 0 };
object obj =
new
{
@@ -40,7 +40,7 @@ namespace Umbraco.Tests.TestHelpers.Entities
public static Content CreateTextpageContent(IContentType contentType, string name, int parentId)
{
- var content = new Content(parentId, contentType) { Name = name, Language = "en-US", ParentId = parentId, CreatorId = 0, WriterId = 0};
+ var content = new Content(parentId, contentType) { Name = name, Language = "en-US", CreatorId = 0, WriterId = 0};
object obj =
new
{
@@ -62,7 +62,7 @@ namespace Umbraco.Tests.TestHelpers.Entities
for (int i = 0; i < amount; i++)
{
var name = "Textpage No-" + i;
- var content = new Content(parentId, contentType) { Name = name, Language = "en-US", ParentId = parentId, CreatorId = 0, WriterId = 0 };
+ var content = new Content(parentId, contentType) { Name = name, Language = "en-US", CreatorId = 0, WriterId = 0 };
object obj =
new
{
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index c828581d34..a59f45a0d3 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -83,6 +83,7 @@
+