Skip to content

Commit 6157c3d

Browse files
committed
Added defconventions for Commands to specify base types
1 parent 0564952 commit 6157c3d

11 files changed

+128
-17
lines changed

Diesel/CodeGeneration/CodeDomCompiler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private static void Add(CodeCompileUnit codeCompileUnit, ConventionsDeclaration
6363

6464
private static void Add(CodeNamespace ns, ConventionsDeclaration conventions, SemanticModel model, NamespaceName namespaceName, CommandDeclaration declaration)
6565
{
66-
ns.Types.Add(CommandGenerator.CreateCommandDeclaration(model, namespaceName, declaration));
66+
ns.Types.Add(CommandGenerator.CreateCommandDeclaration(model, namespaceName, declaration, conventions.CommandConventions));
6767
}
6868

6969
private static void Add(CodeNamespace ns, ConventionsDeclaration conventions, SemanticModel model, NamespaceName namespaceName, DomainEventDeclaration declaration)

Diesel/CodeGeneration/CommandGenerator.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,24 @@ public class CommandGenerator : CodeDomGenerator
1010
{
1111
public static CodeTypeDeclaration CreateCommandDeclaration(
1212
SemanticModel model, NamespaceName namespaceName,
13-
CommandDeclaration declaration)
13+
CommandDeclaration declaration,
14+
CommandConventions conventions)
1415
{
15-
return CreateTypeWithValueSemantics(
16+
var type = CreateTypeWithValueSemantics(
1617
ValueObjectSpecification.CreateClass(
1718
namespaceName, declaration.Name,
1819
declaration.Properties.ToArray(), true, false),
1920
model.KnownTypes);
20-
}
21+
ApplyConventions(conventions, type);
22+
return type;
23+
}
24+
25+
private static void ApplyConventions(CommandConventions conventions, CodeTypeDeclaration typeDeclaration)
26+
{
27+
foreach (var typeName in conventions.BaseTypes.TypeNames)
28+
{
29+
typeDeclaration.BaseTypes.Add(typeName.Name);
30+
}
31+
}
2132
}
2233
}

README.md

+59-5
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ and equals and equality operators are implemented with value semantics.
144144
Commands are DTOs (Data Transfer Objects), so they are also decorated with attributes
145145
to allow them to be serializable with the BinarySerializer and the DataContractSerializer.
146146

147+
Command can be generated with base types, see `defconventions` for a description of
148+
how to configure this.
149+
150+
Do note, however, that since Commands are contracts it is generally best to declare base
151+
__interfaces__ only so that the risk of breaking contracts by modifying a base is
152+
minimal.
153+
154+
147155
## Example
148156

149157
(defcommand ImportEmployee (int EmployeeNumber, string FirstName, string LastName, int? SourceId))
@@ -152,6 +160,27 @@ This generates a class with properties `EmployeeNumber`, `FirstName` and `LastNa
152160
Nullable types are supported in properties with C# short syntax, i.e. "int?" denotes a nullable Int32.
153161

154162

163+
## Adding Base Classes or Interfaces to Domain Events
164+
165+
The code generator reads conventions from the optional `defconventions` declaration
166+
at the top of the Diesel source file.
167+
168+
You can use this to add base types to the generated Commands.
169+
For example, to have all Commands derive from the `GreatApp.ICommand` interface,
170+
just add this declaration:
171+
172+
(defconventions :commands {:inherit [GreatApp.IDomainEvent]})
173+
174+
Note that since Commands are contracts it is generally best to
175+
declare base __interfaces__ only so that the risk of breaking contracts by modifying a base is
176+
minimal.
177+
178+
See the section on defining conventions for a full description of `defconventions`.
179+
180+
181+
182+
183+
155184
# Defining Domain Events
156185

157186
(defdomainevent <typename> <properties>)
@@ -170,17 +199,23 @@ to allow them to be serializable with the BinarySerializer and the DataContractS
170199
This generates a class with properties `Id`, `EmployeeNumber`, `FirstName` and `LastName` and `SourceId`.
171200

172201

173-
## Adding Base Classes or Interfaces to Domain Events
202+
## Adding Base Classes or Interfaces to Domain Events
174203

175204
The code generator reads conventions from the optional `defconventions` declaration
176205
at the top of the Diesel source file.
177206

178-
You can use this to add base types to the generated Domain Events.
207+
You can use this to add base types to the generated Domain Events.
179208
For example, to have all Domain Events derive from the `GreatApp.IDomainEvent` interface,
180209
just add this declaration:
181210

182211
(defconventions :domainevents {:inherit [GreatApp.IDomainEvent]})
183212

213+
Note that since Domain Events are contracts it is generally best to
214+
declare base __interfaces__ only so that the risk of breaking contracts by modifying a base is
215+
minimal.
216+
217+
See the section on defining conventions for a full description of `defconventions`.
218+
184219

185220
# Defining Data Transfer Objects (DTOs)
186221

@@ -269,18 +304,37 @@ and `Employees.EmployeeImported`.
269304

270305
# Defining Conventions for Code-Generation
271306

272-
(defconventions :domainevents {:inherit <list-of-base-types>})
307+
(defconventions :domainevents {:inherit <list-of-base-types>}
308+
:commands {:inherit <list-of-base-types>})
273309

274310
The code-generation conventions can be controlled through the `defconventions` declaration.
275-
It must be placed first in the source file.
276-
For now, it only controls the list of interfaces and base classes for the Domain Events.
311+
For now, it only controls the list of interfaces and base classes for the Domain Events and Commands.
312+
313+
The declaration must be placed first in the source file. The `:domainevents` and `:commands`
314+
declarations inside are both optional and can be in any order, but they can not occur more than
315+
once.
277316

278317
## Example
279318

319+
(defconventions :domainevents {:inherit [Test.Diesel.IDomainEvent]}
320+
:commands {:inherit [Test.Diesel.ICommand]})
321+
322+
This causes the code generator to add the `Test.Diesel.IDomainEvent` interface
323+
as a base on all Domain Events, and the `Test.Diesel.ICommand` interface to all Commands.
324+
325+
326+
## Example: Adding interfaces to Domain Events
327+
280328
(defconventions :domainevents {:inherit [Test.Diesel.IDomainEvent]})
281329

282330
This adds the `Test.Diesel.IDomainEvent` as a base on all generated Domain Events.
283331

332+
## Example: Adding interfaces to Commands
333+
334+
(defconventions :commands {:inherit [Test.Diesel.ICommand]})
335+
336+
This adds the `Test.Diesel.ICommand` as a base on all generated Domain Events.
337+
284338

285339
# Comments
286340

Test/CodeGeneration/CodeDomCompilerTest.cs

+12
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ public void CommandDeclaration_ValidDeclaration_ShouldAddDataContractAttribute()
165165
Assert.That(source, Is.StringContaining(@"DataContractAttribute(Name=""ImportEmployeeCommand"")"));
166166
}
167167

168+
[Test]
169+
public void CommandDeclaration_WithConventionInheritICommand_ShouldProduceClassInheritingICommand()
170+
{
171+
var declaration = CreateImportEmployeeCommandDeclaration();
172+
var conventions = new ConventionsDeclaration(
173+
new DomainEventConventions(new BaseTypes(new TypeName[0])),
174+
new CommandConventions(new BaseTypes(new[] { new TypeName("Test.Diesel.ICommand") })));
175+
var model = CreateSemanticModelWith(conventions, declaration);
176+
var source = CompileToSource(CodeDomCompiler.Compile(model));
177+
Assert.That(source, Is.StringMatching(@"class ImportEmployeeCommand :.* Test.Diesel.ICommand \{"));
178+
}
179+
168180
private CodeCompileUnit CompileImportEmployeeCommand()
169181
{
170182
var commandDeclaration = new CommandDeclaration("ImportEmployeeCommand",

Test/Examples/DieselCompilerIntegrationTestCase.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
(defconventions :domainevents {:inherit [Test.Diesel.IDomainEvent]})
1+
(defconventions
2+
:domainevents {:inherit [Test.Diesel.IDomainEvent]}
3+
:commands {:inherit [Test.Diesel.ICommand]})
24

35
(namespace Employees
46
(defvaluetype EmployeeNumber int)

Test/Generated/Example.dsl

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
(defconventions :domainevents {:inherit [Test.Diesel.IDomainEvent]})
1+
(defconventions
2+
:commands {:inherit [Test.Diesel.ICommand]}
3+
:domainevents {:inherit [Test.Diesel.IDomainEvent]})
24

35
(namespace Test.Diesel.Generated
46

Test/Generated/GenerateExamples.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ public partial interface IImportService {
535535

536536
[System.Runtime.Serialization.DataContractAttribute(Name="ImportEmployee")]
537537
[System.SerializableAttribute()]
538-
public partial class ImportEmployee : System.IEquatable<ImportEmployee> {
538+
public partial class ImportEmployee : System.IEquatable<ImportEmployee>, Test.Diesel.ICommand {
539539

540540
[System.Runtime.Serialization.DataMemberAttribute(Name="CommandId", Order=1)]
541541
private System.Guid _commandId;
@@ -629,7 +629,7 @@ public override bool Equals(object obj) {
629629

630630
[System.Runtime.Serialization.DataContractAttribute(Name="ImportConsultant")]
631631
[System.SerializableAttribute()]
632-
public partial class ImportConsultant : System.IEquatable<ImportConsultant> {
632+
public partial class ImportConsultant : System.IEquatable<ImportConsultant>, Test.Diesel.ICommand {
633633

634634
[System.Runtime.Serialization.DataMemberAttribute(Name="FirstName", Order=1)]
635635
private string _firstName;
@@ -699,7 +699,7 @@ public override bool Equals(object obj) {
699699

700700
[System.Runtime.Serialization.DataContractAttribute(Name="ImportEmployeeNestedTypes")]
701701
[System.SerializableAttribute()]
702-
public partial class ImportEmployeeNestedTypes : System.IEquatable<ImportEmployeeNestedTypes> {
702+
public partial class ImportEmployeeNestedTypes : System.IEquatable<ImportEmployeeNestedTypes>, Test.Diesel.ICommand {
703703

704704
[System.Runtime.Serialization.DataMemberAttribute(Name="CommandId", Order=1)]
705705
private System.Guid _commandId;

Test/GeneratedCommandTest.cs

+7
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,12 @@ private static object GetDataMemberAttributeOrderValue(FieldInfo[] fields, strin
105105
.Single(a => a.AttributeType == typeof (DataMemberAttribute));
106106
return dataMemberAttribute.NamedArguments.Single(a => a.MemberName == "Order").TypedValue.Value;
107107
}
108+
109+
[Test]
110+
public void Instance_WhenConventionsDeclareABaseInterface_MustImplementTheInterface()
111+
{
112+
var instance = new ImportEmployee(CommandId, EmployeeNumber, FirstName, Lastname, null);
113+
Assert.That(instance, Is.AssignableTo<ICommand>());
114+
}
108115
}
109116
}

Test/ICommand.cs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Test.Diesel
2+
{
3+
public interface ICommand
4+
{
5+
}
6+
}

Test/Parsing/GrammarTest.cs

+19-3
Original file line numberDiff line numberDiff line change
@@ -584,15 +584,31 @@ public void ConventionDeclarations_ValidWithCommandInheritance_ShouldParse()
584584
}
585585

586586
[Test]
587-
public void ConventionDeclarations_ValidWithEverything_ShouldParse()
587+
public void ConventionDeclarations_DomainEventsThenCommands_SectionOrderShouldNotMatter()
588588
{
589589
var actual = Grammar.ConventionsDeclaration.Parse(
590590
"(defconventions " +
591591
" :domainevents {:inherit [SomeNamespace.IDomainEvent]}" +
592592
" :commands {:inherit [SomeNamespace.ICommand]})");
593593
Assert.That(actual, Is.Not.Null);
594-
Assert.That(actual.DomainEventConventions, Is.Not.Null);
595-
Assert.That(actual.CommandConventions, Is.Not.Null);
594+
Assert.That(actual.DomainEventConventions.BaseTypes.TypeNames.Single().Name,
595+
Is.EqualTo("SomeNamespace.IDomainEvent"));
596+
Assert.That(actual.CommandConventions.BaseTypes.TypeNames.Single().Name,
597+
Is.EqualTo("SomeNamespace.ICommand"));
598+
}
599+
600+
[Test]
601+
public void ConventionDeclarations_CommandsThenDomainEvents_SectionOrderShouldNotMatter()
602+
{
603+
var actual = Grammar.ConventionsDeclaration.Parse(
604+
"(defconventions " +
605+
" :commands {:inherit [SomeNamespace.ICommand]}" +
606+
" :domainevents {:inherit [SomeNamespace.IDomainEvent]})");
607+
Assert.That(actual, Is.Not.Null);
608+
Assert.That(actual.DomainEventConventions.BaseTypes.TypeNames.Single().Name,
609+
Is.EqualTo("SomeNamespace.IDomainEvent"));
610+
Assert.That(actual.CommandConventions.BaseTypes.TypeNames.Single().Name,
611+
Is.EqualTo("SomeNamespace.ICommand"));
596612
}
597613

598614
[Test]

Test/Test.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
<DesignTime>True</DesignTime>
7575
<DependentUpon>GenerateExamples.tt</DependentUpon>
7676
</Compile>
77+
<Compile Include="ICommand.cs" />
7778
<Compile Include="IDomainEvent.cs" />
7879
<Compile Include="ObjectMothers\CommandDeclarationObjectMother.cs" />
7980
<Compile Include="ObjectMothers\DomainEventDeclarationObjectMother.cs" />

0 commit comments

Comments
 (0)