开发者

Specific entity objects for Add and Update methods in a repository

I'm trying to come up with a way to design a repository where adding and updating only accepts the exact amount of data/properties it can add/update.

I have the following design:

public interface IProduct
{
    int Id { get; set; }
    string Name { get; set; }
    decimal Price { get; set; }
    DateTime Created { get; set; }
    DateTime Updated { get; set; }
}

public interface IProductRepository
{
    void Add(IProduct product);
    void Update(IProduct product);
    IProduct Get(int id);
    IEnumerable<IProduct> GetAll();
}

However, the Created and Updated properties are开发者_如何学Python not really something I want to be modified outside of the database. The Id is not relevant when adding either, so I tried the following:

public interface IProductAdd
{
    string Name { get; set; }
    decimal Price { get; set; }
}

public interface IProductUpdate
{
    int Id { get; set; }
    string Name { get; set; }
    decimal Price { get; set; }
}

And updated the repository accordingly:

public interface IProductRepository
{
    void Add(IProductAdd product);
    void Update(IProductUpdate product);
    IProduct Get(int id);
    IEnumerable<IProduct> GetAll();
}

Now only the relevant properties are present in each individual method.

I could then create a class that implements all product interfaces:

public class Product : IProduct, IProductAdd, IProductUpdate
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public DateTime Created { get; set; }
    public DateTime Updated { get; set; }
}

So my question is: Is this the right way to do this?

My thoughts:

I could have opted to change the Add and Update methods on the Repository to accept every bit of product data as parameters, such as Update(int id, string name, decimal price), but it will get out of hand quickly when the amount of information a product holds increases.

My current solution involves repetition. If a product should hold a Description property, I would have to specify it in different 3 interfaces. I could let the interfaces implement each other to solve this...

public interface IProductAdd
{
    string Name { get; set; }
    decimal Price { get; set; }
    string Description { get; set; }
}

public interface IProductUpdate : IProductAdd
{
    int Id { get; set; }
}

public interface IProduct : IProductUpdate
{
    DateTime Created { get; set; }
    DateTime Updated { get; set; }
}

...but then I would get in trouble if IProductAdd were to have something that IProductUpdate shouldn't have.

Related problem: Let's say I want put products in categories, and have access to the category directly on each product.

public interface ICategory
{
    int Id { get; set; }
    string Name { get; set; }
    string Description { get; set; }
}

public interface IProduct
{
    int Id { get; set; }
    string Name { get; set; }
    decimal Price { get; set; }
    DateTime Created { get; set; }
    DateTime Updated { get; set; }
    ICategory Category { get; set; }
}

When I change a product, I want to specify the id of the category (since I'm adding/updating the relationship, not the category itself):

public interface IProductAdd
{
    string Name { get; set; }
    decimal Price { get; set; }
    int CategoryId { get; set; }
}

public interface IProductUpdate
{
    int Id { get; set; }
    string Name { get; set; }
    decimal Price { get; set; }
    int CategoryId { get; set; }
}

This results in the following implementation:

public class Product : IProduct, IProductAdd, IProductUpdate
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public DateTime Created { get; set; }
    public DateTime Updated { get; set; }
    public ICategory Category { get; set; }
    public int CategoryId { get; set; }
}

Looking at that, do I use Category.Id or CategoryId? Not ideal, IMO.

So I guess I see problems no matter how I do this. Am I too picky? Am I looking at this wrong?

Should I just separate things completely because they ARE different things (eg. [the entity] / [add entity parameters] / [update entity parameters])?


I think you are over-complicating things while not separating your layers properly. In my opinion, the first two classes are how it should be done.

From what I understand, your whole issue is that you do not want the Created and Updated properties to be modified incorrectly. However, you are mixing up data and business concerns. Saying that a product's created date should be set upon the product being created is part of the business logic of creating a new product, and saying that a product's updateddate should be updated when x and y occur is also part of the logical process of updating a product's data. This is the same type of process as validating that the properties of the product are valid, the user is authorized, etc.., all of which are business process concerns, not data storage concerns.

Your repository should be solely part of the data layer, where it's only concern is how it retrieves the requested product from the database, how it updates a product in the database, or creates a product in the database. That's it.

You need a dedicated business layer that handles all the business logic for adding or updating a product's information. You will then call a method in this layer with the name and price of for the product you want to add, and in this method you will perform any validations you want to perform, determine if the user is authorized to be making these edits, and setting the CreatedDate or UpdatedDate if it's warranted. This method will then pass the Product entity to your repository to save it in the database.

Separating out the logic in this manner will make it MUCH easier when you want to change the logic on how things such as UpdatedDate is managed (maybe you want certain actions to change that date but not all actions). If you try and handle all of this in your repository/data layer, it will quickly become overwhelming and confusing when you get away from trivial use cases.

One other point. IProduct is a business entity, which means that you don't have to expose it to your presentation layer at all. Therefore, if you do not want to risk a developer touching certain properties, you can use what the MVC architecture usually calls ViewModels. Essentially, these are data structures that are used on the presentation layer, and the business layer can then translate these ViewModels into actual business entities.

So for example, you could have:

public class ProductViewModel
{
    int Id { get; set; }
    string Name { get; set; }
    decimal Price { get; set; }
    int CategoryId { get; set; }
}

Your presentation layer will pass a filled out ProductViewModel into your business layer's AddProduct() or UpdateProduct() methods, will then retrieve the database's IProduct entity for the one specified and use the ProductViewModel to determine how to update (or create a new) the database entity. This way, you never expose the two DateTime properties but still have full control over how and when they are set.


Forgive me if I've misunderstood you here but to me it appears that your design logic is incorrect. In essence your base entity is Product which has a number of actions Add, Update etc.

So why don't you declare base base IProduct interface which only has the minimum amount of properties required for all actions, e.g. description, category etc.

Then just get each of the actions e.g. IProductAdd inherit from this base interface. The product class itself should only inherit from the IProduct interface.

Then create new classes for each of the actions e.g. add which inherits from IProduct add & just add some methods in the product class which accept parameters of type IProductAdd etc. but which use instances of the action classes to perform work


This is how I'd go about it....I'd use reflection and attribute(s):

namespace StackOverFlowSpike.Attributes
{
    [AttributeUsage(AttributeTargets.Property)]
    public class ReadOnlyAttribute : Attribute
    {
        public ReadOnlyAttribute() { }
    }
}


using StackOverFlowSpike.Attributes;

namespace StackOverFlowSpike.Entities
{
    public interface IEntity
    {
        [ReadOnly]
        public int Id { get; set; }
    }
}

using System;
using StackOverFlowSpike.Attributes;

namespace StackOverFlowSpike.Entities
{
    public class Product : IEntity
    {
        [ReadOnly]
        public int Id { get; set; }

        public string Name { get; set; }
        public decimal Price { get; set; }

        [ReadOnly]
        public DateTime Created { get; set; }

        [ReadOnly]
        public DateTime Updated { get; set; }
    }
}

using StackOverFlowSpike.Entities;
using System.Collections.Generic;

namespace StackOverFlowSpike.Repositories
{
    public interface IRepository<T> where T : IEntity
    {
        void Add(T item);
        void Update(T item);
        T Get(int id);
        IEnumerable<T> GetAll();
    }
}

using System;
using System.Linq;
using System.Threading;
using System.Reflection;
using System.Collections.Generic;
using StackOverFlowSpike.Entities;
using StackOverFlowSpike.Attributes;

namespace StackOverFlowSpike.Repositories
{
    public class ProductRepositoryMock : IRepository<Product>
    {
        #region Fields and constructor

        private IList<Product> _productsStore;

        public ProductRepositoryMock()
        {
            _productsStore = new List<Product>();
        }

        #endregion

        #region private methods

        private int GetNewId()
        {
            return _productsStore
                .OrderByDescending(p => p.Id)
                .Select(p => p.Id).FirstOrDefault() + 1;
        }

        private void PopulateProduct(Product storedProduct, Product incomingProduct)
        {
            foreach (var p in storedProduct.GetType().GetProperties())
            {
                // check if it is NOT decorated with ReadOnly attribute
                if (!(p.GetCustomAttributes(typeof(ReadOnlyAttribute), false).Length > 0))
                {
                    // i will use reflection to set the value
                    p.SetValue(storedProduct, p.GetValue(incomingProduct, null), null);
                }
            }
        }

        private void Synchronise(Product storedProduct, Product incomingProduct)
        {
            foreach (var p in storedProduct.GetType().GetProperties())
                p.SetValue(incomingProduct, p.GetValue(storedProduct, null), null);
        }

        #endregion

        public void Add(Product product)
        {
            Product newProduct = new Product();
            newProduct.Id = GetNewId();
            newProduct.Created = DateTime.Now;
            newProduct.Updated = DateTime.Now;

            PopulateProduct(newProduct, product);
            _productsStore.Add(newProduct);
            Synchronise(newProduct, product);

            // system takes a quick nap so we can it really is updating created and updated date/times
            Thread.Sleep(1000);
        }

        public void Update(Product product)
        {
            var storedProduct = _productsStore.Where(p => p.Id == product.Id).FirstOrDefault();

            if (storedProduct != null)
            {
                PopulateProduct(storedProduct, product);
                storedProduct.Updated = DateTime.Now;

                // system takes a quick nap so we can it really is updating created and updated date/times
                Synchronise(storedProduct, product);
                Thread.Sleep(1000);
            }
        }

        public Product Get(int id)
        {
            Product storedProduct = _productsStore.Where(p => p.Id == id).FirstOrDefault();
            Product resultProduct = new Product()
            {
                Id = storedProduct.Id,
                Name = storedProduct.Name,
                Price = storedProduct.Price,
                Created = storedProduct.Created,
                Updated = storedProduct.Updated
            };

            return resultProduct;
        }

        public IEnumerable<Product> GetAll()
        {
            return _productsStore;
        }
    }
}

Here is a small console program to test

using System;
using System.Text;
using System.Collections.Generic;
using StackOverFlowSpike.Entities;
using StackOverFlowSpike.Repositories;

namespace StackOverFlowSpike
{
    class Program
    {
        static void Main(string[] args)
        {
            Product p1 = new Product()
            {
                Created = Convert.ToDateTime("01/01/2012"), // ReadOnly - so should not be updated with this value
                Updated = Convert.ToDateTime("01/02/2012"), // ReadOnly - so should not be updated with this value
                Id = 99, // ReadOnly - should not be udpated with this value
                Name = "Product 1",
                Price = 12.30m
            };

            Product p2 = new Product()
            {
                Name = "Product 2",
                Price = 18.50m,
            };

            IRepository<Product> repo = new ProductRepositoryMock();

            // test the add
            repo.Add(p1);

            repo.Add(p2);
            PrintProducts(repo.GetAll());

            // p1 should not change because of change in Id
            p1.Id = 5; // no update should happen
            p1.Name = "Product 1 updated";
            p1.Price = 10.50m;

            // p2 should update name and price but not date created
            p2.Name = "Product 2 updated";
            p2.Price = 17m;
            p2.Created = DateTime.Now;

            repo.Update(p1);
            repo.Update(p2);
            PrintProducts(repo.GetAll());

            Console.ReadKey();
        }

        private static void PrintProducts(IEnumerable<Product> products)
        {
            foreach (var p in products)
            {
                Console.WriteLine("Id: {0}\nName: {1}\nPrice: {2}\nCreated: {3}\nUpdated: {4}\n",
                    p.Id, p.Name, p.Price, p.Created, p.Updated);
            }

            Console.WriteLine(new StringBuilder().Append('-', 50).AppendLine().ToString());
        }
    }
}

Test results:

Id: 1 Name: Product 1 Price: 12.30 Created: 29/04/2011 18:41:26 Updated: 29/04/2011 18:41:26

Id: 2 Name: Product 2 Price: 18.50 Created: 29/04/2011 18:41:28

Updated: 29/04/2011 18:41:28

Id: 1 Name: Product 1 Price: 12.30 Created: 29/04/2011 18:41:26 Updated: 29/04/2011 18:41:26

Id: 2 Name: Product 2 updated Price: 17 Created: 29/04/2011 18:41:28

Updated: 29/04/2011 18:41:29

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜