The task is to define with the least amount of code an immutable ValueObject with a large number of properties.

I use NHibernate as ORM , therefore properties should be virtual and public / protected . This object is mapped to a table from the database.

This object should not change, but it is necessary that it can be created.

If there are not many properties, then there are no problems, like this:

 public class ElectricDevice { public virtual int Id { get; protected set; } public virtual string Name { get; protected set; } public virtual bool IsDecommissioned { get; protected set;} public ElectricDevice(int id, string name, bool isDecommissioned) { Id = id; Name = name; IsDecommissioned = isDecommissioned; } } 

But if there are a lot of properties, for example, 20, then it is somehow inconvenient to write a class for a long time (besides, there should be a lot of such classes). It is also not nice to create such an object through the constructor. Any ideas? Maybe in C # 6 any new syntax?

  • What about the Builder pattern? - VladD
  • one
    Like ElectricDevice electricDevice = new ElectricDeviceBuilder() { Id = id, Name = name, OtherProperty = otherValue }.Build(); - VladD
  • Judging by how you use it, you need code generation, not a handy constructor. - Monk
  • @VladD, yes, this is the very thing that I wanted to hear. You can also use fluent notation to form a builder. - Andrey K.
  • PS You can do code generation for the builder. Or write by hand, it will again be long, but beautiful. - Andrey K.

3 answers 3

If you need a fluent interface, I got this solution based on the @Stack answer:

  1. We get attribute to mark classes for which you need to build Builder :

     namespace BuildCodegen { class CreateBuilderAttribute : Attribute { } } 

    Mark this attribute our class:

     [CreateBuilder] public class ElectricDevice { public virtual int Id { get; protected set; } public virtual string Name { get; protected set; } public virtual bool IsDecommissioned { get; protected set; } public ElectricDevice(int id, string name, bool isDecommissioned) { Id = id; Name = name; IsDecommissioned = isDecommissioned; } } 
  2. We start a TextTemplate project in our project (it describes in the next answer exactly how this is done), call it Builders.tt .

    To access the existing code, CodeModel use CodeModel , rather than reflection, CodeModel reflection has known problems (late updating, blocking of assemblies in memory).

    So, put the following code in Builders.tt :

     <# /* hostspecific = true, Ρ‚. ΠΊ. ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ CodeModel Visual Studio */ #> <#@ template debug="false" hostspecific="true" language="C#" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Data" #> <#@ assembly name="EnvDte" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="EnvDTE" #> <#@ output extension=".cs" #> <# // Π·Π°Π΄Π°Ρ‘ΠΌ Π²Ρ€ΡƒΡ‡Π½ΡƒΡŽ имя Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Π° (ΠΊΠ°ΠΊ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Π»ΡƒΡ‡ΡˆΠ΅?) var attributeFullName = "BuildCodegen.CreateBuilderAttribute"; var visualStudio = (EnvDTE.DTE)((this.Host as IServiceProvider) .GetService(typeof(EnvDTE.DTE))); var project = (EnvDTE.Project)visualStudio.Solution .FindProjectItem(this.Host.TemplateFile).ContainingProject; var codeModel = project.CodeModel; // ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ»ΠΈ модСль ΠΊΠΎΠ΄Π°: var classes = codeModel.CodeElements.Cast<CodeElement>().SelectMany(GetClasses); var classesThatNeedBuilder = classes.Where(c => c.Attributes.Cast<CodeAttribute>() .Any(attr => attr.FullName == attributeFullName)); foreach (var ccl in classesThatNeedBuilder) { var namespaceName = ccl.Namespace.FullName; var className = ccl.Name; var builderName = className + "Builder"; var properties = ccl.Children .OfType<CodeProperty>() .Select(p => new PropertyDescriptor(p)) .ToList(); // ΠšΠ°ΠΆΠ΄Ρ‹ΠΉ builder ΠΊΠ»Π°Π΄Ρ‘ΠΌ Π² Ρ‚ΠΎ ΠΆΠ΅ пространство ΠΈΠΌΡ‘Π½, Ρ‡Ρ‚ΠΎ ΠΈ // ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΠΌΡ‹ΠΉ ΠΈΠΌ класс. (Π­Ρ‚ΠΎ Π»Π΅Π³ΠΊΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π΅Π»Π°Ρ‚ΡŒ, разумССтся.) // Π’Π°ΠΊΠΆΠ΅ ΠΌΡ‹ ΠΏΡ€Π΅Π΄ΠΏΠΎΠ»Π°Π³Π°Π΅ΠΌ, Ρ‡Ρ‚ΠΎ Ρƒ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΠΌΠΎΠ³ΠΎ класса Π΅ΡΡ‚ΡŒ // конструктор, ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°ΡŽΡ‰ΠΈΠΉ всС Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Ρ‹, с ΠΈΠΌΠ΅Π½Π°ΠΌΠΈ, ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΌΠΈ // ΠΈΠΌΠ΅Π½Π°ΠΌ свойств, Π½ΠΎ со строчной Π±ΡƒΠΊΠ²Ρ‹ (Ссли это Π½Π΅ Ρ‚Π°ΠΊ, Π½ΡƒΠΆΠ½ΠΎ // ΠΏΠΎΠΈΡΠΊΠ°Ρ‚ΡŒ конструктор ΠΈ ΠΈΠΌΠ΅Π½Π° ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ² Ρ‡Π΅Ρ€Π΅Π· CodeModel) #> namespace <#= namespaceName #> { public class <#= builderName #> { public <#= className #> Build() { return new <#= className #>(<#= string.Join(", ", properties.Select(p => p.LowerName + ": " + p.LowerName)) #>); } <# foreach (var prop in properties) { // для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΈΠ· свойств опрСдСляСм нСсущСС ΠΏΠΎΠ»Π΅ ΠΈ // fluent-ΠΌΠ΅Ρ‚ΠΎΠ΄ Π΅Π³ΠΎ установки #> <#= prop.Type #> <#= prop.LowerName #>; public <#= builderName #> With<#= prop.UpperName #>(<#= prop.Type #> <#= prop.LowerName #>) { this.<#= prop.LowerName #> = <#= prop.LowerName #>; return this; } <# } #> } } <# } #> <#+ // Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΌΠ΅Ρ‚ΠΎΠ΄: ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ рСкурсивно список классов // ΠΌΡ‹ смотрим Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π²Π½ΡƒΡ‚Ρ€ΠΈ пространств ΠΈΠΌΡ‘Π½, Π½ΠΎ Π½Π΅ Π²Π½ΡƒΡ‚Ρ€ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΡ… классов // (ΠΈΡΠΊΠ»ΡŽΡ‡ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ ΠΈΠ·-Π·Π° Π»Π΅Π½ΠΈ, Π½Ρƒ ΠΈ Π½ΡƒΠΆΠ½ΠΎ для Π²Π»ΠΎΠΆΠ΅Π½Π½ΠΎΠ³ΠΎ класса ΠΏΡ€ΠΈΠ΄ΡƒΠΌΠ°Ρ‚ΡŒ, // ΠΊΡƒΠ΄Π° ΠΆΠ΅ ΠΊΠ»Π°ΡΡ‚ΡŒ builder) IEnumerable<CodeClass> GetClasses(CodeElement elt) { CodeClass ccl = elt as CodeClass; if (ccl != null) return new[] { ccl }; CodeNamespace cns = elt as CodeNamespace; if (cns != null) return cns.Members.Cast<CodeElement>().SelectMany(GetClasses); return Enumerable.Empty<CodeClass>(); } // Π½Ρƒ ΠΈ ΠΌΠ΅Π»ΠΊΠΈΠΉ класс-ΠΎΠ±Ρ‘Ρ€Ρ‚ΠΊΠ° для свойства class PropertyDescriptor { public readonly string Type; public readonly string UpperName; public readonly string LowerName; public PropertyDescriptor(CodeProperty property) { this.Type = property.Type.AsString; var name = property.Name; this.UpperName = char.ToUpper(name[0]) + name.Substring(1); this.LowerName = char.ToLower(name[0]) + name.Substring(1); } } #> 
  3. We get this automatically generated class:

     namespace BuildCodegen { public class ElectricDeviceBuilder { public ElectricDevice Build() { return new ElectricDevice(id: id, name: name, isDecommissioned: isDecommissioned); } int id; public ElectricDeviceBuilder WithId(int id) { this.id = id; return this; } string name; public ElectricDeviceBuilder WithName(string name) { this.name = name; return this; } bool isDecommissioned; public ElectricDeviceBuilder WithIsDecommissioned(bool isDecommissioned) { this.isDecommissioned = isDecommissioned; return this; } } } 

By the way, the Entity-class itself can also be generated automatically.

    if there are a lot of properties, for example, 20, then it is somehow inconvenient to write a class

    If you need to define a wrapper class for a table from a database with many properties, you can use T4, the code generator in Visual Studio.
    To do this, in Visual Studio, press Ctrl + Shift + A, Ctrl + E, type t4, and select Text Template. In the created TextTemplate.tt file specify the following code:

     <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Data" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".cs" #> <# var cs = @"Data Source=(localdb)\DB;Initial Catalog=Test;Integrated Security=True;"; var cn = new System.Data.SqlClient.SqlConnection(cs); // Ρ‚ΡƒΡ‚ Ρ‡ΠΈΡ‚Π°Π΅ΠΌ схСму Π±Π°Π·Ρ‹ Π΄Ρ‹Π½Π½Ρ‹Ρ… ΠΈ создаСм ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΡŽ ΠΈΠΌΠ΅Π½ ΠΈ Ρ‚ΠΈΠΏΠΎΠ² свойств // ... var ps = new [] { new [] { "p1", "int" }, new [] { "p2", "string" } // ... ΠΌΠ½ΠΎΠ³ΠΎ Π΄Ρ€ΡƒΠ³ΠΈΡ… элСмСнтов }; #> namespace App { public class Wrapper { <# foreach(var p in ps) { #> public virtual <#= p[1] #> <#= p[0] #> { get; protected set; } <# } #> } } 

    When you save TextTemplate.tt, TextTemplate.c will be created

      namespace App { public class Wrapper { public virtual int p1 { get; protected set; } public virtual string p2 { get; protected set; } } } 
    • By the way, cool. Code generation is always good. - VladD
    • Thank! This may make life easier, but it will be as difficult to use this class as before. What I wanted to hear is ElectricDevice electricDevice = new ElectricDeviceBuilder() { Id = id, Name = name, OtherProperty = otherValue }.Build(); . If I do code generation, I know where to look at an example. - Andrey K.
    • one
      PS For such a builder, you can also do code generation, by the way. - Andrey K.
    • one
      @Stack: It seems to be ready. Enhance your idea. - VladD

    Just in case, I will write about how you can avoid manually writing many parameters into the constructor. If anything, then criticize.

    An object:

     public class RoadDevice { public virtual int Id { get; protected set; } public virtual string Name { get; protected set; } public virtual bool IsDecommissioned { get; protected set;} protected RoadDevice() { } } 

    Builder:

     //Π­Ρ‚ΠΎΡ‚ ΠΎΠ±ΡŠΠΊΡ‚ immutable ΠΈ Π² пСрспСктивС достаточно большой, поэтому для Π½Π΅Π³ΠΎ ΠΌΠΎΠΆΠ½ΠΎ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Π±ΠΈΠ»Π΄Π΅Ρ€ для удобства internal class RoadDeviceBuilder : RoadDevice { public RoadDeviceBuilder() : base() { } public RoadDeviceBuilder WithIdAndName(int id, string name) { this.Id = id; this.Name = name; return this; } public RoadDeviceBuilder WithIsDecomissionedFlag(bool isDecommissioned) { this.IsDecommissioned = isDecommissioned; return this; } public RoadDevice Build() { return this; } }