lucabriguglia / OpenCQRS

.NET Standard framework to create simple and clean design. Advanced features for DDD, CQRS and Event Sourcing.
Apache License 2.0
3 stars 115 forks source link

Offer IRepository<T, TKey> for non-ES repository #77

Closed rulrok closed 4 years ago

rulrok commented 4 years ago

I'm using Kledex for CQRS + DDD without event sourcing.

My project has a legacy EF6 which I inject into my commands. I wanted to create an ACL to inject IRepository<T> and create my own implementation of it;

    public class CompanyRepository : IRepository<CompanyRoot>
    {
        private readonly DbContext context;

        public CompanyRepository(DbContext context)
        {
            this.context = context;
        }

        public Task SaveAsync(CompanyRoot aggregate)
        {
            throw new NotImplementedException();
        }

        public void Save(CompanyRoot aggregate)
        {
            throw new NotImplementedException();
        }

        public Task<CompanyRoot> GetByIdAsync(Guid id)
        {
            throw new NotImplementedException();
        }

        public CompanyRoot GetById(Guid id)
        {
            throw new NotImplementedException();
        }
    }

and register it:

            services.AddKledex(domainsTypes)
                    .AddOptions(options =>
                    {
                        options.PublishEvents   = true;
                        options.SaveCommandData = true;
                    })
                    .AddSqlServerProvider(configuration)
                    .AddServiceBusProvider(configuration)
                ;

            services.AddTransient<IRepository<CompanyRoot>, CompanyRepository>();

The problem is that this interface only works with Guid, but my database uses int for primary key. I was thinking of an interface which would let me specify the TKey

    public interface IRepository<T, TKey> where T : IAggregateRoot where TKey : struct
    {
        Task SaveAsync(T aggregate);

        void Save(T aggregate);

        Task<T> GetByIdAsync(TKey id);

        T GetById(TKey id);       
    }

In that way, I can create my own repositories for ACL and you would still keep using IRepository with Guid for Kledex ES.

I thought doing that because I can inject a simple IRepository interface only with the essential methods and make it easier to create an ACL. Also, I could make it look like the wiki example for non-ES scenario.

rulrok commented 4 years ago

For now I've created my own interface (but I feel it could be part of the framework):

    public interface IAclRepository<T> where T : AggregateRoot
    {
        AmezzeIdentityContext Context { get; }

        Task SaveAsync(T aggregate);

        void Save(T aggregate);

        Task<T> GetByIdAsync(int id);

        T GetById(int id);
    }

Them I can have my aggregate as:

    public class CompanyRoot : AggregateRoot
    {
        public CompanyRoot(int companyId)
        {
            CompanyId = companyId;
        }

        public int CompanyId { get; }

        public bool Active { get; set; }

        public void Activate()
        {
            Active = true;
            AddEvent(CompanyActivated.Create(CompanyId));
        }

        public void Deactivate()
        {
            Active = false;
            AddEvent(new CompanyDeactivated(CompanyId));
        }
    }

and I can easily work only with the IAclRepository from my handler

    public class ActivateCompanyHandler : IDomainCommandHandlerAsync<ActivateCompany>
    {
        private readonly IAclRepository<CompanyRoot> _repository;

        public ActivateCompanyHandler(IAclRepository<CompanyRoot> repository)
        {
            _repository = repository;
        }

        public async Task<IEnumerable<IDomainEvent>> HandleAsync(ActivateCompany command)
        {
            var aggregate = await _repository.GetByIdAsync(command.CompanyId);

            aggregate.Activate();

            await _repository.SaveAsync(aggregate);

            return aggregate.Events;
        }
    }

and for now I'm just mapping my aggregate to main entity model (manually). As the properties between my domain model and database entity increases, I'd use something like automapper or mapster. For now, it seems good enough.

    public class CompanyRepository : IAclRepository<CompanyRoot>
    {
        public CompanyRepository(DbContext context)
        {
            Context = context;
        }

        public DbContext Context { get; }

        public async Task SaveAsync(CompanyRoot aggregate)
        {
            var company = await Context.Companies.FindAsync(aggregate.CompanyId);
            if (company == null)
                return;

            company.Active = aggregate.Active;

            await Context.SaveChangesAsync();
        }

        public void Save(CompanyRoot aggregate)
        {
            SaveAsync(aggregate).ConfigureAwait(false);
        }

        public async Task<CompanyRoot> GetByIdAsync(int id)
        {
            var company = await Context.Companies.FindAsync(id);
            if (company == null)
                throw new Exception("CompanyNotFound");

            return new CompanyRoot(id)
            {
                Active = company.Active
            };
        }

        public CompanyRoot GetById(int id)
        {
            return GetByIdAsync(id).ConfigureAwait(false).GetAwaiter().GetResult();
        }
    }
lucabriguglia commented 4 years ago

The repositories for the write model in non event sourcing scenarios were always meant to be custom in my views but thanks for the suggestion :-)

lucabriguglia commented 4 years ago

Closing as it has not been active for a while.