Реализация слоя доступа к данным на Entity Framework Code First

  • Tutorial


Приветствую!

В данном топике я хочу поговорить о слое доступа к данным (Data Access Level) по отношению к Entity Framework-у, далее EF, о том какие задачи стояли и как я их решил. Весь представленный код из поста, а также прикрепленный демо проект публикуется под либеральной лицензией MIT, то есть вы можете использовать код как вам угодно.
Сразу хочу подчеркнуть, что весь представленный код представляет собой законченное решение и используется более 2-х лет в проекте для достаточно крупной российский компании, но тем не менее не подходит для высоконагруженных систем.

Подробности под катом.

Задачи

При написании приложения, передо мной стояло несколько задач по отношению к слою доступа к данным:
1. Все изменения данных должны логироваться, включая информацию о том какой именно пользователь это сделал
2. Использование паттерна «Репозиторий»
3. Контроль над изменением объектов, то есть если мы хотим обновить в базе данных только один объект, то должен именно один объект.
Поясню:
По умолчанию, EF отслеживает изменения всех объектов в рамках конкретного контекста, при этом возможность сохранить один объект отсутствует, в отличии от NHibernate. Такая ситуация чревата различного рода неприятными ошибками. Например, пользователь редактирует одновременно два объекта, но хочет сохранить только один. В случае, если эти два объекта связанны с один контекстом базы данных, EF сохранит изменения обоих объектов.

Решение

Кода довольно много, поэтому комментарии добавляю к наиболее интересным моментам.
Начну пожалуй с самого главного объекта — контекст базы данных.
В стандартном и упрощенном виде, он представляет собой список объектов базы данных:
UsersContext
namespace TestApp.Models
{
    public partial class UsersContext : DbContext
    {

        public UsersContext()
            : base("Name=UsersContext")
        {
        }

        public DbSet<User> Users { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new UserMap());
        }
    }
}


Расширим его с помощью следующего интерфейса:
IDbContext
    public interface IDbContext
    {
        IQueryable<T> Find<T>() where T : class;

        void MarkAsAdded<T>(T entity) where T : class;

        void MarkAsDeleted<T>(T entity) where T : class;

        void MarkAsModified<T>(T entity) where T : class;

        void Commit(bool withLogging);

       //откатывает изменения во всех модифицированных объектах
        void Rollback();

        // включает или отключает отслеживание изменений объектов
        void EnableTracking(bool isEnable);

        EntityState GetEntityState<T>(T entity) where T : class;

        void SetEntityState<T>(T entity, EntityState state) where T : class;

        // возвращает объект содержащий список объектов с их состоянием
        DbChangeTracker GetChangeTracker();

        DbEntityEntry GetDbEntry<T>(T entity) where T : class;
    }


Получившийся модифицированный DbContext:
DemoAppDbContext
namespace DataAccess.DbContexts
{
    public class DemoAppDbContext : DbContext, IDbContext
    {
        public static User CurrentUser { get; set; }

        private readonly ILogger _logger;

        #region Context Entities

        public DbSet<EntityChange> EntityChanges { get; set; }

        public DbSet<User> Users { get; set; }

        #endregion

        static DemoAppDbContext()
        {
            //устанавливаем инициализатор
            Database.SetInitializer(new CreateDBContextInitializer());
        }

        // метод вызывается при создании базы данных
        public static void Seed(DemoAppDbContext context)
        {
            // добавляем пользователя по умолчанию
            var defaultUser = new User { Email = "UserEmail@email.ru", Login = "login", IsBlocked = false, Name = "Vasy Pupkin" };
            context.Users.Add(defaultUser);
            context.SaveChanges();
        }

        public DemoAppDbContext(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {
           // инициализация логгера
            _logger = new Logger(this);
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new EntityChangeMap());
            modelBuilder.Configurations.Add(new UserMap());
        }

        public void MarkAsAdded<T>(T entity) where T : class
        {
            Entry(entity).State = EntityState.Added;
            Set<T>().Add(entity);
        }

        public void MarkAsDeleted<T>(T entity) where T : class
        {
            Attach(entity);
            Entry(entity).State = EntityState.Deleted;
            Set<T>().Remove(entity);
        }

        public void MarkAsModified<T>(T entity) where T : class
        {
            Attach(entity);
            Entry(entity).State = EntityState.Modified;
        }

        public void Attach<T>(T entity) where T : class
        {
            if (Entry(entity).State == EntityState.Detached)
            {
                Set<T>().Attach(entity);
            }
        }

        public void Commit(bool withLogging)
        {
            BeforeCommit();
            if (withLogging)
            {
                _logger.Run();
            }
            SaveChanges();
        }

        private void BeforeCommit()
        {
            UndoExistAddedEntitys();
        }

        //исправление ситуации, когда есть объекты помеченные как  новые, но при этом существующие в базе данных
        private void UndoExistAddedEntitys()
        {
            IEnumerable<DbEntityEntry> dbEntityEntries = GetChangeTracker().Entries().Where(x => x.State == EntityState.Added);
            foreach (var dbEntityEntry in dbEntityEntries)
            {
                if (GetKeyValue(dbEntityEntry.Entity) > 0)
                {
                    SetEntityState(dbEntityEntry.Entity, EntityState.Unchanged);
                }
            }
        }

        // откат всех изменений в объектах
        public void Rollback()
        {
            ChangeTracker.Entries().ToList().ForEach(x => x.Reload());
        }

        public void EnableTracking(bool isEnable)
        {
            Configuration.AutoDetectChangesEnabled = isEnable;
        }

        public void SetEntityState<T>(T entity, EntityState state) where T : class
        {
            Entry(entity).State = state;
        }

        public DbChangeTracker GetChangeTracker()
        {
            return ChangeTracker;
        }

        public EntityState GetEntityState<T>(T entity) where T : class
        {
            return Entry(entity).State;
        }

        public IQueryable<T> Find<T>() where T : class
        {
            return Set<T>();
        }

        public DbEntityEntry GetDbEntry<T>(T entity) where T : class
        {
            return Entry(entity);
        }

        public static int GetKeyValue<T>(T entity) where T : class
        {
            var dbEntity = entity as IDbEntity;
            if (dbEntity == null)
                throw new ArgumentException("Entity should be IDbEntity type - " + entity.GetType().Name);

            return dbEntity.GetPrimaryKey();
        }
    }
}


Взаимодействие с объектами базы данных происходит через репозитории специфичные для каждого объекта. Все репозитории наследуют базовый класс, который предоставляет базовый CRUD функционал
IRepository
    interface IRepository<T> where T : class
    {
        DemoAppDbContext CreateDatabaseContext();

        List<T> GetAll();

        T Find(int entityId);

        T SaveOrUpdate(T entity);

        T Add(T entity);

        T Update(T entity);

        void Delete(T entity);

        // возвращает список ошибок
        DbEntityValidationResult Validate(T entity);

       // возвращает строку с ошибками
       string ValidateAndReturnErrorString(T entity, out bool isValid);
    }


Реализация IRepository:
BaseRepository
namespace DataAccess.Repositories
{
    public abstract class BaseRepository<T> : IRepository<T> where T : class
    {
        private readonly IContextManager _contextManager;

        protected BaseRepository(IContextManager contextManager)
        {
            _contextManager = contextManager;
        }

        public DbEntityValidationResult Validate(T entity)
        {
            using (var context = CreateDatabaseContext())
            {
                return context.Entry(entity).GetValidationResult();
            }
        }

        public string ValidateAndReturnErrorString(T entity, out bool isValid)
        {
            using (var context = CreateDatabaseContext())
            {
                DbEntityValidationResult dbEntityValidationResult = context.Entry(entity).GetValidationResult();
                isValid = dbEntityValidationResult.IsValid;
                if (!dbEntityValidationResult.IsValid)
                {
                    return DbValidationMessageParser.GetErrorMessage(dbEntityValidationResult);
                }
                return string.Empty;
            }
        }

        // создание контекста базы данных. необходимо использовать using
        public DemoAppDbContext CreateDatabaseContext()
        {
            return _contextManager.CreateDatabaseContext();
        }
      
        public List<T> GetAll()
        {
            using (var context = CreateDatabaseContext())
            {
                return context.Set<T>().ToList();
            }
        }

        public T Find(int entityId)
        {
            using (var context = CreateDatabaseContext())
            {
                return context.Set<T>().Find(entityId);
            }
        }

        // виртуальный метод. вызывает перед сохранением объектов, может быть определен в дочерних классах
        protected virtual void BeforeSave(T entity, DemoAppDbContext db)
        {
            
        }

        public T SaveOrUpdate(T entity)
        {
            var iDbEntity = entity as IDbEntity;

            if (iDbEntity == null)
                throw new ArgumentException("entity should be IDbEntity type", "entity");

            return iDbEntity.GetPrimaryKey() == 0 ? Add(entity) : Update(entity);
        }
      
        public T Add(T entity)
        {
            using (var context = CreateDatabaseContext())
            {
                BeforeSave(entity, context);
                context.MarkAsAdded(entity);
                context.Commit(true);
            }
            return entity;
        }

        public T Update(T entity)
        {
            using (var context = CreateDatabaseContext())
            {
                var iDbEntity = entity as IDbEntity;
                if (iDbEntity == null)
                    throw new ArgumentException("entity should be IDbEntity type", "entity");

                var attachedEntity = context.Set<T>().Find(iDbEntity.GetPrimaryKey());
                context.Entry(attachedEntity).CurrentValues.SetValues(entity);
              
                BeforeSave(attachedEntity, context);
                context.Commit(true);
            }
            return entity;
        }

        public void Delete(T entity)
        {
            using (var context = CreateDatabaseContext())
            {
                context.MarkAsDeleted(entity);
                context.Commit(true);
            }
        }
    }
}


Объект базы данных User:
User
namespace DataAccess.Models
{
    public class User : IDbEntity
    {
        public User()
        {
            this.EntityChanges = new List<EntityChange>();
        }

        public int UserId { get; set; }

        [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Login")]
        [StringLength(50, ErrorMessage = @"Login должен быть меньше 50-ти символов")]
        public string Login { get; set; }

        [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Email")]
        [StringLength(50, ErrorMessage = @"Email должен быть меньше 50-ти символов")]
        public string Email { get; set; }

        [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Name")]
        [StringLength(50, ErrorMessage = @"Имя должно быть меньше 50-ти символов")]
        public string Name { get; set; }

        public bool IsBlocked { get; set; }

        public virtual ICollection<EntityChange> EntityChanges { get; set; }

        public override string ToString()
        {
            return string.Format("Тип: User; Название:{0}, UserId:{1} ", Name, UserId);
        }

        public int GetPrimaryKey()
        {
            return UserId;
        }
    }
}


Репозиторий для объекта «User», c рядом дополнительных методов расширяющий стандартный CRUD функционал базового класса:
UsersRepository
namespace DataAccess.Repositories
{
    public class UsersRepository : BaseRepository<User>
    {
        public UsersRepository(IContextManager contextManager)
            : base(contextManager)
        {

        }

        public User FindByLogin(string login)
        {
            using (var db = CreateDatabaseContext())
            {
                return db.Set<User>().FirstOrDefault(u => u.Login == login);
            }
        }

        public bool ExistUser(string login)
        {
            using (var db = CreateDatabaseContext())
            {
                return db.Set<User>().Count(u => u.Login == login) > 0;
            }
        }

        public User GetByUserId(int userId)
        {
            using (var db = CreateDatabaseContext())
            {
                return db.Set<User>().SingleOrDefault(c => c.UserId == userId);
            }

        }

        public User GetFirst()
        {
            using (var db = CreateDatabaseContext())
            {
                return db.Set<User>().First();
            }
        }
    }
}


В моем случае, все репозитории инициализируются один раз и добавляются в простейший самописный service locator RepositoryContainer. Это сделало для возможности написания тестов.
RepositoryContainer
namespace DataAccess.Container
{
    public class RepositoryContainer
    {
        private readonly IContainer _repositoryContainer = new Container();

        public static readonly RepositoryContainer Instance = new RepositoryContainer();

        private RepositoryContainer()
        {
            
        }

        public T Resolve<T>() where T : class
        {
            return _repositoryContainer.Resolve<T>();
        }

        public void Register<T>(T entity) where T : class
        {
            _repositoryContainer.Register(entity);
        }
    }
}

namespace DataAccess.Container
{
    public static class RepositoryContainerFactory
    {
        public static void RegisterAllRepositories(IContextManager dbContext)
        {
            RepositoryContainer.Instance.Register(dbContext);
            RepositoryContainer.Instance.Register(new EntityChangesRepository(dbContext));
            RepositoryContainer.Instance.Register(new UsersRepository(dbContext));
        }
    }
}


Всем репозиториям, при инициализации передается объект IContextManager, это сделано для возможности работы с несколькими контекстами и их централизованным созданием:
IContextManager
namespace DataAccess.Interfaces
{
    public interface IContextManager
    {
        DemoAppDbContext CreateDatabaseContext();
    }
}


И его реализация ContextManager:
ContextManager
using DataAccess.Interfaces;

namespace DataAccess.DbContexts
{
    public class ContextManager : IContextManager
    {
        private readonly string _connectionString;

        public ContextManager(string connectionString)
        {
            _connectionString = connectionString;
        }

        public DemoAppDbContext CreateDatabaseContext()
        {
            return new DemoAppDbContext(_connectionString);
        }
    }
}


Логирование происходит в объекте реализующем интерфейс ILogger:
ILogger
namespace DataAccess.Interfaces
{
    internal interface ILogger
    {
        void Run();
    }
}


Реализация интерфейса ILogger
Logger
 public class Logger : ILogger
    {
        Dictionary<EntityState, string> _operationTypes;

        private readonly IDbContext _dbContext;

        public Logger(IDbContext dbContext)
        {
            _dbContext = dbContext;
            InitOperationTypes();
        }

        public void Run()
        {
            LogChangedEntities(EntityState.Added);
            LogChangedEntities(EntityState.Modified);
            LogChangedEntities(EntityState.Deleted);
        }

        private void InitOperationTypes()
        {
            _operationTypes = new Dictionary<EntityState, string>
                {
                    {EntityState.Added, "Добавление"},
                    {EntityState.Deleted, "Удаление"},
                    {EntityState.Modified, "Изменение"}
                };
        }

        private string GetOperationName(EntityState entityState)
        {
            return _operationTypes[entityState];
        }

        private void LogChangedEntities(EntityState entityState)
        {
            IEnumerable<DbEntityEntry> dbEntityEntries = _dbContext.GetChangeTracker().Entries().Where(x => x.State == entityState);
            foreach (var dbEntityEntry in dbEntityEntries)
            {
                LogChangedEntitie(dbEntityEntry, entityState);
            }
        }

        private void LogChangedEntitie(DbEntityEntry dbEntityEntry, EntityState entityState)
        {
            string operationHash = HashGenerator.GenerateHash(10);
            int enitityId = DemoAppDbContext.GetKeyValue(dbEntityEntry.Entity);

            Type type = dbEntityEntry.Entity.GetType();

            IEnumerable<string> propertyNames = entityState == EntityState.Deleted
                                                    ? dbEntityEntry.OriginalValues.PropertyNames
                                                    : dbEntityEntry.CurrentValues.PropertyNames;

            foreach (var propertyName in propertyNames)
            {
                DbPropertyEntry property = dbEntityEntry.Property(propertyName);

                if (entityState == EntityState.Modified && !property.IsModified)
                    continue;

                _dbContext.MarkAsAdded(new EntityChange
                {
                    UserId = DemoAppDbContext.CurrentUser.UserId,
                    Created = DateTime.Now,
                    OperationHash = operationHash,
                    EntityName = string.Empty,
                    EntityType = type.ToString(),
                    EntityId = enitityId.ToString(),
                    PropertyName = propertyName,
                    OriginalValue =
                        entityState != EntityState.Added && property.OriginalValue != null
                            ? property.OriginalValue.ToString()
                            : string.Empty,
                    ModifyValue =
                        entityState != EntityState.Deleted && property.CurrentValue != null
                        ? property.CurrentValue.ToString()
                        : string.Empty,
                    OperationType = GetOperationName(entityState),
                });
            }
        }
    }


Использование

Для того чтобы начать работать с базой данных, в приложении необходимо инициализовать фабрику репозиториев:
RepositoryContainerFactory.RegisterAllRepositories(new ContextManager(Settings.Default.DBConnectionString));

После, необходимо пройти авторизацию и указать текущего пользователя. Это необходимо для того, чтобы сохранять в истории информацию о пользователе который сделал то или иное изменение. В демо проекте этот пункт упущен.
InitDefaultUser
private void InitDefaultUser()
{
	User defaultUser = RepositoryContainer.Instance.Resolve<UsersRepository>().GetFirst();
	DemoAppDbContext.CurrentUser = defaultUser;
}


Вызов методов репозитория происходит через получение экземпляра у service locator-a. В приведенном ниже примере, обращение идет к методу GetFirst() репозитория типа UsersRepository:
User defaultUser = RepositoryContainer.Instance.Resolve<UsersRepository>().GetFirst();

Добавление нового пользователя:
var newUser = new User { Email = "UserEmail@email.ru", Login = "login", IsBlocked = false, Name = "Vasy Pupkin"};
RepositoryContainer.Instance.Resolve<UsersRepository>().SaveOrUpdate(newUser);

Валидация перед сохранением объектов

Валидация и получение списка ошибок:
var newUser = new User { Email = "UserEmail@email.ru",  IsBlocked = false,  };
DbEntityValidationResult dbEntityValidationResult = RepositoryContainer.Instance.Resolve<UsersRepository>().Validate(newUser);

Получение строки с ошибками:
var newUser = new User { Email = "UserEmail@email.ru", IsBlocked = false, };

bool isValid=true;
string errors = RepositoryContainer.Instance.Resolve<UsersRepository>().ValidateAndReturnErrorString(newUser, out isValid);
if (!isValid)
{
	MessageBox.Show(errors, "Error..", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

Демо проект

Полностью рабочий проект вы можете забрать на яндекс диске http://yadi.sk/d/P9XDDznpMj6p8.
Пожалуйста, обратите внимания, что для работы требуется установленная СУБД MSSQL.
В случае использования MSSQL Express, необходимо исправить строку подключение с
 <value>Data Source=.\; Initial Catalog=EFDemoApp; Integrated Security=True; Connection Timeout=5</value>

на
 <value>Data Source=.\SQLEXPRESS; Initial Catalog=EFDemoApp; Integrated Security=True; Connection Timeout=5</value>

Послесловие

Весь вышеприведенный код, это мое решение поставленных задач. Оно может быть не правильным, не оптимальным, но тем не менее уже несколько лет с успехом работает на одном из проектов.
В свое время я потратил довольно много времени и сил на то чтобы сделать эту систему и надеюсь что мои результаты будут кому-то полезными.

Всем спасибо!
Метки:
Поделиться публикацией
Похожие публикации
Комментарии 30
  • 0
    Какой-то у вас, мягко говоря, странный репозиторий который наружу и ef'ный контекст отдает и валидацию делает. А должен абстрагировать разработчика от конкретной технологии хранения данных и предоставлять доступ к данным как к обычной коллекции.
    • 0
      Контекст базы данных наружу не отдается, отдается только его статические поле, которое хранит текущего пользователя, хотя конечно получить доступ к нему можно, пжл поправьте меня если я не прав.
      Валидация объектов тесно связана с контекстом, поэтому я посчитал разумным реализовать валидацию в базовом классе репозитория.
      • 0
        Абстрагирует от технологии абстрагирования технологии хранения данных :)
        • 0
          Это вообще не репозиторий, а шлюз к таблице (table gateway).
        • 0
          Не очень понятно зачем создавать интерфейс ILogger, если уже есть реализованное событие SavingChanges у ObjectContext. А DbContext реализует IObjectContextAdapter с пропертей ObjectContext.
          • 0
            Спасибо за комментарий, я о таком событии не знал, при этом согласно MSDN и приведенному примеру, большой разницы с тем что сделал я нет.
            • 0
              Да вы точно так же опрашиваете чендж трекер. Просто то как вы вызываете логгер мне не очень понравилось.

              Посмотрите мой следующий коментарий, в случае если бы вы использовали селф-трекинг то можно было бы опрашивать объекты когда они атачатся в контекст.
          • +1
            Кроме того, мне кажется тредование «реализовать репозиторий» очень странное. Вот требование уметь сохранять всего один объект оно понятней. Фактически оно говорит о том что у вас должен быть реализован паттерн UnitOfWork, и то что должна быть возможность иметь несколько UnitOfWork для одного пользователя одновременно, а он у вас хоть и реализован фактически, но обозван контекстом.

            Хотя наверное правильным решением для вашей задачи было бы использование не POCO, а Self-Tracking Entites. В этом случае требование было бы реализованно естественно с технологией. В контекст бы атачились только те объекты, которые надо в текущий момент сохранить.
            • 0
              Соглашусь с первой частью вашего комментария, я действительно не упомянул название паттерна UnitOfWork, в остальном дело большей частью в дело в использованной терминологии.

              По второй части вашего комментария:
              Если я конечно вас правильно понял, то вы предлагаете отключить Entity Tracking у контекста и всем объектам, которые нужно сохранить вручную задавать их статус. Это действительно правильный и работающий механизм, но проблема в том, насколько я помню, в этом случае свойствам объектов не проставляются флаг «property.IsModified», а это необходимо для логирования.
              • 0
                Есть T4 шаблоны для EF которые генерируют энтитя у которых свой чендж-тракинг. Для всех пропертей по отдельности. И все можно прологировать.
                • 0
                  Опять же не знал, спасибо за разъяснение. Буду признателен за линк.
                  • 0
                    msdn

                    Заметьте, это решение сейчас помечено как not-recomended, хотя оно и удобнее для n-tier.
                • 0
                  Entity Framework и есть самый обычный кусок работы (unit of work). Вы производите работу над объектами, потом сохраняете результат этой работы — именно этим EF и занимается.
              • 0
                1. Все изменения данных должны логироваться, включая информацию о том какой именно пользователь это сделал
                — SavingChanges в DbContext

                2. Использование паттерна «Репозиторий»
                — а чем DbSet — не репозиторий? И зачем сводить всю хитрую систему с IQueryable, ради которой весь EF затевался, к Find/GetAll?

                3. Контроль над изменением объектов, то есть если мы хотим обновить в базе данных только один объект, то должен именно один объект.
                — можно делать через ObjectContext.Attach/Detach и notracking-запросы: msdn.microsoft.com/en-us/library/bb896271(v=vs.110).aspx
                • 0
                  1. В принципе, разницы между тем как был вызван метод для логирования нет.
                  2. Вам ничего не мешает возвращать IQueryable
                  3. При отключенном трекинге у свойств объектов флаг IsModified не проставляется, а это нужно для того чтобы сохранять в историю только измененные свойства.

                  • 0
                    Одними no-tracking запросами не отделаться, если хочется изменения в каждом проперти по отдельности трекать.

                    Фактически контекст это UoW, поэтому он пересоздается на каждый чих, а репозиторий можно реализовать как слой с кэшированием, правда тут про это сказано не было.
                    • 0
                      Небольшой комментарий на тему логирования каждого чиха.
                      Проект для которого был написан код использует примерно 150 сотрудников компании и именно тот факт что все логируется, уже несколько раз очень сильно выручал в разборках типа «Какой гад опять это переделал???», а также восстановить исходное состояние записи до редактирования не обращаясь к бэкапам и услугам разработчика/админа.
                      • +1
                        Я не говорю что логгирование не нужно. Это нормальное требование для маломальски используемой системы.

                        Другой вопрос, а не нужно ли было например для интранет приложения использвовать имперсонализацию, конекшн стринг с интегрэйтедидентити и логировать изменения на уровне базы.
                        • 0
                          Именно это и надо было, это было обязательно условие тех. задания.
                          Просто сам факт того что запись была изменена зачастую может быть не так полезен, как знание того кто именно это сделал, тем более в условиях достаточно большого коллектива.
                          А так конечно вы правы, навесить триггеры на таблицы в БД намного проще.
                          • 0
                            Ключевое слово в ответе выше:
                            конекшн стринг с интегрэйтедидентити

                            через поля connection string вполне можно передать в БД текущего пользователя, тогда логирование «кто именно внес изменения» становится возможным на уровне БД.
                            При этом можно логировать даже ситуацию, когда кто-то полез менять БД руками :)
                            • 0
                              Да именно про это я и говорю. Так например шарепоинт настраивается для интранета.
                              • 0
                                Тот самый случай когда комментарии под постом не намного уступают по полезности самой статье :)
                                Мое гугление адекватного манула результатов не дало.
                                Добавьте пжл для истории линк на how to.
                                Спасибо.
                                • 0
                                  С линком сложно :)
                                  В списке параметров connection string есть, например, Application Name. При открытии соединения с БД мы в параметре ApplicationName передавали служебную мета-информацию (в частности, имя авторизованного пользователя).Эти данные потом не проблема прочитать в триггере БД (SELECT APP_NAME()).

                                  Хотя vittore, скорее всего, имел ввиду чуть-чуть другое (авторизация в приложении и БД под одной — доменной — учеткой)
                                  • 0
                                    Номер раз — имерсонализация
                                    Номер два — sspi
                                    Номер три — пример

                                    Хотя в простом случае вы можете руками насоздавать пользователей в БД, замапить их на пользователей приложения.
                                    • 0
                                      Исользуя код из примера вы будете обращаться к базе используя учетную запись пользователя в AD, и логирование будет производится штатными средствами sql server
                                      • 0
                                        А какие есть штатные средства в sql server для логирования (точнее — аудита)?
                                        • 0
                                          На уровне запроса — OUTPUT
                                          Триггеры
                                          События
                                          Sql Trace

                                          • 0
                                            Если используется Entity Framework, все это выглядит в разы гемморойнее, и чужероднее EF.

                                            Да, если аудит будет жить прямо в сервере — оно будет шустрее. Но EF — уже сам по себе очень тормозная штука, если уж его взяли, то в производительность упираться не расчитывают.
                                        • 0
                                          Это очень дорого в поддержке и развертывании. Чтобы работала имперсонация, особенно через несколько компьютеров, нужны настойки делегации на уровне домена (и все равно будет очень хрупко). Сделать логгироование на уровне сервисного слоя в итоге оказывается дешевле.
                      • 0
                        У меня есть несколько идеи для будущих статей на тему программирования, поэтому у меня большая просьба тем кто поставил минусы прокомментировать что именно не понравилось. Я надеюсь, ваша конструктивная критика поможет мне улучшить качество.
                        Спасибо!

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.