diff --git a/dBASE.NET.Tests/EntityMapperTests.cs b/dBASE.NET.Tests/EntityMapperTests.cs new file mode 100644 index 0000000..0e490da --- /dev/null +++ b/dBASE.NET.Tests/EntityMapperTests.cs @@ -0,0 +1,125 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Text; + +namespace dBASE.NET.Tests +{ + internal class TestEntity + { + [DbfField] + public int ID { get; set; } + + [DbfField("FULLNAME")] + public string Name { get; set; } + + [DbfField("AGE")] + public int Age { get; set; } + + public bool Something { get; set; } + } + + [TestClass] + public class EntityMapperTests + { + internal Dbf CreateDbf() + { + var dbf = new Dbf(Encoding.GetEncoding(1250)); + var idField = new DbfField("ID", DbfFieldType.Integer, 4); + var nameField = new DbfField("FULLNAME", DbfFieldType.Character, 50); + var ageField = new DbfField("AGE", DbfFieldType.Integer, 4); + dbf.Fields.Add(idField); + dbf.Fields.Add(nameField); + dbf.Fields.Add(ageField); + return dbf; + } + + [TestMethod] + public void RecordFromEntity() + { + var entity = new TestEntity + { + ID = 1, + Age = 30, + Name = "John Doe", + Something = true + }; + + var dbf = CreateDbf(); + + var record = dbf.CreateRecord(entity); + + Assert.AreEqual(1, record["ID"]); + Assert.AreEqual(30, record["AGE"]); + Assert.AreEqual("John Doe", record["FULLNAME"]); + Assert.AreEqual(null, record["Something"]); + Assert.AreEqual(null, record["SOMETHING"]); + } + + [TestMethod] + public void EntityFromRecord() + { + var dbf = CreateDbf(); + var record = dbf.CreateRecord(); + record.Data[0] = 2; + record.Data[1] = "Jane Doe"; + record.Data[2] = 25; + + var entity = new TestEntity(); + record.ToEntity(entity); + + Assert.AreEqual(2, entity.ID); + Assert.AreEqual("Jane Doe", entity.Name); + Assert.AreEqual(25, entity.Age); + } + + [TestMethod] + public void EntityFromDbf() + { + var dbf = new Dbf(); + dbf.Read("fixtures/mapper/mapper.dbf"); + var entites = new List(dbf.GetEntities()); + Assert.AreEqual(2, entites.Count); + Assert.AreEqual(1, entites[0].ID); + Assert.AreEqual("John Doe", entites[0].Name); + Assert.AreEqual(30, entites[0].Age); + Assert.AreEqual(2, entites[1].ID); + Assert.AreEqual("Jane Doe", entites[1].Name); + Assert.AreEqual(25, entites[1].Age); + } + + [TestMethod] + public void EntityToDbf() + { + var dbf = CreateDbf(); + var entities = new List(); + entities.Add(new TestEntity + { + ID = 1, + Age = 12, + Name = "Bobby Doe" + }); + + entities.Add(new TestEntity + { + ID = 2, + Age = 14, + Name = "Stacy Doe" + }); + dbf.AddEntities(entities); + dbf.Write("mapper-test.dbf"); + + var check = new Dbf(); + check.Read("mapper-test.dbf"); + var entitiesFromDBf = new List(check.GetEntities()); + + Assert.AreEqual(2, entitiesFromDBf.Count); + Assert.AreEqual(1, entitiesFromDBf[0].ID); + Assert.AreEqual("Bobby Doe", entitiesFromDBf[0].Name); + Assert.AreEqual(12, entitiesFromDBf[0].Age); + Assert.AreEqual(2, entitiesFromDBf[1].ID); + Assert.AreEqual("Stacy Doe", entitiesFromDBf[1].Name); + Assert.AreEqual(14, entitiesFromDBf[1].Age); + } + } +} diff --git a/dBASE.NET.Tests/dBASE.NET.Tests.csproj b/dBASE.NET.Tests/dBASE.NET.Tests.csproj index 2f8d54b..6501f9c 100644 --- a/dBASE.NET.Tests/dBASE.NET.Tests.csproj +++ b/dBASE.NET.Tests/dBASE.NET.Tests.csproj @@ -70,6 +70,7 @@ + @@ -265,6 +266,9 @@ PreserveNewest + + Always + diff --git a/dBASE.NET.Tests/fixtures/mapper/mapper.dbf b/dBASE.NET.Tests/fixtures/mapper/mapper.dbf new file mode 100644 index 0000000..bef761f Binary files /dev/null and b/dBASE.NET.Tests/fixtures/mapper/mapper.dbf differ diff --git a/dBASE.NET/Dbf.cs b/dBASE.NET/Dbf.cs index fb66fa7..d03d90c 100644 --- a/dBASE.NET/Dbf.cs +++ b/dBASE.NET/Dbf.cs @@ -60,6 +60,50 @@ public DbfRecord CreateRecord() return record; } + public DbfRecord CreateRecord(T entity) + { + var record = CreateRecord(); + record.FromEntity(entity); + return record; + } + + /// + /// Add a list of entities to the DBF. + /// + /// + /// + /// + public IEnumerable AddEntities(IEnumerable entities) + { + var records = new List(); + + foreach (var entity in entities) + { + records.Add(CreateRecord(entity)); + } + + return records; + } + + /// + /// Get records from the DBF mapped into entities. + /// + /// + /// + public IEnumerable GetEntities() + { + var entities = new List(); + + foreach (var record in Records) + { + var entity = (T) Activator.CreateInstance(typeof(T)); + record.ToEntity(entity); + entities.Add(entity); + } + + return entities; + } + /// /// Opens a DBF file, reads the contents that initialize the current instance, and then closes the file. /// diff --git a/dBASE.NET/DbfFieldAttribute.cs b/dBASE.NET/DbfFieldAttribute.cs new file mode 100644 index 0000000..02039b1 --- /dev/null +++ b/dBASE.NET/DbfFieldAttribute.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace dBASE.NET +{ + /// + /// Defines a field within an entity + /// + public class DbfFieldAttribute : Attribute + { + /// + /// Name of the property in the DBF + /// + public string Name { get; private set; } + + /// + /// Create an attribute to be able to bind to a DBF field. + /// + /// + /// The name of the field. Can be omitted, in that case it takes the name of the property. + /// + public DbfFieldAttribute([CallerMemberName] string name = null) + { + Name = name; + } + } +} diff --git a/dBASE.NET/DbfRecord.cs b/dBASE.NET/DbfRecord.cs index 551a0ab..edcfb62 100644 --- a/dBASE.NET/DbfRecord.cs +++ b/dBASE.NET/DbfRecord.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; + using System.Reflection; using System.Text; /// @@ -64,7 +65,7 @@ public object this[string name] { get { - int index = fields.FindIndex(x => x.Name.Equals(name)); + int index = GetFieldIndex(name); if (index == -1) return null; return Data[index]; } @@ -74,12 +75,118 @@ public object this[DbfField field] { get { - int index = fields.IndexOf(field); + int index = GetFieldIndex(field); if (index == -1) return null; return Data[index]; } } + /// + /// Return the index of the field by its name. + /// + /// Name of the field + /// + public int GetFieldIndex(string fieldName) + { + return fields.FindIndex(x => x.Name.ToLower().Equals(fieldName.ToLower())); + } + + /// + /// Return the index of a field + /// + /// The field + /// + public int GetFieldIndex(DbfField field) + { + return fields.IndexOf(field); + } + + /// + /// Return the list of fields in the current record. + /// + public List Fields + { + get + { + return fields; + } + } + + /// + /// Fill values of the record from an entity. + /// + /// + /// + public void FromEntity(T obj) + { + var properties = GetDecoratedProperties(obj); + + foreach (var property in properties) + { + var attribute = property.GetCustomAttribute(typeof(DbfFieldAttribute)) as DbfFieldAttribute; + + if (attribute == null) + { + throw new InvalidOperationException( + $"Property {property.Name} does not have the DbfField attribute!" + ); + } + + if (property.CanRead) + { + Data[GetFieldIndex(attribute.Name)] = property.GetValue(obj); + } + } + } + + /// + /// Write values of the record into an entity. + /// + /// + /// + public void ToEntity(T obj) + { + var properties = GetDecoratedProperties(obj); + + foreach (var property in properties) + { + var attribute = property.GetCustomAttribute(typeof(DbfFieldAttribute)) as DbfFieldAttribute; + + if(attribute == null) + { + throw new InvalidOperationException( + $"Property {property.Name} does not have the DbfField attribute!" + ); + } + + if(property.CanWrite) + { + property.SetValue(obj, Data[GetFieldIndex(attribute.Name)]); + } + } + } + + internal PropertyInfo[] GetDecoratedProperties(object obj) + { + var decoratedProperties = new List(); + + var allProperties = obj.GetType().GetProperties(); + + foreach (var property in allProperties) + { + var attributes = property.GetCustomAttributes(); + foreach (var attr in attributes) + { + if(attr is DbfFieldAttribute) + { + decoratedProperties.Add(property); + } + } + } + + return decoratedProperties.ToArray(); + } + /// /// Returns a string that represents the current object. ///