Evans wrote a good book with good ideas. But these ideas lack a methodological basis. Experienced developers and architects, on an intuitive level, understand that you need to be as close as possible to the customer’s domain, and you need to talk to the customer. But it is not clear how to evaluate the project for compliance with the Ubiquitous Language and the customer's real language? How to understand that the domain is divided into Bounded Context correctly? How generally to define DDD in the project is used or not?
Not specifically of course :) The fact is that business applications are created for a wide range of tasks and to satisfy the interests of various groups of users. At best, only top management understands business processes from start to finish. Not rarely misunderstand, by the way. Inside the subdivision, users see only a certain part. Therefore, the result of interviewing all stakeholders usually becomes a tangle of contradictions. The following follows from this rule.
You need to start not from the database structure or set of classes, but from business processes. We use BPMN and UML Activity in conjunction with test cases . Charts are well read even by those who are not familiar with the standards. Test cases in tabular form help to better identify the boundary cases and eliminate inconsistencies.
A single subject model for the entire application can be created only if the policy of using a single consistent language throughout the organization is adopted and implemented at the top management level. Those. when the sales department says “account production”, they both understand the word in the same way. This is the same account, not "account in CRM" and "legal entity. Client."
DAL . And DAL is a solid "transaction", "table", "lock", etc. Clean Architecture allows you to invert dependencies and separate flies from cutlets. Of course completely abstract from the details of the implementation will not work. RDBMS, ORM, network interaction will still impose its limitations. But in the case of using Clean Architecture this can be controlled. In n-layer sticking to a “single language” is much more difficult because of the storage structure on the bottom layer.Clean Architecture works well with Bounded Context. Different contexts may represent different subsystems. Simple contexts are best implemented using simple CRUD . For asymmetric load contexts, CQRS is well suited. For subsystems requiring Audit Log, it makes sense to use Event Sourcing. For the subsystems loaded for reading and writing with limitations on bandwidth and latency, it makes sense to consider an event driven approach. At first glance, this may seem inconvenient. For example, I worked with the CRUD subsystem and I received a task from the CQRS subsystem. It will take some time to look at all these Command and Query as a new gate. The alternative - to design a system in the same style - is short-sighted. Architecture is a set of tools, and each tool is suitable for solving a specific problem. /App /ProjectName.Web.Public /ProjectName.Web.Admin /ProjectName.Web.SomeOtherStuff /Domain /ProjectName.Domain.Core /ProjectName.Domain.BoundedContext1 /ProjectName.Domain.BoundedContext1.Services /ProjectName.Domain.BoundedContext2 /ProjectName.Domain.BoundedContext2.Command /ProjectName.Domain.BoundedContext2.Query /ProjectName.Domain.BoundedContext3 /Data /ProjectName.Data /Libs /Problem1Resolver /Problem2Resolver Libs folder do not depend on the domain. They only solve their local problem, for example, generating reports, parsing csv, caching mechanisms, etc. The domain structure corresponds to the BoundedContext' . Projects from the Domain folder are independent of Data . Data contains DbContext , migrations, configurations related to DAL. Data depends on Domain entities for building migrations. Projects from the App folder use an IOC container to inject dependencies. Thus it turns out to achieve maximum isolation of the domain code from the infrastructure. [DisplayName(" ()")] public class Company : LongIdBase , IHasState<CompanyState> { public static class Specs { public static Spec<Supplier> ByInnAndKpp(string inn, string kpp) => new Spec<Supplier>(x => x.Inn == inn && x.Kpp == kpp); public static Spec<Supplier> ByInn(string inn) => new Spec<Supplier>(x => x.Inn == inn); } // EF protected Company () { } public Company (string inn, string kpp) { DangerouslyChangeInnAndKpp(inn, kpp); } public void DangerouslyChangeInnAndKpp(string inn, string kpp) { Inn = inn.NullIfEmpty() ?? throw new ArgumentNullException(nameof(inn)); Kpp = kpp.NullIfEmpty() ?? throw new ArgumentNullException(nameof(kpp)); this.ValidateProperties(); } [Display(Name = "")] [Required] [DisplayFormat(ConvertEmptyStringToNull = true)] [Inn] public string Inn { get; protected set; } [Display(Name = "")] [DisplayFormat(ConvertEmptyStringToNull = true)] [Kpp] public string Kpp { get; protected set; } [Display(Name = " ")] public CompanyState State { get; protected set; } [DisplayFormat(ConvertEmptyStringToNull = true)] public string Comment { get; protected set; } [Display(Name = " ")] public DateTime? StateChangeDate { get; protected set; } public void Accept() { StateChangeDate = DateTime.UtcNow; State = AccreditationState.Accredited; } public void Decline(string comment) { StateChangeDate = DateTime.UtcNow; State = AccreditationState.Declined; Comment = comment.NullIfEmpty() ?? throw new ArgumentNullException(nameof(comment)); } GUID or Id generated by database. Sometimes it is advisable to use as an Id identifier other than the domain, even if the latter exists. For example, if the entity must be versioned and the system must store all previous versions, or if the identifier from the subject model is a complex composite and is not friendly with persistance.ValidateProperties extension method validates against DataAnnotation attributes, but NullIfEmpty does not allow you to pass empty strings. public static class Extensions { public static void ValidateProperties(this object obj) { var context = new ValidationContext(obj); Validator.ValidateObject(obj, context, true); } public static string NullIfEmpty(this string str) => string.IsNullOrEmpty(str) ? null : str; } public class InnAttribute : RegularExpressionAttribute { public InnAttribute() : base(@"^(\d{10}|\d{12})$") { ErrorMessage = " 10/12 ."; } public InnAttribute(CivilLawSubject civilLawSubject) : base(civilLawSubject == CivilLawSubject.Individual ? @"^\d{12}$" : @"^\d{10}$") { ErrorMessage = civilLawSubject == CivilLawSubject.Individual ? " 12 ." : " 10 ."; } } DangerouslyChangeInnAndKpp function. The name of the function clearly hints that changing the TIN and the checkpoint is not a regular situation. Two parameters are passed to the function, which means that if you change the TIN and PPC, then only together. TIN + PPC could even be made a composite key. But for compatibility, I left a long Id . Finally, when calling this function, validators will work and if the TIN and checkpoint are not correct, a ValidationException will be thrown.You can further strengthen the type system . However, the approach described by reference has a significant drawback: the lack of support from the standard ASP.NET infrastructure. Support can be added, but such an infrastructure code is worth something and needs to be accompanied.
An alternative is to use the “ state ” pattern and put the behavior into separate classes.
Queryable or to mess with expression trees . In the end, the LinqSpec implementation was the most convenient. public interface IHasId { object Id { get; } } public interface IHasId<out TKey> : IHasId where TKey: IEquatable<TKey> { new TKey Id { get; } } public static bool IsNew<TKey>(this IHasId<TKey> obj) where TKey : IEquatable<TKey> { return obj.Id == null || obj.Id.Equals(default(TKey)); } ByInnAndKpp and ByInn cannot be used inside other expressions. They can not make out the provider. In more detail about the use of extension-methods a la DSL told Dino Esposito on one of the DotNext . public static class CompanyDataExtensions { public static CompanyData ByInnAndKpp( this IQueryable<CompanyData> query, string inn, string kpp) => query .Where(x => x.Company, Supplier.Specs.ByInnAndKpp(inn, kpp)) .FirstOrDefault(); public static CompanyData ByInn( this IQueryable<CompanyData> query, string inn) => query .Where(x => x.Company, Supplier.Specs.ByInn(inn)); } EF Core began to support InvokeExpression . The application code is used as follows: var priceInfos = DbContext .CompanyData .ByInn("") .ToList(); SelectMany . var priceInfos = DbContext .Company // extension- .ByInnAndKpp("", "") .SelectMany(x => x.Company) .ToList(); I have not fully studied the equivalence of options withSelectandSelectManyfrom the point of view ofIQueryProvider. I would be grateful for any information on this topic in the comments.
public virtual ICollection<Document> Documents { get; protected set; } Select block to convert to an SQL query, because the code of the form company.Documents.Where(…).ToList() does not build a query to the database, but first lifts all the related entities into RAM, and therefore applies Where to sampling in memory. Thus, the presence of collections in the model can adversely affect the performance of the application. At the same time, refactoring will be difficult, because you will have to transfer the necessary IQueryable from outside. To control the quality of requests you need to glance in miniProfiler .Task Based UI .Result type. Exceptions remain for exceptional situations.Queryable Extensions in AutoMapper and Mapster, you can use mappings to translate into expressions for Select , which allows you not to drag the whole entity from the database as a whole.AspNet.Identity , for example, contains a UserManager . Basically, managers are needed when it is necessary to implement logic on an aggregate that is not directly related to the domain.DataMapper allows reducing the number of boilerplate code, and using Queryable Extensions - to build requests for receiving DTO without the need to write Select manually. Thus, you can reuse expressions for in-memory mapping and expression tree construction for IQueryProvider . AutoMapper quite AutoMapper in memory and not fast, so it eventually replaced it with Mapster .IQuery returns identical results on identical input data. Therefore, the bodies of such methods can be aggressively cached. Thus, after replacing implementations, the infrastructure code (controllers) will remain unchanged, and only the body of the IQuery method will have to be modified. The approach allows you to optimize the application pointwise in small pieces, and not all of it.The approach is limited to a very, very busy resource due to the overhead of the IOC container and memory traffic for per-request lifestyle. However, all IQuery can be made singleton'ami, if you do not inject dependencies from the database into the constructor, and instead use the using construct.Source: https://habr.com/ru/post/334126/
All Articles