diff --git a/src/FluentNHibernate.Testing/DomainModel/Mapping/ManyToManyTester.cs b/src/FluentNHibernate.Testing/DomainModel/Mapping/ManyToManyTester.cs index 9c3711f28..3df85e0d5 100644 --- a/src/FluentNHibernate.Testing/DomainModel/Mapping/ManyToManyTester.cs +++ b/src/FluentNHibernate.Testing/DomainModel/Mapping/ManyToManyTester.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using NUnit.Framework; @@ -333,4 +334,58 @@ public void CanSpecifyMultipleChildKeyColumns() .Element("class/bag/many-to-many/column[@name='ID1']").Exists() .Element("class/bag/many-to-many/column[@name='ID2']").Exists(); } + + [Test] + public void CanSpecifyIdBag() + { + new MappingTester() + .ForMapping(m => m.HasManyToMany(x => x.MapOfChildren) + .AsIdBag(x => x.Column("Id").GeneratedBy.Identity())) + .Element("class/idbag/collection-id").Exists() + .HasAttribute("column", "Id") + .HasAttribute("type", typeof(int).AssemblyQualifiedName) + .Element("class/idbag/collection-id/generator").Exists() + .HasAttribute("class", "identity"); + } + + [Test] + public void CanSpecifyIdBagWithLength() + { + new MappingTester() + .ForMapping(m => m.HasManyToMany(x => x.MapOfChildren) + .AsIdBag(x => x.Column("Id").Length(10))) + .Element("class/idbag/collection-id").Exists() + .HasAttribute("column", "Id") + .HasAttribute("type", typeof(string).AssemblyQualifiedName) + .HasAttribute("length", "10") + .Element("class/idbag/collection-id/generator").Exists() + .HasAttribute("class", "assigned"); + } + + [Test] + public void CanSpecifyIdBagWithNonGenericType() + { + new MappingTester() + .ForMapping(m => m.HasManyToMany(x => x.MapOfChildren) + .AsIdBag(typeof(string), x => x.Column("Id").Length(10))) + .Element("class/idbag/collection-id").Exists() + .HasAttribute("column", "Id") + .HasAttribute("type", typeof(string).AssemblyQualifiedName) + .HasAttribute("length", "10") + .Element("class/idbag/collection-id/generator").Exists() + .HasAttribute("class", "assigned"); + } + + [Test] + public void CanSpecifyIdBagWithGenerator() + { + new MappingTester() + .ForMapping(m => m.HasManyToMany(x => x.MapOfChildren) + .AsIdBag(typeof(int), x => x.GeneratedBy.Identity())) + .Element("class/idbag/collection-id").Exists() + .HasAttribute("column", "Id") + .HasAttribute("type", typeof(int).AssemblyQualifiedName) + .Element("class/idbag/collection-id/generator").Exists() + .HasAttribute("class", "identity"); + } } diff --git a/src/FluentNHibernate/Automapping/Steps/IdentityStep.cs b/src/FluentNHibernate/Automapping/Steps/IdentityStep.cs index e91b16d34..3a6cbea97 100644 --- a/src/FluentNHibernate/Automapping/Steps/IdentityStep.cs +++ b/src/FluentNHibernate/Automapping/Steps/IdentityStep.cs @@ -9,8 +9,6 @@ namespace FluentNHibernate.Automapping.Steps; public class IdentityStep(IAutomappingConfiguration cfg) : IAutomappingStep { - readonly List identityCompatibleTypes = new List { typeof(long), typeof(int), typeof(short), typeof(byte) }; - public bool ShouldMap(Member member) { return cfg.IsId(member); @@ -52,15 +50,7 @@ void SetDefaultAccess(Member member, IdMapping mapping) GeneratorMapping GetDefaultGenerator(Member property) { var generatorMapping = new GeneratorMapping(); - var defaultGenerator = new GeneratorBuilder(generatorMapping, property.PropertyType, Layer.Defaults); - - if (property.PropertyType == typeof(Guid)) - defaultGenerator.GuidComb(); - else if (identityCompatibleTypes.Contains(property.PropertyType)) - defaultGenerator.Identity(); - else - defaultGenerator.Assigned(); - + new GeneratorBuilder(generatorMapping, property.PropertyType, Layer.Defaults).SetDefault(); return generatorMapping; } } diff --git a/src/FluentNHibernate/Mapping/CollectionIdPart.cs b/src/FluentNHibernate/Mapping/CollectionIdPart.cs new file mode 100644 index 000000000..655b90f3c --- /dev/null +++ b/src/FluentNHibernate/Mapping/CollectionIdPart.cs @@ -0,0 +1,70 @@ +using System; +using FluentNHibernate.Mapping.Providers; +using FluentNHibernate.MappingModel; +using FluentNHibernate.MappingModel.Collections; +using FluentNHibernate.MappingModel.Identity; + +namespace FluentNHibernate.Mapping; + +public class CollectionIdPart : ICollectionIdMappingProvider +{ + readonly Type entity; + readonly AttributeStore attributes = new(); + + /// + /// Specify the generator + /// + /// + /// .AsIdBag<int>(x => x.Column("Id").GeneratedBy.Identity()) + /// + public IdentityGenerationStrategyBuilder GeneratedBy { get; } + + public CollectionIdPart(Type entityType, Type idColumnType) + { + attributes.Set("Type", Layer.UserSupplied, new TypeReference(idColumnType)); + entity = entityType; + GeneratedBy = new IdentityGenerationStrategyBuilder(this, idColumnType, entityType); + SetDefaultGenerator(idColumnType); + } + + /// + /// Specifies the id column length + /// + /// Column length + public CollectionIdPart Length(int length) + { + attributes.Set("Length", Layer.UserSupplied, length); + return this; + } + + /// + /// Specifies the column name for the collection id. + /// + /// Column name + public CollectionIdPart Column(string idColumnName) + { + attributes.Set("Column", Layer.UserSupplied, idColumnName); + return this; + } + + void SetDefaultGenerator(Type idColumnType) + { + var generatorMapping = new GeneratorMapping(); + new GeneratorBuilder(generatorMapping, idColumnType, Layer.UserSupplied).SetDefault(); + attributes.Set("Generator", Layer.Defaults, generatorMapping); + } + + CollectionIdMapping ICollectionIdMappingProvider.GetCollectionIdMapping() + { + var mapping = new CollectionIdMapping(attributes.Clone()) + { + ContainingEntityType = entity + }; + + mapping.Set(x => x.Column, Layer.Defaults, "Id"); + if (GeneratedBy.IsDirty) + mapping.Set(x => x.Generator, Layer.UserSupplied, GeneratedBy.GetGeneratorMapping()); + + return mapping; + } +} diff --git a/src/FluentNHibernate/Mapping/GeneratorBuilder.cs b/src/FluentNHibernate/Mapping/GeneratorBuilder.cs index c2cd7bf3a..656d0f27a 100644 --- a/src/FluentNHibernate/Mapping/GeneratorBuilder.cs +++ b/src/FluentNHibernate/Mapping/GeneratorBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using FluentNHibernate.MappingModel.Identity; using NHibernate.Id; @@ -6,6 +7,8 @@ namespace FluentNHibernate.Mapping; class GeneratorBuilder(GeneratorMapping mapping, Type identityType, int layer) { + readonly HashSet identityCompatibleTypes = new HashSet{ typeof(long), typeof(int), typeof(short), typeof(byte) }; + void SetGenerator(string generator) { mapping.Set(x => x.Class, layer, generator); @@ -31,6 +34,16 @@ void EnsureStringIdentityType() if (identityType != typeof(string)) throw new InvalidOperationException("Identity type must be string"); } + internal void SetDefault() + { + if (identityType == typeof(Guid)) + GuidComb(); + else if (identityCompatibleTypes.Contains(identityType)) + Identity(); + else + Assigned(); + } + static bool IsIntegralType(Type t) { // do we think we'll encounter more? diff --git a/src/FluentNHibernate/Mapping/IdentityPart.cs b/src/FluentNHibernate/Mapping/IdentityPart.cs index 0b5ecc342..117accd26 100644 --- a/src/FluentNHibernate/Mapping/IdentityPart.cs +++ b/src/FluentNHibernate/Mapping/IdentityPart.cs @@ -250,15 +250,7 @@ internal void SetName(string newName) void SetDefaultGenerator() { var generatorMapping = new GeneratorMapping(); - var defaultGenerator = new GeneratorBuilder(generatorMapping, identityType, Layer.UserSupplied); - - if (identityType == typeof(Guid)) - defaultGenerator.GuidComb(); - else if (identityType == typeof(int) || identityType == typeof(long)) - defaultGenerator.Identity(); - else - defaultGenerator.Assigned(); - + new GeneratorBuilder(generatorMapping, identityType, Layer.UserSupplied).SetDefault(); attributes.Set("Generator", Layer.Defaults, generatorMapping); } diff --git a/src/FluentNHibernate/Mapping/Providers/ICollectionIdMappingProvider.cs b/src/FluentNHibernate/Mapping/Providers/ICollectionIdMappingProvider.cs new file mode 100644 index 000000000..397cd0ca5 --- /dev/null +++ b/src/FluentNHibernate/Mapping/Providers/ICollectionIdMappingProvider.cs @@ -0,0 +1,8 @@ +using FluentNHibernate.MappingModel.Collections; + +namespace FluentNHibernate.Mapping.Providers; + +public interface ICollectionIdMappingProvider +{ + CollectionIdMapping GetCollectionIdMapping(); +} diff --git a/src/FluentNHibernate/Mapping/ToManyBase.cs b/src/FluentNHibernate/Mapping/ToManyBase.cs index ba827c149..92590beed 100644 --- a/src/FluentNHibernate/Mapping/ToManyBase.cs +++ b/src/FluentNHibernate/Mapping/ToManyBase.cs @@ -22,6 +22,7 @@ public abstract class ToManyBase : ICollectionMappingProvider protected readonly AttributeStore relationshipAttributes = new AttributeStore(); Func collectionBuilder; IndexMapping indexMapping; + CollectionIdMapping collectionIdMapping; protected Member member; readonly List filters = []; @@ -154,6 +155,26 @@ public T AsBag() return (T)this; } + /// + /// Use an idbag collection + /// + public T AsIdBag(Action customId = null) + { + return AsIdBag(typeof(TIdType), customId); + } + + /// + /// Use an idbag collection + /// + public T AsIdBag(Type idColType, Action customId = null) + { + collectionBuilder = attrs => CollectionMapping.IdBag(attrs); + var builder = new CollectionIdPart(typeof(T), idColType); + customId?.Invoke(builder); + collectionIdMapping = ((ICollectionIdMappingProvider)builder).GetCollectionIdMapping(); + return (T)this; + } + /// /// Use a list collection /// @@ -725,6 +746,8 @@ protected virtual CollectionMapping GetCollectionMapping() // HACK: Index only on list and map - shouldn't have to do this! if (mapping.Collection == Collection.Array || mapping.Collection == Collection.List || mapping.Collection == Collection.Map) mapping.Set(x => x.Index, Layer.Defaults, indexMapping); + else if (mapping.Collection == Collection.IdBag) + mapping.Set(x => x.CollectionId, Layer.Defaults, collectionIdMapping); if (elementPart is not null) { diff --git a/src/FluentNHibernate/MappingModel/Collections/Collection.cs b/src/FluentNHibernate/MappingModel/Collections/Collection.cs index 23d9e4a70..daa88474d 100644 --- a/src/FluentNHibernate/MappingModel/Collections/Collection.cs +++ b/src/FluentNHibernate/MappingModel/Collections/Collection.cs @@ -6,5 +6,6 @@ public enum Collection Bag, Map, List, - Set + Set, + IdBag } diff --git a/src/FluentNHibernate/MappingModel/Collections/CollectionIdMapping.cs b/src/FluentNHibernate/MappingModel/Collections/CollectionIdMapping.cs new file mode 100644 index 000000000..3494e1304 --- /dev/null +++ b/src/FluentNHibernate/MappingModel/Collections/CollectionIdMapping.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq.Expressions; +using FluentNHibernate.MappingModel.Identity; +using FluentNHibernate.Utils; +using FluentNHibernate.Visitors; + +namespace FluentNHibernate.MappingModel.Collections; + +[Serializable] +public sealed class CollectionIdMapping(AttributeStore attributes) : MappingBase, IEquatable +{ + readonly AttributeStore attributes = attributes; + + public override void AcceptVisitor(IMappingModelVisitor visitor) + { + visitor.ProcessCollectionId(this); + if (Generator is not null) + visitor.Visit(Generator); + } + + public GeneratorMapping Generator => attributes.GetOrDefault(); + + public string Column => attributes.GetOrDefault(); + + public int Length => attributes.GetOrDefault(); + + public TypeReference Type => attributes.GetOrDefault(); + + public Type ContainingEntityType { get; set; } + + public bool Equals(CollectionIdMapping other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(other.attributes, attributes) && + other.ContainingEntityType == ContainingEntityType; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != typeof(CollectionIdMapping)) return false; + return Equals((CollectionIdMapping)obj); + } + + public override int GetHashCode() + { + unchecked + { + int result = (attributes is not null ? attributes.GetHashCode() : 0); + result = (result * 397) ^ (ContainingEntityType is not null ? ContainingEntityType.GetHashCode() : 0); + return result; + } + } + + public void Set(Expression> expression, int layer, T value) + { + Set(expression.ToMember().Name, layer, value); + } + + protected override void Set(string attribute, int layer, object value) + { + attributes.Set(attribute, layer, value); + } + + public override bool IsSpecified(string attribute) + { + return attributes.IsSpecified(attribute); + } +} diff --git a/src/FluentNHibernate/MappingModel/Collections/CollectionMapping.cs b/src/FluentNHibernate/MappingModel/Collections/CollectionMapping.cs index 47f703d78..f8a90bcf9 100644 --- a/src/FluentNHibernate/MappingModel/Collections/CollectionMapping.cs +++ b/src/FluentNHibernate/MappingModel/Collections/CollectionMapping.cs @@ -7,7 +7,7 @@ namespace FluentNHibernate.MappingModel.Collections; [Serializable] -public class CollectionMapping : MappingBase, IRelationship, IEquatable +public sealed class CollectionMapping : MappingBase, IRelationship, IEquatable { readonly AttributeStore attributes; readonly List filters = []; @@ -32,6 +32,9 @@ public override void AcceptVisitor(IMappingModelVisitor visitor) { visitor.ProcessCollection(this); + if (CollectionId is not null && Collection == Collection.IdBag) + visitor.Visit(CollectionId); + if (Key is not null) visitor.Visit(Key); @@ -109,6 +112,8 @@ public override void AcceptVisitor(IMappingModelVisitor visitor) public string Sort => attributes.GetOrDefault(); public IIndexMapping Index => attributes.GetOrDefault(); + + public CollectionIdMapping CollectionId => attributes.GetOrDefault(); public bool Equals(CollectionMapping other) { @@ -174,6 +179,11 @@ public static CollectionMapping Bag(AttributeStore underlyingStore) { return For(Collection.Bag, underlyingStore); } + + public static CollectionMapping IdBag(AttributeStore underlyingStore) + { + return For(Collection.IdBag, underlyingStore); + } public static CollectionMapping List() { diff --git a/src/FluentNHibernate/MappingModel/Output/XmlCollectionIdWriter.cs b/src/FluentNHibernate/MappingModel/Output/XmlCollectionIdWriter.cs new file mode 100644 index 000000000..83187cc0b --- /dev/null +++ b/src/FluentNHibernate/MappingModel/Output/XmlCollectionIdWriter.cs @@ -0,0 +1,43 @@ +using System.Xml; +using FluentNHibernate.MappingModel.Collections; +using FluentNHibernate.MappingModel.Identity; +using FluentNHibernate.Utils; +using FluentNHibernate.Visitors; + +namespace FluentNHibernate.MappingModel.Output; + +public class XmlCollectionIdWriter(IXmlWriterServiceLocator serviceLocator) : NullMappingModelVisitor, IXmlWriter +{ + XmlDocument document; + + public XmlDocument Write(CollectionIdMapping mappingModel) + { + document = null; + mappingModel.AcceptVisitor(this); + return document; + } + + public override void ProcessCollectionId(CollectionIdMapping mapping) + { + document = new XmlDocument(); + + var element = document.AddElement("collection-id"); + + if (mapping.IsSpecified("Column")) + element.WithAtt("column", mapping.Column); + + if (mapping.IsSpecified("Type")) + element.WithAtt("type", mapping.Type); + + if (mapping.IsSpecified("Length")) + element.WithAtt("length", mapping.Length); + } + + public override void Visit(GeneratorMapping mapping) + { + var writer = serviceLocator.GetWriter(); + var generatorXml = writer.Write(mapping); + + document.ImportAndAppendChild(generatorXml); + } +} diff --git a/src/FluentNHibernate/MappingModel/Output/XmlCollectionWriter.cs b/src/FluentNHibernate/MappingModel/Output/XmlCollectionWriter.cs index 7f469f1c9..587f2269b 100644 --- a/src/FluentNHibernate/MappingModel/Output/XmlCollectionWriter.cs +++ b/src/FluentNHibernate/MappingModel/Output/XmlCollectionWriter.cs @@ -22,28 +22,16 @@ public XmlDocument Write(CollectionMapping mappingModel) public override void ProcessCollection(CollectionMapping mapping) { - IXmlWriter writer = null; - - switch (mapping.Collection) + IXmlWriter writer = mapping.Collection switch { - case Collection.Array: - writer = new XmlArrayWriter(serviceLocator); - break; - case Collection.Bag: - writer = new XmlBagWriter(serviceLocator); - break; - case Collection.List: - writer = new XmlListWriter(serviceLocator); - break; - case Collection.Map: - writer = new XmlMapWriter(serviceLocator); - break; - case Collection.Set: - writer = new XmlSetWriter(serviceLocator); - break; - default: - throw new InvalidOperationException("Unrecognised collection type " + mapping.Collection); - } + Collection.Array => new XmlArrayWriter(serviceLocator), + Collection.Bag => new XmlBagWriter(serviceLocator), + Collection.List => new XmlListWriter(serviceLocator), + Collection.Map => new XmlMapWriter(serviceLocator), + Collection.Set => new XmlSetWriter(serviceLocator), + Collection.IdBag => new XmlIdBagWriter(serviceLocator), + _ => throw new InvalidOperationException("Unrecognised collection type " + mapping.Collection) + }; document = writer.Write(mapping); } diff --git a/src/FluentNHibernate/MappingModel/Output/XmlIdBagWriter.cs b/src/FluentNHibernate/MappingModel/Output/XmlIdBagWriter.cs new file mode 100644 index 000000000..60a90fcd2 --- /dev/null +++ b/src/FluentNHibernate/MappingModel/Output/XmlIdBagWriter.cs @@ -0,0 +1,39 @@ +using System.Xml; +using FluentNHibernate.MappingModel.Collections; +using FluentNHibernate.MappingModel.Identity; +using FluentNHibernate.Utils; + +namespace FluentNHibernate.MappingModel.Output; + +public class XmlIdBagWriter(IXmlWriterServiceLocator serviceLocator) : BaseXmlCollectionWriter(serviceLocator), IXmlWriter +{ + readonly IXmlWriterServiceLocator _serviceLocator = serviceLocator; + + public XmlDocument Write(CollectionMapping mappingModel) + { + document = null; + mappingModel.AcceptVisitor(this); + return document; + } + + public override void ProcessCollection(CollectionMapping mapping) + { + document = new XmlDocument(); + var element = document.AddElement("idbag"); + WriteBaseCollectionAttributes(element, mapping); + } + + public override void Visit(CollectionIdMapping mapping) + { + var writer = _serviceLocator.GetWriter(); + var xml = writer.Write(mapping); + document.ImportAndAppendChild(xml); + } + + public override void Visit(GeneratorMapping mapping) + { + var writer = _serviceLocator.GetWriter(); + var generatorXml = writer.Write(mapping); + document.ImportAndAppendChild(generatorXml); + } +} diff --git a/src/FluentNHibernate/MappingModel/Output/XmlWriterContainer.cs b/src/FluentNHibernate/MappingModel/Output/XmlWriterContainer.cs index a931b1624..81ac240a3 100644 --- a/src/FluentNHibernate/MappingModel/Output/XmlWriterContainer.cs +++ b/src/FluentNHibernate/MappingModel/Output/XmlWriterContainer.cs @@ -129,6 +129,9 @@ void RegisterIdWriters() RegisterWriter(c => new XmlKeyManyToOneWriter(c.Resolve())); + + RegisterWriter(c => + new XmlCollectionIdWriter(c.Resolve())); } void RegisterComponentWriters() diff --git a/src/FluentNHibernate/Visitors/ConventionVisitor.cs b/src/FluentNHibernate/Visitors/ConventionVisitor.cs index 70be75c4c..5c244cbe9 100644 --- a/src/FluentNHibernate/Visitors/ConventionVisitor.cs +++ b/src/FluentNHibernate/Visitors/ConventionVisitor.cs @@ -95,7 +95,10 @@ public override void ProcessCollection(CollectionMapping mapping) Apply(conventions, new OneToManyCollectionInstance(mapping)); } - collections[mapping.Collection](mapping); + if (collections.TryGetValue(mapping.Collection, out var processor)) + { + processor(mapping); + } } #pragma warning disable 612,618 diff --git a/src/FluentNHibernate/Visitors/DefaultMappingModelVisitor.cs b/src/FluentNHibernate/Visitors/DefaultMappingModelVisitor.cs index 7ca334469..161b3088b 100644 --- a/src/FluentNHibernate/Visitors/DefaultMappingModelVisitor.cs +++ b/src/FluentNHibernate/Visitors/DefaultMappingModelVisitor.cs @@ -184,4 +184,8 @@ public override void Visit(KeyManyToOneMapping mapping) mapping.AcceptVisitor(this); } + public override void Visit(CollectionIdMapping mapping) + { + mapping.AcceptVisitor(this); + } } diff --git a/src/FluentNHibernate/Visitors/IMappingModelVisitor.cs b/src/FluentNHibernate/Visitors/IMappingModelVisitor.cs index 00490d283..5d455ef98 100644 --- a/src/FluentNHibernate/Visitors/IMappingModelVisitor.cs +++ b/src/FluentNHibernate/Visitors/IMappingModelVisitor.cs @@ -44,6 +44,7 @@ public interface IMappingModelVisitor void ProcessStoredProcedure(StoredProcedureMapping mapping); void ProcessTuplizer(TuplizerMapping mapping); void ProcessCollection(MappingModel.Collections.CollectionMapping mapping); + void ProcessCollectionId(CollectionIdMapping mapping); /// /// This bad boy is the entry point to the visitor @@ -84,4 +85,5 @@ public interface IMappingModelVisitor void Visit(FilterDefinitionMapping mapping); void Visit(StoredProcedureMapping mapping); void Visit(TuplizerMapping mapping); + void Visit(CollectionIdMapping mapping); } diff --git a/src/FluentNHibernate/Visitors/NullMappingModelVisitor.cs b/src/FluentNHibernate/Visitors/NullMappingModelVisitor.cs index b10af2113..d70f09286 100644 --- a/src/FluentNHibernate/Visitors/NullMappingModelVisitor.cs +++ b/src/FluentNHibernate/Visitors/NullMappingModelVisitor.cs @@ -188,6 +188,11 @@ public virtual void ProcessCollection(MappingModel.Collections.CollectionMapping } + public virtual void ProcessCollectionId(CollectionIdMapping mapping) + { + + } + public virtual void Visit(IEnumerable mappings) { @@ -356,4 +361,9 @@ public virtual void Visit(TuplizerMapping mapping) { } + + public virtual void Visit(CollectionIdMapping mapping) + { + + } }