Knowledge-based authentication using Domain-driven Design in C#First domain-driven User entity...
Is there a minimum number of transactions in a block?
Why doesn't Newton's third law mean a person bounces back to where they started when they hit the ground?
A Journey Through Space and Time
How can I fix this gap between bookcases I made?
What typically incentivizes a professor to change jobs to a lower ranking university?
How old can references or sources in a thesis be?
What do you call something that goes against the spirit of the law, but is legal when interpreting the law to the letter?
Is it legal to have the "// (c) 2019 John Smith" header in all files when there are hundreds of contributors?
Why is this code 6.5x slower with optimizations enabled?
Could a US political party gain complete control over the government by removing checks & balances?
How does one intimidate enemies without having the capacity for violence?
Draw simple lines in Inkscape
Chess with symmetric move-square
Why do we use polarized capacitor?
Can an x86 CPU running in real mode be considered to be basically an 8086 CPU?
Finding files for which a command fails
N.B. ligature in Latex
declaring a variable twice in IIFE
cryptic clue: mammal sounds like relative consumer (8)
Non-Jewish family in an Orthodox Jewish Wedding
how to create a data type and make it available in all Databases?
What makes Graph invariants so useful/important?
What are these boxed doors outside store fronts in New York?
Can I interfere when another PC is about to be attacked?
Knowledge-based authentication using Domain-driven Design in C#
First domain-driven User entity classList<List<string>> vs DataTableRectangle ClassDomain driven designed game to play with city namesDomain Driven Design Model for handling imagesDisplay items from DB using MVC and Domain Driven DesignBusiness Rule DSL for Values in Domain-Driven DesignWeb-app for tracking containersModeling the Aggregates in the following Bounded Context - Domain Driven DesignDesign Web API with Command Handler
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{ margin-bottom:0;
}
$begingroup$
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
Below are before and after screen shots of the Data Protection part of the Add Call view:
The current system was written five years ago and we did not attempt to use any design patterns or modern approaches (such as Domain-Driven Design) because we lacked the time and understanding.
We now have the opportunity to re-write this software and would like to follow a Domain-driven approach.
First draft of a DataProtectionQuestion entity class:
public class DataProtectionQuestion
{
public string Question { get; set; }
public IEnumerable<string> Answers { get; set; }
public bool IsRequired { get; set; }
public string Answer { get; set; }
public bool IsAnswered => !string.IsNullOrWhiteSpace(Answer);
public bool AnswerIsCorrect => Answers?.Contains(Answer) ?? false;
}
Questions that arose from this design:
- Is this an anaemic model?
- Is the DataProtectionQuestion entity the 'right' place to validate the answer?
- Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
- Should the setters be private and should clients supply data through a constructor?
- Should an Answer be an entity in its own right with a property for 'IsValid'?
- How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'? (I realise the UI is not the concern of the Domain, but having the ability to choose these as answers is)
Second draft (in an attempt to answer some of the above):
public class DataProtectionAnswer
{
public DataProtectionAnswer(string answer, bool isValid)
{
Answer = answer;
IsValid = isValid;
}
public string Answer { get; private set; }
public bool IsValid { get; private set; }
}
public class DataProtectionQuestion
{
public DataProtectionQuestion(string question, bool isRequired, IEnumerable<DataProtectionAnswer> answers)
{
// validate non-null parameters?
Question = question;
IsRequired = isRequired;
Answers = answers;
}
public string Question { get; private set; }
public bool IsRequired { get; private set; }
public IEnumerable<DataProtectionAnswer> Answers { get; private set; }
public DataProtectionAnswer SelectedAnswer { get; private set; }
public bool IsAnswered => SelectedAnswer != null;
public bool AnswerIsCorrect => SelectedAnswer?.IsValid ?? false;
public void SetSelectedAnswer(DataProtectionAnswer answer)
{
// Should validate that answer is not null and contained in Answers?
SelectedAnswer = answer;
}
}
Some answers..leading to more questions?:
- Q. Should the entity have methods like 'SetAnswer' or
'SetAnswerIsValid'? - A. I have added a 'SetSelectedAnswer' method but I still don't know if this 'feels' right?
- Q. Should the setters be private and should clients supply data through a constructor?
- A. I don't know but that's what I've done in draft 2.
- Q. Should an Answer be an entity in its own right with a property for 'IsValid'?
- A. As per previous question, this is what I've done but should I have?
- Q. How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
- A. I can now do this by adding an 'Unanswered' and 'Incorrect Answer' DataProtectionAnswer to the DataProtectionQuestion, but this
'feels' wrong. Isn't this the responsibility of the Presenter?
As you can probably tell, I'm floundering and really struggling to get my head around how to model this scenario using a DDD approach. Perhaps DDD isn't right for this situation. I don't know and I feel very stupid right now!
Can anyone please offer some guidance / suggestions on a way forward / better approach?
c# ddd
New contributor
$endgroup$
add a comment |
$begingroup$
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
Below are before and after screen shots of the Data Protection part of the Add Call view:
The current system was written five years ago and we did not attempt to use any design patterns or modern approaches (such as Domain-Driven Design) because we lacked the time and understanding.
We now have the opportunity to re-write this software and would like to follow a Domain-driven approach.
First draft of a DataProtectionQuestion entity class:
public class DataProtectionQuestion
{
public string Question { get; set; }
public IEnumerable<string> Answers { get; set; }
public bool IsRequired { get; set; }
public string Answer { get; set; }
public bool IsAnswered => !string.IsNullOrWhiteSpace(Answer);
public bool AnswerIsCorrect => Answers?.Contains(Answer) ?? false;
}
Questions that arose from this design:
- Is this an anaemic model?
- Is the DataProtectionQuestion entity the 'right' place to validate the answer?
- Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
- Should the setters be private and should clients supply data through a constructor?
- Should an Answer be an entity in its own right with a property for 'IsValid'?
- How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'? (I realise the UI is not the concern of the Domain, but having the ability to choose these as answers is)
Second draft (in an attempt to answer some of the above):
public class DataProtectionAnswer
{
public DataProtectionAnswer(string answer, bool isValid)
{
Answer = answer;
IsValid = isValid;
}
public string Answer { get; private set; }
public bool IsValid { get; private set; }
}
public class DataProtectionQuestion
{
public DataProtectionQuestion(string question, bool isRequired, IEnumerable<DataProtectionAnswer> answers)
{
// validate non-null parameters?
Question = question;
IsRequired = isRequired;
Answers = answers;
}
public string Question { get; private set; }
public bool IsRequired { get; private set; }
public IEnumerable<DataProtectionAnswer> Answers { get; private set; }
public DataProtectionAnswer SelectedAnswer { get; private set; }
public bool IsAnswered => SelectedAnswer != null;
public bool AnswerIsCorrect => SelectedAnswer?.IsValid ?? false;
public void SetSelectedAnswer(DataProtectionAnswer answer)
{
// Should validate that answer is not null and contained in Answers?
SelectedAnswer = answer;
}
}
Some answers..leading to more questions?:
- Q. Should the entity have methods like 'SetAnswer' or
'SetAnswerIsValid'? - A. I have added a 'SetSelectedAnswer' method but I still don't know if this 'feels' right?
- Q. Should the setters be private and should clients supply data through a constructor?
- A. I don't know but that's what I've done in draft 2.
- Q. Should an Answer be an entity in its own right with a property for 'IsValid'?
- A. As per previous question, this is what I've done but should I have?
- Q. How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
- A. I can now do this by adding an 'Unanswered' and 'Incorrect Answer' DataProtectionAnswer to the DataProtectionQuestion, but this
'feels' wrong. Isn't this the responsibility of the Presenter?
As you can probably tell, I'm floundering and really struggling to get my head around how to model this scenario using a DDD approach. Perhaps DDD isn't right for this situation. I don't know and I feel very stupid right now!
Can anyone please offer some guidance / suggestions on a way forward / better approach?
c# ddd
New contributor
$endgroup$
add a comment |
$begingroup$
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
Below are before and after screen shots of the Data Protection part of the Add Call view:
The current system was written five years ago and we did not attempt to use any design patterns or modern approaches (such as Domain-Driven Design) because we lacked the time and understanding.
We now have the opportunity to re-write this software and would like to follow a Domain-driven approach.
First draft of a DataProtectionQuestion entity class:
public class DataProtectionQuestion
{
public string Question { get; set; }
public IEnumerable<string> Answers { get; set; }
public bool IsRequired { get; set; }
public string Answer { get; set; }
public bool IsAnswered => !string.IsNullOrWhiteSpace(Answer);
public bool AnswerIsCorrect => Answers?.Contains(Answer) ?? false;
}
Questions that arose from this design:
- Is this an anaemic model?
- Is the DataProtectionQuestion entity the 'right' place to validate the answer?
- Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
- Should the setters be private and should clients supply data through a constructor?
- Should an Answer be an entity in its own right with a property for 'IsValid'?
- How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'? (I realise the UI is not the concern of the Domain, but having the ability to choose these as answers is)
Second draft (in an attempt to answer some of the above):
public class DataProtectionAnswer
{
public DataProtectionAnswer(string answer, bool isValid)
{
Answer = answer;
IsValid = isValid;
}
public string Answer { get; private set; }
public bool IsValid { get; private set; }
}
public class DataProtectionQuestion
{
public DataProtectionQuestion(string question, bool isRequired, IEnumerable<DataProtectionAnswer> answers)
{
// validate non-null parameters?
Question = question;
IsRequired = isRequired;
Answers = answers;
}
public string Question { get; private set; }
public bool IsRequired { get; private set; }
public IEnumerable<DataProtectionAnswer> Answers { get; private set; }
public DataProtectionAnswer SelectedAnswer { get; private set; }
public bool IsAnswered => SelectedAnswer != null;
public bool AnswerIsCorrect => SelectedAnswer?.IsValid ?? false;
public void SetSelectedAnswer(DataProtectionAnswer answer)
{
// Should validate that answer is not null and contained in Answers?
SelectedAnswer = answer;
}
}
Some answers..leading to more questions?:
- Q. Should the entity have methods like 'SetAnswer' or
'SetAnswerIsValid'? - A. I have added a 'SetSelectedAnswer' method but I still don't know if this 'feels' right?
- Q. Should the setters be private and should clients supply data through a constructor?
- A. I don't know but that's what I've done in draft 2.
- Q. Should an Answer be an entity in its own right with a property for 'IsValid'?
- A. As per previous question, this is what I've done but should I have?
- Q. How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
- A. I can now do this by adding an 'Unanswered' and 'Incorrect Answer' DataProtectionAnswer to the DataProtectionQuestion, but this
'feels' wrong. Isn't this the responsibility of the Presenter?
As you can probably tell, I'm floundering and really struggling to get my head around how to model this scenario using a DDD approach. Perhaps DDD isn't right for this situation. I don't know and I feel very stupid right now!
Can anyone please offer some guidance / suggestions on a way forward / better approach?
c# ddd
New contributor
$endgroup$
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
Below are before and after screen shots of the Data Protection part of the Add Call view:
The current system was written five years ago and we did not attempt to use any design patterns or modern approaches (such as Domain-Driven Design) because we lacked the time and understanding.
We now have the opportunity to re-write this software and would like to follow a Domain-driven approach.
First draft of a DataProtectionQuestion entity class:
public class DataProtectionQuestion
{
public string Question { get; set; }
public IEnumerable<string> Answers { get; set; }
public bool IsRequired { get; set; }
public string Answer { get; set; }
public bool IsAnswered => !string.IsNullOrWhiteSpace(Answer);
public bool AnswerIsCorrect => Answers?.Contains(Answer) ?? false;
}
Questions that arose from this design:
- Is this an anaemic model?
- Is the DataProtectionQuestion entity the 'right' place to validate the answer?
- Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
- Should the setters be private and should clients supply data through a constructor?
- Should an Answer be an entity in its own right with a property for 'IsValid'?
- How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'? (I realise the UI is not the concern of the Domain, but having the ability to choose these as answers is)
Second draft (in an attempt to answer some of the above):
public class DataProtectionAnswer
{
public DataProtectionAnswer(string answer, bool isValid)
{
Answer = answer;
IsValid = isValid;
}
public string Answer { get; private set; }
public bool IsValid { get; private set; }
}
public class DataProtectionQuestion
{
public DataProtectionQuestion(string question, bool isRequired, IEnumerable<DataProtectionAnswer> answers)
{
// validate non-null parameters?
Question = question;
IsRequired = isRequired;
Answers = answers;
}
public string Question { get; private set; }
public bool IsRequired { get; private set; }
public IEnumerable<DataProtectionAnswer> Answers { get; private set; }
public DataProtectionAnswer SelectedAnswer { get; private set; }
public bool IsAnswered => SelectedAnswer != null;
public bool AnswerIsCorrect => SelectedAnswer?.IsValid ?? false;
public void SetSelectedAnswer(DataProtectionAnswer answer)
{
// Should validate that answer is not null and contained in Answers?
SelectedAnswer = answer;
}
}
Some answers..leading to more questions?:
- Q. Should the entity have methods like 'SetAnswer' or
'SetAnswerIsValid'? - A. I have added a 'SetSelectedAnswer' method but I still don't know if this 'feels' right?
- Q. Should the setters be private and should clients supply data through a constructor?
- A. I don't know but that's what I've done in draft 2.
- Q. Should an Answer be an entity in its own right with a property for 'IsValid'?
- A. As per previous question, this is what I've done but should I have?
- Q. How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
- A. I can now do this by adding an 'Unanswered' and 'Incorrect Answer' DataProtectionAnswer to the DataProtectionQuestion, but this
'feels' wrong. Isn't this the responsibility of the Presenter?
As you can probably tell, I'm floundering and really struggling to get my head around how to model this scenario using a DDD approach. Perhaps DDD isn't right for this situation. I don't know and I feel very stupid right now!
Can anyone please offer some guidance / suggestions on a way forward / better approach?
c# ddd
c# ddd
New contributor
New contributor
New contributor
asked Apr 2 at 15:19
datahandlerdatahandler
864
864
New contributor
New contributor
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
$begingroup$
To start with, you're not doing DDD here.
DDD (Domain-Driven Design / Development) is based around the idea that we start with the domain. We don't touch code yet—we develop the domain models on-paper (or whiteboard, whatever is preferred). Once that is done, we build the code as closely to the domain as possible. The point of DDD is that the code should mirror the domain design.
Before we get going, I highly, highly, highly recommend this book, by Scott Wlaschin, a prominent F# developer who brings DDD into a very easy-to-understand view (the examples are F#, but they apply to C# as well): Domain Modeling made Functional
DDD is about:
Define the domain, the inputs, and the outputs. That is, as a user of the system, what does the domain need to do. Here it sounds like we have part of the domain defined:
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
From there, we define our types. Generally, I do DDD with F#, but it's just as applicable to C#. We model the physical domain, so here we're not modeling the questions, we're modeling the validation. That is: the user must answer various questions and prove they are knowledgeable on the claim.
This is the root of our domain model: we need to validate some information. You have mixed multiple pieces here, so we're going to separate them a bit.
After building the types, we build the work. That is, the functions. We build the types as just data-structures, then we build the functions next to encapsulate the domain rules.
So, you've defined the domain (at least, as far as I see it) via the quoted-blurb, so what I want to do is move that into some types.
To start with, we'll define a DataProtectionResponse
(we're going to use the exact language from the domain model, the purpose of DDD is to translate the human-language into code).
class DataProtectionResponse {
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
}
Now, we need to come up with a model for DataProtectionQuestion
:
class DataProtectionQuestion {
public string Question { get; set; }
public bool Required { get; set; }
}
As you see, we are ONLY modeling two components of the question: the actual question, and whether or not it's required. The questions themselves are a different part of the domain, they're generated as a question, and using this is how we get into building a flexible model. We can now take these same questions somewhere else, and use them as a whole other tool, assuming it needs to interact with our current domain.
Next, we have ValidQuestionAnswer
. This is going to be the answer that are valid for this particular claim:
class ValidQuestionAnswer {
public Response Response { get; set; }
}
We made this a class as we absolutely want to consider a situation where an answer might have more data to it.
Finally, the Response
. You might say, "Der Kommissar, why does that need to be a class, it's always a string?" Again, we might need to add more to this model, including functionality, so we do that by using a class.
class Response {
public string Value { get; set; }
}
So now, our domain will consume an IEnumerable<DataProtectionResponse>
, but not directly.
class DataProtection {
public IEnumerable<DataProtectionResponse> Questions { get; set; }
}
Why another class? Well, let's start talking functionality.
First and foremost, the primary component of our design is that DataProtection
must validate. For this to work, we need a IsValid
function or property there:
public bool IsValid => Questions.All(x => x.IsSufficient);
Alright, so we have some concepts now. We have a IsValid
that indicates if our DataProtection
is valid or not, and we have decided that all of the questions must be sufficiently answered.
Next, we need to prove that a question is sufficiently answered.
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
Again, we are going to encode our actual logic: this DataProtectionResponse
is sufficient if any of the ValidQuestionAnswers
are acceptable with the question and response.
Next, how do we prove they're acceptable?
Well, the first rule is that if it's not required and there is no response, then it's valid:
if (!question.Required && response?.IsEmpty ?? false == false)
{
return true;
}
And of course, Response.IsEmpty
:
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
Otherwise, we want to prove that this response and the provided response are acceptable:
return response.Satisfies(Response);
And this is why we made it a class right-off-the-bat: we might have more logic that goes into Satisfies
that might do heuristic analysis. I.e. if you provide an address, the logic might say 123 Main St.
and 123 Main St
and 123 Main Street
are all the same.
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
Next, our Response.Satisfies
:
public bool Satisfies(Response response) => Value == response.Value;
And viola, we're done. We've encoded the entire domain, concisely, and using the actual terms the domain considers. Only 37 lines of code:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
We don't have any odd logic, we don't have any conflated values: each model concerns itself with it's own work, no one else's.
Additionally, when our domain changes now (or we have to change the satisfaction logic) we have built the flexibility in-place without needing major infrastructure rewrites. If we need to override a response, we encode that in DataProtectionResponse
and modify IsSufficient
.
Finally, you could even shorten Acceptable
to a single statement, since the logic is relatively straightforward:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || response.Satisfies(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
You noted in the comments that I missed the requirement of at least one optional question, and you're absolutely right. So let's talk about another strength of DDD: verifying our code matches the domain.
At least one non-mandatory question must be answered in order to validate the caller.
So, looking through our code we see that we can start with DataProtection.IsValid
:
public bool IsValid => Questions.All(x => x.IsSufficient);
Aha, we already know our problem is here. We make sure all questions are sufficient, but that does not take at least one optional into account. So how do we fix that?
Well, to start, we'll modify IsValid
to support some theoretical work:
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
Here, we've decided that if each DataProtectionResponse
tells us if it's required or not, and if it's answered or not, we can prove that we have at least one non-required question answered.
Next, we need to implement those two items. Both are trivial, and actually help with our other code:
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
Now we have another method to implement: ValidQuestionAnswer.AnsweredBy(Response)
:
public bool AnsweredBy(Response response) => response.Satisfies(Response);
You'll notice we've repeated one bit of code: response.Satisfies(Response)
is in both AnsweredBy
, and Acceptable
, let's change that:
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
Once again, our contract is now satisfied, and we ONLY go at-most one-level-deep in each model from a parent model. (That is, we could have done !x.Question.Required
instead of !x.IsRequired
, but it's not the responsibility of DataProtection
to know what makes a response required or not.)
So, 36 lines of code to build our new requirements:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool AnsweredBy(Response response) => response.Satisfies(Response);
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
}
Finally, you asked a few questions about your implementation: as you see, I ignored them, purposefully. With DDD, those questions are not things to ask about the implementation but things to ask about the domain. With DDD we do iterations of "design", "implement", "design", "implement"—all the questions you have should go in the design stage, which is where you (and the other domain experts) gather and hash-out the principles of the project. This means, now that you have an implementation, we go back to design and clarify those questions. As the developer, when you see these things you should be creating a working list of potential problem-points, you might find out the domain-experts have considered them, or they may not, so you take your concerns back to them and refine the design. (Again, DDD is an iterative concept.)
But, suppose I were to answer them:
Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
A: That's actually a design question, do you need to override whether an answer is valid or not? (I briefly touched on that in the first part of the answer.)
Should the setters be private and should clients supply data through a constructor?
A: This seems like an implementation question at first glance, but if we reword it things are different: can a client change an answer? If the answer is yes, the proposed design is fine. If not, model for immutability.
Should an Answer be an entity in its own right with a property for 'IsValid'?
A: In my humble opinion, yes. Answers aren't a string, they're a concept. Additionally, with a base-class, you can override that for answers that are bool
, DateTime
, etc. But, again: take it back to the DDD drawing board. The domain model will tell you what needs done.
How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
A: That's a design question, but my suggestion is to ditch the Valid
checkbox and provide a red/green "Needs Answered", "Incorrect Answer", or "Correct Answer" state. Again, do what the domain calls for. You're mixing some concerns here, and with DDD we create a clear separation. When you have a question like this, we go back into the design stage and hash it out. (It might turn out that you must not indicate if an answer is incorrect. Compliance laws are weird.)
$endgroup$
$begingroup$
Thank you for taking the time to write such a comprehensive response. It is much appreciated. You're absolutely right when you say I'm mixing concerns. I am struggling to get my mindset away from data-driven towards domain-drive design. Your suggested approach to DDD has given me much needed food for thought. One thing I mentioned in my original post that I don't think your answer covers: "At least one non-mandatory question must be answered in order to validate the caller." I believe the code you have suggested allows for all non-mandatory questions to be left unanswered?
$endgroup$
– datahandler
Apr 3 at 8:27
$begingroup$
@datahandler I just edited the answer to address that (I did not change the existing answer, but instead showed how we would validate that the model we built conforms to the DDD specification). Hopefully all of this helps you figure out where you can modify the process to do DDD a little more effectively. :)
$endgroup$
– Der Kommissar
Apr 3 at 13:24
$begingroup$
@DerKommissar Excellent! Thanks again!
$endgroup$
– datahandler
Apr 3 at 14:38
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
datahandler is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f216727%2fknowledge-based-authentication-using-domain-driven-design-in-c%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
To start with, you're not doing DDD here.
DDD (Domain-Driven Design / Development) is based around the idea that we start with the domain. We don't touch code yet—we develop the domain models on-paper (or whiteboard, whatever is preferred). Once that is done, we build the code as closely to the domain as possible. The point of DDD is that the code should mirror the domain design.
Before we get going, I highly, highly, highly recommend this book, by Scott Wlaschin, a prominent F# developer who brings DDD into a very easy-to-understand view (the examples are F#, but they apply to C# as well): Domain Modeling made Functional
DDD is about:
Define the domain, the inputs, and the outputs. That is, as a user of the system, what does the domain need to do. Here it sounds like we have part of the domain defined:
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
From there, we define our types. Generally, I do DDD with F#, but it's just as applicable to C#. We model the physical domain, so here we're not modeling the questions, we're modeling the validation. That is: the user must answer various questions and prove they are knowledgeable on the claim.
This is the root of our domain model: we need to validate some information. You have mixed multiple pieces here, so we're going to separate them a bit.
After building the types, we build the work. That is, the functions. We build the types as just data-structures, then we build the functions next to encapsulate the domain rules.
So, you've defined the domain (at least, as far as I see it) via the quoted-blurb, so what I want to do is move that into some types.
To start with, we'll define a DataProtectionResponse
(we're going to use the exact language from the domain model, the purpose of DDD is to translate the human-language into code).
class DataProtectionResponse {
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
}
Now, we need to come up with a model for DataProtectionQuestion
:
class DataProtectionQuestion {
public string Question { get; set; }
public bool Required { get; set; }
}
As you see, we are ONLY modeling two components of the question: the actual question, and whether or not it's required. The questions themselves are a different part of the domain, they're generated as a question, and using this is how we get into building a flexible model. We can now take these same questions somewhere else, and use them as a whole other tool, assuming it needs to interact with our current domain.
Next, we have ValidQuestionAnswer
. This is going to be the answer that are valid for this particular claim:
class ValidQuestionAnswer {
public Response Response { get; set; }
}
We made this a class as we absolutely want to consider a situation where an answer might have more data to it.
Finally, the Response
. You might say, "Der Kommissar, why does that need to be a class, it's always a string?" Again, we might need to add more to this model, including functionality, so we do that by using a class.
class Response {
public string Value { get; set; }
}
So now, our domain will consume an IEnumerable<DataProtectionResponse>
, but not directly.
class DataProtection {
public IEnumerable<DataProtectionResponse> Questions { get; set; }
}
Why another class? Well, let's start talking functionality.
First and foremost, the primary component of our design is that DataProtection
must validate. For this to work, we need a IsValid
function or property there:
public bool IsValid => Questions.All(x => x.IsSufficient);
Alright, so we have some concepts now. We have a IsValid
that indicates if our DataProtection
is valid or not, and we have decided that all of the questions must be sufficiently answered.
Next, we need to prove that a question is sufficiently answered.
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
Again, we are going to encode our actual logic: this DataProtectionResponse
is sufficient if any of the ValidQuestionAnswers
are acceptable with the question and response.
Next, how do we prove they're acceptable?
Well, the first rule is that if it's not required and there is no response, then it's valid:
if (!question.Required && response?.IsEmpty ?? false == false)
{
return true;
}
And of course, Response.IsEmpty
:
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
Otherwise, we want to prove that this response and the provided response are acceptable:
return response.Satisfies(Response);
And this is why we made it a class right-off-the-bat: we might have more logic that goes into Satisfies
that might do heuristic analysis. I.e. if you provide an address, the logic might say 123 Main St.
and 123 Main St
and 123 Main Street
are all the same.
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
Next, our Response.Satisfies
:
public bool Satisfies(Response response) => Value == response.Value;
And viola, we're done. We've encoded the entire domain, concisely, and using the actual terms the domain considers. Only 37 lines of code:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
We don't have any odd logic, we don't have any conflated values: each model concerns itself with it's own work, no one else's.
Additionally, when our domain changes now (or we have to change the satisfaction logic) we have built the flexibility in-place without needing major infrastructure rewrites. If we need to override a response, we encode that in DataProtectionResponse
and modify IsSufficient
.
Finally, you could even shorten Acceptable
to a single statement, since the logic is relatively straightforward:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || response.Satisfies(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
You noted in the comments that I missed the requirement of at least one optional question, and you're absolutely right. So let's talk about another strength of DDD: verifying our code matches the domain.
At least one non-mandatory question must be answered in order to validate the caller.
So, looking through our code we see that we can start with DataProtection.IsValid
:
public bool IsValid => Questions.All(x => x.IsSufficient);
Aha, we already know our problem is here. We make sure all questions are sufficient, but that does not take at least one optional into account. So how do we fix that?
Well, to start, we'll modify IsValid
to support some theoretical work:
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
Here, we've decided that if each DataProtectionResponse
tells us if it's required or not, and if it's answered or not, we can prove that we have at least one non-required question answered.
Next, we need to implement those two items. Both are trivial, and actually help with our other code:
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
Now we have another method to implement: ValidQuestionAnswer.AnsweredBy(Response)
:
public bool AnsweredBy(Response response) => response.Satisfies(Response);
You'll notice we've repeated one bit of code: response.Satisfies(Response)
is in both AnsweredBy
, and Acceptable
, let's change that:
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
Once again, our contract is now satisfied, and we ONLY go at-most one-level-deep in each model from a parent model. (That is, we could have done !x.Question.Required
instead of !x.IsRequired
, but it's not the responsibility of DataProtection
to know what makes a response required or not.)
So, 36 lines of code to build our new requirements:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool AnsweredBy(Response response) => response.Satisfies(Response);
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
}
Finally, you asked a few questions about your implementation: as you see, I ignored them, purposefully. With DDD, those questions are not things to ask about the implementation but things to ask about the domain. With DDD we do iterations of "design", "implement", "design", "implement"—all the questions you have should go in the design stage, which is where you (and the other domain experts) gather and hash-out the principles of the project. This means, now that you have an implementation, we go back to design and clarify those questions. As the developer, when you see these things you should be creating a working list of potential problem-points, you might find out the domain-experts have considered them, or they may not, so you take your concerns back to them and refine the design. (Again, DDD is an iterative concept.)
But, suppose I were to answer them:
Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
A: That's actually a design question, do you need to override whether an answer is valid or not? (I briefly touched on that in the first part of the answer.)
Should the setters be private and should clients supply data through a constructor?
A: This seems like an implementation question at first glance, but if we reword it things are different: can a client change an answer? If the answer is yes, the proposed design is fine. If not, model for immutability.
Should an Answer be an entity in its own right with a property for 'IsValid'?
A: In my humble opinion, yes. Answers aren't a string, they're a concept. Additionally, with a base-class, you can override that for answers that are bool
, DateTime
, etc. But, again: take it back to the DDD drawing board. The domain model will tell you what needs done.
How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
A: That's a design question, but my suggestion is to ditch the Valid
checkbox and provide a red/green "Needs Answered", "Incorrect Answer", or "Correct Answer" state. Again, do what the domain calls for. You're mixing some concerns here, and with DDD we create a clear separation. When you have a question like this, we go back into the design stage and hash it out. (It might turn out that you must not indicate if an answer is incorrect. Compliance laws are weird.)
$endgroup$
$begingroup$
Thank you for taking the time to write such a comprehensive response. It is much appreciated. You're absolutely right when you say I'm mixing concerns. I am struggling to get my mindset away from data-driven towards domain-drive design. Your suggested approach to DDD has given me much needed food for thought. One thing I mentioned in my original post that I don't think your answer covers: "At least one non-mandatory question must be answered in order to validate the caller." I believe the code you have suggested allows for all non-mandatory questions to be left unanswered?
$endgroup$
– datahandler
Apr 3 at 8:27
$begingroup$
@datahandler I just edited the answer to address that (I did not change the existing answer, but instead showed how we would validate that the model we built conforms to the DDD specification). Hopefully all of this helps you figure out where you can modify the process to do DDD a little more effectively. :)
$endgroup$
– Der Kommissar
Apr 3 at 13:24
$begingroup$
@DerKommissar Excellent! Thanks again!
$endgroup$
– datahandler
Apr 3 at 14:38
add a comment |
$begingroup$
To start with, you're not doing DDD here.
DDD (Domain-Driven Design / Development) is based around the idea that we start with the domain. We don't touch code yet—we develop the domain models on-paper (or whiteboard, whatever is preferred). Once that is done, we build the code as closely to the domain as possible. The point of DDD is that the code should mirror the domain design.
Before we get going, I highly, highly, highly recommend this book, by Scott Wlaschin, a prominent F# developer who brings DDD into a very easy-to-understand view (the examples are F#, but they apply to C# as well): Domain Modeling made Functional
DDD is about:
Define the domain, the inputs, and the outputs. That is, as a user of the system, what does the domain need to do. Here it sounds like we have part of the domain defined:
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
From there, we define our types. Generally, I do DDD with F#, but it's just as applicable to C#. We model the physical domain, so here we're not modeling the questions, we're modeling the validation. That is: the user must answer various questions and prove they are knowledgeable on the claim.
This is the root of our domain model: we need to validate some information. You have mixed multiple pieces here, so we're going to separate them a bit.
After building the types, we build the work. That is, the functions. We build the types as just data-structures, then we build the functions next to encapsulate the domain rules.
So, you've defined the domain (at least, as far as I see it) via the quoted-blurb, so what I want to do is move that into some types.
To start with, we'll define a DataProtectionResponse
(we're going to use the exact language from the domain model, the purpose of DDD is to translate the human-language into code).
class DataProtectionResponse {
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
}
Now, we need to come up with a model for DataProtectionQuestion
:
class DataProtectionQuestion {
public string Question { get; set; }
public bool Required { get; set; }
}
As you see, we are ONLY modeling two components of the question: the actual question, and whether or not it's required. The questions themselves are a different part of the domain, they're generated as a question, and using this is how we get into building a flexible model. We can now take these same questions somewhere else, and use them as a whole other tool, assuming it needs to interact with our current domain.
Next, we have ValidQuestionAnswer
. This is going to be the answer that are valid for this particular claim:
class ValidQuestionAnswer {
public Response Response { get; set; }
}
We made this a class as we absolutely want to consider a situation where an answer might have more data to it.
Finally, the Response
. You might say, "Der Kommissar, why does that need to be a class, it's always a string?" Again, we might need to add more to this model, including functionality, so we do that by using a class.
class Response {
public string Value { get; set; }
}
So now, our domain will consume an IEnumerable<DataProtectionResponse>
, but not directly.
class DataProtection {
public IEnumerable<DataProtectionResponse> Questions { get; set; }
}
Why another class? Well, let's start talking functionality.
First and foremost, the primary component of our design is that DataProtection
must validate. For this to work, we need a IsValid
function or property there:
public bool IsValid => Questions.All(x => x.IsSufficient);
Alright, so we have some concepts now. We have a IsValid
that indicates if our DataProtection
is valid or not, and we have decided that all of the questions must be sufficiently answered.
Next, we need to prove that a question is sufficiently answered.
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
Again, we are going to encode our actual logic: this DataProtectionResponse
is sufficient if any of the ValidQuestionAnswers
are acceptable with the question and response.
Next, how do we prove they're acceptable?
Well, the first rule is that if it's not required and there is no response, then it's valid:
if (!question.Required && response?.IsEmpty ?? false == false)
{
return true;
}
And of course, Response.IsEmpty
:
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
Otherwise, we want to prove that this response and the provided response are acceptable:
return response.Satisfies(Response);
And this is why we made it a class right-off-the-bat: we might have more logic that goes into Satisfies
that might do heuristic analysis. I.e. if you provide an address, the logic might say 123 Main St.
and 123 Main St
and 123 Main Street
are all the same.
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
Next, our Response.Satisfies
:
public bool Satisfies(Response response) => Value == response.Value;
And viola, we're done. We've encoded the entire domain, concisely, and using the actual terms the domain considers. Only 37 lines of code:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
We don't have any odd logic, we don't have any conflated values: each model concerns itself with it's own work, no one else's.
Additionally, when our domain changes now (or we have to change the satisfaction logic) we have built the flexibility in-place without needing major infrastructure rewrites. If we need to override a response, we encode that in DataProtectionResponse
and modify IsSufficient
.
Finally, you could even shorten Acceptable
to a single statement, since the logic is relatively straightforward:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || response.Satisfies(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
You noted in the comments that I missed the requirement of at least one optional question, and you're absolutely right. So let's talk about another strength of DDD: verifying our code matches the domain.
At least one non-mandatory question must be answered in order to validate the caller.
So, looking through our code we see that we can start with DataProtection.IsValid
:
public bool IsValid => Questions.All(x => x.IsSufficient);
Aha, we already know our problem is here. We make sure all questions are sufficient, but that does not take at least one optional into account. So how do we fix that?
Well, to start, we'll modify IsValid
to support some theoretical work:
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
Here, we've decided that if each DataProtectionResponse
tells us if it's required or not, and if it's answered or not, we can prove that we have at least one non-required question answered.
Next, we need to implement those two items. Both are trivial, and actually help with our other code:
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
Now we have another method to implement: ValidQuestionAnswer.AnsweredBy(Response)
:
public bool AnsweredBy(Response response) => response.Satisfies(Response);
You'll notice we've repeated one bit of code: response.Satisfies(Response)
is in both AnsweredBy
, and Acceptable
, let's change that:
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
Once again, our contract is now satisfied, and we ONLY go at-most one-level-deep in each model from a parent model. (That is, we could have done !x.Question.Required
instead of !x.IsRequired
, but it's not the responsibility of DataProtection
to know what makes a response required or not.)
So, 36 lines of code to build our new requirements:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool AnsweredBy(Response response) => response.Satisfies(Response);
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
}
Finally, you asked a few questions about your implementation: as you see, I ignored them, purposefully. With DDD, those questions are not things to ask about the implementation but things to ask about the domain. With DDD we do iterations of "design", "implement", "design", "implement"—all the questions you have should go in the design stage, which is where you (and the other domain experts) gather and hash-out the principles of the project. This means, now that you have an implementation, we go back to design and clarify those questions. As the developer, when you see these things you should be creating a working list of potential problem-points, you might find out the domain-experts have considered them, or they may not, so you take your concerns back to them and refine the design. (Again, DDD is an iterative concept.)
But, suppose I were to answer them:
Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
A: That's actually a design question, do you need to override whether an answer is valid or not? (I briefly touched on that in the first part of the answer.)
Should the setters be private and should clients supply data through a constructor?
A: This seems like an implementation question at first glance, but if we reword it things are different: can a client change an answer? If the answer is yes, the proposed design is fine. If not, model for immutability.
Should an Answer be an entity in its own right with a property for 'IsValid'?
A: In my humble opinion, yes. Answers aren't a string, they're a concept. Additionally, with a base-class, you can override that for answers that are bool
, DateTime
, etc. But, again: take it back to the DDD drawing board. The domain model will tell you what needs done.
How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
A: That's a design question, but my suggestion is to ditch the Valid
checkbox and provide a red/green "Needs Answered", "Incorrect Answer", or "Correct Answer" state. Again, do what the domain calls for. You're mixing some concerns here, and with DDD we create a clear separation. When you have a question like this, we go back into the design stage and hash it out. (It might turn out that you must not indicate if an answer is incorrect. Compliance laws are weird.)
$endgroup$
$begingroup$
Thank you for taking the time to write such a comprehensive response. It is much appreciated. You're absolutely right when you say I'm mixing concerns. I am struggling to get my mindset away from data-driven towards domain-drive design. Your suggested approach to DDD has given me much needed food for thought. One thing I mentioned in my original post that I don't think your answer covers: "At least one non-mandatory question must be answered in order to validate the caller." I believe the code you have suggested allows for all non-mandatory questions to be left unanswered?
$endgroup$
– datahandler
Apr 3 at 8:27
$begingroup$
@datahandler I just edited the answer to address that (I did not change the existing answer, but instead showed how we would validate that the model we built conforms to the DDD specification). Hopefully all of this helps you figure out where you can modify the process to do DDD a little more effectively. :)
$endgroup$
– Der Kommissar
Apr 3 at 13:24
$begingroup$
@DerKommissar Excellent! Thanks again!
$endgroup$
– datahandler
Apr 3 at 14:38
add a comment |
$begingroup$
To start with, you're not doing DDD here.
DDD (Domain-Driven Design / Development) is based around the idea that we start with the domain. We don't touch code yet—we develop the domain models on-paper (or whiteboard, whatever is preferred). Once that is done, we build the code as closely to the domain as possible. The point of DDD is that the code should mirror the domain design.
Before we get going, I highly, highly, highly recommend this book, by Scott Wlaschin, a prominent F# developer who brings DDD into a very easy-to-understand view (the examples are F#, but they apply to C# as well): Domain Modeling made Functional
DDD is about:
Define the domain, the inputs, and the outputs. That is, as a user of the system, what does the domain need to do. Here it sounds like we have part of the domain defined:
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
From there, we define our types. Generally, I do DDD with F#, but it's just as applicable to C#. We model the physical domain, so here we're not modeling the questions, we're modeling the validation. That is: the user must answer various questions and prove they are knowledgeable on the claim.
This is the root of our domain model: we need to validate some information. You have mixed multiple pieces here, so we're going to separate them a bit.
After building the types, we build the work. That is, the functions. We build the types as just data-structures, then we build the functions next to encapsulate the domain rules.
So, you've defined the domain (at least, as far as I see it) via the quoted-blurb, so what I want to do is move that into some types.
To start with, we'll define a DataProtectionResponse
(we're going to use the exact language from the domain model, the purpose of DDD is to translate the human-language into code).
class DataProtectionResponse {
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
}
Now, we need to come up with a model for DataProtectionQuestion
:
class DataProtectionQuestion {
public string Question { get; set; }
public bool Required { get; set; }
}
As you see, we are ONLY modeling two components of the question: the actual question, and whether or not it's required. The questions themselves are a different part of the domain, they're generated as a question, and using this is how we get into building a flexible model. We can now take these same questions somewhere else, and use them as a whole other tool, assuming it needs to interact with our current domain.
Next, we have ValidQuestionAnswer
. This is going to be the answer that are valid for this particular claim:
class ValidQuestionAnswer {
public Response Response { get; set; }
}
We made this a class as we absolutely want to consider a situation where an answer might have more data to it.
Finally, the Response
. You might say, "Der Kommissar, why does that need to be a class, it's always a string?" Again, we might need to add more to this model, including functionality, so we do that by using a class.
class Response {
public string Value { get; set; }
}
So now, our domain will consume an IEnumerable<DataProtectionResponse>
, but not directly.
class DataProtection {
public IEnumerable<DataProtectionResponse> Questions { get; set; }
}
Why another class? Well, let's start talking functionality.
First and foremost, the primary component of our design is that DataProtection
must validate. For this to work, we need a IsValid
function or property there:
public bool IsValid => Questions.All(x => x.IsSufficient);
Alright, so we have some concepts now. We have a IsValid
that indicates if our DataProtection
is valid or not, and we have decided that all of the questions must be sufficiently answered.
Next, we need to prove that a question is sufficiently answered.
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
Again, we are going to encode our actual logic: this DataProtectionResponse
is sufficient if any of the ValidQuestionAnswers
are acceptable with the question and response.
Next, how do we prove they're acceptable?
Well, the first rule is that if it's not required and there is no response, then it's valid:
if (!question.Required && response?.IsEmpty ?? false == false)
{
return true;
}
And of course, Response.IsEmpty
:
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
Otherwise, we want to prove that this response and the provided response are acceptable:
return response.Satisfies(Response);
And this is why we made it a class right-off-the-bat: we might have more logic that goes into Satisfies
that might do heuristic analysis. I.e. if you provide an address, the logic might say 123 Main St.
and 123 Main St
and 123 Main Street
are all the same.
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
Next, our Response.Satisfies
:
public bool Satisfies(Response response) => Value == response.Value;
And viola, we're done. We've encoded the entire domain, concisely, and using the actual terms the domain considers. Only 37 lines of code:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
We don't have any odd logic, we don't have any conflated values: each model concerns itself with it's own work, no one else's.
Additionally, when our domain changes now (or we have to change the satisfaction logic) we have built the flexibility in-place without needing major infrastructure rewrites. If we need to override a response, we encode that in DataProtectionResponse
and modify IsSufficient
.
Finally, you could even shorten Acceptable
to a single statement, since the logic is relatively straightforward:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || response.Satisfies(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
You noted in the comments that I missed the requirement of at least one optional question, and you're absolutely right. So let's talk about another strength of DDD: verifying our code matches the domain.
At least one non-mandatory question must be answered in order to validate the caller.
So, looking through our code we see that we can start with DataProtection.IsValid
:
public bool IsValid => Questions.All(x => x.IsSufficient);
Aha, we already know our problem is here. We make sure all questions are sufficient, but that does not take at least one optional into account. So how do we fix that?
Well, to start, we'll modify IsValid
to support some theoretical work:
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
Here, we've decided that if each DataProtectionResponse
tells us if it's required or not, and if it's answered or not, we can prove that we have at least one non-required question answered.
Next, we need to implement those two items. Both are trivial, and actually help with our other code:
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
Now we have another method to implement: ValidQuestionAnswer.AnsweredBy(Response)
:
public bool AnsweredBy(Response response) => response.Satisfies(Response);
You'll notice we've repeated one bit of code: response.Satisfies(Response)
is in both AnsweredBy
, and Acceptable
, let's change that:
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
Once again, our contract is now satisfied, and we ONLY go at-most one-level-deep in each model from a parent model. (That is, we could have done !x.Question.Required
instead of !x.IsRequired
, but it's not the responsibility of DataProtection
to know what makes a response required or not.)
So, 36 lines of code to build our new requirements:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool AnsweredBy(Response response) => response.Satisfies(Response);
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
}
Finally, you asked a few questions about your implementation: as you see, I ignored them, purposefully. With DDD, those questions are not things to ask about the implementation but things to ask about the domain. With DDD we do iterations of "design", "implement", "design", "implement"—all the questions you have should go in the design stage, which is where you (and the other domain experts) gather and hash-out the principles of the project. This means, now that you have an implementation, we go back to design and clarify those questions. As the developer, when you see these things you should be creating a working list of potential problem-points, you might find out the domain-experts have considered them, or they may not, so you take your concerns back to them and refine the design. (Again, DDD is an iterative concept.)
But, suppose I were to answer them:
Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
A: That's actually a design question, do you need to override whether an answer is valid or not? (I briefly touched on that in the first part of the answer.)
Should the setters be private and should clients supply data through a constructor?
A: This seems like an implementation question at first glance, but if we reword it things are different: can a client change an answer? If the answer is yes, the proposed design is fine. If not, model for immutability.
Should an Answer be an entity in its own right with a property for 'IsValid'?
A: In my humble opinion, yes. Answers aren't a string, they're a concept. Additionally, with a base-class, you can override that for answers that are bool
, DateTime
, etc. But, again: take it back to the DDD drawing board. The domain model will tell you what needs done.
How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
A: That's a design question, but my suggestion is to ditch the Valid
checkbox and provide a red/green "Needs Answered", "Incorrect Answer", or "Correct Answer" state. Again, do what the domain calls for. You're mixing some concerns here, and with DDD we create a clear separation. When you have a question like this, we go back into the design stage and hash it out. (It might turn out that you must not indicate if an answer is incorrect. Compliance laws are weird.)
$endgroup$
To start with, you're not doing DDD here.
DDD (Domain-Driven Design / Development) is based around the idea that we start with the domain. We don't touch code yet—we develop the domain models on-paper (or whiteboard, whatever is preferred). Once that is done, we build the code as closely to the domain as possible. The point of DDD is that the code should mirror the domain design.
Before we get going, I highly, highly, highly recommend this book, by Scott Wlaschin, a prominent F# developer who brings DDD into a very easy-to-understand view (the examples are F#, but they apply to C# as well): Domain Modeling made Functional
DDD is about:
Define the domain, the inputs, and the outputs. That is, as a user of the system, what does the domain need to do. Here it sounds like we have part of the domain defined:
As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.
The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.
Notes about Data Protection Questions:
- Some questions are mandatory and some are not.
- All mandatory questions must be answered in order to validate the caller.
- At least one non-mandatory question must be answered in order to validate the
caller. - Additional non-mandatory questions can remain unanswered.
- Each question may have multiple correct answers.
From there, we define our types. Generally, I do DDD with F#, but it's just as applicable to C#. We model the physical domain, so here we're not modeling the questions, we're modeling the validation. That is: the user must answer various questions and prove they are knowledgeable on the claim.
This is the root of our domain model: we need to validate some information. You have mixed multiple pieces here, so we're going to separate them a bit.
After building the types, we build the work. That is, the functions. We build the types as just data-structures, then we build the functions next to encapsulate the domain rules.
So, you've defined the domain (at least, as far as I see it) via the quoted-blurb, so what I want to do is move that into some types.
To start with, we'll define a DataProtectionResponse
(we're going to use the exact language from the domain model, the purpose of DDD is to translate the human-language into code).
class DataProtectionResponse {
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
}
Now, we need to come up with a model for DataProtectionQuestion
:
class DataProtectionQuestion {
public string Question { get; set; }
public bool Required { get; set; }
}
As you see, we are ONLY modeling two components of the question: the actual question, and whether or not it's required. The questions themselves are a different part of the domain, they're generated as a question, and using this is how we get into building a flexible model. We can now take these same questions somewhere else, and use them as a whole other tool, assuming it needs to interact with our current domain.
Next, we have ValidQuestionAnswer
. This is going to be the answer that are valid for this particular claim:
class ValidQuestionAnswer {
public Response Response { get; set; }
}
We made this a class as we absolutely want to consider a situation where an answer might have more data to it.
Finally, the Response
. You might say, "Der Kommissar, why does that need to be a class, it's always a string?" Again, we might need to add more to this model, including functionality, so we do that by using a class.
class Response {
public string Value { get; set; }
}
So now, our domain will consume an IEnumerable<DataProtectionResponse>
, but not directly.
class DataProtection {
public IEnumerable<DataProtectionResponse> Questions { get; set; }
}
Why another class? Well, let's start talking functionality.
First and foremost, the primary component of our design is that DataProtection
must validate. For this to work, we need a IsValid
function or property there:
public bool IsValid => Questions.All(x => x.IsSufficient);
Alright, so we have some concepts now. We have a IsValid
that indicates if our DataProtection
is valid or not, and we have decided that all of the questions must be sufficiently answered.
Next, we need to prove that a question is sufficiently answered.
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
Again, we are going to encode our actual logic: this DataProtectionResponse
is sufficient if any of the ValidQuestionAnswers
are acceptable with the question and response.
Next, how do we prove they're acceptable?
Well, the first rule is that if it's not required and there is no response, then it's valid:
if (!question.Required && response?.IsEmpty ?? false == false)
{
return true;
}
And of course, Response.IsEmpty
:
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
Otherwise, we want to prove that this response and the provided response are acceptable:
return response.Satisfies(Response);
And this is why we made it a class right-off-the-bat: we might have more logic that goes into Satisfies
that might do heuristic analysis. I.e. if you provide an address, the logic might say 123 Main St.
and 123 Main St
and 123 Main Street
are all the same.
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
Next, our Response.Satisfies
:
public bool Satisfies(Response response) => Value == response.Value;
And viola, we're done. We've encoded the entire domain, concisely, and using the actual terms the domain considers. Only 37 lines of code:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response)
{
if (!question.Required && response?.IsEmpty ?? true)
{
return true;
}
return response.Satisfies(Response);
}
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
We don't have any odd logic, we don't have any conflated values: each model concerns itself with it's own work, no one else's.
Additionally, when our domain changes now (or we have to change the satisfaction logic) we have built the flexibility in-place without needing major infrastructure rewrites. If we need to override a response, we encode that in DataProtectionResponse
and modify IsSufficient
.
Finally, you could even shorten Acceptable
to a single statement, since the logic is relatively straightforward:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || response.Satisfies(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient);
}
You noted in the comments that I missed the requirement of at least one optional question, and you're absolutely right. So let's talk about another strength of DDD: verifying our code matches the domain.
At least one non-mandatory question must be answered in order to validate the caller.
So, looking through our code we see that we can start with DataProtection.IsValid
:
public bool IsValid => Questions.All(x => x.IsSufficient);
Aha, we already know our problem is here. We make sure all questions are sufficient, but that does not take at least one optional into account. So how do we fix that?
Well, to start, we'll modify IsValid
to support some theoretical work:
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
Here, we've decided that if each DataProtectionResponse
tells us if it's required or not, and if it's answered or not, we can prove that we have at least one non-required question answered.
Next, we need to implement those two items. Both are trivial, and actually help with our other code:
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
Now we have another method to implement: ValidQuestionAnswer.AnsweredBy(Response)
:
public bool AnsweredBy(Response response) => response.Satisfies(Response);
You'll notice we've repeated one bit of code: response.Satisfies(Response)
is in both AnsweredBy
, and Acceptable
, let's change that:
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
Once again, our contract is now satisfied, and we ONLY go at-most one-level-deep in each model from a parent model. (That is, we could have done !x.Question.Required
instead of !x.IsRequired
, but it's not the responsibility of DataProtection
to know what makes a response required or not.)
So, 36 lines of code to build our new requirements:
class Response
{
public string Value { get; set; }
public bool IsEmpty => String.IsNullOrWhiteSpace(Value);
public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
public Response Response { get; set; }
public bool AnsweredBy(Response response) => response.Satisfies(Response);
public bool Acceptable(DataProtectionQuestion question, Response response) =>
(!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
}
class DataProtectionQuestion
{
public string Question { get; set; }
public bool Required { get; set; }
}
class DataProtectionResponse
{
public DataProtectionQuestion Question { get; set; }
public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
public Response Response { get; set; }
public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
public IEnumerable<DataProtectionResponse> Questions { get; set; }
public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
}
Finally, you asked a few questions about your implementation: as you see, I ignored them, purposefully. With DDD, those questions are not things to ask about the implementation but things to ask about the domain. With DDD we do iterations of "design", "implement", "design", "implement"—all the questions you have should go in the design stage, which is where you (and the other domain experts) gather and hash-out the principles of the project. This means, now that you have an implementation, we go back to design and clarify those questions. As the developer, when you see these things you should be creating a working list of potential problem-points, you might find out the domain-experts have considered them, or they may not, so you take your concerns back to them and refine the design. (Again, DDD is an iterative concept.)
But, suppose I were to answer them:
Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
A: That's actually a design question, do you need to override whether an answer is valid or not? (I briefly touched on that in the first part of the answer.)
Should the setters be private and should clients supply data through a constructor?
A: This seems like an implementation question at first glance, but if we reword it things are different: can a client change an answer? If the answer is yes, the proposed design is fine. If not, model for immutability.
Should an Answer be an entity in its own right with a property for 'IsValid'?
A: In my humble opinion, yes. Answers aren't a string, they're a concept. Additionally, with a base-class, you can override that for answers that are bool
, DateTime
, etc. But, again: take it back to the DDD drawing board. The domain model will tell you what needs done.
How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
A: That's a design question, but my suggestion is to ditch the Valid
checkbox and provide a red/green "Needs Answered", "Incorrect Answer", or "Correct Answer" state. Again, do what the domain calls for. You're mixing some concerns here, and with DDD we create a clear separation. When you have a question like this, we go back into the design stage and hash it out. (It might turn out that you must not indicate if an answer is incorrect. Compliance laws are weird.)
edited Apr 4 at 20:46
answered Apr 2 at 16:19
Der KommissarDer Kommissar
15.8k251136
15.8k251136
$begingroup$
Thank you for taking the time to write such a comprehensive response. It is much appreciated. You're absolutely right when you say I'm mixing concerns. I am struggling to get my mindset away from data-driven towards domain-drive design. Your suggested approach to DDD has given me much needed food for thought. One thing I mentioned in my original post that I don't think your answer covers: "At least one non-mandatory question must be answered in order to validate the caller." I believe the code you have suggested allows for all non-mandatory questions to be left unanswered?
$endgroup$
– datahandler
Apr 3 at 8:27
$begingroup$
@datahandler I just edited the answer to address that (I did not change the existing answer, but instead showed how we would validate that the model we built conforms to the DDD specification). Hopefully all of this helps you figure out where you can modify the process to do DDD a little more effectively. :)
$endgroup$
– Der Kommissar
Apr 3 at 13:24
$begingroup$
@DerKommissar Excellent! Thanks again!
$endgroup$
– datahandler
Apr 3 at 14:38
add a comment |
$begingroup$
Thank you for taking the time to write such a comprehensive response. It is much appreciated. You're absolutely right when you say I'm mixing concerns. I am struggling to get my mindset away from data-driven towards domain-drive design. Your suggested approach to DDD has given me much needed food for thought. One thing I mentioned in my original post that I don't think your answer covers: "At least one non-mandatory question must be answered in order to validate the caller." I believe the code you have suggested allows for all non-mandatory questions to be left unanswered?
$endgroup$
– datahandler
Apr 3 at 8:27
$begingroup$
@datahandler I just edited the answer to address that (I did not change the existing answer, but instead showed how we would validate that the model we built conforms to the DDD specification). Hopefully all of this helps you figure out where you can modify the process to do DDD a little more effectively. :)
$endgroup$
– Der Kommissar
Apr 3 at 13:24
$begingroup$
@DerKommissar Excellent! Thanks again!
$endgroup$
– datahandler
Apr 3 at 14:38
$begingroup$
Thank you for taking the time to write such a comprehensive response. It is much appreciated. You're absolutely right when you say I'm mixing concerns. I am struggling to get my mindset away from data-driven towards domain-drive design. Your suggested approach to DDD has given me much needed food for thought. One thing I mentioned in my original post that I don't think your answer covers: "At least one non-mandatory question must be answered in order to validate the caller." I believe the code you have suggested allows for all non-mandatory questions to be left unanswered?
$endgroup$
– datahandler
Apr 3 at 8:27
$begingroup$
Thank you for taking the time to write such a comprehensive response. It is much appreciated. You're absolutely right when you say I'm mixing concerns. I am struggling to get my mindset away from data-driven towards domain-drive design. Your suggested approach to DDD has given me much needed food for thought. One thing I mentioned in my original post that I don't think your answer covers: "At least one non-mandatory question must be answered in order to validate the caller." I believe the code you have suggested allows for all non-mandatory questions to be left unanswered?
$endgroup$
– datahandler
Apr 3 at 8:27
$begingroup$
@datahandler I just edited the answer to address that (I did not change the existing answer, but instead showed how we would validate that the model we built conforms to the DDD specification). Hopefully all of this helps you figure out where you can modify the process to do DDD a little more effectively. :)
$endgroup$
– Der Kommissar
Apr 3 at 13:24
$begingroup$
@datahandler I just edited the answer to address that (I did not change the existing answer, but instead showed how we would validate that the model we built conforms to the DDD specification). Hopefully all of this helps you figure out where you can modify the process to do DDD a little more effectively. :)
$endgroup$
– Der Kommissar
Apr 3 at 13:24
$begingroup$
@DerKommissar Excellent! Thanks again!
$endgroup$
– datahandler
Apr 3 at 14:38
$begingroup$
@DerKommissar Excellent! Thanks again!
$endgroup$
– datahandler
Apr 3 at 14:38
add a comment |
datahandler is a new contributor. Be nice, and check out our Code of Conduct.
datahandler is a new contributor. Be nice, and check out our Code of Conduct.
datahandler is a new contributor. Be nice, and check out our Code of Conduct.
datahandler is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f216727%2fknowledge-based-authentication-using-domain-driven-design-in-c%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown