0

I am back trying to get into the .NET again, I have been of development for the past 10 years, though I started with .NET 1.0 and now it's a bit different. I was used to the N-tier model, with ADO.NET or what i liked even better was the recordSet in ASP. I am trying to get my head around this Repository pattern, Genneric pattern and the Entity Framework.

Please have patience with my knowledge as I have only been back for the past month. I am building a product / order service, just for fun, trying to de-couple it both to use as microservice and MVC or WinForms if I would like. I just want to learn the proper way of doing this.

The problem is that I do not get nested objects and I do understand this, maybe I need to properly join them together, but then again, why would I then use the EF if I don't get this for free? I understand you get all the modelling from the framework etc, but you get my point.

I have two models

Orders Model with the foreign key pointed out - which in my world would map automatically

 public class Orders
    {
        [Key]
        public int OrderId { get; set; }
        public int ProductId { get; set; }
        [ForeignKey("ProductId")]
        public IEnumerable<Product> Product { get; set; }
        public DateTime Datetime { get; set; }

    }



Product Model

 public class Product
{
  [Key]
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string Description { get; set; }
    public bool IsActive { get; set; }
    public decimal Price { get; set; }

}

I have a Generic repository interface

 public interface IGenericRepository <T> : IDisposable where T : class
    {
        IEnumerable<T> GetAllRecords();
        IEnumerable<T> FindRecord(Expression<Func<T,bool>> predicate);
        T GetRecordById(int objId);
        void AddRecord(T obj);
        void DeleteRecord(T obj);

        void UpdateRecord(T obj);
    }

I Implement this interface through

   public class GenericRepository<T> : IGenericRepository<T>, IDisposable where T : class
    {

        private readonly DBContext _context;
        private readonly DbSet<T> _DbSet;

        public GenericRepository(DBContext context)
        {
            this._context = context;
            this._DbSet = this._context.Set<T>();
        }

        public IEnumerable<T> GetAllRecords()
        {
            return _DbSet.ToList();
        }

        public IEnumerable<T> FindRecord(Expression<Func<T, bool>> predicate)
        {
            throw new NotImplementedException();
        }

        public T GetRecordById(int objId)
        {
          return  _DbSet.Find(objId);
        }

        public void AddRecord(T obj)
        {
            _DbSet.Add(obj);
        }

        public void DeleteRecord(T obj)
        {
            _DbSet.Remove(obj);
        }

        public void UpdateRecord(T obj)
        {
            _DbSet.Attach(obj);
            _context.Entry(obj).State = EntityState.Modified;
        }

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

        void IDisposable.Dispose()
        {
            throw new NotImplementedException();
        }




    }

And last i have the UnitOfWork with an interface that I implement through

    public class UnitOfWork : IUnitOfWork
    {
        private DBContext _context;
        //  public ProductRepository productRepository { get; private set; }
        //public OrderRepository OrderReposity  { get; private set; }
        public IGenericRepository<Product> productRepository { get; set; }
        public IGenericRepository<Orders> OrderRepository { get; set; }

        public UnitOfWork(DBContext context)
        {
            this._context = context;
            this.productRepository = new GenericRepository<Product>(this._context);
            this.OrderRepository = new GenericRepository<Orders>(this._context);
        }
        public void SaveChanges()
        {
            _context.SaveChanges();
        }
    }

In the WebAPI controller I call the unitOfWork through

[Route("api/[controller]")] [ApiController]

public class OrdersController : ControllerBase
{
    public readonly UnitOfWork UoW;
    public OrdersController(IUnitOfWork _uow)
    {
        this.UoW = _uow as UnitOfWork;
    }

And pointing to the API method GET / Orders

    // GET: api/<OrdersController>
    [HttpGet]
    public IEnumerable<Orders> Get()
    {
        return UoW.OrderRepository.GetAllRecords();
    }

It works like a charm, I think its a good way to implement this. I can easily create another application to use the back-end, I can mock and test this pretty well. Everything is in 4 different projects in the solution.

But the problem is the returns a null on the products objects. Feel free to give me all feedback you can, how I should build this if my solution is not preferred or if I am doing it too "de-coupled".

Thanks for a great forum and a great inspiration for learning

Best regards Martin

  • Hello, Martin! Are you using .NET Core or Full Framework (4.5, 4.6, etc) ? – Andre.Santarosa Dec 25 '20 at 18:35
  • Please post a [mcve], with an emphasis on *minimal*. Also, note that the repository pattern used on top of EF's context + DbSet is often frowned upon. And in this case it doesn't really help you to learn the essentials of EF modelling. – Gert Arnold Dec 25 '20 at 22:23
  • Have a look at my recent response to this question (https://stackoverflow.com/questions/65445251/how-to-set-multiple-services-from-entity-framework-core-on-repository-pattern) for some ideas on structuring a Repository pattern efficiently, and why Generic Repositories aren't really that well suited for the task. The problem like you are encountering is expressing through the repository what related data you actually want to include. Then you run into limitations and complexity around sorting, filtering, pagination, etc on top of that. – Steve Py Dec 26 '20 at 08:54
  • Thanks a lot everyone, yea Steve, I see that very clearly now. Of course I also understand that the Entity Framework, somewhat is a repository / Unit of work pattern. But if you want to have a data layer and logic layer to wind your other applications with it would be nice. I could just make a lot of microservices but then again, a microservice over HTTP/Rest is not faster than a compiled project :) – Martin Bengtson Dec 26 '20 at 11:50

1 Answers1

0

I'm assuming you are using .NET Core here, so if you are not, please leave a comment and I'll re-write the solution to fit your needs. =)

First of all I believe that you have a miss modeled your data classes since um have a 1-n between products x orders, the way it was build means that a product can only be related by one order which is not true, so we need a table to represent the n-n relation between these two entities and a little bit changes in the original 2 ones to support the mapping between them.

Product.cs

public class Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string Description { get; set; }
    public bool IsActive { get; set; }
    public decimal Price { get; set; }

    public List<OrderProduct> OrderProducts { get; set; }
}

Order.cs

public class Order
{
    public int OrderId { get; set; }
    public DateTime Datetime { get; set; }

    public List<OrderProduct> OrderProducts { get; set; }
}

OrderProduct.cs (the one that will ties things up)

public class OrderProduct
{
    public int OrderProductId { get; set; }
    public int ProductId { get; set; }
    public int OrderId { get; set; }

    public Product Product { get; set; }
    public Order Order { get; set; }
}

With the entities now set up we can run for the part were entity will load the related entities automatically for you =)

To Entity Framework knows what is related to what we need more than convention naming (name thing equally), entity expects for explicit configuration.

You were configuring this relations with DataAnnotations which is not wrong, but personally I prefer to work with Fluent API since DataAnnotations create a hard bound between your entity models and Entity Framework making it harder to change your architecture for a DDD model in the future for example.

So let's configure our entities. To achieve this we will create a configuration file for each entity.

ProductConfig.cs

public class ProductConfig : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        // Tells entity that ProductId is your primary key
        builder.HasKey(b => b.ProductId);
    }
}

OrderConfig.cs

public class OrderConfig : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        // Tells entity that OrderId is your primary key
        builder.HasKey(b => b.OrderId);
    }
}

OrderProductConfig.cs

public class OrderProductConfig : IEntityTypeConfiguration<OrderProduct>
{
    public void Configure(EntityTypeBuilder<OrderProduct> builder)
    {
        // Tells entity that OrderProductId is your primary key
        builder.HasKey(b => b.OrderProductId);

        
        // Configure the navigation property Order telling that an Order may be related to a bunch of OrderProducts
        // setting up the OrderId as your foreign key constraint and telling Entity that is a required field (not null)
        builder.HasOne(b => b.Order)
            .WithMany(b => b.OrderProducts)
            .HasForeignKey(b => b.OrderId)
            .IsRequired();

        // Configure the navigation property Product telling that a Product may be related to a bunch of OrderProducts
        // setting up the ProductId as your foreign key constraint and telling Entity that is a required field (not null)
        builder.HasOne(b => b.Product)
            .WithMany(b => b.OrderProducts)
            .HasForeignKey(b => b.ProductId)
            .IsRequired();
    }
}

Now we need a place to apply this configurations, so we need to create a context for your application, is a good practice to create one also =)

SalesSoftwareContext.cs

// This class inherits DbContext class, and will be your new "DbContext"
public class SalesSoftwareContext : DbContext
{
    // During runtime this method will be called so Entity will load the configurations and knows how to handle things
    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.ApplyConfiguration(new OrderConfig());
        builder.ApplyConfiguration(new ProductConfig());
        builder.ApplyConfiguration(new OrderProductConfig());
    }
}

Inside GenericRepository.cs and UnitOfWork.cs replace the references for DBContext by SalesSoftwareContext and run your code!

Now when you query the Order you will have a List of OrderProduct that you will be able to interact and get the product info, for an example:

public class OrderViewModel 
{
    public int OrderId {get;set;}
    List<OrderItemViewModel> OrderItems {get;set;}
}

public class OrderItemViewModel
{
    public int ProductId {get;set;}
    public string Name {get;set;}
    public decimal Price {get;set;}
}

[HttpGet]
public IEnumerable<OrderViewModel> Get()
{
    List<OrderViewModel> returnValue = new List<OrderViewModel>();
    List<Order> orders = UoW.OrderRepository.GetAllRecords();

    foreach(var order in orders)
    {
        OrderViewModel orderToReturn = new OrderViewModel();
        orderToReturn.OrderId = order.OrderId;
        orderToReturn.OrderItems = order.OrderProducts
                                        .Select(x => new OrderItemViewModel
                                                         {
                                                            ProductId = x.Product.ProductId,
                                                            Name = x.Product.ProductName,
                                                            Price = x.Product.Price
                                                         }).ToList());
        returnValue.Add(orderToReturn);
    }
    
    return returnValue;
}

I hope this helps =)

Since I don't have your 'full-code' I ended up typing in the dark, sorry for any typo or error that came along with the posted solution

It's nice to have another developer coming back to .NET world, the .NET evolved a lot in the last years, specially when .NET Core came around, but I believe that all the changes were for better.

If you have specific questions or need any guidance, leave a comment with your email so we can get in touch.

Happy Holidays!

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Andre.Santarosa
  • 1,150
  • 1
  • 9
  • 22
  • Hey Andre, thanks a lot for your answer, yea I see that i really messed up and forgot about the relational table, I should have done that. Though, if we say i just want a single order for a single product. Do i have to design all that mess? I thought i could use the design i have. I have my repository for this here https://github.com/martinbengtsson/WebShopDemo You almost got all code. :) – Martin Bengtson Dec 25 '20 at 20:22
  • I am using dotnetcore / entityframeworkcore ;) its martinbengtzon@gmail.com Thanks for being nice :) /Martin – Martin Bengtson Dec 25 '20 at 20:32
  • if i go into debug -> put a break point on [HttpGet] public IEnumerable Get() { return UoW.OrderRepository.GetAllRecords(); //Break Point here } I go down in the list, i expand context -> resultView -> and watch the result, everything is there and the objects are retreived, when i click continue, its perfectly populated as a nested object with all the data in it. Why is this? – Martin Bengtson Dec 25 '20 at 20:44
  • The only way for me to get this to work is in the controller to do: public IEnumerable Get() { UoW.productRepository.GetAllRecords(); return UoW.OrderRepository.GetAllRecords(); } But this seems really stupid, I have to populate a complete dataSet of all the products, and then bind them to my Order. How do i do this manually on the fly? – Martin Bengtson Dec 25 '20 at 23:59
  • Hi, Martin, I’ve sent you an e-mail, please take a look, I believe that will be easier to explain some of your questions =) – Andre.Santarosa Dec 26 '20 at 03:26